diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardManagerVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardManagerVo.java index a884977ec..2868093af 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardManagerVo.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/manager/HardManagerVo.java @@ -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; + } diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java index c9cd15ec9..b6de25260 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/hard/HardApiController.java @@ -151,7 +151,8 @@ public class HardApiController { @GetMapping("/get") @Operation(summary = "获取硬件配置信息") public R 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") diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardManagerService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardManagerService.java index ecc7a5032..1501d20b6 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardManagerService.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardManagerService.java @@ -33,6 +33,14 @@ public interface HardManagerService extends IService { 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); /** diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardManagerServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardManagerServiceImpl.java index bdd2d078c..7d9469c63 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardManagerServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/manager/HardManagerServiceImpl.java @@ -231,11 +231,17 @@ public class HardManagerServiceImpl extends ServiceImpl 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 walletResult = remoteWalletService.getWallet(dto.getUserId(), WalletTypeEnum.EAT); - wallet = Optional.of(walletResult).map(R::getData).orElseThrow(() -> new RuntimeException("用户钱包不存在")); - } else { - R walletResult = remoteWalletService.getWallet(dto.getUserId(), WalletTypeEnum.PAY); - wallet = Optional.of(walletResult).map(R::getData).orElseThrow(() -> new RuntimeException("用户钱包不存在")); - } - } else { - R 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 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 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; + } + /** * 查询与当前终端请求同本地订单号、同设备、同商户的已支付订单,供超时重试幂等复用。 * diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/WalletMessageOutboxMapper.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/WalletMessageOutboxMapper.java index 8f59baed8..74ef567f3 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/WalletMessageOutboxMapper.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/WalletMessageOutboxMapper.java @@ -32,7 +32,7 @@ public interface WalletMessageOutboxMapper extends PjcloudBaseMapper必须在租户上下文内调用(租户拦截器自动附加 tenant_id 条件), * 与成功清理同样沿 (tenant_id, status, create_time) 索引范围删除。

diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletMessageOutboxServiceImpl.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletMessageOutboxServiceImpl.java index ba1f86b8b..2e4ecfec2 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletMessageOutboxServiceImpl.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletMessageOutboxServiceImpl.java @@ -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;