feat:对接读写卡器
feat:优化用户下拉框搜索 feat:优化下单卡顿问题 feat:优化商户列表样式
This commit is contained in:
1 parent
e407fe3da6
commit
9a6a188074
21 files changed
+569
-259
No files matched your search
@@ -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';
|
||||
@@ -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;
|
||||
@@ -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';
|
||||
+6
@@ -132,6 +132,12 @@ public class HardOrderDto extends HardOrderEntity implements BaseIsIncludeTreeIn
|
||||
*/
|
||||
private Boolean productDescIsNotEmpty = false;
|
||||
|
||||
/**
|
||||
* 是否只查终端本地订单号重复的订单(消费机/超市机超时重试会造成多单同号);
|
||||
* 仅食堂、超市设备来源的订单存在终端订单号,其余来源自动排除
|
||||
*/
|
||||
private Boolean duplicateAndroidOrderCode = false;
|
||||
|
||||
/**
|
||||
* 权限相关
|
||||
*/
|
||||
|
||||
+37
@@ -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;
|
||||
|
||||
/**
|
||||
* 收银终端按本地订单号对账查询到的服务端订单最小状态。
|
||||
*
|
||||
* <p>专用于终端创建订单请求超时后的结果核对:终端用同一 androidOrderCode 反查,
|
||||
* 若服务端已存在已支付订单则按已有订单收尾,避免把"响应丢失"误判为"下单失败"后重复提交。</p>
|
||||
*/
|
||||
@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;
|
||||
}
|
||||
+11
-1
@@ -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<HardOrderByAndroidCodeVo> 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<Long> createOrder(@Valid @RequestBody HardCreateOrderDto dto) {
|
||||
// 处理商品类型
|
||||
if (ObjectUtil.isNotEmpty(dto.getList())) {
|
||||
|
||||
+13
@@ -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<HardOrderEntity> {
|
||||
*/
|
||||
Long createOrder(HardCreateOrderDto dto);
|
||||
|
||||
/**
|
||||
* 终端按本地订单号对账查询服务端订单最小状态。
|
||||
*
|
||||
* <p>创建订单请求超时后,终端用同一 androidOrderCode 反查服务端是否已建单扣款,
|
||||
* 查询范围限定为当前设备自己的订单。</p>
|
||||
*
|
||||
* @param androidOrderCode 终端本地订单号
|
||||
* @param hardId 发起查询的设备 ID
|
||||
* @return 服务端订单最小状态;该设备下不存在同号订单时返回 {@code null}
|
||||
*/
|
||||
HardOrderByAndroidCodeVo getOrderByAndroidCode(String androidOrderCode, Long hardId);
|
||||
|
||||
/**
|
||||
* 创建设备二维码扫一扫订单
|
||||
*/
|
||||
|
||||
+83
@@ -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<HardOrderMapper, HardOrder
|
||||
/* ************************************* start 支付相关 start ************************************* */
|
||||
@Override
|
||||
// @GlobalTransactional
|
||||
// 终端超时重试携带同一 androidOrderCode 再次请求,按本地订单号加锁串行化,
|
||||
// 防止上一次请求仍在处理(曾观测到 15~47 秒)时重试穿透幂等复用再次建单扣款。
|
||||
// expire 覆盖远超正常耗时的抖动窗口,避免锁先于长事务过期。
|
||||
@Lock4j(keys = {"#dto.androidOrderCode"}, expire = 90000)
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Long createOrder(HardCreateOrderDto dto) {
|
||||
MchManageVo mch = validateCreateOrderDto(dto);
|
||||
@@ -887,6 +892,16 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
totalPrice = hardCarService.computerCarTotalPrice(dto.getSysType(), dto.getOrderType(), dto.getList());
|
||||
if (isExtraPrice) extraPrice = computeExtraPrice(totalPrice, mch, sysUser);
|
||||
payPrice = totalPrice + extraPrice;
|
||||
// 幂等复用:终端请求超时后重试会携带同一 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);
|
||||
log.info("|{}|终端订单号重复提交命中已支付订单,直接返回原订单|androidOrderCode:{}|orderId:{}|",
|
||||
TAG, dto.getAndroidOrderCode(), reusablePaidOrder.getId());
|
||||
return reusablePaidOrder.getId();
|
||||
}
|
||||
if (isWalletPay) {
|
||||
// 如果是支付的话,不能小于等于0
|
||||
if (payPrice <= 0) throw new I18nBizException(ErrorCodes.HARD_PAY_AMOUNT_ZERO);
|
||||
@@ -967,6 +982,74 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
|
||||
return orderObj.getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询与当前终端请求同本地订单号、同设备、同商户的已支付订单,供超时重试幂等复用。
|
||||
*
|
||||
* <p>本接口创建的订单在同一事务内完成扣款并置为已支付,理论上不存在同号未支付残留;
|
||||
* 仅复用已支付状态,退款后的同号历史订单不复用,由终端重新结算走新单。</p>
|
||||
*
|
||||
* @param dto 当前终端创建请求
|
||||
* @return 同号已支付订单;不存在时返回 {@code null}
|
||||
*/
|
||||
private HardOrderEntity getReusablePaidOrder(HardCreateOrderDto dto) {
|
||||
if (ObjectUtil.isEmpty(dto.getAndroidOrderCode())) {
|
||||
return null;
|
||||
}
|
||||
return getOne(new LambdaQueryWrapper<HardOrderEntity>()
|
||||
.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"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验同号已支付订单与当前请求的业务类型、商品摘要和金额完全一致。
|
||||
*
|
||||
* <p>金额或内容不一致说明终端在上一笔已扣款后又修改了购物车复用同一本地订单号提交,
|
||||
* 此时不允许静默复用旧单,也不允许再次建单造成重复收款,必须显式报错让终端重新结算。</p>
|
||||
*
|
||||
* @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<HardOrderEntity>()
|
||||
.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();
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅为人脸支付订单在主事务提交后登记证据占位,证据库异常不回滚已成功的订单和资金结果。
|
||||
*
|
||||
|
||||
+17
@@ -229,6 +229,23 @@
|
||||
<if test="dto.createTimeList != null and dto.createTimeList.length == 2">
|
||||
and a.create_time between #{dto.createTimeList[0]} and #{dto.createTimeList[1]}
|
||||
</if>
|
||||
<!-- 消费机订单号重复筛选:仅食堂、超市终端订单存在 android_order_code;同号多单一般为
|
||||
网络抖动超时重试造成的重复建单扣款。子查询与主查询共用创建时间窗口缩小扫描范围,
|
||||
租户条件由公共租户拦截器统一追加到子查询表。 -->
|
||||
<if test="dto.duplicateAndroidOrderCode != null and dto.duplicateAndroidOrderCode == true">
|
||||
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
|
||||
<if test="dto.createTimeList != null and dto.createTimeList.length == 2">
|
||||
and dup.create_time between #{dto.createTimeList[0]} and #{dto.createTimeList[1]}
|
||||
</if>
|
||||
group by dup.android_order_code
|
||||
having count(*) > 1
|
||||
)
|
||||
</if>
|
||||
<if test="dto.updateTimeList != null and dto.updateTimeList.length == 2">
|
||||
and a.update_time >= #{dto.updateTimeList[0]}
|
||||
and a.update_time < #{dto.updateTimeList[1]}
|
||||
|
||||
+201
@@ -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 重复提交;服务端必须命中同号已支付订单时直接返回
|
||||
* 原订单,不再建购物车、不再扣款;内容不一致时显式拒绝;无同号订单时保持原有校验流程。
|
||||
* <p>
|
||||
* 钱包余额固定为 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();
|
||||
}
|
||||
}
|
||||
@@ -406,7 +406,17 @@
|
||||
</if>
|
||||
</where>
|
||||
<if test="_databaseId != 'mssql'">
|
||||
ORDER BY u.create_time DESC, u.user_id DESC
|
||||
ORDER BY
|
||||
<!-- 关键词搜索时精确匹配排最前、姓名前缀匹配次之:下拉场景 total 固定为 0 且不翻页,
|
||||
精确同名的目标用户(如老批次学生)必须保证出现在第一页,否则永远搜不到。
|
||||
排序表达式含绑定参数,分页 COUNT 优化不会移除该 ORDER BY,count 参数个数不受影响。 -->
|
||||
<if test="query.nameOrPhone != null and query.nameOrPhone != ''">
|
||||
(u.name = #{query.nameOrPhone}) DESC,
|
||||
(u.phone = #{query.nameOrPhone}) DESC,
|
||||
(u.username = #{query.nameOrPhone}) DESC,
|
||||
(u.name LIKE CONCAT(#{query.nameOrPhone}, '%')) DESC,
|
||||
</if>
|
||||
u.create_time DESC, u.user_id DESC
|
||||
</if>
|
||||
</select>
|
||||
|
||||
|
||||
-9
@@ -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 {
|
||||
|
||||
}
|
||||
+6
@@ -197,6 +197,12 @@ public class MchManageEntity extends Model<MchManageEntity> {
|
||||
@TableField(typeHandler = MchManageJacksonTypeHandler.class)
|
||||
private List<WalletAlias> walletAliasList;
|
||||
|
||||
/**
|
||||
* 饭卡扇区密码
|
||||
*/
|
||||
@Schema(description = "饭卡扇区密码(6字节hex)")
|
||||
private String cardKey;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
|
||||
+51
-1
@@ -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<String, Object> 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 = "新增商户管理")
|
||||
|
||||
+6
-76
@@ -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<List<MchRuleJoinDtoVo>> 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<MchRuleJoinDtoEntity> 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<MchRuleJoinDtoEntity> export(MchRuleJoinDtoEntity walletRuleMchConfig, Long[] ids) {
|
||||
return mchRuleJoinService.list(Wrappers.lambdaQuery(walletRuleMchConfig).in(ArrayUtil.isNotEmpty(ids), MchRuleJoinDtoEntity::getId, ids));
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-7
@@ -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<MchRuleJoinDtoEntity> {
|
||||
|
||||
Page<MchRuleJoinDtoVo> pageScopeList(Page page, @Param("dto") MchRuleJoinDto dto, DataScope dataScope);
|
||||
|
||||
List<MchRuleJoinDtoVo> scopeList(@Param("dto") MchRuleJoinDtoEntity dto, DataScope dataScope);
|
||||
|
||||
MchRuleJoinDtoVo joinDetail(@Param("id") Long id);
|
||||
}
|
||||
}
|
||||
+1
-29
@@ -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<MchRuleJoinDtoEntity> {
|
||||
|
||||
/**
|
||||
* 权限分页查询
|
||||
*/
|
||||
Page<MchRuleJoinDtoVo> pageScopeList(Page page, MchRuleJoinDto dto, DataScope dataScope);
|
||||
|
||||
List<MchRuleJoinDtoVo> scopeList(MchRuleJoinDtoEntity dto, DataScope dataScope);
|
||||
|
||||
/**
|
||||
* 新增
|
||||
*/
|
||||
Long create(MchRuleJoinDtoEntity dto);
|
||||
|
||||
/**
|
||||
* 修改
|
||||
*/
|
||||
Long edit(MchRuleJoinDtoEntity dto);
|
||||
|
||||
/**
|
||||
* 删除
|
||||
*/
|
||||
void delByIds(ArrayList<Long> list);
|
||||
|
||||
/**
|
||||
* 复杂详情
|
||||
*/
|
||||
MchRuleJoinDtoVo joinDetail(Long id);
|
||||
}
|
||||
}
|
||||
+14
@@ -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<MchManageMapper, MchManageEntity> 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<MchManageMapper, MchManage
|
||||
public Long create(MchManageEntity dto) {
|
||||
normalizeTableReservationConf(dto);
|
||||
normalizeTopUpConfig(dto);
|
||||
// 饭卡密码由后端统一兜底默认值,前端不感知
|
||||
if (StrUtil.isBlank(dto.getCardKey())) {
|
||||
dto.setCardKey(DEFAULT_CARD_KEY);
|
||||
}
|
||||
if (ObjectUtil.isEmpty(dto.getParentId())) dto.setParentId(-1L);
|
||||
if (existName(dto.getName(), dto.getTypes(), null)) {
|
||||
throw new RuntimeException("商户名不能重复");
|
||||
@@ -144,6 +154,10 @@ public class MchManageServiceImpl extends ServiceImpl<MchManageMapper, MchManage
|
||||
public void edit(MchManageEntity dto) {
|
||||
normalizeTableReservationConf(dto);
|
||||
normalizeTopUpConfig(dto);
|
||||
// 饭卡密码:脱敏回显值或空值一律不更新,防止无权限界面把真实密码覆盖掉
|
||||
if (dto.getCardKey() != null && (dto.getCardKey().contains("*") || StrUtil.isBlank(dto.getCardKey()))) {
|
||||
dto.setCardKey(null);
|
||||
}
|
||||
if (existName(dto.getName(), dto.getTypes(), dto.getId())) {
|
||||
throw new RuntimeException("商户名不能重复");
|
||||
}
|
||||
|
||||
+1
-100
@@ -1,26 +1,16 @@
|
||||
package com.bsd.pjcloud.wallet.service.impl.mch;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bsd.pjcloud.common.data.datascope.DataScope;
|
||||
import com.bsd.pjcloud.wallet.api.constant.mch.WalletMchRulePriceTypeEnum;
|
||||
import com.bsd.pjcloud.wallet.api.dto.mch.MchRuleJoinDto;
|
||||
import com.bsd.pjcloud.wallet.api.entity.mch.MchManageEntity;
|
||||
import com.bsd.pjcloud.wallet.api.entity.mch.MchRuleJoinDtoEntity;
|
||||
import com.bsd.pjcloud.wallet.api.entity.mch.MchRulePriceConfigEntity;
|
||||
import com.bsd.pjcloud.wallet.api.vo.mch.MchRuleJoinDtoVo;
|
||||
import com.bsd.pjcloud.wallet.mapper.mch.WalletRuleMchConfigMapper;
|
||||
import com.bsd.pjcloud.wallet.service.MchManageService;
|
||||
import com.bsd.pjcloud.wallet.service.MchRuleJoinService;
|
||||
import com.bsd.pjcloud.wallet.service.WalletRulePriceConfigService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
|
||||
@@ -30,97 +20,8 @@ import java.util.List;
|
||||
@Service
|
||||
public class MchRuleJoinControllerServiceImpl extends ServiceImpl<WalletRuleMchConfigMapper, MchRuleJoinDtoEntity> 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<Long> list) {
|
||||
if (ObjectUtil.isEmpty(list)) return;
|
||||
baseMapper.deleteByIds(list);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MchRuleJoinDtoVo joinDetail(Long id) {
|
||||
return baseMapper.joinDetail(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限分页查询
|
||||
*/
|
||||
@Override
|
||||
public Page<MchRuleJoinDtoVo> pageScopeList(Page page, MchRuleJoinDto dto, DataScope dataScope) {
|
||||
return baseMapper.pageScopeList(page, dto, dataScope);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<MchRuleJoinDtoVo> scopeList(MchRuleJoinDtoEntity dto, DataScope dataScope) {
|
||||
return baseMapper.scopeList(dto, dataScope);
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否重复
|
||||
*/
|
||||
private boolean existMchRuleJoin(MchRuleJoinDtoEntity dto) {
|
||||
Long count = baseMapper.selectCount(new LambdaQueryWrapper<MchRuleJoinDtoEntity>()
|
||||
.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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@
|
||||
typeHandler="com.bsd.pjcloud.wallet.api.handler.MchManageJacksonTypeHandler"/>
|
||||
<result column="wallet_alias_list" property="walletAliasList" javaType="java.util.List"
|
||||
typeHandler="com.bsd.pjcloud.wallet.api.handler.MchManageJacksonTypeHandler"/>
|
||||
<result column="card_key" property="cardKey"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectA">
|
||||
@@ -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,
|
||||
|
||||
@@ -2,20 +2,6 @@
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.bsd.pjcloud.wallet.mapper.mch.WalletRuleMchConfigMapper">
|
||||
<resultMap id="walletRuleMchConfigMap" type="com.bsd.pjcloud.wallet.api.entity.mch.MchRuleJoinDtoEntity">
|
||||
<result property="id" column="id"/>
|
||||
<result property="sourceMchId" column="source_mch_id"/>
|
||||
<result property="targetMchId" column="target_mch_id"/>
|
||||
<result property="targetDeptId" column="target_dept_id"/>
|
||||
<result property="rulePriceConfigId" column="rule_price_config_id"/>
|
||||
<result property="status" column="status"/>
|
||||
<result property="createBy" column="create_by"/>
|
||||
<result property="createTime" column="create_time"/>
|
||||
<result property="updateBy" column="update_by"/>
|
||||
<result property="updateTime" column="update_time"/>
|
||||
<result property="delFlag" column="del_flag"/>
|
||||
<result property="tenantId" column="tenant_id"/>
|
||||
</resultMap>
|
||||
|
||||
<sql id="selectA">
|
||||
select a.id,
|
||||
@@ -65,28 +51,7 @@
|
||||
order by a.create_time desc , a.id desc
|
||||
</sql>
|
||||
|
||||
<select id="pageScopeList" resultType="com.bsd.pjcloud.wallet.api.vo.mch.MchRuleJoinDtoVo">
|
||||
<include refid="pageList" />
|
||||
</select>
|
||||
|
||||
<select id="scopeList" resultType="com.bsd.pjcloud.wallet.api.vo.mch.MchRuleJoinDtoVo">
|
||||
<include refid="pageList"/>
|
||||
</select>
|
||||
|
||||
<select id="joinDetail" resultType="com.bsd.pjcloud.wallet.api.vo.mch.MchRuleJoinDtoVo">
|
||||
<include refid="selectA"/>
|
||||
p.id as rulePirceConfigId,
|
||||
p.rule_price_type as rulePriceType,
|
||||
p.base_price as basePrice,
|
||||
p.multiple as multiple,
|
||||
source.name as sourceMchName,
|
||||
target.name as targetMchName
|
||||
from mch_rule_join a
|
||||
left join mch_rule_price_config p on a.rule_price_config_id = p.id
|
||||
left join mch_manage source on source.id = a.source_mch_id
|
||||
left join mch_manage target on target.id = a.target_mch_id
|
||||
<where>
|
||||
a.id = #{id}
|
||||
</where>
|
||||
</select>
|
||||
</mapper>
|
||||
Reference in new issue
Block a user