From 416bf5d626b0e268f6eae8b5173699278638f2e3 Mon Sep 17 00:00:00 2001
From: dzc <2388224167@qq.com>
Date: Mon, 12 Oct 2026 13:44:22 +0800
Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E9=92=B1=E5=8C=85=E6=B5=81?=
=?UTF-8?q?=E6=B0=B4=E5=A2=9E=E5=8A=A0=E8=AE=BE=E5=A4=87=E7=B1=BB=E5=9E=8B?=
=?UTF-8?q?=20fix=EF=BC=9A=E9=80=80=E6=AC=BEbug?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
...61012_add_wallet_transaction_hard_type.sql | 81 +++
.../api/feign/RemoteHardOrderService.java | 12 +
.../order/HardAfterSaleController.java | 23 +
.../controller/order/HardOrderController.java | 10 +-
.../impl/order/HardAfterSaleServiceImpl.java | 4 +-
.../impl/order/HardOrderServiceImpl.java | 14 +
.../service/impl/xfj/HardXfjServiceImpl.java | 3 +-
.../admin/controller/SysUserController.java | 9 +-
.../pjcloud/admin/service/SysUserService.java | 12 +
.../service/impl/SysUserServiceImpl.java | 18 +
.../main/resources/mapper/SysUserMapper.xml | 4 +
.../api/dto/WalletBalanceTransactionDto.java | 6 +
.../wallet/api/dto/WalletTransactionDto.java | 6 +
.../dto/recharge/WalletRechargeRefundDto.java | 6 +
.../api/entity/WalletTransactionEntity.java | 7 +
.../WalletTransactionExportExcelVo.java | 16 +-
.../vo/transaction/WalletTransactionVo.java | 38 ++
.../mapper/WalletTransactionMapper.java | 2 +
.../impl/WalletTransactionServiceImpl.java | 510 ++++++++++++++++--
.../export/WalletUserExportServiceImpl.java | 2 +
.../recharge/WalletRechargeServiceImpl.java | 26 +-
.../WalletTransactionSysTypeFilterTest.java | 334 ++++++++++++
22 files changed, 1071 insertions(+), 72 deletions(-)
create mode 100644 db/20261012_add_wallet_transaction_hard_type.sql
create mode 100644 pjcloud-wallet/pjcloud-wallet-biz/src/test/java/com/bsd/pjcloud/wallet/service/impl/WalletTransactionSysTypeFilterTest.java
diff --git a/db/20261012_add_wallet_transaction_hard_type.sql b/db/20261012_add_wallet_transaction_hard_type.sql
new file mode 100644
index 000000000..7f0f61a09
--- /dev/null
+++ b/db/20261012_add_wallet_transaction_hard_type.sql
@@ -0,0 +1,81 @@
+-- 钱包流水页设备类型筛选(/wallet/wallet-transaction):wallet_transaction 固化设备类型快照列。
+-- 幂等迁移脚本:可反复执行。列与索引均先查 information_schema,已存在则跳过(执行 SELECT 1 占位);
+-- 回填带 hard_type IS NULL 条件,重跑只处理增量。
+-- 背景:设备类型筛选原走「wallet Feign 中转订单 id 集合」方案,大类型(咖啡机等)订单量大,
+-- 触发上限拦截导致筛选不可用;改为流水表固化 hard_type 快照列(与 hard_order.sys_type 同值域),
+-- 筛选变为普通索引列过滤,新流水由订单支付/退款调用链路实时携带(见 HardOrderServiceImpl 两个
+-- WalletBalanceTransactionDto 构造点 + WalletTransactionServiceImpl.save 的 fillHardTypeFromContext)。
+-- 注意:
+-- 1) biz_type='5' 退款流水 biz_id 多源:订单售后退款主路径=售后单主键(hard_after_sale.id);
+-- 补偿退款=订单号;充值/套餐退款=售后单主键或 refund_ 前缀退款单号(与设备无关,hard_type
+-- 保持为空是正确语义)。因此退款回填分两段(4a 补偿路径按支付流水、4b 售后路径经
+-- hard_after_sale.order_id 关联原订单支付流水),充值售后 order_id 无效天然跳过。
+-- 2) 回填 JOIN 跨 schema(pjcloudx_wallet × pjcloudx_hardware,同实例),需用同实例账号执行。
+-- 3) 严格 SQL 模式下 CAST 非数字 biz_id 会以 1292 报错中断,REGEXP 前置过滤保证只对纯数字主键 CAST。
+
+-- 【可选诊断】确认当前列与索引状态。
+SELECT COLUMN_NAME FROM information_schema.COLUMNS
+WHERE TABLE_SCHEMA = 'pjcloudx_wallet' AND TABLE_NAME = 'wallet_transaction' AND COLUMN_NAME = 'hard_type';
+
+-- 1) 固化列:缺失才创建,与 hard_order.sys_type 同值域(HardTypeEnum 编码,'-1' 为无设备)。
+SET @ddl = (SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `pjcloudx_wallet`.`wallet_transaction` ADD COLUMN `hard_type` varchar(10) DEFAULT NULL COMMENT ''订单设备类型快照编码(HardTypeEnum.type),仅设备订单支付/退款流水有值'' AFTER `biz_id`',
+ 'SELECT 1')
+ FROM information_schema.COLUMNS
+ WHERE TABLE_SCHEMA = 'pjcloudx_wallet' AND TABLE_NAME = 'wallet_transaction' AND COLUMN_NAME = 'hard_type');
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 2) 筛选索引:缺失才创建(租户+删除标记前缀与既有 idx_wallet_tx_* 风格一致)。
+-- 设备类型筛选 = tenant+del_flag+hard_type IN(...),其余条件(用户/时间)回表过滤,命中分区远小于全表。
+SET @ddl = (SELECT IF(COUNT(*) = 0,
+ 'ALTER TABLE `pjcloudx_wallet`.`wallet_transaction` ADD KEY `idx_wallet_tx_tenant_del_hard_type` (`tenant_id`, `del_flag`, `hard_type`)',
+ 'SELECT 1')
+ FROM information_schema.STATISTICS
+ WHERE TABLE_SCHEMA = 'pjcloudx_wallet' AND TABLE_NAME = 'wallet_transaction' AND INDEX_NAME = 'idx_wallet_tx_tenant_del_hard_type');
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 3) 支付流水回填:biz_id 为纯数字订单号才 JOIN 订单表(REGEXP 防严格模式下 CAST 报错,
+-- 也排除个别非订单号形态的杂音)。CAST 后按订单主键点查。
+-- 大事务限制提示:若实例限制单事务行数,可追加 AND w.create_time >= '<分段日期>' 分批执行,脚本幂等可重跑。
+UPDATE pjcloudx_wallet.wallet_transaction w
+JOIN pjcloudx_hardware.hard_order o ON o.id = CAST(w.biz_id AS UNSIGNED)
+SET w.hard_type = o.sys_type
+WHERE w.biz_type = '4' AND w.hard_type IS NULL
+ AND w.biz_id REGEXP '^[0-9]+$';
+
+-- 4a) 退款流水回填·补偿路径:biz_id 与支付流水一致(订单号形态,compensatePayment 补偿退款)。
+UPDATE pjcloudx_wallet.wallet_transaction w
+JOIN pjcloudx_wallet.wallet_transaction pay
+ ON pay.wallet_id = w.wallet_id
+ AND pay.biz_id = w.biz_id
+ AND pay.biz_type = '4'
+ AND pay.del_flag = '0'
+ AND pay.hard_type IS NOT NULL
+SET w.hard_type = pay.hard_type
+WHERE w.biz_type = '5' AND w.hard_type IS NULL AND w.del_flag = '0';
+
+-- 4b) 退款流水回填·售后主路径:订单售后退款 biz_id 存售后单主键(hard_after_sale.id,
+-- 经 WalletRechargeServiceImpl.refund 落库),需经售后单 order_id 关联原订单支付流水;
+-- 充值售后 order_id 无效不命中,hard_type 保持为空是正确语义。
+UPDATE pjcloudx_wallet.wallet_transaction w
+JOIN pjcloudx_hardware.hard_after_sale s ON s.id = CAST(w.biz_id AS UNSIGNED)
+JOIN pjcloudx_wallet.wallet_transaction pay
+ ON pay.wallet_id = w.wallet_id
+ AND pay.biz_id = CAST(s.order_id AS CHAR)
+ AND pay.biz_type = '4'
+ AND pay.del_flag = '0'
+ AND pay.hard_type IS NOT NULL
+SET w.hard_type = pay.hard_type
+WHERE w.biz_type = '5' AND w.hard_type IS NULL AND w.del_flag = '0'
+ AND w.biz_id REGEXP '^[0-9]+$';
+
+-- 【可选诊断】回填结果分布:设备类型 × 收支业务类型行数。
+SELECT w.biz_type, w.hard_type, COUNT(*) AS cnt
+FROM pjcloudx_wallet.wallet_transaction w
+WHERE w.hard_type IS NOT NULL
+GROUP BY w.biz_type, w.hard_type
+ORDER BY w.biz_type, w.hard_type;
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/feign/RemoteHardOrderService.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/feign/RemoteHardOrderService.java
index 73b5e8a34..49311ecef 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/feign/RemoteHardOrderService.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/feign/RemoteHardOrderService.java
@@ -24,6 +24,18 @@ public interface RemoteHardOrderService {
@GetMapping("/hard-order/get-by-ids")
R> getByIds(@RequestParam List ids);
+ /**
+ * 按售后单主键批量查询售后单,供钱包退款流水(biz_id 为售后单主键)装配消费场景:
+ * wallet 侧经售后单 orderId 再反查原订单。
+ *
+ * @param ids 售后单主键集合
+ * @return 售后单集合;不存在的主键不返回
+ */
+ @NoToken
+ @GetMapping("/hard-after-sale/inner/get-by-ids")
+ R> listAfterSalesByIds(
+ @RequestParam List ids);
+
@PostMapping("/hard-order/page")
R>> getPage(@RequestParam Page page, @RequestBody HardOrderPreferencesDto hardOrderPreferencesDto);
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardAfterSaleController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardAfterSaleController.java
index f5ec1f8f5..14e99112d 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardAfterSaleController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardAfterSaleController.java
@@ -29,6 +29,8 @@ import com.bsd.pjcloud.hard.service.HardBatchRefundService;
import com.bsd.pjcloud.wallet.api.dto.MchManageDto;
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
+import com.bsd.pjcloud.hard.api.entity.order.HardAfterSaleEntity;
+import com.bsd.pjcloud.hard.mapper.order.HardAfterSaleMapper;
import com.github.yulichang.query.MPJLambdaQueryWrapper;
import org.springframework.security.access.prepost.PreAuthorize;
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
@@ -55,6 +57,9 @@ public class HardAfterSaleController {
private final HardAfterSaleService hardAfterSaleService;
+ /** 售后单 Mapper:钱包退款流水 biz_id 存售后单主键,供跨服务按主键批量反查售后单。 */
+ private final HardAfterSaleMapper hardAfterSaleMapper;
+
/** 批量退款服务,负责权限预览和最多 1000 笔订单的异步逐笔退款。 */
private final HardBatchRefundService hardBatchRefundService;
@@ -73,6 +78,24 @@ public class HardAfterSaleController {
return R.ok(hardAfterSaleService.refreshOrderRefundState(orderId));
}
+ /**
+ * 钱包退款流水的 biz_id 存售后单主键(订单售后退款经 WalletRechargeServiceImpl.refund 落库),
+ * 消费场景装配按售后单主键批量反查售后单再经 orderId 取原订单;
+ * Inner 调用无用户上下文,按主键批量查询本身就是精确范围,走普通条件查询。
+ *
+ * @param ids 售后单主键集合
+ * @return 售后单集合;不存在的主键不返回
+ */
+ @Inner
+ @GetMapping("/inner/get-by-ids")
+ @Operation(summary = "按主键批量查询售后单", description = "供钱包退款流水(biz_id 为售后单主键)装配消费场景")
+ public R> getByIds(@RequestParam List ids) {
+ if (ObjectUtil.isEmpty(ids)) {
+ return R.ok(List.of());
+ }
+ return R.ok(hardAfterSaleMapper.selectBatchIds(ids));
+ }
+
/* ************************************* start app start ************************************* */
@ApiCryptoAes
@PostMapping(value = "/app/create")
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java
index 24ff7e12c..1f1158c52 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java
@@ -75,6 +75,7 @@ import org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
+import java.util.Objects;
/**
* 订单表
@@ -108,9 +109,12 @@ public class HardOrderController {
@Operation(summary = "查询订单列表", description = "查询订单列表")
@GetMapping("/get-by-ids")
public R> getByIds(@RequestParam List ids) {
- HardOrderDto dto = new HardOrderDto();
- dto.setOrderIds(ids);
- return R.ok(hardOrderMapper.list(dto));
+ // Inner 调用没有后台用户上下文,走 selectListByScope 会被数据权限拦截器按匿名角色解析炸掉;
+ // 按主键集合批量查询本身就是精确范围,直接走普通条件查询(租户与逻辑删除拦截器照常生效)
+ if (ObjectUtil.isEmpty(ids)) {
+ return R.ok(List.of());
+ }
+ return R.ok(hardOrderMapper.selectBatchIds(ids));
}
@Inner
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardAfterSaleServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardAfterSaleServiceImpl.java
index 4e28a0122..970fb7fe2 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardAfterSaleServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardAfterSaleServiceImpl.java
@@ -682,7 +682,9 @@ public class HardAfterSaleServiceImpl extends ServiceImpl walletRefundResult = walletRechargeService.refund(hardRefundDto);
if (walletRefundResult == null || !walletRefundResult.isOk() || ObjectUtil.isEmpty(walletRefundResult.getData())) {
throw new I18nBizException(ErrorCodes.AFTER_SALE_WALLET_REFUND_FAIL);
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 1cec454c3..4f053bd82 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
@@ -48,6 +48,7 @@ import com.bsd.pjcloud.common.core.util.BeanUtils;
import com.bsd.pjcloud.common.core.util.CollectionUtils;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.data.datascope.DataScope;
+import com.bsd.pjcloud.common.idempotent.annotation.Idempotent;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.hard.api.constant.HardProductNatureEnum;
@@ -2750,6 +2751,8 @@ public class HardOrderServiceImpl extends ServiceImpl Math.multiplyExact(car.getProductCount(), factor),
Math::addExact,
LinkedHashMap::new));
+ // 部分退款时后台会把未选中的商品以数量 0 回传,售后快照原样保存;
+ // 远程幂等库存协议拒绝 incrCount=0 的行(报“库存业务参数错误”),发送前统一剔除零增量。
+ stockChanges.values().removeIf(delta -> delta == 0);
+ if (stockChanges.isEmpty()) {
+ // 纯称重等份数为 0 的退款没有份数库存可变更,跳过远程调用,避免被远程参数校验误拒。
+ log.info("|{}|库存变更增量全为0,跳过远程库存调用,orderId={} isReduce={} afterSaleId={}",
+ TAG, order.getId(), isReduce, afterSaleId);
+ return;
+ }
HardParentCarEntity parentCar = hardCarService.getParentCarById(order.getParentCarId());
// 带菜谱标记的购物车(App 预约每日菜谱)整单走分池;设备购物车(createCard 不写 cartScene)
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/xfj/HardXfjServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/xfj/HardXfjServiceImpl.java
index 797c96ab5..b061741fe 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/xfj/HardXfjServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/xfj/HardXfjServiceImpl.java
@@ -94,8 +94,7 @@ public class HardXfjServiceImpl implements HardXfjService {
* 保持未配置商户的原有查询行为。
*/
private static String resolveQueryCardNumber(String cardNo, Long merchantId) {
- if (ObjectUtil.isEmpty(merchantId) || !DECIMAL_UID_MERCHANT_IDS.contains(merchantId)
- || ObjectUtil.isEmpty(cardNo) || !cardNo.matches("\\d+")) {
+ if (ObjectUtil.isEmpty(merchantId) || !DECIMAL_UID_MERCHANT_IDS.contains(merchantId) || ObjectUtil.isEmpty(cardNo) || !cardNo.matches("\\d+")) {
return cardNo;
}
long value = Long.parseLong(cardNo);
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java
index 57b4bb6e0..1fbbb6148 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/controller/SysUserController.java
@@ -482,11 +482,16 @@ public class SysUserController {
return R.ok(userService.getExportUsersPage(page, userDTO, DataScope.of()));
}
+ /**
+ * 分页查询用户下拉选项
+ *
+ * 供管理端、移动端用户选择器远程搜索使用,只返回用户基础字段,
+ * 不回填角色、岗位、身份、人脸和审批信息,也避免下拉场景触发远程流程服务调用。
+ */
@ApiCryptoAes
@PostMapping("/select")
public R userPage(@ParameterObject Page page, @RequestBody UserDTO userDTO) {
- userDTO.setIsFindCheckUser(true);
- return R.ok(userService.getUsersWithRolePage(page, userDTO, null));
+ return R.ok(userService.getSelectUsersPage(page, userDTO));
}
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java
index b828b9b22..4783f1736 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/SysUserService.java
@@ -68,6 +68,18 @@ public interface SysUserService extends IService {
*/
IPage getBasicUsersPage(Page page, UserDTO userDTO, DataScope dataScope);
+ /**
+ * 分页查询用户下拉选项,仅返回用户、账号、手机号等基础字段。
+ *
+ * 下拉组件不展示角色、身份、人脸和审批信息,也不使用分页总条数,
+ * 因此该方法只执行一次索引分页查询,不回填任何扩展资料。
+ *
+ * @param page 分页参数
+ * @param userDTO 姓名、手机号、账号关键词及指定用户筛选条件
+ * @return 仅含基础字段的用户分页结果,total 固定为 0
+ */
+ IPage getSelectUsersPage(Page page, UserDTO userDTO);
+
/**
* 分页查询导出所需的用户主资料、角色岗位和身份资料,不加载人脸及审批流程。
*
diff --git a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
index 35873a601..63df6731f 100644
--- a/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
+++ b/pjcloud-upms/pjcloud-upms-biz/src/main/java/com/bsd/pjcloud/admin/service/impl/SysUserServiceImpl.java
@@ -547,6 +547,24 @@ public class SysUserServiceImpl extends ServiceImpl impl
return list;
}
+ /**
+ * 分页查询用户下拉选项。
+ *
+ * 下拉组件只消费用户 ID、姓名、账号和手机号,因此复用轻量用户分页 SQL,
+ * 不回填角色、岗位、身份、人脸和审批标记;原先的八次串行数据库往返
+ * (含分页 COUNT 全量扫描当前租户用户索引)和一次远程流程服务调用压缩为一次索引分页查询。
+ *
+ * @param page 分页参数
+ * @param userDTO 姓名、手机号、账号关键词及指定用户筛选条件
+ * @return 仅含基础字段的用户分页结果,total 固定为 0
+ */
+ @Override
+ public IPage getSelectUsersPage(Page page, UserDTO userDTO) {
+ // 下拉组件按关键词远程搜索,不展示总条数,关闭 COUNT 查询。
+ page.setSearchCount(false);
+ return baseMapper.getBasicUserPage(page, userDTO, null);
+ }
+
/**
* 分页查询导出所需的完整用户主资料并批量补齐身份资料。
*
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 600e94c95..0486ff90f 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
@@ -357,6 +357,10 @@
LEFT JOIN sys_dept d ON d.dept_id = u.dept_id
u.del_flag = '0'
+
+
+ AND u.user_id = #{query.userId}
+
AND (u.phone LIKE #{basicNameOrPhone}
diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletBalanceTransactionDto.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletBalanceTransactionDto.java
index e4e06d32e..73c23393b 100644
--- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletBalanceTransactionDto.java
+++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletBalanceTransactionDto.java
@@ -51,6 +51,12 @@ public class WalletBalanceTransactionDto {
*/
private String sysRemark;
+ /**
+ * 订单设备类型快照编码(HardTypeEnum.type);订单支付/退款调用传订单 sys_type,
+ * 随流水固化为 hard_type 供设备类型筛选,非订单调用不传。
+ */
+ private String sysType;
+
/** 订单实际所属商户ID;账户级操作允许为空并由钱包服务解析当前食堂。 */
private Long sourceMchId;
diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletTransactionDto.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletTransactionDto.java
index 4131053c9..224c0dbad 100644
--- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletTransactionDto.java
+++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletTransactionDto.java
@@ -43,6 +43,12 @@ public class WalletTransactionDto extends WalletTransactionEntity implements Bas
private List bizTypes;
+ /**
+ * 订单设备类型快照编码多选(HardTypeEnum.type),直接按流水固化列 hard_type 过滤。
+ * 仅支付/退款流水有设备类型快照,其余流水该列为空、天然不参与匹配。
+ */
+ private List sysTypes;
+
/**
* 财务统计标签筛选值;传入多个字典值时匹配包含任一标签的流水。
*/
diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/recharge/WalletRechargeRefundDto.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/recharge/WalletRechargeRefundDto.java
index 2e9477cc6..a29088837 100644
--- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/recharge/WalletRechargeRefundDto.java
+++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/recharge/WalletRechargeRefundDto.java
@@ -78,4 +78,10 @@ public class WalletRechargeRefundDto {
*/
@Length(max = 256, message = "系统备注长度不能超过 256")
private String sysRemark;
+
+ /**
+ * 原订单设备类型快照编码(HardTypeEnum.type);订单售后退款传订单 sys_type,
+ * 随退款流水固化为 hard_type 供设备类型筛选,充值退款不传。
+ */
+ private String sysType;
}
diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/WalletTransactionEntity.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/WalletTransactionEntity.java
index 0a7012f41..1ecc6c9b2 100644
--- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/WalletTransactionEntity.java
+++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/entity/WalletTransactionEntity.java
@@ -62,6 +62,13 @@ public class WalletTransactionEntity extends Model {
@Schema(description = "关联业务编号")
private String bizId;
+ /**
+ * 订单设备类型快照(HardTypeEnum.type);
+ * 仅支付/退款流水(biz_id 存订单主键)由订单调用方携带,其余流水为空。
+ */
+ @Schema(description = "订单设备类型快照编码")
+ private String hardType;
+
/**
* 流水标题
*/
diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/export/WalletTransactionExportExcelVo.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/export/WalletTransactionExportExcelVo.java
index b44bae09e..cac449c0f 100644
--- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/export/WalletTransactionExportExcelVo.java
+++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/export/WalletTransactionExportExcelVo.java
@@ -41,32 +41,36 @@ public class WalletTransactionExportExcelVo extends WalletUserExportExcelVo {
@ExcelProperty(value = {"流水", "标题"}, index = 13)
private String title;
+ /** 订单设备类型中文名称;充值、转账等非订单流水为空。 */
+ @ExcelProperty(value = {"流水", "设备类型"}, index = 14)
+ private String sysTypeName;
+
/** 本次交易金额,单位为元并保留两位小数,支出保留负数。 */
- @ExcelProperty(value = {"流水", "交易金额(元)"}, index = 14)
+ @ExcelProperty(value = {"流水", "交易金额(元)"}, index = 15)
@NumberFormat("#,##0.00")
private BigDecimal price;
/** 交易完成后的钱包余额,单位为元并保留两位小数。 */
- @ExcelProperty(value = {"流水", "交易后余额(元)"}, index = 15)
+ @ExcelProperty(value = {"流水", "交易后余额(元)"}, index = 16)
@NumberFormat("#,##0.00")
private BigDecimal balance;
/** 财务统计标签原始字典值,多个值使用英文逗号分隔。 */
- @ExcelProperty(value = {"流水", "财务标签"}, index = 16)
+ @ExcelProperty(value = {"流水", "财务标签"}, index = 17)
private String tags;
/** 后台补充的流水业务备注;历史记录允许为空。 */
- @ExcelProperty(value = {"流水", "备注"}, index = 17)
+ @ExcelProperty(value = {"流水", "备注"}, index = 18)
@ColumnWidth(24)
private String remark;
/** 服务端按场景模板自动生成的流水简介(如"在食堂线下消费10元");历史未回填记录允许为空。 */
- @ExcelProperty(value = {"流水", "系统备注"}, index = 18)
+ @ExcelProperty(value = {"流水", "系统备注"}, index = 19)
@ColumnWidth(24)
private String sysRemark;
/** 流水创建时间,格式为 yyyy-MM-dd HH:mm:ss。 */
- @ExcelProperty(value = {"流水", "交易时间"}, index = 19)
+ @ExcelProperty(value = {"流水", "交易时间"}, index = 20)
@ColumnWidth(22)
private String createTime;
}
diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/transaction/WalletTransactionVo.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/transaction/WalletTransactionVo.java
index 6f95628fd..20f528bc9 100644
--- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/transaction/WalletTransactionVo.java
+++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/transaction/WalletTransactionVo.java
@@ -10,6 +10,7 @@ import com.bsd.pjcloud.wallet.api.vo.base.BaseWalletInterface;
import com.bsd.pjcloud.wallet.api.vo.base.BaseWalletTypeInterface;
import lombok.Data;
+import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@@ -148,6 +149,43 @@ public class WalletTransactionVo extends WalletTransactionEntity implements Base
private LocalDateTime orderPayTime;
+ /**
+ * 订单设备类型快照编码(HardTypeEnum.type);按 biz_id 反查订单回填,
+ * 充值、转账等非订单流水为空。
+ */
+ private String sysType;
+
+ /** 订单设备类型中文名称;编码不在枚举内时回退展示原编码。 */
+ private String sysTypeName;
+
+ /**
+ * 消费场景摘要(查询时按页批量回填,历史流水全量有效)。
+ * 订单支付/退款按 biz_id 反查订单:商户名、商品摘要(productDesc 原文);
+ * 预约单(hourType 非空)才有预约日期与餐次,非预约单 apptUseDate 为下单当天不可展示。
+ */
+ private String merchantName;
+
+ /** 商品摘要原文("商品名x数量;商品名x数量;");由客户端截断展示、展开区显示完整。 */
+ private String productName;
+
+ /** 预约用餐日期;仅 hourType 非空的预约单有意义。 */
+ private LocalDate apptUseDate;
+
+ /** 预约餐次编码(HardHourTypeEnum:1早餐/2午餐/3晚餐/4夜间餐);非预约单为空。 */
+ private String hourType;
+
+ /** 充值套餐名;仅充值流水(biz_type=3)回填。 */
+ private String rechargePackageName;
+
+ /** 充值实付金额(分);仅充值流水回填。 */
+ private Integer rechargePayPrice;
+
+ /** 充值赠送金额(分);仅充值流水回填。 */
+ private Integer rechargeBonusPrice;
+
+ /** 转账方向描述(如"饭卡 → 消费钱包");仅转账/转账退款流水回填。 */
+ private String transferDesc;
+
/**
* 导出相关
*/
diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/WalletTransactionMapper.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/WalletTransactionMapper.java
index 4011848c9..04c2b4054 100644
--- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/WalletTransactionMapper.java
+++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/mapper/WalletTransactionMapper.java
@@ -45,6 +45,8 @@ public interface WalletTransactionMapper extends PjcloudBaseMapper 0;
if (saved) {
createMessageOutbox(entity);
@@ -206,6 +239,26 @@ public class WalletTransactionServiceImpl extends ServiceImpl订单支付/退款调用在 Controller 层经 {@link WalletMessageContextHolder#supplyWith}
+ * 绑定明细(含 sysType),这里按钱包+业务号+业务类型取回落库;非订单调用
+ * (充值、转账等)没有上下文,hard_type 保持为空。
+ *
+ * @param entity 待持久化钱包流水
+ */
+ private void fillHardTypeFromContext(WalletTransactionEntity entity) {
+ if (ObjectUtil.isNotEmpty(entity.getHardType())) {
+ return;
+ }
+ WalletBalanceTransactionDto context = WalletMessageContextHolder.find(
+ entity.getWalletId(), entity.getBizId(), entity.getBizType());
+ if (context != null) {
+ entity.setHardType(context.getSysType());
+ }
+ }
+
/**
* 根据新钱包流水和当前调用上下文创建不可变消息快照;来源商户无法解析时保留空值,由消息中心仅入本人收件箱。
*
@@ -263,7 +316,7 @@ public class WalletTransactionServiceImpl extends ServiceImpl payments, List rollbackMarkerKeys) {
@@ -629,7 +682,7 @@ public class WalletTransactionServiceImpl extends ServiceImpl locks, List rollbackMarkerKeys) {
@@ -701,7 +754,7 @@ public class WalletTransactionServiceImpl extends ServiceImpl rollbackMarkerKeys) {
@@ -756,9 +809,9 @@ public class WalletTransactionServiceImpl extends ServiceImpl walletManageMapper.updateWhenRecharge(walletId, price);
- case REFUND, TRANSFER_ACCOUNTS_REFUND, RETURN_CANTEEN -> walletManageMapper.updateWhenConsumptionRefund(walletId, price);
+ case REFUND, TRANSFER_ACCOUNTS_REFUND, RETURN_CANTEEN ->
+ walletManageMapper.updateWhenConsumptionRefund(walletId, price);
default -> throw new UnsupportedOperationException("待实现");
}
WalletTransactionEntity transaction = buildWalletTransaction(wallet, walletId, price,
@@ -831,10 +885,10 @@ public class WalletTransactionServiceImpl extends ServiceImpl scopeRechargePersonExportPage(Page page, WalletTransactionDto dto,
- DataScope dataScope, List excludedDeptIds) {
+ DataScope dataScope, List excludedDeptIds) {
Page list = baseMapper.scopeRechargePersonExportPage(page, dto, dataScope,
excludedDeptIds);
disposeExportList(list.getRecords(), dto);
@@ -1127,16 +1181,16 @@ public class WalletTransactionServiceImpl extends ServiceImpl pageSelfOrGuardianWardTransactions(Long loginUserId, Long targetUserId,
- Page page,
- WalletTransactionDto dto) {
+ Page page,
+ WalletTransactionDto dto) {
validateSelfOrGuardianWardUser(loginUserId, targetUserId);
WalletManageEntity wallet = walletManageService.getWallet(targetUserId, WalletTypeEnum.EAT);
if (wallet == null) {
@@ -1154,7 +1208,7 @@ public class WalletTransactionServiceImpl extends ServiceImpl list, WalletTransactionDto dto) {
if (ObjectUtil.isEmpty(list)) {
@@ -1453,7 +1507,7 @@ public class WalletTransactionServiceImpl extends ServiceImpl list, WalletTransactionDto dto) {
if (ObjectUtil.isNotEmpty(list)) {
@@ -1495,13 +1549,17 @@ public class WalletTransactionServiceImpl extends ServiceImpl list) {
+ if (ObjectUtil.isEmpty(list)) {
+ return;
+ }
+ list.stream()
+ .filter(row -> ObjectUtil.isNotEmpty(row.getHardType()))
+ .forEach(row -> {
+ row.setSysType(row.getHardType());
+ row.setSysTypeName(resolveHardSysTypeName(row.getHardType()));
+ });
+ }
+
+ /**
+ * 设备类型编码转中文名称;编码不在枚举内(历史脏数据)时回退展示原编码。
+ */
+ private static String resolveHardSysTypeName(String sysType) {
+ return Arrays.stream(HardTypeEnum.values())
+ .filter(type -> ObjectUtil.equals(type.getType(), sysType))
+ .findFirst()
+ .map(HardTypeEnum::getDescription)
+ .orElse(sysType);
+ }
+
+ /** 消费场景装配的单批远程/数据库查询上限,与系统备注回填的 500 一批口径一致。 */
+ private static final int SCENE_BACKFILL_BATCH_SIZE = 500;
+
+ /**
+ * 按页回填消费场景摘要,让用户明确知道每笔钱的去向。
+ *
+ * 按业务类型分流批量装配:订单支付/退款(hard_type 非空)反查硬件订单取商品摘要、
+ * 预约日期餐次和商户名;充值反查充值记录取套餐与实付/赠送;转账反查转账记录拼
+ * 「源钱包 → 目标钱包」。充值退款(hard_type 为空)沿用系统备注文案不额外装配。
+ * 各查询失败均降级留空,不阻断列表返回。
+ *
+ * @param list 当前分页记录
+ */
+ private void fillSceneSummary(List list) {
+ if (ObjectUtil.isEmpty(list)) {
+ return;
+ }
+ fillOrderSceneSummary(list.stream().filter(this::isOrderSceneRow).toList());
+ fillRechargeSceneSummary(list.stream()
+ .filter(row -> WalletBizEnum.RECHARGE.getCode().equals(row.getBizType())).toList());
+ fillTransferSceneSummary(list.stream()
+ .filter(row -> WalletBizEnum.TRANSFER_ACCOUNTS.getCode().equals(row.getBizType())
+ || WalletBizEnum.TRANSFER_ACCOUNTS_REFUND.getCode().equals(row.getBizType()))
+ .toList());
+ }
+
+ /**
+ * 订单场景行:支付/退款流水且固化过设备类型(区分于充值退款,后者的 biz_id 是售后单号)。
+ */
+ private boolean isOrderSceneRow(WalletTransactionVo row) {
+ return ObjectUtil.isNotEmpty(row.getHardType())
+ && (WalletBizEnum.PAYMENT.getCode().equals(row.getBizType())
+ || WalletBizEnum.REFUND.getCode().equals(row.getBizType()));
+ }
+
+ /**
+ * 订单场景:支付流水 biz_id 即订单主键直接反查;退款流水 biz_id 是售后单主键
+ * (订单售后退款经 WalletRechargeServiceImpl.refund 落库),需先反查售后单再经
+ * orderId 取原订单。查到订单后回填商品摘要、预约信息、商户名与 orderId(供 App 跳转订单详情)。
+ */
+ private void fillOrderSceneSummary(List rows) {
+ if (rows.isEmpty()) {
+ return;
+ }
+ // 支付行:biz_id(订单主键) -> 订单
+ Map paymentOrderMap = fetchOrdersByBizIds(rows.stream()
+ .filter(row -> WalletBizEnum.PAYMENT.getCode().equals(row.getBizType())).toList());
+ // 退款行:biz_id(售后单主键) -> 售后单 orderId -> 订单
+ Map refundOrderMap = fetchOrdersByAfterSaleIds(rows.stream()
+ .filter(row -> WalletBizEnum.REFUND.getCode().equals(row.getBizType())).toList());
+ if (paymentOrderMap.isEmpty() && refundOrderMap.isEmpty()) {
+ return;
+ }
+ Map allOrderMap = new HashMap<>(paymentOrderMap);
+ allOrderMap.putAll(refundOrderMap);
+ List merchantIds = allOrderMap.values().stream()
+ .map(HardOrderEntity::getMerchantId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+ Map merchantNameMap = merchantIds.isEmpty() ? Map.of() : fetchMerchantNames(merchantIds);
+ rows.forEach(row -> {
+ HardOrderEntity order = resolveRowOrder(row, paymentOrderMap, refundOrderMap);
+ if (order == null) {
+ return;
+ }
+ row.setOrderId(order.getId());
+ row.setProductName(order.getProductDesc());
+ row.setApptUseDate(order.getApptUseDate());
+ row.setHourType(order.getHourType());
+ row.setMerchantName(order.getMerchantId() == null ? null : merchantNameMap.get(order.getMerchantId()));
+ });
+ }
+
+ /**
+ * 解析流水行关联的原订单:支付行按 biz_id(订单主键) 命中支付订单集合;
+ * 退款行按 biz_id(售后单主键) 命中经售后反查的订单集合。
+ */
+ private HardOrderEntity resolveRowOrder(WalletTransactionVo row, Map paymentOrderMap,
+ Map refundOrderMap) {
+ Long bizId = parseNumericBizId(row.getBizId());
+ if (bizId == null) {
+ return null;
+ }
+ return WalletBizEnum.PAYMENT.getCode().equals(row.getBizType())
+ ? paymentOrderMap.get(bizId) : refundOrderMap.get(bizId);
+ }
+
+ /**
+ * 支付流水:biz_id 为纯数字订单主键时批量反查订单;杂音业务号不参与。
+ */
+ private Map fetchOrdersByBizIds(List rows) {
+ List orderIds = rows.stream()
+ .map(WalletTransactionVo::getBizId)
+ .map(WalletTransactionServiceImpl::parseNumericBizId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+ if (orderIds.isEmpty()) {
+ return Map.of();
+ }
+ return fetchHardOrdersByIds(orderIds);
+ }
+
+ /**
+ * 退款流水:biz_id 为纯数字售后单主键时先反查售后单(保留 售后主键→订单 的行级映射),
+ * 再按 orderId 批量取原订单;充值售后 orderId 无效查不到订单,天然只命中订单退款。
+ * 返回集合的键为售后单主键(即退款流水 biz_id),供行级直接命中。
+ */
+ private Map fetchOrdersByAfterSaleIds(List rows) {
+ List afterSaleIds = rows.stream()
+ .map(WalletTransactionVo::getBizId)
+ .map(WalletTransactionServiceImpl::parseNumericBizId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+ if (afterSaleIds.isEmpty()) {
+ return Map.of();
+ }
+ Map saleOrderMap = new HashMap<>(afterSaleIds.size());
+ for (List idBatch : ListUtil.partition(afterSaleIds, SCENE_BACKFILL_BATCH_SIZE)) {
+ try {
+ R> result = remoteHardOrderService.listAfterSalesByIds(idBatch);
+ if (result == null || !result.isOk() || ObjectUtil.isEmpty(result.getData())) {
+ continue;
+ }
+ result.getData().stream()
+ .filter(sale -> sale.getId() != null && sale.getOrderId() != null)
+ .forEach(sale -> saleOrderMap.put(sale.getId(), sale.getOrderId()));
+ } catch (Exception exception) {
+ log.warn("|{}|退款消费场景反查售后单失败,批次大小:|{}|", TAG, idBatch.size(), exception);
+ }
+ }
+ if (saleOrderMap.isEmpty()) {
+ return Map.of();
+ }
+ Map orderMap = fetchHardOrdersByIds(List.copyOf(saleOrderMap.values()));
+ // 键换回售后单主键,与退款流水 biz_id 对齐
+ Map saleOrderRef = new HashMap<>(saleOrderMap.size());
+ saleOrderMap.forEach((saleId, orderId) -> {
+ HardOrderEntity order = orderMap.get(orderId);
+ if (order != null) {
+ saleOrderRef.put(saleId, order);
+ }
+ });
+ return saleOrderRef;
+ }
+
+ /**
+ * 批量查询硬件订单;远程失败仅告警并返回已命中部分,不阻断流水列表。
+ */
+ private Map fetchHardOrdersByIds(List orderIds) {
+ Map orderMap = new HashMap<>(orderIds.size());
+ for (List idBatch : ListUtil.partition(orderIds, SCENE_BACKFILL_BATCH_SIZE)) {
+ try {
+ R> result = remoteHardOrderService.getByIds(idBatch);
+ if (result != null && result.isOk() && ObjectUtil.isNotEmpty(result.getData())) {
+ result.getData().forEach(order -> orderMap.put(order.getId(), order));
+ }
+ } catch (Exception exception) {
+ log.warn("|{}|消费场景回填查询订单失败,批次大小:|{}|", TAG, idBatch.size(), exception);
+ }
+ }
+ return orderMap;
+ }
+
+ /**
+ * 批量查询商户名(wallet 本地);失败降级返回空映射。
+ */
+ private Map fetchMerchantNames(List merchantIds) {
+ try {
+ List merchants = mchManageService.list(MchManageDto.builder().ids(merchantIds).build());
+ if (ObjectUtil.isEmpty(merchants)) {
+ return Map.of();
+ }
+ return merchants.stream()
+ .filter(merchant -> merchant.getId() != null)
+ .collect(Collectors.toMap(MchManageVo::getId, MchManageVo::getName, (left, right) -> left));
+ } catch (Exception exception) {
+ log.warn("|{}|消费场景回填查询商户失败,商户数:|{}|", TAG, merchantIds.size(), exception);
+ return Map.of();
+ }
+ }
+
+ /**
+ * 充值场景:biz_id 反查充值记录,回填套餐名与实付/赠送金额(到账金额即流水 price)。
+ */
+ private void fillRechargeSceneSummary(List rows) {
+ if (rows.isEmpty()) {
+ return;
+ }
+ List rechargeIds = rows.stream()
+ .map(WalletTransactionVo::getBizId)
+ .map(WalletTransactionServiceImpl::parseNumericBizId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+ if (rechargeIds.isEmpty()) {
+ return;
+ }
+ Map rechargeMap = walletRechargeMapper.selectBatchIds(rechargeIds).stream()
+ .collect(Collectors.toMap(WalletRechargeEntity::getId, recharge -> recharge, (left, right) -> left));
+ if (rechargeMap.isEmpty()) {
+ return;
+ }
+ List packageIds = rechargeMap.values().stream()
+ .map(WalletRechargeEntity::getPackageId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+ Map packageNameMap = packageIds.isEmpty() ? Map.of()
+ : walletRechargePackageMapper.selectBatchIds(packageIds).stream()
+ .filter(pkg -> ObjectUtil.isNotEmpty(pkg.getName()))
+ .collect(Collectors.toMap(WalletRechargePackageEntity::getId,
+ WalletRechargePackageEntity::getName, (left, right) -> left));
+ rows.forEach(row -> {
+ Long rechargeId = parseNumericBizId(row.getBizId());
+ WalletRechargeEntity recharge = rechargeId == null ? null : rechargeMap.get(rechargeId);
+ if (recharge == null) {
+ return;
+ }
+ row.setRechargePackageName(recharge.getPackageId() == null ? null
+ : packageNameMap.get(recharge.getPackageId()));
+ row.setRechargePayPrice(recharge.getPayPrice());
+ row.setRechargeBonusPrice(recharge.getBonusPrice());
+ });
+ }
+
+ /**
+ * 转账场景:biz_id 反查转账记录,按源/目标钱包的钱包类型拼「饭卡 → 消费钱包」方向描述。
+ */
+ private void fillTransferSceneSummary(List rows) {
+ if (rows.isEmpty()) {
+ return;
+ }
+ List ruleIds = rows.stream()
+ .map(WalletTransactionVo::getBizId)
+ .map(WalletTransactionServiceImpl::parseNumericBizId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+ if (ruleIds.isEmpty()) {
+ return;
+ }
+ Map ruleMap = walletRuleTransactionMapper.selectBatchIds(ruleIds).stream()
+ .collect(Collectors.toMap(WalletRuleTransactionEntity::getId, rule -> rule, (left, right) -> left));
+ if (ruleMap.isEmpty()) {
+ return;
+ }
+ Set walletIds = new HashSet<>();
+ ruleMap.values().forEach(rule -> {
+ if (rule.getSourceWalletId() != null) {
+ walletIds.add(rule.getSourceWalletId());
+ }
+ if (rule.getTargetWalletId() != null) {
+ walletIds.add(rule.getTargetWalletId());
+ }
+ });
+ Map walletTypeNameMap = resolveWalletTypeNames(new ArrayList<>(walletIds));
+ rows.forEach(row -> {
+ Long ruleId = parseNumericBizId(row.getBizId());
+ WalletRuleTransactionEntity rule = ruleId == null ? null : ruleMap.get(ruleId);
+ if (rule == null) {
+ return;
+ }
+ String fromName = walletTypeNameMap.getOrDefault(rule.getSourceWalletId(), "钱包");
+ String toName = walletTypeNameMap.getOrDefault(rule.getTargetWalletId(), "钱包");
+ row.setTransferDesc(String.format("%s → %s", fromName, toName));
+ });
+ }
+
+ /**
+ * 按钱包主键批量解析钱包类型名称;类型缺失时回退「钱包」。
+ */
+ private Map resolveWalletTypeNames(List walletIds) {
+ if (walletIds.isEmpty()) {
+ return Map.of();
+ }
+ List wallets = walletManageMapper.selectBatchIds(walletIds);
+ if (ObjectUtil.isEmpty(wallets)) {
+ return Map.of();
+ }
+ List typeIds = wallets.stream()
+ .map(WalletManageEntity::getWalletTypeId)
+ .filter(Objects::nonNull)
+ .distinct()
+ .toList();
+ Map typeNameMap = typeIds.isEmpty() ? Map.of()
+ : walletTypeMapper.selectBatchIds(typeIds).stream()
+ .filter(type -> ObjectUtil.isNotEmpty(type.getName()))
+ .collect(Collectors.toMap(WalletTypeEntity::getId, WalletTypeEntity::getName,
+ (left, right) -> left));
+ return wallets.stream()
+ .filter(wallet -> wallet.getId() != null)
+ .collect(Collectors.toMap(WalletManageEntity::getId,
+ wallet -> typeNameMap.getOrDefault(wallet.getWalletTypeId(), "钱包"),
+ (left, right) -> left));
+ }
+
+ /**
+ * 解析流水 biz_id 中的数字主键;非数字业务号(退款单号、回滚标记等)返回 null。
+ */
+ private static Long parseNumericBizId(String bizId) {
+ if (ObjectUtil.isEmpty(bizId)) {
+ return null;
+ }
+ try {
+ return Long.valueOf(bizId);
+ } catch (NumberFormatException exception) {
+ return null;
+ }
+ }
+
}
diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/export/WalletUserExportServiceImpl.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/export/WalletUserExportServiceImpl.java
index 0385a0b1d..623192df6 100644
--- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/export/WalletUserExportServiceImpl.java
+++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/export/WalletUserExportServiceImpl.java
@@ -721,6 +721,7 @@ public class WalletUserExportServiceImpl implements WalletUserExportService {
row.setWalletTypeName(source.getWalletTypeName());
row.setCardNumber(source.getWalletCardNumber());
row.setTitle(source.getTitle());
+ row.setSysTypeName(source.getSysTypeName());
row.setPrice(formatPrice(source.getPrice()));
row.setBalance(formatPrice(source.getBalance()));
row.setTags(source.getTags());
@@ -1173,6 +1174,7 @@ public class WalletUserExportServiceImpl implements WalletUserExportService {
BeanUtils.copyProperties(source, target);
target.setUserIds(copyList(source.getUserIds()));
target.setBizTypes(copyList(source.getBizTypes()));
+ target.setSysTypes(copyList(source.getSysTypes()));
target.setTagList(copyList(source.getTagList()));
target.setTotalDpetIds(copyList(source.getTotalDpetIds()));
target.setDeptIds(copyList(source.getDeptIds()));
diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeServiceImpl.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeServiceImpl.java
index 44f484aa9..3287e9798 100644
--- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeServiceImpl.java
+++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/recharge/WalletRechargeServiceImpl.java
@@ -65,6 +65,9 @@ import java.util.Map;
import java.util.Objects;
import java.util.Set;
+import com.bsd.pjcloud.wallet.api.dto.WalletBalanceTransactionDto;
+import com.bsd.pjcloud.wallet.support.WalletMessageContextHolder;
+
/**
* 会员钱包充值
@@ -331,10 +334,28 @@ public class WalletRechargeServiceImpl extends ServiceImpl walletTransactionService.addWalletBalance(walletId, dto.getPrice(),
+ String.valueOf(dto.getAfterSaleId()), WalletBizEnum.REFUND, null, null, dto.getSysRemark()));
+ }
+
/**
* 对已存在退款记录区分“幂等重试”和“人工冲正后的再次同意”。退款状态为空兼容历史成功记录,
* 状态为未售后时只在当前钱包行锁内重新入账一次,并恢复成功状态。
@@ -362,8 +383,7 @@ public class WalletRechargeServiceImpl extends ServiceImpl {
+ invokeFillHardType(entity);
+ return null;
+ });
+
+ assertEquals(HardTypeEnum.COFFEE.getType(), entity.getHardType());
+ }
+
+ /**
+ * 非订单调用(充值、转账等)没有上下文,hard_type 保持为空。
+ */
+ @Test
+ void nonOrderCallShouldLeaveHardTypeEmpty() {
+ WalletTransactionEntity entity = transaction(20L, "201", WalletBizEnum.RECHARGE.getCode());
+
+ invokeFillHardType(entity);
+
+ assertNull(entity.getHardType());
+ }
+
+ /**
+ * 调用方已显式设置 hard_type 时不被上下文覆盖。
+ */
+ @Test
+ void presetHardTypeShouldNotBeOverridden() {
+ WalletBalanceTransactionDto context = new WalletBalanceTransactionDto();
+ context.setWalletId(20L);
+ context.setBizId("101");
+ context.setBizType(WalletBizEnum.PAYMENT);
+ context.setSysType(HardTypeEnum.COFFEE.getType());
+ WalletTransactionEntity entity = transaction(20L, "101", WalletBizEnum.PAYMENT.getCode());
+ entity.setHardType(HardTypeEnum.EY_PRINT.getType());
+
+ WalletMessageContextHolder.supplyWith(List.of(context), () -> {
+ invokeFillHardType(entity);
+ return null;
+ });
+
+ assertEquals(HardTypeEnum.EY_PRINT.getType(), entity.getHardType());
+ }
+
+ /**
+ * 展示列回填:hard_type 非空行做本地名称映射,空行(非订单流水)保持为空。
+ */
+ @Test
+ void fillSysTypeColumnsShouldMapNamesLocally() {
+ WalletTransactionVo payment = vo(WalletBizEnum.PAYMENT.getCode(), HardTypeEnum.COFFEE.getType());
+ WalletTransactionVo recharge = vo(WalletBizEnum.RECHARGE.getCode(), null);
+ WalletTransactionVo legacy = vo(WalletBizEnum.PAYMENT.getCode(), "99");
+
+ ReflectionTestUtils.invokeMethod(service, "fillSysTypeColumns", List.of(payment, recharge, legacy));
+
+ assertEquals(HardTypeEnum.COFFEE.getType(), payment.getSysType());
+ assertEquals("咖啡机", payment.getSysTypeName());
+ assertNull(recharge.getSysTypeName());
+ // 编码不在枚举内的历史脏数据回退展示原编码
+ assertEquals("99", legacy.getSysTypeName());
+ }
+
+ /**
+ * 订单场景摘要:支付行反查订单回填商品/预约/商户;充值退款行(hard_type 空)不触发订单查询。
+ */
+ @Test
+ void orderSceneShouldFillProductApptAndMerchant() {
+ WalletTransactionVo payment = vo(WalletBizEnum.PAYMENT.getCode(), HardTypeEnum.CANTEEN.getType());
+ payment.setBizId("901");
+ WalletTransactionVo rechargeRefund = vo(WalletBizEnum.REFUND.getCode(), null);
+ rechargeRefund.setBizId("refund_1776925896139LIjX0rUNes");
+ HardOrderEntity order = new HardOrderEntity();
+ order.setId(901L);
+ order.setProductDesc("红烧肉套餐x1;米饭x1;");
+ order.setApptUseDate(LocalDate.of(2026, 10, 13));
+ order.setHourType("2");
+ order.setMerchantId(30L);
+ when(remoteHardOrderService.getByIds(anyList())).thenReturn(R.ok(List.of(order)));
+ MchManageVo merchant = new MchManageVo();
+ merchant.setId(30L);
+ merchant.setName("三食堂");
+ when(mchManageService.list(any(MchManageDto.class))).thenReturn(List.of(merchant));
+
+ ReflectionTestUtils.invokeMethod(service, "fillSceneSummary", List.of(payment, rechargeRefund));
+
+ assertEquals("红烧肉套餐x1;米饭x1;", payment.getProductName());
+ assertEquals(LocalDate.of(2026, 10, 13), payment.getApptUseDate());
+ assertEquals("2", payment.getHourType());
+ assertEquals("三食堂", payment.getMerchantName());
+ assertEquals(901L, payment.getOrderId());
+ assertNull(rechargeRefund.getProductName());
+ }
+
+ /**
+ * 订单退款场景:biz_id 是售后单主键,经售后单 orderId 反查原订单回填摘要与 orderId。
+ */
+ @Test
+ void refundSceneShouldResolveOrderThroughAfterSale() {
+ WalletTransactionVo refund = vo(WalletBizEnum.REFUND.getCode(), HardTypeEnum.COFFEE.getType());
+ refund.setBizId("801");
+ HardAfterSaleEntity sale = new HardAfterSaleEntity();
+ sale.setId(801L);
+ sale.setOrderId(902L);
+ when(remoteHardOrderService.listAfterSalesByIds(anyList())).thenReturn(R.ok(List.of(sale)));
+ HardOrderEntity order = new HardOrderEntity();
+ order.setId(902L);
+ order.setProductDesc("美式咖啡x1;");
+ order.setMerchantId(31L);
+ when(remoteHardOrderService.getByIds(anyList())).thenReturn(R.ok(List.of(order)));
+ MchManageVo merchant = new MchManageVo();
+ merchant.setId(31L);
+ merchant.setName("楼下咖啡");
+ when(mchManageService.list(any(MchManageDto.class))).thenReturn(List.of(merchant));
+
+ ReflectionTestUtils.invokeMethod(service, "fillSceneSummary", List.of(refund));
+
+ assertEquals("美式咖啡x1;", refund.getProductName());
+ assertEquals("楼下咖啡", refund.getMerchantName());
+ assertEquals(902L, refund.getOrderId());
+ }
+
+ /**
+ * 充值售后退款:售后单 orderId 关联不到订单支付流水时不回填摘要(hard_type 也保持为空)。
+ */
+ @Test
+ void refundSceneWithoutOrderShouldLeaveSummaryEmpty() {
+ WalletTransactionVo rechargeRefund = vo(WalletBizEnum.REFUND.getCode(), HardTypeEnum.COFFEE.getType());
+ rechargeRefund.setBizId("802");
+ HardAfterSaleEntity sale = new HardAfterSaleEntity();
+ sale.setId(802L);
+ sale.setOrderId(null);
+ when(remoteHardOrderService.listAfterSalesByIds(anyList())).thenReturn(R.ok(List.of(sale)));
+
+ ReflectionTestUtils.invokeMethod(service, "fillSceneSummary", List.of(rechargeRefund));
+
+ assertNull(rechargeRefund.getProductName());
+ assertNull(rechargeRefund.getOrderId());
+ }
+
+ /**
+ * 订单远程查询失败时摘要整体降级留空,不阻断列表。
+ */
+ @Test
+ void orderSceneShouldDegradeOnRemoteFailure() {
+ WalletTransactionVo payment = vo(WalletBizEnum.PAYMENT.getCode(), HardTypeEnum.COFFEE.getType());
+ payment.setBizId("902");
+ when(remoteHardOrderService.getByIds(anyList())).thenThrow(new RuntimeException("feign timeout"));
+
+ ReflectionTestUtils.invokeMethod(service, "fillSceneSummary", List.of(payment));
+
+ assertNull(payment.getProductName());
+ }
+
+ /**
+ * 充值场景摘要:回填套餐名与实付/赠送金额。
+ */
+ @Test
+ void rechargeSceneShouldFillPackageAndPrices() {
+ WalletTransactionVo recharge = vo(WalletBizEnum.RECHARGE.getCode(), null);
+ recharge.setBizId("701");
+ WalletRechargeEntity record = new WalletRechargeEntity();
+ record.setId(701L);
+ record.setPackageId(8L);
+ record.setPayPrice(3000);
+ record.setBonusPrice(300);
+ when(walletRechargeMapper.selectBatchIds(anyList())).thenReturn(List.of(record));
+ WalletRechargePackageEntity pkg = new WalletRechargePackageEntity();
+ pkg.setId(8L);
+ pkg.setName("30元套餐");
+ when(walletRechargePackageMapper.selectBatchIds(anyList())).thenReturn(List.of(pkg));
+
+ ReflectionTestUtils.invokeMethod(service, "fillSceneSummary", List.of(recharge));
+
+ assertEquals("30元套餐", recharge.getRechargePackageName());
+ assertEquals(3000, recharge.getRechargePayPrice());
+ assertEquals(300, recharge.getRechargeBonusPrice());
+ }
+
+ /**
+ * 转账场景摘要:按源/目标钱包类型拼方向描述。
+ */
+ @Test
+ void transferSceneShouldFillDirectionDesc() {
+ WalletTransactionVo transfer = vo(WalletBizEnum.TRANSFER_ACCOUNTS.getCode(), null);
+ transfer.setBizId("601");
+ WalletRuleTransactionEntity rule = new WalletRuleTransactionEntity();
+ rule.setId(601L);
+ rule.setSourceWalletId(10L);
+ rule.setTargetWalletId(20L);
+ when(walletRuleTransactionMapper.selectBatchIds(anyList())).thenReturn(List.of(rule));
+ WalletManageEntity eatWallet = new WalletManageEntity();
+ eatWallet.setId(10L);
+ eatWallet.setWalletTypeId(1L);
+ WalletManageEntity payWallet = new WalletManageEntity();
+ payWallet.setId(20L);
+ payWallet.setWalletTypeId(2L);
+ when(walletManageMapper.selectBatchIds(anyList())).thenReturn(List.of(eatWallet, payWallet));
+ WalletTypeEntity eatType = new WalletTypeEntity();
+ eatType.setId(1L);
+ eatType.setName("饭卡");
+ WalletTypeEntity payType = new WalletTypeEntity();
+ payType.setId(2L);
+ payType.setName("消费钱包");
+ when(walletTypeMapper.selectBatchIds(anyList())).thenReturn(List.of(eatType, payType));
+
+ ReflectionTestUtils.invokeMethod(service, "fillSceneSummary", List.of(transfer));
+
+ assertEquals("饭卡 → 消费钱包", transfer.getTransferDesc());
+ }
+
+ /**
+ * 非消费场景流水(退还食堂等)不触发任何摘要查询。
+ */
+ @Test
+ void unrelatedBizTypeShouldSkipAllSceneQueries() {
+ WalletTransactionVo canteenReturn = vo(WalletBizEnum.RETURN_CANTEEN.getCode(), null);
+ canteenReturn.setBizId("501");
+
+ ReflectionTestUtils.invokeMethod(service, "fillSceneSummary", List.of(canteenReturn));
+
+ verify(remoteHardOrderService, never()).getByIds(anyList());
+ verify(walletRechargeMapper, never()).selectBatchIds(anyList());
+ verify(walletRuleTransactionMapper, never()).selectBatchIds(anyList());
+ }
+
+ private void invokeFillHardType(WalletTransactionEntity entity) {
+ ReflectionTestUtils.invokeMethod(service, "fillHardTypeFromContext", entity);
+ }
+
+ private WalletTransactionEntity transaction(Long walletId, String bizId, String bizType) {
+ WalletTransactionEntity entity = new WalletTransactionEntity();
+ entity.setWalletId(walletId);
+ entity.setBizId(bizId);
+ entity.setBizType(bizType);
+ return entity;
+ }
+
+ private WalletTransactionVo vo(String bizType, String hardType) {
+ WalletTransactionVo vo = new WalletTransactionVo();
+ vo.setBizType(bizType);
+ vo.setHardType(hardType);
+ return vo;
+ }
+}