feat:优化下单速度

This commit is contained in:
dzc committed 2026-10-14 13:50:35 +08:00
1 parent 0ddf7e5d06
commit 3ca15d8f34
7 files changed
+176 -94

No files matched your search

@@ -89,4 +89,10 @@ public class HardManagerVo extends HardManagerEntity implements BaseDeptInterfac
/** 图片指令模式,首选 GS_V0,兼容 ESC_STAR。 */
private String imageMode;
/**
* 商户饭卡扇区密码(6 字节 hex);仅下发给已绑定的终端用于 M1 扇区 1 认证,
* 管理端详情等其余路径不填充,避免明文扩散。
*/
private String cardKey;
}
@@ -151,7 +151,8 @@ public class HardApiController {
@GetMapping("/get")
@Operation(summary = "获取硬件配置信息")
public R<HardManagerVo> get(@RequestParam Long id) {
return R.ok(hardManagerService.getByHardId(id));
// 终端借此同步商户饭卡密码(mch_manage.card_key),管理端详情仍走不携带密钥的原方法。
return R.ok(hardManagerService.getByHardId(id, true));
}
@GetMapping("/get-by-order-id")
@@ -33,6 +33,14 @@ public interface HardManagerService extends IService<HardManagerEntity> {
HardManagerVo getByHardId(Long hardId);
/**
* 查询设备详情;终端路径 includeCardKey 传 true 时额外携带商户饭卡密码。
*
* @param hardId 设备 ID
* @param includeCardKey 是否填充 mch_manage.card_key,仅限 /hard-api/ad 终端接口使用
*/
HardManagerVo getByHardId(Long hardId, boolean includeCardKey);
HardManagerVo getByUniqueCode(String uniqueCode);
/**
@@ -231,11 +231,17 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
}
HardManagerEntity updateObj = HardManagerEntity.builder().id(detail.getId()).uniqueCode(dto.getUniqueCode()).build();
baseMapper.updateById(updateObj);
return getByHardId(dto.getId());
// bind 仅被 /hard-api/ad 终端接口调用,直接携带饭卡密码供终端同步。
return getByHardId(dto.getId(), true);
}
@Override
public HardManagerVo getByHardId(Long hardId) {
return getByHardId(hardId, false);
}
@Override
public HardManagerVo getByHardId(Long hardId, boolean includeCardKey) {
HardManagerEntity hard = baseMapper.selectById(hardId);
if (ObjectUtil.isEmpty(hard)) {
return null;
@@ -252,6 +258,9 @@ public class HardManagerServiceImpl extends ServiceImpl<HardManagerMapper, HardM
if (ObjectUtil.isNotEmpty(mch)) {
WalletDataUtil.convertMch(vo, mch);
vo.setMerchantDeptId(mch.getDeptId());
if (includeCardKey) {
vo.setCardKey(mch.getCardKey());
}
}
if (ObjectUtil.isNotEmpty(hard.getHardGroupId())) {
HardUtil.convertHardGroup(vo, hardGroupMapper.selectById(hard.getHardGroupId()));
@@ -824,94 +824,90 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
/* ************************************* start 支付相关 start ************************************* */
@Override
// @GlobalTransactional
// 终端超时重试携带同一 androidOrderCode 再次请求,按本地订单号加锁串行化,
// 防止上一次请求仍在处理(曾观测到 15~47 秒)时重试穿透幂等复用再次建单扣款。
// expire 覆盖远超正常耗时的抖动窗口,避免锁先于长事务过期。
// 事务瘦身:商户/设备/用户/钱包/商品价格等只读查询统一放到无事务的 prepareCreateOrder,
// 数据库事务只包住建单、扣库存、钱包扣款、核销等写副作用,避免餐峰远程查询慢
// 把本地事务与数据库行锁拖长至几十秒(重复扣款事故的放大器)。
@Lock4j(keys = {"#dto.androidOrderCode"}, expire = 90000)
@Transactional(rollbackFor = Exception.class)
public Long createOrder(HardCreateOrderDto dto) {
MchManageVo mch = validateCreateOrderDto(dto);
if (ObjectUtil.isEmpty(mch)) throw new RuntimeException("机器绑定的商户不存在");
// 是否需要钱包支付
boolean isWalletPay = isPay(dto.getOrderType(), dto.getOrderPayType());
// 是否为线下支付
boolean isOffline = HardPayTypeEnum.isOffline.contains(dto.getOrderPayType());
// 是否需要核销
boolean isVerify = isVerify(dto.getOrderPayType());
// 是否微信支付
boolean isWeChatPay = ObjectUtil.equals(dto.getOrderPayType(), HardPayTypeEnum.WECHANT_SELF_QR_CODE_PAY);
// 是否支付宝支付
boolean isAliPay = ObjectUtil.equals(dto.getOrderPayType(), HardPayTypeEnum.ALI_SELF_QR_CODE_PAY);
// 查询用户
UserInfo userDetail = null;
SysUser sysUser = null;
if (isWalletPay || isVerify) {
userDetail = userService.getInnerByUserId(dto.getUserId()).getData();
if (ObjectUtil.isEmpty(userDetail)) throw new I18nBizException(ErrorCodes.COMMON_USER_NOT_EXIST_DUP);
sysUser = userDetail.getSysUser();
CreateOrderContext ctx = prepareCreateOrder(dto);
if (ObjectUtil.isNotEmpty(ctx.reusablePaidOrder)) {
return ctx.reusablePaidOrder.getId();
}
// 是否需要支付额外金额
boolean isExtraPrice = isExtraPrice(dto.getSysType());
// 默认使用该商户的钱包如果没有就用饭卡钱包
WalletManageEntity wallet = null;
if (isWalletPay) {
if (ObjectUtil.isNotEmpty(dto.getWalletId()) && ObjectUtil.equals(dto.getSysType(), HardTypeEnum.SUPERMARKET)) {
R<WalletManageVo> walletResult = remoteWalletService.getWalletById(dto.getWalletId());
wallet = Optional.of(walletResult).map(R::getData).orElseThrow(() -> new RuntimeException("用户钱包不存在"));
} else {
if (ObjectUtil.isEmpty(mch.getDefaultWalletTypeId())) {
if (dto.getSysType().equals(HardTypeEnum.CANTEEN)) {
R<WalletManageEntity> walletResult = remoteWalletService.getWallet(dto.getUserId(), WalletTypeEnum.EAT);
wallet = Optional.of(walletResult).map(R::getData).orElseThrow(() -> new RuntimeException("用户钱包不存在"));
} else {
R<WalletManageEntity> walletResult = remoteWalletService.getWallet(dto.getUserId(), WalletTypeEnum.PAY);
wallet = Optional.of(walletResult).map(R::getData).orElseThrow(() -> new RuntimeException("用户钱包不存在"));
}
} else {
R<WalletManageEntity> walletResult = remoteWalletService.getWalletByWalletTypeId(dto.getUserId(), mch.getDefaultWalletTypeId());
wallet = Optional.of(walletResult).map(R::getData).orElseThrow(() -> new RuntimeException("用户钱包不存在"));
}
}
if (ObjectUtil.isEmpty(wallet)) {
throw new I18nBizException(ErrorCodes.COMMON_WALLET_NOT_FOUND);
} else {
if (ObjectUtil.isNotEmpty(wallet.getUserId()) && ObjectUtil.isNotEmpty(dto.getUserId())) {
if (ObjectUtil.notEqual(wallet.getUserId(), dto.getUserId())) {
throw new I18nBizException(ErrorCodes.HARD_USER_NO_WALLET);
}
}
}
} else if (isVerify) {
return getSelf().createOrderInTransaction(dto, ctx);
}
/**
* 下单只读准备:商户与设备校验、用户与钱包查询、核销码校验、金额与商品摘要计算、
* 同号已支付订单幂等复用检查。全部为只读的远程与本地查询,刻意放在数据库事务之外执行;
* 钱包余额仅做快速失败预检,权威校验在钱包服务扣款 SQL 的余额条件中。
*/
private CreateOrderContext prepareCreateOrder(HardCreateOrderDto dto) {
CreateOrderContext ctx = new CreateOrderContext();
ctx.mch = validateCreateOrderDto(dto);
if (ObjectUtil.isEmpty(ctx.mch)) throw new RuntimeException("机器绑定的商户不存在");
// 是否需要钱包支付
ctx.walletPay = isPay(dto.getOrderType(), dto.getOrderPayType());
// 是否为线下支付
ctx.offline = HardPayTypeEnum.isOffline.contains(dto.getOrderPayType());
// 是否需要核销
ctx.verify = isVerify(dto.getOrderPayType());
// 是否微信支付
ctx.weChatPay = ObjectUtil.equals(dto.getOrderPayType(), HardPayTypeEnum.WECHANT_SELF_QR_CODE_PAY);
// 是否支付宝支付
ctx.aliPay = ObjectUtil.equals(dto.getOrderPayType(), HardPayTypeEnum.ALI_SELF_QR_CODE_PAY);
// 查询用户
if (ctx.walletPay || ctx.verify) {
ctx.userDetail = userService.getInnerByUserId(dto.getUserId()).getData();
if (ObjectUtil.isEmpty(ctx.userDetail)) throw new I18nBizException(ErrorCodes.COMMON_USER_NOT_EXIST_DUP);
ctx.sysUser = ctx.userDetail.getSysUser();
}
WalletPaymentSnapshot wallet = null;
if (ctx.walletPay) {
// 默认使用该商户的钱包如果没有就用饭卡钱包
wallet = resolvePaymentWallet(dto, ctx.mch);
ctx.walletId = wallet.walletId();
} else if (ctx.verify) {
// 判断该二维码有没有被核销
boolean isCancel = validateCodeIsCancel(dto.getOrderType(), dto.getOrderSourceTypeId());
if (isCancel) throw new I18nBizException(ErrorCodes.HARD_QR_ALREADY_VERIFIED);
}
int totalPrice = 0, extraPrice = 0, payPrice = 0;
totalPrice = hardCarService.computerCarTotalPrice(dto.getSysType(), dto.getOrderType(), dto.getList());
if (isExtraPrice) extraPrice = computeExtraPrice(totalPrice, mch, sysUser);
payPrice = totalPrice + extraPrice;
// 是否需要支付额外金额
boolean isExtraPrice = isExtraPrice(dto.getSysType());
ctx.totalPrice = hardCarService.computerCarTotalPrice(dto.getSysType(), dto.getOrderType(), dto.getList());
ctx.extraPrice = isExtraPrice ? computeExtraPrice(ctx.totalPrice, ctx.mch, ctx.sysUser) : 0;
ctx.payPrice = ctx.totalPrice + ctx.extraPrice;
// 商品描述(建单落库与幂等复用校验共用,准备阶段计算一次)
ctx.subject = hardCarService.computerCarSubject(dto.getSysType(), dto.getOrderType(), dto.getList());
// 幂等复用:终端请求超时后重试会携带同一 androidOrderCode,若服务端已存在同号已支付
// 订单,直接返回原订单收尾(余额校验前返回,避免重复扣款后余额不足把重试误判为失败)。
HardOrderEntity reusablePaidOrder = getReusablePaidOrder(dto);
if (ObjectUtil.isNotEmpty(reusablePaidOrder)) {
String subject = hardCarService.computerCarSubject(dto.getSysType(), dto.getOrderType(), dto.getList());
validateReusablePaidOrder(reusablePaidOrder, dto, totalPrice, extraPrice, subject);
ctx.reusablePaidOrder = getReusablePaidOrder(dto);
if (ObjectUtil.isNotEmpty(ctx.reusablePaidOrder)) {
validateReusablePaidOrder(ctx.reusablePaidOrder, dto, ctx.totalPrice, ctx.extraPrice, ctx.subject);
log.info("|{}|终端订单号重复提交命中已支付订单,直接返回原订单|androidOrderCode:{}|orderId:{}|",
TAG, dto.getAndroidOrderCode(), reusablePaidOrder.getId());
return reusablePaidOrder.getId();
TAG, dto.getAndroidOrderCode(), ctx.reusablePaidOrder.getId());
return ctx;
}
if (isWalletPay) {
if (ctx.walletPay) {
// 如果是支付的话,不能小于等于0
if (payPrice <= 0) throw new I18nBizException(ErrorCodes.HARD_PAY_AMOUNT_ZERO);
// 判断余额够不够支付
if (wallet.getBalance() < payPrice) throw new I18nBizException(ErrorCodes.COMMON_WALLET_BALANCE_INSUFFICIENT);
if (ctx.payPrice <= 0) throw new I18nBizException(ErrorCodes.HARD_PAY_AMOUNT_ZERO);
// 判断余额够不够支付(准备阶段快照预检,权威校验在钱包扣款 SQL 的余额条件)
if (wallet.balance() < ctx.payPrice) throw new I18nBizException(ErrorCodes.COMMON_WALLET_BALANCE_INSUFFICIENT);
}
return ctx;
}
/**
* 下单写副作用事务:购物车与订单落库、库存扣减、钱包扣款、核销、消息与播报收尾。
* 与 prepareCreateOrder 配合,事务内不再包含任何只读远程查询。
*/
// @GlobalTransactional
@Transactional(rollbackFor = Exception.class)
public Long createOrderInTransaction(HardCreateOrderDto dto, CreateOrderContext ctx) {
// 添加购物车
Long parentCarId = hardCarService.createCard(dto.getUserId(), dto.getMerchantId(), dto.getSysType(), dto.getOrderType(), dto.getList());
// 获取商品描述
String subject = hardCarService.computerCarSubject(dto.getSysType(), dto.getOrderType(), dto.getList());
// 添加硬件订单
HardOrderEntity orderObj = HardOrderEntity.builder()
.androidOrderCode(dto.getAndroidOrderCode())
@@ -921,59 +917,59 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
.hardId(dto.getHardwareId())
.orderStatus(HardOrderStatusEnum.CREATE.getType())
.merchantId(dto.getMerchantId())
.merchantType(mch.getTypes())
.merchantDeptId(mch.getDeptId())
.merchantType(ctx.mch.getTypes())
.merchantDeptId(ctx.mch.getDeptId())
.deptId(dto.getDeptId())
.orderType(dto.getOrderType().getType())
.payType(dto.getOrderPayType().getType())
.orderSourceTypeId(dto.getOrderSourceTypeId())
.totalPrice(totalPrice)
.extraPrice(extraPrice)
.payPrice(totalPrice + extraPrice)
.totalPrice(ctx.totalPrice)
.extraPrice(ctx.extraPrice)
.payPrice(ctx.payPrice)
.isOfflineOrder(YesNoEnum.NO.getCode())
.apptUseDate(LocalDate.now())
.productDesc(subject)
.productDesc(ctx.subject)
.build();
if (ObjectUtil.isNotEmpty(sysUser)) {
orderObj.setUserId(sysUser.getUserId());
orderObj.setUserDeptId(sysUser.getDeptId());
if (ObjectUtil.isNotEmpty(ctx.sysUser)) {
orderObj.setUserId(ctx.sysUser.getUserId());
orderObj.setUserDeptId(ctx.sysUser.getDeptId());
} else {
orderObj.setUserId(-1L);
}
if (isWalletPay) {
orderObj.setWalletId(wallet.getId());
if (ctx.walletPay) {
orderObj.setWalletId(ctx.walletId);
}
baseMapper.insert(orderObj);
registerEvidenceAfterCommit(orderObj, dto);
// 扣减库存
getSelf().fastenerInventory(orderObj, YesNoEnum.YES);
// 钱包支付或者核销
if (isVerify || isWalletPay) {
if (ctx.verify || ctx.walletPay) {
// 钱包扣款
getSelf().fastReduceBalance(orderObj);
// 核销二维码
getSelf().cancelCode(orderObj, userDetail);
if (isVerify) {
getSelf().cancelCode(orderObj, ctx.userDetail);
if (ctx.verify) {
recordVerificationTime(orderObj);
}
}
// 现金支付
if (isOffline) {
if (ctx.offline) {
// 更改为支付成功
getSelf().orderSuccess(orderObj, Boolean.FALSE);
}
// 微信支付
if (isWeChatPay) {
if (ctx.weChatPay) {
}
// 支付宝支付
if (isAliPay) {
if (ctx.aliPay) {
}
// 发送mq消息
getSelf().createOrderAfterSendMqMsg(orderObj.getId(), userDetail);
getSelf().createOrderAfterSendMqMsg(orderObj.getId(), ctx.userDetail);
// 云音箱播报
String userPhone = Optional.of(userDetail).map(UserInfo::getSysUser).map(SysUser -> SysUser.getPhone()).orElse(null);
String userPhone = Optional.of(ctx.userDetail).map(UserInfo::getSysUser).map(SysUser -> SysUser.getPhone()).orElse(null);
CompletableFuture.runAsync(() -> {
if (ObjectUtil.isNotEmpty(userPhone)) {
yyxService.send(orderObj.getMerchantId(), orderObj.getPayPrice(), userPhone);
@@ -982,6 +978,68 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
return orderObj.getId();
}
/**
* 按场景解析支付钱包:超市指定钱包 → 商户默认钱包类型 → 食堂饭卡(EAT)→ 通用钱包(PAY)。
* 四个场景互斥,单次下单只发起一次钱包查询 Feign;余额快照仅用于下单前快速失败预检。
*/
private WalletPaymentSnapshot resolvePaymentWallet(HardCreateOrderDto dto, MchManageVo mch) {
WalletPaymentSnapshot snapshot;
if (ObjectUtil.isNotEmpty(dto.getWalletId()) && ObjectUtil.equals(dto.getSysType(), HardTypeEnum.SUPERMARKET)) {
WalletManageVo wallet = Optional.of(remoteWalletService.getWalletById(dto.getWalletId()))
.map(R::getData).orElseThrow(() -> new RuntimeException("用户钱包不存在"));
snapshot = new WalletPaymentSnapshot(wallet.getId(), wallet.getUserId(), wallet.getBalance());
} else if (ObjectUtil.isEmpty(mch.getDefaultWalletTypeId())) {
if (dto.getSysType().equals(HardTypeEnum.CANTEEN)) {
WalletManageEntity wallet = Optional.of(remoteWalletService.getWallet(dto.getUserId(), WalletTypeEnum.EAT))
.map(R::getData).orElseThrow(() -> new RuntimeException("用户钱包不存在"));
snapshot = new WalletPaymentSnapshot(wallet.getId(), wallet.getUserId(), wallet.getBalance());
} else {
WalletManageEntity wallet = Optional.of(remoteWalletService.getWallet(dto.getUserId(), WalletTypeEnum.PAY))
.map(R::getData).orElseThrow(() -> new RuntimeException("用户钱包不存在"));
snapshot = new WalletPaymentSnapshot(wallet.getId(), wallet.getUserId(), wallet.getBalance());
}
} else {
WalletManageEntity wallet = Optional.of(remoteWalletService.getWalletByWalletTypeId(dto.getUserId(), mch.getDefaultWalletTypeId()))
.map(R::getData).orElseThrow(() -> new RuntimeException("用户钱包不存在"));
snapshot = new WalletPaymentSnapshot(wallet.getId(), wallet.getUserId(), wallet.getBalance());
}
if (ObjectUtil.isEmpty(snapshot.walletId())) {
throw new I18nBizException(ErrorCodes.COMMON_WALLET_NOT_FOUND);
}
if (ObjectUtil.isNotEmpty(snapshot.userId()) && ObjectUtil.isNotEmpty(dto.getUserId())
&& ObjectUtil.notEqual(snapshot.userId(), dto.getUserId())) {
throw new I18nBizException(ErrorCodes.HARD_USER_NO_WALLET);
}
return snapshot;
}
/**
* 钱包查询快照:下单准备阶段仅需主键、归属人与余额三项用于建单和预检。
*/
private record WalletPaymentSnapshot(Long walletId, Long userId, Integer balance) {
}
/**
* createOrder 只读准备阶段产出的上下文:校验与查询结果、金额与支付标记,
* 供写事务阶段使用,避免事务内重复发起远程只读查询。
*/
private static final class CreateOrderContext {
private MchManageVo mch;
private UserInfo userDetail;
private SysUser sysUser;
private Long walletId;
private boolean walletPay;
private boolean offline;
private boolean verify;
private boolean weChatPay;
private boolean aliPay;
private int totalPrice;
private int extraPrice;
private int payPrice;
private String subject;
private HardOrderEntity reusablePaidOrder;
}
/**
* 查询与当前终端请求同本地订单号、同设备、同商户的已支付订单,供超时重试幂等复用。
*
@@ -32,7 +32,7 @@ public interface WalletMessageOutboxMapper extends PjcloudBaseMapper<WalletMessa
int physicalDeleteSuccessBefore(@Param("before") LocalDateTime before, @Param("limit") int limit);
/**
* 物理删除当前租户超过一年的失败终态Outbox,每次限制数量以保留排查窗口。
* 物理删除当前租户超过保留期(30天)的失败终态Outbox,每次限制数量以控制锁范围。
*
* <p>必须在租户上下文内调用(租户拦截器自动附加 tenant_id 条件),
* 与成功清理同样沿 (tenant_id, status, create_time) 索引范围删除。</p>
@@ -64,8 +64,8 @@ public class WalletMessageOutboxServiceImpl implements WalletMessageOutboxServic
/** 成功Outbox保留天数。 */
private static final long SUCCESS_RETENTION_DAYS = 30L;
/** 失败终态Outbox保留天数。 */
private static final long FAILED_RETENTION_DAYS = 365L;
/** 失败终态Outbox保留天数;与成功保留期对齐为30天,风暴期失败事件长期滞留会持续放大表体积与扫描成本。 */
private static final long FAILED_RETENTION_DAYS = 30L;
/** 租户ID列表的本地缓存有效期,单位毫秒;扫描任务每10秒触发,缓存避免每次远程查询租户列表。 */
private static final long TENANT_CACHE_TTL_MS = 60_000L;