feat: 移除mock支付、卡不可复用、新增设备端取消订单接口

1. 移除所有mock支付逻辑,订单强制走微信支付
2. 卡状态流转改为单向:UNUSED->RESERVED->USED(终态不可复用)
3. 解绑接口改为仅清除device_id,卡状态保持USED
4. 新增设备端取消订单接口 POST /api/jst/daycard/cancel/{orderNo}
5. 修复微信回调验签header大小写问题
6. 临时注释分账标记(商户号未开通分账权限)
7. 删除多余的App UI辅助接口(ApiJstUiController)
8. 清理application.yml中无用UI配置
This commit is contained in:
zhaogd
2026-08-06 13:43:34 +08:00
parent ec1f300c53
commit 8385196b38
13 changed files with 100 additions and 462 deletions
@@ -159,8 +159,6 @@ public final class DayCardConstants {
public static final String REFUND_STATUS_CLOSED = "CLOSED";
// ==================== 支付渠道 ====================
/** Mock支付 */
public static final String PAY_CHANNEL_MOCK = "MOCK";
/** 微信支付 */
public static final String PAY_CHANNEL_WECHAT = "WECHAT";
}
@@ -125,7 +125,7 @@ public class DayCardManageController {
/**
* 解绑设备天卡权益(Web端手动操作)
* <p>将设备绑定的 USED 卡转为 REUSABLE,释放设备。橘视厅权益保留,后续新设备下单可复用此卡。</p>
* <p>清除卡上的 device_id 绑定,卡状态保持 USED 不变,不可被其他设备复用。</p>
*/
@AuditAction(value = "DAYCARD_DEVICE_UNBIND", description = "解绑设备天卡权益")
@PostMapping("/device/unbind")
@@ -56,7 +56,7 @@ public class DayCardOrderEntity implements Serializable {
private Date createdAt;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private Date updatedAt;
/** 支付渠道 MOCK/WECHAT */
/** 支付渠道 WECHAT */
private String payChannel;
/** 微信预支付ID */
private String prepayId;
@@ -66,6 +66,15 @@ public interface DayCardOrderService extends IService<DayCardOrderEntity> {
*/
DayCardOrderEntity getByOrderNo(String orderNo);
/**
* 设备端取消未支付订单
* <p>校验订单归属设备后,关单微信支付、释放卡、更新订单CANCELLED、清理Redis锁</p>
*
* @param orderNo 订单号
* @param deviceId 设备ID(从 sign 解析,校验订单归属)
*/
void cancelOrder(String orderNo, Long deviceId);
/**
* 查询设备的天卡权益(激活后 App 获取播放凭证)
* <p>从 day_card_pool 表查询 status=USED 且 expire_at > NOW() 的卡,返回 thirdCardNo 和 expireAt</p>
@@ -86,7 +95,7 @@ public interface DayCardOrderService extends IService<DayCardOrderEntity> {
/**
* 解绑设备天卡权益(Web端手动操作)
* <p>将 USED 卡转为 REUSABLE,释放设备绑定,保留橘视厅权益供后续设备复用</p>
* <p>清除卡上的 device_id 绑定,卡状态保持 USED 不变,不可被其他设备复用</p>
*
* @param deviceId 设备ID
*/
@@ -165,7 +165,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
order.setChannelOrgId(channelOrgId);
order.setAmount(amount);
order.setStatus(DayCardConstants.ORDER_STATUS_CREATED);
order.setPayChannel("MOCK");
order.setPayChannel(DayCardConstants.PAY_CHANNEL_WECHAT);
order.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
Date now = new Date();
order.setCreatedAt(now);
@@ -179,35 +179,12 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
baseMapper.insert(order);
// 7. 微信 Native 支付下单
if (wechatPayService.isConfigured()) {
try {
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount, channelOrgId, agentOrgId, storeOrgId);
order.setCodeUrl(codeUrl);
order.setPayChannel("WECHAT");
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", codeUrl)
.set("pay_channel", "WECHAT");
baseMapper.update(null, payUpdate);
} catch (Exception e) {
logger.warn("微信下单失败,回退到MOCK模式: orderNo={}", orderNo, e);
order.setPayChannel("MOCK");
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", order.getCodeUrl())
.set("pay_channel", "MOCK");
baseMapper.update(null, payUpdate);
}
} else {
order.setPayChannel("MOCK");
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", order.getCodeUrl())
.set("pay_channel", "MOCK");
baseMapper.update(null, payUpdate);
}
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount, channelOrgId, agentOrgId, storeOrgId);
order.setCodeUrl(codeUrl);
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", codeUrl);
baseMapper.update(null, payUpdate);
// 8. SKIP LOCKED 抢卡(通用池,不按 partner 过滤)
DayCardPoolEntity pool = dayCardPoolDao.selectOneForUpdateSkipLocked();
@@ -219,7 +196,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
// 9. 乐观锁更新卡状态 UNUSED -> RESERVED
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
poolUpdate.eq("id", pool.getId())
.in("status", DayCardConstants.POOL_STATUS_UNUSED, DayCardConstants.POOL_STATUS_REUSABLE)
.eq("status", DayCardConstants.POOL_STATUS_UNUSED)
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
.set("status", DayCardConstants.POOL_STATUS_RESERVED)
.set("order_no", orderNo)
@@ -298,7 +275,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
order.setChannelOrgId(channelOrgId);
order.setAmount(amount);
order.setStatus(DayCardConstants.ORDER_STATUS_CREATED);
order.setPayChannel("MOCK");
order.setPayChannel(DayCardConstants.PAY_CHANNEL_WECHAT);
order.setDeleteFlag(DayCardConstants.DELETE_FLAG_UNDEL);
Date now = new Date();
order.setCreatedAt(now);
@@ -314,36 +291,12 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
baseMapper.insert(order);
// 6.5 微信 Native 支付下单
if (wechatPayService.isConfigured()) {
try {
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount, channelOrgId, agentOrgId, storeOrgId);
order.setCodeUrl(codeUrl);
order.setPayChannel("WECHAT");
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", codeUrl)
.set("pay_channel", "WECHAT");
baseMapper.update(null, payUpdate);
} catch (Exception e) {
logger.warn("微信下单失败,回退到MOCK模式: orderNo={}", orderNo, e);
order.setPayChannel("MOCK");
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", order.getCodeUrl())
.set("pay_channel", "MOCK");
baseMapper.update(null, payUpdate);
}
} else {
// 开发模式:使用 Mock 支付
order.setPayChannel("MOCK");
order.setCodeUrl("mock://pay?orderNo=" + orderNo);
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", order.getCodeUrl())
.set("pay_channel", "MOCK");
baseMapper.update(null, payUpdate);
}
String codeUrl = wechatPayService.createNativePay(orderNo, "天卡订单", amount, channelOrgId, agentOrgId, storeOrgId);
order.setCodeUrl(codeUrl);
UpdateWrapper<DayCardOrderEntity> payUpdate = new UpdateWrapper<>();
payUpdate.eq("id", order.getId())
.set("code_url", codeUrl);
baseMapper.update(null, payUpdate);
// 7. SKIP LOCKED 抢卡
DayCardPoolEntity pool = dayCardPoolDao.selectOneForUpdateSkipLocked();
@@ -355,7 +308,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
// 8. 乐观锁更新卡状态 UNUSED -> RESERVED
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
poolUpdate.eq("id", pool.getId())
.in("status", DayCardConstants.POOL_STATUS_UNUSED, DayCardConstants.POOL_STATUS_REUSABLE)
.eq("status", DayCardConstants.POOL_STATUS_UNUSED)
.eq("delete_flag", DayCardConstants.DELETE_FLAG_UNDEL)
.set("status", DayCardConstants.POOL_STATUS_RESERVED)
.set("order_no", orderNo)
@@ -444,12 +397,12 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
redisUtils.delete(DayCardConstants.REDIS_DEVICE_ORDER_PREFIX + order.getDeviceSn());
}
// 6. 触发分账(异步)
try {
profitSharingExecuteService.executeSharing(orderNo);
} catch (Exception e) {
logger.error("触发分账失败: orderNo={}", orderNo, e);
}
// 6. 触发分账(商户号开通分账权限后取消注释启用)
// try {
// profitSharingExecuteService.executeSharing(orderNo);
// } catch (Exception e) {
// logger.error("触发分账失败: orderNo={}", orderNo, e);
// }
}
/**
@@ -646,7 +599,7 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
// 更新订单为CANCELLED
UpdateWrapper<DayCardOrderEntity> orderUpdate = new UpdateWrapper<>();
orderUpdate.eq("id", order.getId())
.eq("status", DayCardConstants.ORDER_STATUS_CREATED)
.in("status", DayCardConstants.ORDER_STATUS_CREATED, DayCardConstants.ORDER_STATUS_PAYING)
.set("status", DayCardConstants.ORDER_STATUS_CANCELLED)
.set("cancel_time", now)
.set("updated_at", now);
@@ -672,6 +625,29 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
return baseMapper.selectOne(wrapper);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void cancelOrder(String orderNo, Long deviceId) {
if (StringUtils.isBlank(orderNo) || deviceId == null) {
throw new ASException("订单号和设备ID不能为空");
}
DayCardOrderEntity order = getByOrderNo(orderNo);
if (order == null) {
throw new ASException("订单不存在: " + orderNo);
}
if (!deviceId.equals(order.getDeviceId())) {
throw new ASException("无权操作此订单");
}
if (DayCardConstants.ORDER_STATUS_PAID.equals(order.getStatus())) {
throw new ASException("订单已支付,无法取消");
}
if (DayCardConstants.ORDER_STATUS_CANCELLED.equals(order.getStatus())) {
return;
}
cancelSingleOrder(order);
logger.info("设备端取消订单: orderNo={}, deviceId={}", orderNo, deviceId);
}
@Override
public DayCardPoolEntity getDeviceBenefits(Long deviceId) {
QueryWrapper<DayCardPoolEntity> wrapper = new QueryWrapper<>();
@@ -706,7 +682,6 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
UpdateWrapper<DayCardPoolEntity> update = new UpdateWrapper<>();
update.eq("id", pool.getId())
.eq("status", DayCardConstants.POOL_STATUS_USED)
.set("status", DayCardConstants.POOL_STATUS_REUSABLE)
.set("device_id", null)
.set("updated_at", new Date());
int updated = dayCardPoolDao.update(null, update);
@@ -723,17 +698,16 @@ public class DayCardOrderServiceImpl extends ServiceImpl<DayCardOrderDao, DayCar
throw new ASException("设备ID不能为空");
}
Date now = new Date();
// 1. 释放设备当前天卡权益(若有):USED -> REUSABLE,清卡上的 device_id
// 1. 解绑设备当前天卡权益(若有):清除 device_id,卡状态保持 USED 不变
DayCardPoolEntity pool = getDeviceBenefits(deviceId);
if (pool != null) {
UpdateWrapper<DayCardPoolEntity> poolUpdate = new UpdateWrapper<>();
poolUpdate.eq("id", pool.getId())
.eq("status", DayCardConstants.POOL_STATUS_USED)
.set("status", DayCardConstants.POOL_STATUS_REUSABLE)
.set("device_id", null)
.set("updated_at", now);
dayCardPoolDao.update(null, poolUpdate);
logger.info("设备组织解绑-释放天卡权益: deviceId={}, poolId={}, cardCode={}",
logger.info("设备组织解绑-解除天卡权益绑定: deviceId={}, poolId={}, cardCode={}",
deviceId, pool.getId(), pool.getCardCode());
}
// 2. 清除设备的门店组织关联(storeOrgId),设备可在新场所用新激活码重新绑定
@@ -157,18 +157,13 @@ public class DayCardRefundServiceImpl extends ServiceImpl<DayCardRefundDao, DayC
// 7. 尝试回退分账(设置关联分账单状态为 CLOSED)
rollbackSettleRecords(orderNo, now);
// 8. 调用微信退款(Mock支付则跳过)
String payNo = order.getPayNo();
if (isWechatPayment(payNo)) {
BigDecimal totalAmount = order.getAmount();
WechatPayResult refundResult = wechatPayService.refund(orderNo, refundNo, refundAmount, totalAmount, reason);
if (!refundResult.isSuccess()) {
throw new ASException("微信退款失败: " + refundResult.getErrorMsg());
}
logger.info("微信退款成功: orderNo={}, refundNo={}", orderNo, refundNo);
} else {
logger.info("Mock支付退款,无需调用微信接口: orderNo={}, refundNo={}", orderNo, refundNo);
// 8. 调用微信退款
BigDecimal totalAmount = order.getAmount();
WechatPayResult refundResult = wechatPayService.refund(orderNo, refundNo, refundAmount, totalAmount, reason);
if (!refundResult.isSuccess()) {
throw new ASException("微信退款失败: " + refundResult.getErrorMsg());
}
logger.info("微信退款成功: orderNo={}, refundNo={}", orderNo, refundNo);
// 9. 更新退款记录 status=SUCCESS
refund.setStatus(DayCardConstants.REFUND_STATUS_SUCCESS);
@@ -274,21 +269,6 @@ public class DayCardRefundServiceImpl extends ServiceImpl<DayCardRefundDao, DayC
}
}
/**
* 判断是否为微信支付
* <p>Mock 支付的 payNo 以 "MOCK_" 开头,其余视为微信支付</p>
*
* @param payNo 支付单号
* @return true=微信支付
*/
private boolean isWechatPayment(String payNo) {
if (StringUtils.isBlank(payNo)) {
// 没有支付单号,默认视为微信支付
return true;
}
return !payNo.startsWith(DayCardConstants.PAY_CHANNEL_MOCK + "_");
}
/**
* 生成退款单号:REFUND + yyyyMMddHHmmss + 6位随机数
*/