短信通知

This commit is contained in:
磷叶 2025-04-16 15:00:52 +08:00
parent 4f4e3a3c56
commit cc628ffc94
16 changed files with 195 additions and 49 deletions

View File

@ -10,6 +10,7 @@ import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
import com.aliyuncs.profile.IClientProfile;
import com.ruoyi.common.exception.ServiceException;
@Service
public class SmsAliService {
@ -34,28 +35,30 @@ public class SmsAliService {
/**
* 发送短信验证码
*/
public SendSmsResponse send(SmsSendDTO dto)
throws ClientException {
// 可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
// 初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
// 组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
// 必填:待发送手机号
request.setPhoneNumbers(dto.getMobile());
// 必填:短信签名-可在短信控制台中找到
request.setSignName(signName);
// 必填:短信模板-可在短信控制台中找到
request.setTemplateCode(dto.getTemplateCode());
// 可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}",此处的值为
request.setTemplateParam(dto.getParam().toJSONString());
// hint 此处可能会抛出异常注意catch
return acsClient.getAcsResponse(request);
public SendSmsResponse send(SmsSendDTO dto) {
try {
// 可自助调整超时时间
System.setProperty("sun.net.client.defaultConnectTimeout", "10000");
System.setProperty("sun.net.client.defaultReadTimeout", "10000");
// 初始化acsClient,暂不支持region化
IClientProfile profile = DefaultProfile.getProfile("cn-hangzhou", accessKeyId, accessKeySecret);
DefaultProfile.addEndpoint("cn-hangzhou", "cn-hangzhou", product, domain);
IAcsClient acsClient = new DefaultAcsClient(profile);
// 组装请求对象-具体描述见控制台-文档部分内容
SendSmsRequest request = new SendSmsRequest();
// 必填:待发送手机号
request.setPhoneNumbers(dto.getMobile());
// 必填:短信签名-可在短信控制台中找到
request.setSignName(signName);
// 必填:短信模板-可在短信控制台中找到
request.setTemplateCode(dto.getTemplateCode());
// 可选:模板中的变量替换JSON串,如模板内容为"亲爱的${name},您的验证码为${code}",此处的值为
request.setTemplateParam(dto.getParam().toJSONString());
// hint 此处可能会抛出异常注意catch
return acsClient.getAcsResponse(request);
} catch (ClientException e) {
throw new ServiceException("短信发送失败,错误信息:" + e.getMessage());
}
}
}

View File

@ -8,15 +8,18 @@ import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
public class AreaVO extends Area {
// 所属用户
@ApiModelProperty("用户名称")
private String userName;
@ApiModelProperty("用户手机号")
private String userPhone;
@ApiModelProperty("创建人名称")
private String createName;
@ApiModelProperty("停车区数量")
private Integer parkingAreaCount;
@ApiModelProperty("禁停区数量")
private Integer noParkingAreaCount;

View File

@ -48,6 +48,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
ba.outage_distance,
su.nick_name as user_name,
su.agent_id as agent_id,
su.user_name as user_phone,
suc.nick_name as create_name
from <include refid="searchTables"/>
</sql>
@ -80,6 +81,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="query.deleted == null "> and ba.deleted = false</if>
<if test="query.userName != null and query.userName != ''"> and su.nick_name like concat('%', #{query.userName}, '%')</if>
<if test="query.createName != null and query.createName != ''"> and suc.nick_name like concat('%', #{query.createName}, '%')</if>
<if test="query.userPhone != null and query.userPhone != ''"> and su.user_name like concat('%', #{query.userPhone}, '%')</if>
<if test="query.ids != null and query.ids.size() > 0">
and ba.id in
<foreach collection="query.ids" item="item" open="(" separator="," close=")">

View File

@ -10,7 +10,8 @@ import lombok.Getter;
public enum BalanceLogBstType {
ORDER("ORDER", "订单"),
WITHDRAW("WITHDRAW", "提现");
WITHDRAW("WITHDRAW", "提现"),
SMS("SMS", "短信");
private final String code;
private final String name;

View File

@ -39,10 +39,6 @@ public class OrderVO extends Order {
// 运营区
@ApiModelProperty("运营区名称")
private String areaName;
@ApiModelProperty("运营区联系人")
private String areaContact;
@ApiModelProperty("运营区联系电话")
private String areaPhone;
// 用户
@ApiModelProperty("用户名称")

View File

@ -14,9 +14,6 @@ public class OrderEndDTO {
@NotNull(message = "订单ID不能为空")
private Long orderId;
@ApiModelProperty("是否辅助还车(管理员还车)")
private Boolean isAdmin;
@ApiModelProperty("手机定位(经度)")
private BigDecimal lon;
@ -28,4 +25,7 @@ public class OrderEndDTO {
@ApiModelProperty("车辆图片")
private String picture;
@ApiModelProperty("还车类型")
private String returnType;
}

View File

@ -1,5 +1,8 @@
package com.ruoyi.bst.order.domain.enums;
import java.util.Arrays;
import java.util.List;
import lombok.AllArgsConstructor;
import lombok.Getter;
@ -8,10 +11,20 @@ import lombok.Getter;
public enum OrderReturnType {
NORMAL("1", "正常还车"),
AUXILIARY("2", "辅助还车");
AUXILIARY("2", "辅助还车"),
SYSTEM("3", "系统还车");
private String code;
private String name;
// 是否是管理员还车
public static boolean isAdmin(String returnType) {
return AUXILIARY.getCode().equals(returnType) || SYSTEM.getCode().equals(returnType);
}
// 需要审核的还车类型
public static List<String> needVerify() {
return Arrays.asList(NORMAL.getCode(), SYSTEM.getCode());
}
}

View File

@ -52,8 +52,6 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
bo.area_user_id,
bo.area_agent_id,
ba.name as area_name,
ba.contact as area_contact,
ba.phone as area_phone,
su.nick_name as user_name,
su.user_name as user_phone,
bp.no as pay_no,

View File

@ -133,7 +133,7 @@ public interface OrderService
* @param dto 参数
* @return 结果
*/
public OrderFeeVO preEndCheck(OrderEndDTO dto);
public OrderFeeVO calcFee(OrderEndDTO dto);
/**
* 退款

View File

@ -65,6 +65,7 @@ import com.ruoyi.bst.pay.domain.enums.PayBstType;
import com.ruoyi.bst.pay.domain.vo.DoPayVO;
import com.ruoyi.bst.pay.service.PayConverter;
import com.ruoyi.bst.pay.service.PayService;
import com.ruoyi.bst.sms.service.SmsService;
import com.ruoyi.common.core.domain.vo.UserVO;
import com.ruoyi.common.core.redis.RedisLock;
import com.ruoyi.common.core.redis.enums.RedisLockKey;
@ -142,6 +143,9 @@ public class OrderServiceImpl implements OrderService
@Autowired
private UserService userService;
@Autowired
private SmsService smsService;
/**
* 查询订单
*
@ -235,7 +239,7 @@ public class OrderServiceImpl implements OrderService
ServiceUtil.assertion(dto == null || dto.getDeviceId() == null, "参数错误");
// 加锁防止设备重复下单
// TODO可以考虑加锁60秒存储车辆用户ID60秒内只允许同一个用户下单
// TODO 可以考虑加锁60秒存储车辆用户ID60秒内只允许同一个用户下单
Long lockKey = dto.getDeviceId();
boolean lock = redisLock.lock(RedisLockKey.ORDER_CREATE, lockKey);
ServiceUtil.assertion(!lock, "当前车辆使用的人太多了,请稍后再试");
@ -398,10 +402,13 @@ public class OrderServiceImpl implements OrderService
// 转为BO对象
OrderEndBO bo = orderConverter.toEndBO(dto);
// 校验参数
boolean isAdmin = dto.getIsAdmin() != null && dto.getIsAdmin();
String returnType = dto.getReturnType();
boolean isAdmin = OrderReturnType.isAdmin(returnType);
orderValidator.validate(bo, isAdmin);
// 提取数据
OrderVO order = bo.getOrder();
OrderInParkingVO inParkingVO = bo.getInParkingVO();
AreaVO area = bo.getArea();
@ -413,13 +420,13 @@ public class OrderServiceImpl implements OrderService
order.setDistance(LocationLogUtil.calcDistance(bo.getPositionList()));
order.setEndReason(dto.getEndReason());
// 还车类型
order.setReturnType(isAdmin ? OrderReturnType.AUXILIARY.getCode() : OrderReturnType.NORMAL.getCode());
order.setReturnType(returnType);
// 订单状态
this.setOrderStatus(order, isAdmin);
this.setOrderStatus(order, returnType);
// 订单费用
this.setOrderFee(order, area, inParkingVO);
Integer result = transactionTemplate.execute(status -> {
transactionTemplate.execute(status -> {
// 更新订单数据
Order data = orderConverter.toPOByEnd(order);
OrderQuery query = new OrderQuery();
@ -437,6 +444,9 @@ public class OrderServiceImpl implements OrderService
// 预分成
boolean bonus = bonusService.prepayByBst(BonusBstType.ORDER, order.getId());
ServiceUtil.assertion(!bonus, "ID为%s的订单预分成失败", order.getId());
} else if (OrderStatus.WAIT_VERIFY.getCode().equals(order.getStatus())) {
// 发送还车审核通知
smsService.sendOrderWaitVerifyMsg(order);
}
// 结束订单设备
@ -444,7 +454,7 @@ public class OrderServiceImpl implements OrderService
ServiceUtil.assertion(finish != 1, "结束ID为%s的订单设备失败", orderDevice.getId());
// 设备上锁必须放最后因为会操作设备
DeviceIotVO lock = deviceIotService.lock(orderDevice.getDeviceId(), dto.getIsAdmin(), "订单结束上锁:" + orderDevice.getOrderNo(), false);
DeviceIotVO lock = deviceIotService.lock(orderDevice.getDeviceId(), isAdmin, "订单结束上锁:" + orderDevice.getOrderNo(), false);
ServiceUtil.assertion(lock.getDb() != 1, "ID为%s的设备上锁失败", orderDevice.getDeviceId());
vo.setIot(lock.getIot());
@ -455,9 +465,9 @@ public class OrderServiceImpl implements OrderService
}
// 设置订单状态
private void setOrderStatus(OrderVO order, boolean isAdmin) {
private void setOrderStatus(OrderVO order, String returnType) {
// 根据是否需要审核设置订单状态
if (order.getAreaReturnVerify() != null && order.getAreaReturnVerify() && !isAdmin) {
if (order.getAreaReturnVerify() != null && order.getAreaReturnVerify() && OrderReturnType.needVerify().contains(returnType)) {
// 更新订单状态为待审核
order.setStatus(OrderStatus.WAIT_VERIFY.getCode());
} else {
@ -530,7 +540,7 @@ public class OrderServiceImpl implements OrderService
}
@Override
public OrderFeeVO preEndCheck(OrderEndDTO dto) {
public OrderFeeVO calcFee(OrderEndDTO dto) {
// 查询订单
OrderVO order = this.selectOrderById(dto.getOrderId());
if (order == null) {

View File

@ -0,0 +1,16 @@
package com.ruoyi.bst.sms.constants;
public class SmsTemplateCode {
// 还车待审核模板您名下有一笔租车订单已归还订单金额${name}
public static final String ORDER_WAIT_VERIFY = "SMS_470225045";
// 订单支付成功通知模板您有一笔订单${orderNo}用户已付款可在小程序管理端查看
public static final String ORDER_PAY_SUCCESS = "SMS_470585077";
// 押金提现申请通知模板用户${name}发起押金提现申请请尽快审核
public static final String DEPOSIT_WITHDRAW_APPLY = "SMS_470400077";
// 验证码模板您的验证码为${code}请勿泄露于他人
public static final String VERIFICATION_CODE = "SMS_470385109";
}

View File

@ -0,0 +1,12 @@
package com.ruoyi.bst.sms.service;
import com.ruoyi.bst.order.domain.OrderVO;
public interface SmsService {
/**
* 还车待审核通知
*/
void sendOrderWaitVerifyMsg(OrderVO order);
}

View File

@ -0,0 +1,87 @@
package com.ruoyi.bst.sms.service.impl;
import java.math.BigDecimal;
import java.util.concurrent.ScheduledExecutorService;
import com.ruoyi.common.utils.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import com.alibaba.fastjson2.JSONObject;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.ruoyi.bst.area.domain.AreaVO;
import com.ruoyi.bst.area.service.AreaService;
import com.ruoyi.bst.balanceLog.domain.enums.BalanceLogBstType;
import com.ruoyi.bst.order.domain.OrderVO;
import com.ruoyi.bst.sms.constants.SmsTemplateCode;
import com.ruoyi.bst.sms.service.SmsService;
import com.ruoyi.common.sms.SmsAliService;
import com.ruoyi.common.sms.SmsSendDTO;
import com.ruoyi.common.utils.ServiceUtil;
import com.ruoyi.system.user.service.UserService;
import lombok.extern.slf4j.Slf4j;
@Service
@Slf4j
public class SmsServiceImpl implements SmsService {
@Autowired
private SmsAliService smsAliService;
@Autowired
private AreaService areaService;
@Autowired
private ScheduledExecutorService scheduledExecutorService;
@Autowired
private TransactionTemplate transactionTemplate;
@Autowired
private UserService userService;
@Override
public void sendOrderWaitVerifyMsg(OrderVO order) {
AreaVO area = areaService.selectAreaById(order.getAreaId());
if (area == null || area.getMsgSwitch() == null || !area.getMsgSwitch()) {
return;
}
String mobile = area.getUserPhone();
if (StringUtils.isBlank(mobile)) {
return;
}
String reason = "订单" + order.getNo() + "还车待审核短信通知,接收手机:" + mobile;
SmsSendDTO dto = new SmsSendDTO();
dto.setMobile(mobile);
dto.setTemplateCode(SmsTemplateCode.ORDER_WAIT_VERIFY);
JSONObject param = new JSONObject();
param.put("name", order.getTotalFee());
dto.setParam(param);
this.sendAsync(area.getUserId(), reason, dto);
}
// 异步发送短信
private void sendAsync(Long userId, String reason, SmsSendDTO dto) {
scheduledExecutorService.execute(() -> {
transactionTemplate.execute(status -> {
// 扣费
int rows = userService.subtractBalance(userId, BigDecimal.valueOf(0.1), BalanceLogBstType.SMS, userId, reason, true);
ServiceUtil.assertion(rows != 1, "短信扣费失败,用户%s余额不足", userId);
// 发送短信
SendSmsResponse res = smsAliService.send(dto);
ServiceUtil.assertion(res == null, "给用户%s发送短信失败未知原因", userId);
ServiceUtil.assertion(!"OK".equals(res.getCode()), "给用户%s短信发送失败%s", userId, res.getMessage());
return rows;
});
});
}
}

View File

@ -11,6 +11,7 @@ import org.springframework.stereotype.Component;
import com.ruoyi.bst.order.domain.OrderQuery;
import com.ruoyi.bst.order.domain.OrderVO;
import com.ruoyi.bst.order.domain.dto.OrderEndDTO;
import com.ruoyi.bst.order.domain.enums.OrderReturnType;
import com.ruoyi.bst.order.domain.enums.OrderStatus;
import com.ruoyi.bst.order.mapper.OrderMapper;
import com.ruoyi.bst.order.service.OrderService;
@ -63,7 +64,7 @@ public class OrderTask implements ApplicationRunner {
for (OrderVO order : orderList) {
try {
OrderEndDTO dto = new OrderEndDTO();
dto.setIsAdmin(true);
dto.setReturnType(OrderReturnType.SYSTEM.getCode());
dto.setOrderId(order.getId());
dto.setEndReason("超时系统自动结束");
orderService.endOrder(dto);

View File

@ -2,6 +2,7 @@ package com.ruoyi.web.app;
import java.util.List;
import com.ruoyi.common.annotation.Anonymous;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
@ -19,6 +20,7 @@ import com.ruoyi.bst.order.domain.dto.OrderCloseDeviceDTO;
import com.ruoyi.bst.order.domain.dto.OrderCreateDTO;
import com.ruoyi.bst.order.domain.dto.OrderEndDTO;
import com.ruoyi.bst.order.domain.dto.OrderPayDTO;
import com.ruoyi.bst.order.domain.enums.OrderReturnType;
import com.ruoyi.bst.order.domain.enums.OrderStatus;
import com.ruoyi.bst.order.domain.vo.OrderEndVO;
import com.ruoyi.bst.order.domain.vo.OrderFeeVO;
@ -75,6 +77,7 @@ public class AppOrderController extends BaseController {
@ApiOperation("支付前计算订单价格")
@PostMapping("/calculatePrice")
@Anonymous
public AjaxResult calculatePrice(@RequestBody @Validated OrderCalcPrePriceDTO dto) {
OrderPrePriceVO vo = orderConverter.toOrderPrePriceVO(dto);
if (vo == null) {
@ -107,7 +110,7 @@ public class AppOrderController extends BaseController {
public AjaxResult endOrder(@RequestBody @Validated OrderEndDTO dto) {
OrderVO order = orderService.selectOrderById(dto.getOrderId());
ServiceUtil.assertion(!orderValidator.canEnd(order, getUserId()), "您无权结束ID为%s的订单", order.getId());
dto.setIsAdmin(false);
dto.setReturnType(OrderReturnType.NORMAL.getCode());
dto.setEndReason("用户【" + getNickName() + "】手动还车");
OrderEndVO vo = orderService.endOrder(dto);
if (vo.getDb() > 0) {
@ -119,8 +122,8 @@ public class AppOrderController extends BaseController {
@ApiOperation("计算订单费用")
@PostMapping("/calcFee")
public AjaxResult calcFee(@RequestBody @Validated OrderEndDTO dto) {
dto.setIsAdmin(false);
OrderFeeVO fee = orderService.preEndCheck(dto);
dto.setReturnType(OrderReturnType.NORMAL.getCode());
OrderFeeVO fee = orderService.calcFee(dto);
ServiceUtil.assertion(fee == null, "价格计算失败");
return success(fee);
}

View File

@ -23,6 +23,7 @@ import com.ruoyi.bst.order.domain.OrderVO;
import com.ruoyi.bst.order.domain.dto.OrderEndDTO;
import com.ruoyi.bst.order.domain.dto.OrderRefundDTO;
import com.ruoyi.bst.order.domain.dto.OrderVerifyDTO;
import com.ruoyi.bst.order.domain.enums.OrderReturnType;
import com.ruoyi.bst.order.domain.vo.OrderEndVO;
import com.ruoyi.bst.order.service.OrderAssembler;
import com.ruoyi.bst.order.service.OrderService;
@ -109,7 +110,7 @@ public class OrderController extends BaseController
if (!orderValidator.canOperate(dto.getOrderId())) {
return error("您无权结束ID为" + dto.getOrderId() + "的订单");
}
dto.setIsAdmin(true);
dto.setReturnType(OrderReturnType.AUXILIARY.getCode());
dto.setEndReason("管理员【" + getNickName() + "】手动还车");
OrderEndVO vo = orderService.endOrder(dto);
if (vo.getDb() > 0) {