From 9a6a188074a0946d75d1b1b9fa293a6288aaaf91 Mon Sep 17 00:00:00 2001 From: dzc <2388224167@qq.com> Date: Tue, 13 Oct 2026 17:25:31 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E5=AF=B9=E6=8E=A5=E8=AF=BB?= =?UTF-8?q?=E5=86=99=E5=8D=A1=E5=99=A8=20feat=EF=BC=9A=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E4=B8=8B=E6=8B=89=E6=A1=86=E6=90=9C=E7=B4=A2?= =?UTF-8?q?=20feat=EF=BC=9A=E4=BC=98=E5=8C=96=E4=B8=8B=E5=8D=95=E5=8D=A1?= =?UTF-8?q?=E9=A1=BF=E9=97=AE=E9=A2=98=20feat=EF=BC=9A=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=95=86=E6=88=B7=E5=88=97=E8=A1=A8=E6=A0=B7=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- db/20261013_add_mch_card_key_perm.sql | 43 ++++ db/20261013_add_mch_manage_card_key.sql | 23 ++ ...1013_add_wallet_active_write_card_perm.sql | 42 ++++ .../hard/api/dto/order/HardOrderDto.java | 6 + .../vo/order/HardOrderByAndroidCodeVo.java | 37 ++++ .../controller/hard/HardApiController.java | 12 +- .../hard/service/HardOrderService.java | 13 ++ .../impl/order/HardOrderServiceImpl.java | 83 ++++++++ .../mapper/order/HardOrderMapper.xml | 17 ++ ...rServiceImplCreateOrderIdempotentTest.java | 201 ++++++++++++++++++ .../main/resources/mapper/SysUserMapper.xml | 12 +- .../wallet/api/dto/mch/MchRuleJoinDto.java | 9 - .../api/entity/mch/MchManageEntity.java | 6 + .../controller/mch/MchManageController.java | 52 ++++- .../controller/mch/MchRuleJoinController.java | 82 +------ .../mapper/mch/WalletRuleMchConfigMapper.java | 8 +- .../wallet/service/MchRuleJoinService.java | 30 +-- .../impl/mch/MchManageServiceImpl.java | 14 ++ .../mch/MchRuleJoinControllerServiceImpl.java | 101 +-------- .../resources/mapper/mch/MchManageMapper.xml | 2 + .../mapper/mch/MchRuleJoinMapper.xml | 35 --- 21 files changed, 569 insertions(+), 259 deletions(-) create mode 100644 db/20261013_add_mch_card_key_perm.sql create mode 100644 db/20261013_add_mch_manage_card_key.sql create mode 100644 db/20261013_add_wallet_active_write_card_perm.sql create mode 100644 pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderByAndroidCodeVo.java create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImplCreateOrderIdempotentTest.java delete mode 100644 pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/mch/MchRuleJoinDto.java diff --git a/db/20261013_add_mch_card_key_perm.sql b/db/20261013_add_mch_card_key_perm.sql new file mode 100644 index 000000000..0d9856662 --- /dev/null +++ b/db/20261013_add_mch_card_key_perm.sql @@ -0,0 +1,43 @@ +-- 20261013 商户饭卡密码查看按钮权限(可重复执行) +-- +-- 背景:商户饭卡密码(mch_manage.card_key)属敏感信息,仅授权人员可查看完整值, +-- 其余人在管理端读卡器/商户表单中只见脱敏值(前2位+********+后2位)。 +-- 方案:sys_menu 在各「商户管理」菜单下新增按钮权限 wallet_mchManage_viewCardKey, +-- 需要查看完整密码的角色在 系统管理-角色管理 中勾选该按钮。 +-- 执行:对 pjcloudx 库执行;幂等,按 permission 不存在才插入。 +-- 回滚:文末 DELETE。 + +USE pjcloudx; + +-- 在每个已存在 wallet_mchManage_view 按钮的商户管理菜单下,追加同级的查看密码按钮 +INSERT INTO sys_menu (menu_id, name, permission, path, component, icon, visible, sort_order, keep_alive, menu_type, create_by, create_time, update_by, update_time, del_flag, tenant_id, parent_id) +SELECT + UUID_SHORT(), + '查看饭卡密码', + 'wallet_mchManage_viewCardKey', + NULL, + NULL, + NULL, + '1', + 90, + '0', + '1', + 'admin', + NOW(), + 'admin', + NOW(), + '0', + m.tenant_id, + m.parent_id +FROM sys_menu m +WHERE m.permission = 'wallet_mchManage_view' + AND m.del_flag = '0' + AND NOT EXISTS ( + SELECT 1 FROM sys_menu x + WHERE x.parent_id = m.parent_id + AND x.permission = 'wallet_mchManage_viewCardKey' + AND x.del_flag = '0' + ); + +-- 回滚: +-- DELETE FROM pjcloudx.sys_menu WHERE permission = 'wallet_mchManage_viewCardKey'; diff --git a/db/20261013_add_mch_manage_card_key.sql b/db/20261013_add_mch_manage_card_key.sql new file mode 100644 index 000000000..65dc94bc2 --- /dev/null +++ b/db/20261013_add_mch_manage_card_key.sql @@ -0,0 +1,23 @@ +-- 20261013 商户饭卡扇区密码(可重复执行) +-- +-- 背景:饭卡为 M1 卡,按商户发卡,每个商户可自定义本商户饭卡的扇区密码; +-- 管理后台读卡器工具按所选商户的密码认证后读写扇区。 +-- 方案:mch_manage 新增 card_key 列,6 字节十六进制(12 个字符)。 +-- NOT NULL DEFAULT '424324548800',存量商户自动获得系统默认密码。 +-- 执行:Online DDL,小字段变更,秒级完成。 +-- 回滚:文末 DROP COLUMN。 + +USE pjcloudx_wallet; + +SET @ddl = IF( + (SELECT COUNT(*) FROM information_schema.columns + WHERE table_schema = 'pjcloudx_wallet' AND table_name = 'mch_manage' + AND column_name = 'card_key') = 0, + 'ALTER TABLE mch_manage ADD COLUMN card_key varchar(16) NOT NULL DEFAULT ''424324548800'' COMMENT ''饭卡扇区密码(6字节hex,系统默认424324548800)'' AFTER wallet_alias_list', + 'SELECT ''skip: card_key 已存在'' AS msg'); +PREPARE stmt FROM @ddl; +EXECUTE stmt; +DEALLOCATE PREPARE stmt; + +-- 回滚: +-- ALTER TABLE pjcloudx_wallet.mch_manage DROP COLUMN card_key; diff --git a/db/20261013_add_wallet_active_write_card_perm.sql b/db/20261013_add_wallet_active_write_card_perm.sql new file mode 100644 index 000000000..c00b9fd07 --- /dev/null +++ b/db/20261013_add_wallet_active_write_card_perm.sql @@ -0,0 +1,42 @@ +-- 20261013 开卡列表「写卡」按钮权限(可重复执行) +-- +-- 背景:钱包开卡列表操作列新增「写卡」入口,打开 USB 读卡器弹窗按行商户写卡号, +-- 需要独立按钮权限控制可见性。 +-- 方案:sys_menu 在已存在 wallet_walletActive_edit 按钮的开卡菜单下,追加同级 +-- wallet_walletActive_write_card 按钮。 +-- 执行:对 pjcloudx 库执行;幂等,按 permission 不存在才插入。 +-- 回滚:文末 DELETE。 + +USE pjcloudx; + +INSERT INTO sys_menu (menu_id, name, permission, path, component, icon, visible, sort_order, keep_alive, menu_type, create_by, create_time, update_by, update_time, del_flag, tenant_id, parent_id) +SELECT + UUID_SHORT(), + '写卡', + 'wallet_walletActive_write_card', + NULL, + NULL, + NULL, + '1', + 95, + '0', + '1', + 'admin', + NOW(), + 'admin', + NOW(), + '0', + m.tenant_id, + m.parent_id +FROM sys_menu m +WHERE m.permission = 'wallet_walletActive_edit' + AND m.del_flag = '0' + AND NOT EXISTS ( + SELECT 1 FROM sys_menu x + WHERE x.parent_id = m.parent_id + AND x.permission = 'wallet_walletActive_write_card' + AND x.del_flag = '0' + ); + +-- 回滚: +-- DELETE FROM pjcloudx.sys_menu WHERE permission = 'wallet_walletActive_write_card'; diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java index 56a07c424..e4869b2ec 100644 --- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardOrderDto.java @@ -132,6 +132,12 @@ public class HardOrderDto extends HardOrderEntity implements BaseIsIncludeTreeIn */ private Boolean productDescIsNotEmpty = false; + /** + * 是否只查终端本地订单号重复的订单(消费机/超市机超时重试会造成多单同号); + * 仅食堂、超市设备来源的订单存在终端订单号,其余来源自动排除 + */ + private Boolean duplicateAndroidOrderCode = false; + /** * 权限相关 */ diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderByAndroidCodeVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderByAndroidCodeVo.java new file mode 100644 index 000000000..f2201dbcb --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderByAndroidCodeVo.java @@ -0,0 +1,37 @@ +package com.bsd.pjcloud.hard.api.vo.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 收银终端按本地订单号对账查询到的服务端订单最小状态。 + * + *

专用于终端创建订单请求超时后的结果核对:终端用同一 androidOrderCode 反查, + * 若服务端已存在已支付订单则按已有订单收尾,避免把"响应丢失"误判为"下单失败"后重复提交。

+ */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Schema(description = "终端本地订单号对应的服务端订单状态") +public class HardOrderByAndroidCodeVo { + + /** Hardware 服务业务订单 ID。 */ + @Schema(description = "硬件业务订单ID") + private Long orderId; + + /** 硬件业务订单状态编码(5=已支付)。 */ + @Schema(description = "硬件业务订单状态编码") + private String orderStatus; + + /** 支付方式编码。 */ + @Schema(description = "支付方式编码") + private String payType; + + /** 实付金额,单位为分。 */ + @Schema(description = "实付金额(分)") + private Integer payPrice; +} 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 25f32d130..c9cd15ec9 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 @@ -33,6 +33,7 @@ import com.bsd.pjcloud.hard.api.vo.android.VerificationRecordVo; import com.bsd.pjcloud.hard.api.vo.order.HardCarVo; import com.bsd.pjcloud.hard.api.vo.order.HardAggregateOrderCodePayStatusVo; import com.bsd.pjcloud.hard.api.vo.order.HardAggregateOrderCodePayVo; +import com.bsd.pjcloud.hard.api.vo.order.HardOrderByAndroidCodeVo; import com.bsd.pjcloud.hard.api.vo.order.HardCreateHardQrCodeScanOrderVo; import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo; import com.bsd.pjcloud.hard.service.*; @@ -219,9 +220,18 @@ public class HardApiController { return R.ok(hardwareOrderService.join(id)); } + @GetMapping("/get-order-by-android-code") + @Operation(summary = "终端按本地订单号对账查询服务端订单") + public R getOrderByAndroidCode(@RequestParam String androidOrderCode, @RequestParam Long hardId) { + return R.ok(hardwareOrderService.getOrderByAndroidCode(androidOrderCode, hardId)); + } + @PostMapping("/create-order") @Operation(summary = "创建订单") - @Idempotent(expireTime = 10, info = "正在创建订单中,请勿重复操作") + // 幂等键包含请求体(含终端本地订单号),有效期需覆盖服务端慢处理窗口(曾观测到 15~47 秒) + // 并长于 Service 层 90 秒终端订单锁:delKey 在请求返回后立即删键,TTL 仅作进程崩溃兜底。 + // 处理期间的重复请求直接拦截;返回后的重试由 Service 层按 androidOrderCode 复用已支付订单。 + @Idempotent(expireTime = 120, delKey = true, info = "正在创建订单中,请勿重复操作") public R createOrder(@Valid @RequestBody HardCreateOrderDto dto) { // 处理商品类型 if (ObjectUtil.isNotEmpty(dto.getList())) { diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java index 417621ab7..eb069c084 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java @@ -32,6 +32,7 @@ import com.bsd.pjcloud.hard.api.vo.order.HardCreateHardQrCodeScanOrderVo; import com.bsd.pjcloud.hard.api.vo.order.HardAggregateOrderCodePayStatusVo; import com.bsd.pjcloud.hard.api.vo.order.HardAggregateOrderCodePayVo; import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo; +import com.bsd.pjcloud.hard.api.vo.order.HardOrderByAndroidCodeVo; import com.bsd.pjcloud.hard.api.vo.order.HardOrderDinerCountVo; import com.bsd.pjcloud.hard.api.vo.order.HardPendingReservationCountVo; import com.bsd.pjcloud.hard.api.vo.order.WardMenuMapDayVo; @@ -151,6 +152,18 @@ public interface HardOrderService extends IService { */ Long createOrder(HardCreateOrderDto dto); + /** + * 终端按本地订单号对账查询服务端订单最小状态。 + * + *

创建订单请求超时后,终端用同一 androidOrderCode 反查服务端是否已建单扣款, + * 查询范围限定为当前设备自己的订单。

+ * + * @param androidOrderCode 终端本地订单号 + * @param hardId 发起查询的设备 ID + * @return 服务端订单最小状态;该设备下不存在同号订单时返回 {@code null} + */ + HardOrderByAndroidCodeVo getOrderByAndroidCode(String androidOrderCode, Long hardId); + /** * 创建设备二维码扫一扫订单 */ diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java index 4f053bd82..21ac63ac3 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java @@ -94,6 +94,7 @@ import com.bsd.pjcloud.hard.api.vo.order.HardAggregateOrderCodePayStatusVo; import com.bsd.pjcloud.hard.api.vo.order.HardAggregateOrderCodePayVo; import com.bsd.pjcloud.hard.api.vo.order.HardCreateHardQrCodeScanOrderVo; import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo; +import com.bsd.pjcloud.hard.api.vo.order.HardOrderByAndroidCodeVo; import com.bsd.pjcloud.hard.api.vo.order.HardOrderDinerCountVo; import com.bsd.pjcloud.hard.api.vo.order.WardMenuMapDayVo; import com.bsd.pjcloud.hard.api.vo.order.HardPendingReservationCountVo; @@ -824,6 +825,10 @@ public class HardOrderServiceImpl extends ServiceImpl本接口创建的订单在同一事务内完成扣款并置为已支付,理论上不存在同号未支付残留; + * 仅复用已支付状态,退款后的同号历史订单不复用,由终端重新结算走新单。

+ * + * @param dto 当前终端创建请求 + * @return 同号已支付订单;不存在时返回 {@code null} + */ + private HardOrderEntity getReusablePaidOrder(HardCreateOrderDto dto) { + if (ObjectUtil.isEmpty(dto.getAndroidOrderCode())) { + return null; + } + return getOne(new LambdaQueryWrapper() + .eq(HardOrderEntity::getAndroidOrderCode, dto.getAndroidOrderCode()) + .eq(HardOrderEntity::getHardId, dto.getHardwareId()) + .eq(HardOrderEntity::getMerchantId, dto.getMerchantId()) + .eq(HardOrderEntity::getOrderStatus, HardOrderStatusEnum.PAY.getType()) + .orderByDesc(HardOrderEntity::getId) + .last("limit 1")); + } + + /** + * 校验同号已支付订单与当前请求的业务类型、商品摘要和金额完全一致。 + * + *

金额或内容不一致说明终端在上一笔已扣款后又修改了购物车复用同一本地订单号提交, + * 此时不允许静默复用旧单,也不允许再次建单造成重复收款,必须显式报错让终端重新结算。

+ * + * @param order 已存在的同号已支付订单 + * @param dto 当前终端创建请求 + * @param totalPrice 当前购物车商品总金额,单位为分 + * @param extraPrice 当前额外金额,单位为分 + * @param subject 当前购物车商品摘要 + */ + private void validateReusablePaidOrder(HardOrderEntity order, HardCreateOrderDto dto, int totalPrice, + int extraPrice, String subject) { + if (!Objects.equals(order.getSysType(), dto.getSysType().getType()) + || !Objects.equals(order.getOrderType(), dto.getOrderType().getType()) + || !Objects.equals(order.getPayType(), dto.getOrderPayType().getType()) + || !Objects.equals(order.getTotalPrice(), totalPrice) + || !Objects.equals(order.getExtraPrice(), extraPrice) + || !Objects.equals(order.getProductDesc(), subject)) { + throw new RuntimeException("该终端订单号已存在不同内容的已支付订单,请勿修改购物车后重复提交,请重新结算"); + } + } + + @Override + public HardOrderByAndroidCodeVo getOrderByAndroidCode(String androidOrderCode, Long hardId) { + if (ObjectUtil.isEmpty(androidOrderCode) || ObjectUtil.isEmpty(hardId)) { + return null; + } + // 查询范围限定本设备订单,防止终端枚举他人订单号;逻辑删除由公共拦截过滤。 + HardOrderEntity order = getOne(new LambdaQueryWrapper() + .eq(HardOrderEntity::getAndroidOrderCode, androidOrderCode) + .eq(HardOrderEntity::getHardId, hardId) + .orderByDesc(HardOrderEntity::getId) + .last("limit 1")); + if (ObjectUtil.isEmpty(order)) { + return null; + } + return HardOrderByAndroidCodeVo.builder() + .orderId(order.getId()) + .orderStatus(order.getOrderStatus()) + .payType(order.getPayType()) + .payPrice(order.getPayPrice()) + .build(); + } + /** * 仅为人脸支付订单在主事务提交后登记证据占位,证据库异常不回滚已成功的订单和资金结果。 * diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml index bc9f0bf36..43fb3eade 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/resources/mapper/order/HardOrderMapper.xml @@ -229,6 +229,23 @@ and a.create_time between #{dto.createTimeList[0]} and #{dto.createTimeList[1]} + + + and a.android_order_code is not null + and a.android_order_code in ( + select dup.android_order_code + from hard_order dup + where dup.del_flag = '0' + and dup.android_order_code is not null + + and dup.create_time between #{dto.createTimeList[0]} and #{dto.createTimeList[1]} + + group by dup.android_order_code + having count(*) > 1 + ) + and a.update_time >= #{dto.updateTimeList[0]} and a.update_time < #{dto.updateTimeList[1]} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImplCreateOrderIdempotentTest.java b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImplCreateOrderIdempotentTest.java new file mode 100644 index 000000000..d892c280c --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/test/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImplCreateOrderIdempotentTest.java @@ -0,0 +1,201 @@ +package com.bsd.pjcloud.hard.service.impl.order; + +import com.baomidou.mybatisplus.core.conditions.Wrapper; +import com.bsd.pjcloud.admin.api.dto.UserInfo; +import com.bsd.pjcloud.admin.api.entity.SysUser; +import com.bsd.pjcloud.admin.api.feign.RemoteUserService; +import com.bsd.pjcloud.common.core.exception.ErrorCodes; +import com.bsd.pjcloud.common.core.util.R; +import com.bsd.pjcloud.hard.api.constant.HardTypeEnum; +import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum; +import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum; +import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum; +import com.bsd.pjcloud.hard.api.dto.hard.HardCreateOrderDto; +import com.bsd.pjcloud.hard.api.dto.order.HardCreateCarDto; +import com.bsd.pjcloud.hard.api.entity.order.HardOrderEntity; +import com.bsd.pjcloud.hard.api.vo.manager.HardManagerVo; +import com.bsd.pjcloud.hard.service.HardCarService; +import com.bsd.pjcloud.hard.service.HardManagerService; +import com.bsd.pjcloud.wallet.api.entity.WalletManageEntity; +import com.bsd.pjcloud.wallet.api.feign.RemoteMchRuleJoinService; +import com.bsd.pjcloud.wallet.api.feign.RemoteMchService; +import com.bsd.pjcloud.wallet.api.feign.RemoteWalletService; +import com.bsd.pjcloud.wallet.api.vo.MchManageVo; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Answers.CALLS_REAL_METHODS; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * 终端创建订单幂等复用测试:/hard/hard-api/ad/create-order 在网络抖动下响应超时后, + * 收银端会携带同一 androidOrderCode 重复提交;服务端必须命中同号已支付订单时直接返回 + * 原订单,不再建购物车、不再扣款;内容不一致时显式拒绝;无同号订单时保持原有校验流程。 + *

+ * 钱包余额固定为 0 且订单金额 350 分:若未在余额校验前命中复用返回,会先抛余额不足, + * 借此断言复用返回发生在重复扣款风险点之前。getSelf() 需要 Spring 上下文, + * 本组用例均在到达 getSelf() 调用点前结束。 + */ +class HardOrderServiceImplCreateOrderIdempotentTest { + + /** 复用命中的既有已支付订单 ID。 */ + private static final Long EXISTING_ORDER_ID = 2109484384436817922L; + + /** 终端本地订单号,复现 2026-10-12 消费机超时重试事故中的请求形态。 */ + private static final String ANDROID_ORDER_CODE = "918dd4ffaef8c71a_1791775235020_291198"; + + /** 商户远程服务,提供下单商户校验数据。 */ + private final RemoteMchService remoteMchService = mock(RemoteMchService.class); + + /** 设备管理服务,提供设备部门归属。 */ + private final HardManagerService hardManagerService = mock(HardManagerService.class); + + /** 购物车服务,提供金额与商品摘要计算。 */ + private final HardCarService hardCarService = mock(HardCarService.class); + + /** 用户远程服务,提供下单用户信息。 */ + private final RemoteUserService userService = mock(RemoteUserService.class); + + /** 钱包远程服务,提供钱包查询。 */ + private final RemoteWalletService remoteWalletService = mock(RemoteWalletService.class); + + /** 商户费率规则远程服务,额外餐费规则为空时返回 0。 */ + private final RemoteMchRuleJoinService mchRuleJoinService = mock(RemoteMchRuleJoinService.class); + + /** 被测订单服务,仅执行真实创建方法,同号订单查询按用例打桩。 */ + private final HardOrderServiceImpl service = mock(HardOrderServiceImpl.class, CALLS_REAL_METHODS); + + /** + * 注入创建链路前置依赖并准备公共桩数据。 + */ + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(service, "remoteMchService", remoteMchService); + ReflectionTestUtils.setField(service, "hardManagerService", hardManagerService); + ReflectionTestUtils.setField(service, "hardCarService", hardCarService); + ReflectionTestUtils.setField(service, "userService", userService); + ReflectionTestUtils.setField(service, "remoteWalletService", remoteWalletService); + ReflectionTestUtils.setField(service, "mchRuleJoinService", mchRuleJoinService); + + MchManageVo mch = new MchManageVo(); + mch.setId(2078051759222632449L); + mch.setTypes("1"); + mch.setDeptId(2072660926797369346L); + when(remoteMchService.getById(mch.getId())).thenReturn(R.ok(mch)); + + HardManagerVo hard = new HardManagerVo(); + hard.setDeptId(2072660926797369346L); + when(hardManagerService.getByHardId(any())).thenReturn(hard); + + SysUser sysUser = new SysUser(); + sysUser.setUserId(2087822644200497154L); + sysUser.setDeptId(2087820903597891586L); + UserInfo userInfo = new UserInfo(); + userInfo.setSysUser(sysUser); + when(userService.getInnerByUserId(sysUser.getUserId())).thenReturn(R.ok(userInfo)); + + // 余额固定 0:未在余额校验前复用返回的用例会先抛余额不足 + WalletManageEntity wallet = new WalletManageEntity(); + wallet.setId(2087822783308775426L); + wallet.setUserId(sysUser.getUserId()); + wallet.setBalance(0); + when(remoteWalletService.getWallet(any(), any())).thenReturn(R.ok(wallet)); + + when(mchRuleJoinService.innerMchRoleList(any())).thenReturn(R.ok(List.of())); + when(hardCarService.computerCarTotalPrice(any(), any(), any())).thenReturn(350); + when(hardCarService.computerCarSubject(any(), any(), any())).thenReturn("水果3.5x1;"); + } + + /** + * 验证同号同内容重复提交直接返回既有已支付订单,不再创建购物车和扣款。 + */ + @Test + void duplicateSubmissionReturnsExistingPaidOrderWithoutNewCharge() { + doReturn(paidOrder(350)).when(service).getOne(any(Wrapper.class)); + + Long result = service.createOrder(createOrderDto()); + + assertEquals(EXISTING_ORDER_ID, result); + verify(hardCarService, never()).createCard(any(), any(), any(), any(), any()); + verify(service, never()).fastReduceBalance(any()); + } + + /** + * 验证同号但金额被调整后的重复提交被显式拒绝,避免静默复用错单或再次扣款。 + */ + @Test + void duplicateSubmissionWithAdjustedAmountIsRejected() { + doReturn(paidOrder(500)).when(service).getOne(any(Wrapper.class)); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> service.createOrder(createOrderDto())); + + assertTrue(exception.getMessage().contains("请重新结算")); + verify(hardCarService, never()).createCard(any(), any(), any(), any(), any()); + verify(service, never()).fastReduceBalance(any()); + } + + /** + * 验证无同号订单时保持原有流程:本用例余额 0 小于应付金额,应在复用检查之后 + * 抛出余额不足,证明复用查询未误拦首次下单。 + */ + @Test + void firstSubmissionWithoutExistingOrderFallsThroughToBalanceCheck() { + doReturn(null).when(service).getOne(any(Wrapper.class)); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> service.createOrder(createOrderDto())); + + assertEquals(ErrorCodes.COMMON_WALLET_BALANCE_INSUFFICIENT, exception.getMessage()); + } + + /** + * 构造与请求参数一致的终端创建请求。 + * + * @return 人脸支付线下食堂订单请求 + */ + private HardCreateOrderDto createOrderDto() { + HardCreateOrderDto dto = new HardCreateOrderDto(); + dto.setAndroidOrderCode(ANDROID_ORDER_CODE); + dto.setUserId(2087822644200497154L); + dto.setMerchantId(2078051759222632449L); + dto.setHardwareId(2086644819628052481L); + dto.setSysType(HardTypeEnum.CANTEEN); + dto.setOrderType(HardOrderTypeEnum.BELOW); + dto.setOrderPayType(HardPayTypeEnum.FACE); + dto.setHourType(2); + dto.setList(List.of(new HardCreateCarDto())); + return dto; + } + + /** + * 构造与请求同号、同内容的既有已支付订单。 + * + * @param totalPrice 已支付订单的商品总额,单位为分 + * @return 已支付状态的服务端订单 + */ + private HardOrderEntity paidOrder(int totalPrice) { + return HardOrderEntity.builder() + .id(EXISTING_ORDER_ID) + .androidOrderCode(ANDROID_ORDER_CODE) + .sysType(HardTypeEnum.CANTEEN.getType()) + .orderType(HardOrderTypeEnum.BELOW.getType()) + .payType(HardPayTypeEnum.FACE.getType()) + .orderStatus(HardOrderStatusEnum.PAY.getType()) + .totalPrice(totalPrice) + .extraPrice(0) + .productDesc("水果3.5x1;") + .build(); + } +} diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml index 0486ff90f..c368e0c19 100644 --- a/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml +++ b/pjcloud-upms/pjcloud-upms-biz/src/main/resources/mapper/SysUserMapper.xml @@ -406,7 +406,17 @@ - ORDER BY u.create_time DESC, u.user_id DESC + ORDER BY + + + (u.name = #{query.nameOrPhone}) DESC, + (u.phone = #{query.nameOrPhone}) DESC, + (u.username = #{query.nameOrPhone}) DESC, + (u.name LIKE CONCAT(#{query.nameOrPhone}, '%')) DESC, + + u.create_time DESC, u.user_id DESC diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/mch/MchRuleJoinDto.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/mch/MchRuleJoinDto.java deleted file mode 100644 index f5bcfde8d..000000000 --- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/mch/MchRuleJoinDto.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.bsd.pjcloud.wallet.api.dto.mch; - -import com.bsd.pjcloud.wallet.api.entity.mch.MchRuleJoinDtoEntity; -import lombok.Data; - -@Data -public class MchRuleJoinDto extends MchRuleJoinDtoEntity { - -} diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java index 4e76055e1..fbf118571 100644 --- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java +++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/mch/MchManageEntity.java @@ -197,6 +197,12 @@ public class MchManageEntity extends Model { @TableField(typeHandler = MchManageJacksonTypeHandler.class) private List walletAliasList; + /** + * 饭卡扇区密码 + */ + @Schema(description = "饭卡扇区密码(6字节hex)") + private String cardKey; + /** * 创建人 */ diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/mch/MchManageController.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/mch/MchManageController.java index 943b812c8..68dd9e584 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/mch/MchManageController.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/mch/MchManageController.java @@ -21,6 +21,7 @@ import com.bsd.pjcloud.common.data.datascope.DataScope; import com.bsd.pjcloud.common.log.annotation.SysLog; import com.bsd.pjcloud.common.security.annotation.Inner; import com.bsd.pjcloud.common.security.service.PjcloudUser; +import com.bsd.pjcloud.common.security.component.PermissionService; import com.bsd.pjcloud.common.security.util.SecurityUtils; import com.bsd.pjcloud.wallet.api.dto.MchManageDto; import com.bsd.pjcloud.wallet.api.entity.mch.MchManageEntity; @@ -38,7 +39,9 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; /** * 商户管理 @@ -52,6 +55,11 @@ public class MchManageController { private final MchManageService mchManageService; + /** 饭卡密码查看权限标识 */ + public static final String PERM_VIEW_CARD_KEY = "wallet_mchManage_viewCardKey"; + + private final PermissionService pms; + private final MchDeptViewMapper mchDeptViewMapper; private final RemoteDeptService remoteDeptService; @@ -304,7 +312,49 @@ public class MchManageController { @GetMapping("/{id}") @PreAuthorize("@pms.hasPermission('wallet_mchManage_view')") public R getDetail(@PathVariable("id") Long id) { - return R.ok(mchManageService.join(id)); + MchManageVo vo = mchManageService.join(id); + maskCardKeyIfNeeded(vo); + return R.ok(vo); + } + + /** + * 读卡器工具取商户饭卡密码:完整值仅供本机读卡认证使用; + * 无 wallet_mchManage_viewCardKey 权限时界面只应展示脱敏值。 + */ + @Operation(summary = "查询商户饭卡扇区密码", description = "读卡器工具按商户取饭卡扇区密码(含脱敏值与查看权限标记)") + @GetMapping("/card-key/{id}") + @PreAuthorize("@pms.hasPermission('wallet_mchManage_view')") + public R getCardKey(@PathVariable("id") Long id) { + MchManageEntity mchManage = mchManageService.getById(id); + if (mchManage == null) { + return R.failed("商户不存在"); + } + String cardKey = mchManage.getCardKey(); + Map result = new HashMap<>(4); + result.put("id", id); + result.put("cardKey", cardKey); + result.put("masked", maskCardKey(cardKey)); + result.put("canViewFull", pms.hasPermission(PERM_VIEW_CARD_KEY)); + return R.ok(result); + } + + /** + * 饭卡密码脱敏:前两位 + 8 个星 + 后两位;空值原样返回。 + */ + private String maskCardKey(String cardKey) { + if (ObjectUtil.isEmpty(cardKey) || cardKey.length() < 4) { + return cardKey; + } + return cardKey.substring(0, 2) + "********" + cardKey.substring(cardKey.length() - 2); + } + + /** + * 无饭卡密码查看权限时,将返回给前端的商户数据中的密码脱敏。 + */ + private void maskCardKeyIfNeeded(MchManageEntity entity) { + if (entity != null && !pms.hasPermission(PERM_VIEW_CARD_KEY)) { + entity.setCardKey(maskCardKey(entity.getCardKey())); + } } @Operation(summary = "新增商户管理", description = "新增商户管理") diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/mch/MchRuleJoinController.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/mch/MchRuleJoinController.java index 4a1aba4e4..4d45d040d 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/mch/MchRuleJoinController.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/controller/mch/MchRuleJoinController.java @@ -1,27 +1,18 @@ package com.bsd.pjcloud.wallet.controller.mch; -import cn.hutool.core.util.ArrayUtil; -import cn.hutool.core.collection.CollUtil; -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.toolkit.Wrappers; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum; import com.bsd.pjcloud.common.core.util.R; import com.bsd.pjcloud.common.data.datascope.DataScope; -import com.bsd.pjcloud.common.log.annotation.SysLog; import com.bsd.pjcloud.common.security.annotation.Inner; -import com.bsd.pjcloud.wallet.api.dto.mch.MchRuleJoinDto; import com.bsd.pjcloud.wallet.api.entity.mch.MchRuleJoinDtoEntity; import com.bsd.pjcloud.wallet.api.vo.mch.MchRuleJoinDtoVo; import com.bsd.pjcloud.wallet.service.MchRuleJoinService; -import org.springframework.security.access.prepost.PreAuthorize; -import com.bsd.pjcloud.common.excel.annotation.ResponseExcel; +import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.security.SecurityRequirement; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; import org.springdoc.core.annotations.ParameterObject; import org.springframework.http.HttpHeaders; -import io.swagger.v3.oas.annotations.tags.Tag; -import io.swagger.v3.oas.annotations.Operation; -import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; import java.util.List; @@ -30,7 +21,7 @@ import java.util.List; @RestController @RequiredArgsConstructor @RequestMapping("/mch-rule-join") -@Tag(description = "mch-rule-join", name = "商户规格配置表管理") +@Tag(description = "mch-rule-join", name = "商户钱包规则查询") @SecurityRequirement(name = HttpHeaders.AUTHORIZATION) public class MchRuleJoinController { @@ -39,7 +30,7 @@ public class MchRuleJoinController { /* ************************************* start 远程调用 start ************************************* */ @Inner - @Operation(summary = "获取钱包规则(android离线计算展示使用)", description = "获取钱包规则(android离线计算展示使用)") + @Operation(summary = "获取钱包规则(硬件订单额外金额计算使用)", description = "获取钱包规则(硬件订单额外金额计算使用)") @GetMapping("/inner/list") public R> innerMchRoleList(@RequestParam Long targetMchId) { DataScope dataScope = null; @@ -63,65 +54,4 @@ public class MchRuleJoinController { } /* ************************************* end andorid end ************************************* */ - - @Operation(summary = "分页查询", description = "分页查询") - @GetMapping("/page") - @PreAuthorize("@pms.hasPermission('dzc_walletRuleMchConfig_view')") - public R getWalletRuleMchConfigPage(@ParameterObject Page page, @ParameterObject MchRuleJoinDtoEntity walletRuleMchConfig) { - LambdaQueryWrapper wrapper = Wrappers.lambdaQuery(); - return R.ok(mchRuleJoinService.page(page, wrapper)); - } - - @Operation(summary = "分页复杂查询", description = "分页复杂查询") - @GetMapping("/page-join") - @PreAuthorize("@pms.hasPermission('dzc_walletRuleMchConfig_view')") - public R pageJoin(@ParameterObject Page page, @ParameterObject MchRuleJoinDto dto) { - return R.ok(mchRuleJoinService.pageScopeList(page, dto, DataScope.of())); - } - - @Operation(summary = "通过id查询", description = "通过id查询") - @GetMapping("/{id}") - @PreAuthorize("@pms.hasPermission('dzc_walletRuleMchConfig_view')") - public R getById(@PathVariable("id") Long id) { - return R.ok(mchRuleJoinService.getById(id)); - } - - @Operation(summary = "通过id查询", description = "通过id查询") - @GetMapping("join-detail/{id}") - @PreAuthorize("@pms.hasPermission('dzc_walletRuleMchConfig_view')") - public R joinDetail(@PathVariable("id") Long id) { - return R.ok(mchRuleJoinService.joinDetail(id)); - } - - @Operation(summary = "新增商户规格配置表", description = "新增商户规格配置表") - @SysLog("新增商户规格配置表") - @PostMapping - @PreAuthorize("@pms.hasPermission('dzc_walletRuleMchConfig_add')") - public R save(@RequestBody MchRuleJoinDtoEntity dto) { - return R.ok(mchRuleJoinService.create(dto)); - } - - @Operation(summary = "修改商户规格配置表", description = "修改商户规格配置表") - @SysLog("修改商户规格配置表") - @PutMapping - @PreAuthorize("@pms.hasPermission('dzc_walletRuleMchConfig_edit')") - public R updateById(@RequestBody MchRuleJoinDtoEntity dto) { - return R.ok(mchRuleJoinService.edit(dto)); - } - - @Operation(summary = "通过id删除商户规格配置表", description = "通过id删除商户规格配置表") - @SysLog("通过id删除商户规格配置表") - @DeleteMapping - @PreAuthorize("@pms.hasPermission('dzc_walletRuleMchConfig_del')") - public R removeById(@RequestBody Long[] ids) { - mchRuleJoinService.delByIds(CollUtil.toList(ids)); - return R.ok(); - } - - @ResponseExcel - @GetMapping("/export") - @PreAuthorize("@pms.hasPermission('dzc_walletRuleMchConfig_export')") - public List export(MchRuleJoinDtoEntity walletRuleMchConfig, Long[] ids) { - return mchRuleJoinService.list(Wrappers.lambdaQuery(walletRuleMchConfig).in(ArrayUtil.isNotEmpty(ids), MchRuleJoinDtoEntity::getId, ids)); - } -} \ No newline at end of file +} diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/mch/WalletRuleMchConfigMapper.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/mch/WalletRuleMchConfigMapper.java index b05677408..0068539b5 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/mch/WalletRuleMchConfigMapper.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/mch/WalletRuleMchConfigMapper.java @@ -1,9 +1,7 @@ package com.bsd.pjcloud.wallet.mapper.mch; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.bsd.pjcloud.common.data.datascope.DataScope; import com.bsd.pjcloud.common.data.datascope.PjcloudBaseMapper; -import com.bsd.pjcloud.wallet.api.dto.mch.MchRuleJoinDto; import com.bsd.pjcloud.wallet.api.entity.mch.MchRuleJoinDtoEntity; import com.bsd.pjcloud.wallet.api.vo.mch.MchRuleJoinDtoVo; import org.apache.ibatis.annotations.Mapper; @@ -14,9 +12,5 @@ import java.util.List; @Mapper public interface WalletRuleMchConfigMapper extends PjcloudBaseMapper { - Page pageScopeList(Page page, @Param("dto") MchRuleJoinDto dto, DataScope dataScope); - List scopeList(@Param("dto") MchRuleJoinDtoEntity dto, DataScope dataScope); - - MchRuleJoinDtoVo joinDetail(@Param("id") Long id); -} \ No newline at end of file +} diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/MchRuleJoinService.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/MchRuleJoinService.java index 332c0f5cf..0ef05c8c8 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/MchRuleJoinService.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/MchRuleJoinService.java @@ -1,41 +1,13 @@ package com.bsd.pjcloud.wallet.service; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.service.IService; import com.bsd.pjcloud.common.data.datascope.DataScope; -import com.bsd.pjcloud.wallet.api.dto.mch.MchRuleJoinDto; import com.bsd.pjcloud.wallet.api.entity.mch.MchRuleJoinDtoEntity; import com.bsd.pjcloud.wallet.api.vo.mch.MchRuleJoinDtoVo; -import java.util.ArrayList; import java.util.List; public interface MchRuleJoinService extends IService { - /** - * 权限分页查询 - */ - Page pageScopeList(Page page, MchRuleJoinDto dto, DataScope dataScope); - List scopeList(MchRuleJoinDtoEntity dto, DataScope dataScope); - - /** - * 新增 - */ - Long create(MchRuleJoinDtoEntity dto); - - /** - * 修改 - */ - Long edit(MchRuleJoinDtoEntity dto); - - /** - * 删除 - */ - void delByIds(ArrayList list); - - /** - * 复杂详情 - */ - MchRuleJoinDtoVo joinDetail(Long id); -} \ No newline at end of file +} diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/mch/MchManageServiceImpl.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/mch/MchManageServiceImpl.java index a21962126..1e71e1744 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/mch/MchManageServiceImpl.java +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/mch/MchManageServiceImpl.java @@ -2,6 +2,7 @@ package com.bsd.pjcloud.wallet.service.impl.mch; import cn.hutool.core.codec.Base64; import cn.hutool.core.util.ObjectUtil; +import cn.hutool.core.util.StrUtil; import cn.hutool.extra.spring.SpringUtil; import cn.hutool.json.JSONUtil; import com.alibaba.fastjson.JSON; @@ -81,6 +82,11 @@ import java.util.stream.Stream; @Service public class MchManageServiceImpl extends ServiceImpl implements MchManageService { + /** + * 饭卡扇区密码默认值(6 字节 hex)。仅后端持有,新增商户未填时统一兜底,不下发前端代码。 + */ + public static final String DEFAULT_CARD_KEY = "424324548800"; + private final String TAG = "商户管理"; private final MchPaySettingService paySettingService; @@ -112,6 +118,10 @@ public class MchManageServiceImpl extends ServiceImpl implements MchRuleJoinService { - private final MchManageService mchManageService; - private final WalletRulePriceConfigService rulePriceConfigService; - - @Override - public Long create(MchRuleJoinDtoEntity dto) { - if (existMchRuleJoin(dto)) { - throw new RuntimeException("绑定的规则类型不能重复"); - } - // 查询配置规则 - MchRulePriceConfigEntity rulePriceConfig = rulePriceConfigService.getById(dto.getRulePriceConfigId()); - if (ObjectUtil.notEqual(rulePriceConfig.getRulePriceType(), WalletMchRulePriceTypeEnum.EVERY_LIMIT.getCode())) { - if (areMchIdsDistinct(dto)) { - throw new RuntimeException("源商户与目标商户不能重复"); - } - } - MchManageEntity targetMch = mchManageService.getById(dto.getTargetMchId()); - if (ObjectUtil.isEmpty(targetMch)) { - throw new RuntimeException("目标商户不存在"); - } - dto.setTargetDeptId(targetMch.getDeptId()); - baseMapper.insert(dto); - return dto.getId(); - } - - @Override - public Long edit(MchRuleJoinDtoEntity dto) { - if (existMchRuleJoin(dto)) { - throw new RuntimeException("绑定的规则类型不能重复"); - } - // 查询配置规则 - MchRulePriceConfigEntity rulePriceConfig = rulePriceConfigService.getById(dto.getRulePriceConfigId()); - if (ObjectUtil.notEqual(rulePriceConfig.getRulePriceType(), WalletMchRulePriceTypeEnum.EVERY_LIMIT.getCode())) { - if (areMchIdsDistinct(dto)) { - throw new RuntimeException("源商户与目标商户不能重复"); - } - } - MchManageEntity targetMch = mchManageService.getById(dto.getTargetMchId()); - if (ObjectUtil.isEmpty(targetMch)) { - throw new RuntimeException("目标商户不存在"); - } - dto.setTargetDeptId(targetMch.getDeptId()); - baseMapper.updateById(dto); - return dto.getId(); - } - - @Override - public void delByIds(ArrayList list) { - if (ObjectUtil.isEmpty(list)) return; - baseMapper.deleteByIds(list); - } - - @Override - public MchRuleJoinDtoVo joinDetail(Long id) { - return baseMapper.joinDetail(id); - } - - /** - * 权限分页查询 - */ - @Override - public Page pageScopeList(Page page, MchRuleJoinDto dto, DataScope dataScope) { - return baseMapper.pageScopeList(page, dto, dataScope); - } - @Override public List scopeList(MchRuleJoinDtoEntity dto, DataScope dataScope) { return baseMapper.scopeList(dto, dataScope); } - - /** - * 是否重复 - */ - private boolean existMchRuleJoin(MchRuleJoinDtoEntity dto) { - Long count = baseMapper.selectCount(new LambdaQueryWrapper() - .eq(ObjectUtil.isNotEmpty(dto.getSourceMchId()), MchRuleJoinDtoEntity::getSourceMchId, dto.getSourceMchId()) - .eq(ObjectUtil.isNotEmpty(dto.getRulePriceConfigId()), MchRuleJoinDtoEntity::getRulePriceConfigId, dto.getRulePriceConfigId()) - .ne(ObjectUtil.isNotEmpty(dto.getId()), MchRuleJoinDtoEntity::getId, dto.getId()) - ); - if (ObjectUtil.isNotEmpty(dto.getId())) { - return count >= 1; - } - return count > 0; - } - - /** - * sourceMchId和targetMchId是否重复 - */ - private boolean areMchIdsDistinct(MchRuleJoinDtoEntity dto) { - if (!ObjectUtil.isAllNotEmpty(dto.getSourceMchId(), dto.getTargetMchId())) { - return false; - } - return ObjectUtil.equals(dto.getSourceMchId(), dto.getTargetMchId()); - } -} \ No newline at end of file +} diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/mch/MchManageMapper.xml b/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/mch/MchManageMapper.xml index fd8b66af9..d4d0e1ea8 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/mch/MchManageMapper.xml +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/mch/MchManageMapper.xml @@ -19,6 +19,7 @@ typeHandler="com.bsd.pjcloud.wallet.api.handler.MchManageJacksonTypeHandler"/> + @@ -48,6 +49,7 @@ a.table_reservation_conf, a.top_up_config, a.wallet_alias_list, + a.card_key, a.create_by, a.create_time, a.update_by, diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/mch/MchRuleJoinMapper.xml b/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/mch/MchRuleJoinMapper.xml index c0d5416ac..19296fe56 100644 --- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/mch/MchRuleJoinMapper.xml +++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/resources/mapper/mch/MchRuleJoinMapper.xml @@ -2,20 +2,6 @@ - - - - - - - - - - - - - - select a.id, @@ -65,28 +51,7 @@ order by a.create_time desc , a.id desc - - - - \ No newline at end of file