From 388ef914fcae2fd441fcec0cbe48d491d904a348 Mon Sep 17 00:00:00 2001
From: dzc <2388224167@qq.com>
Date: Tue, 25 Aug 2026 13:07:03 +0800
Subject: [PATCH] 11
---
.../hard/api/dto/spu/HardSpuRepertoryDto.java | 5 +-
.../hard/api/vo/order/HardAfterSaleVo.java | 8 +-
.../hard/api/vo/order/HardOrderVo.java | 3 +
.../hard/api/vo/spu/HardSpuRepertoryVo.java | 22 +-
.../impl/spu/HardSpuRepertoryServiceImpl.java | 21 +-
.../hard/support/HardUserIdentitySupport.java | 255 ++++++++++++
.../ThirdMchFzRelationServiceImplTest.java | 366 ++++++++++++++++++
.../api/vo/base/BaseUserDeptInterface.java | 7 +-
.../vo/base/BaseUserIdentityInterface.java | 19 +-
.../api/vo/base/BaseUserPhoneInterface.java | 24 ++
.../wallet/api/dto/WalletCodeUseDto.java | 19 +-
.../wallet/api/vo/WalletCodeUseVo.java | 38 +-
.../pjcloud/wallet/api/vo/WalletManageVo.java | 3 +-
.../vo/transaction/WalletTransactionVo.java | 3 +-
.../impl/WalletCodeUseServiceImpl.java | 41 +-
.../service/impl/WalletManageServiceImpl.java | 4 +-
16 files changed, 793 insertions(+), 45 deletions(-)
create mode 100644 pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/support/HardUserIdentitySupport.java
create mode 100644 pjcloud-thirdparty/pjcloud-thirdparty-biz/src/test/java/com/bsd/pjcloud/thirdparty/service/impl/pay/ThirdMchFzRelationServiceImplTest.java
create mode 100644 pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserPhoneInterface.java
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/spu/HardSpuRepertoryDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/spu/HardSpuRepertoryDto.java
index 5e62601a9..eebb0bd53 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/spu/HardSpuRepertoryDto.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/spu/HardSpuRepertoryDto.java
@@ -9,14 +9,17 @@ import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
-@Data
/** 硬件商品库存更新记录查询条件,支持按上架用户身份快速筛选。 */
+@Data
public class HardSpuRepertoryDto extends HardSpuRepertoryEntity implements BaseUserIdentityQueryInterface {
+ /** 库存更新记录创建时间闭区间;数组长度为 2 时生效。 */
private LocalDateTime[] createTimeList;
+ /** 身份预筛选后用于库存记录查询的上架用户主键集合。 */
private List userIds;
+ /** 目标柜门主键;为空时不限制柜门。 */
private Long cabinetId;
/** UPMS 用户身份协议编码集合;为空表示不限制身份。 */
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardAfterSaleVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardAfterSaleVo.java
index e265efba8..c4a1faac8 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardAfterSaleVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardAfterSaleVo.java
@@ -5,6 +5,7 @@ import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
import com.bsd.pjcloud.admin.api.vo.base.BaseDeptInterface;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserInterface;
import com.bsd.pjcloud.hard.api.entity.order.HardAfterSaleEntity;
import com.bsd.pjcloud.hard.api.vo.base.BaseOrderInterface;
import lombok.Data;
@@ -12,8 +13,13 @@ import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
+/**
+ * 硬件订单售后详情。
+ *
+ * 同时保留历史用户、用户部门和用户身份协议,兼容售后详情的旧用户装配逻辑及管理端身份展示。
+ */
@Data
-public class HardAfterSaleVo extends HardAfterSaleEntity implements BaseUserDeptInterface, BaseUserIdentityInterface, BaseDeptInterface, BaseOrderInterface {
+public class HardAfterSaleVo extends HardAfterSaleEntity implements BaseUserInterface, BaseUserDeptInterface, BaseUserIdentityInterface, BaseDeptInterface, BaseOrderInterface {
/**
* 自动退款任务状态;为空表示当前售后仍走人工审核,非空时前端不得展示审核或取消操作。
*/
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java
index afa0bf539..1fe36132b 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/order/HardOrderVo.java
@@ -30,6 +30,9 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
private String userMobile;
+ /** 用户标准手机号字段,供统一身份组件展示;与历史 userMobile 字段并存。 */
+ private String phone;
+
private String userAvatar;
/** 用户身份协议编码集合;没有身份时为空集合或空值。 */
diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/spu/HardSpuRepertoryVo.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/spu/HardSpuRepertoryVo.java
index d9157f4b1..58a369c31 100644
--- a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/spu/HardSpuRepertoryVo.java
+++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/vo/spu/HardSpuRepertoryVo.java
@@ -10,44 +10,58 @@ import com.bsd.pjcloud.hard.api.vo.base.BaseHardInterface;
import com.bsd.pjcloud.wallet.api.vo.base.BaseMchInterface;
import lombok.Data;
-@Data
+import java.util.List;
+
/** 硬件商品库存更新记录展示对象,包含上架用户主资料、部门和身份信息。 */
+@Data
public class HardSpuRepertoryVo extends HardSpuRepertoryEntity implements BaseMchInterface, BaseUserInterface, BaseUserDeptInterface, BaseUserIdentityInterface, BaseHardInterface {
/**
* 商品相关
*/
+ /** 本次更新对应的商品名称。 */
private String spuName;
+ /** 商品分类主键。 */
private Long spuCategoryId;
+ /** 商品所属商户主键。 */
private Long spuMchId;
+ /** 商品所在硬件柜门数量。 */
private Long hardDoorNumber;
/**
* 分类相关
*/
+ /** 商品分类名称。 */
private String spuCategoryName;
/**
* 商户相关
*/
+ /** 商品所属商户主键,供公共商户展示契约读取。 */
private Long merchantId;
+ /** 商品所属商户名称。 */
private String merchantName;
+ /** 商品所属商户 Logo 地址。 */
private String logoPic;
+ /** 商品所属商户类型协议编码。 */
private String merchantType;
/**
* 用户相关
*/
+ /** 上架用户姓名,保留历史接口字段名。 */
private String username;
+ /** 上架用户手机号,保留历史接口字段名。 */
private String userMobile;
+ /** 上架用户头像地址,保留历史接口字段名。 */
private String userAvatar;
/** 用户标准手机号字段,供统一身份组件展示;与历史 userMobile 字段并存。 */
@@ -63,7 +77,7 @@ public class HardSpuRepertoryVo extends HardSpuRepertoryEntity implements BaseMc
private String deptName;
/** 用户拥有的 UPMS 身份协议编码集合。 */
- private java.util.List identityTypes;
+ private List identityTypes;
/** 用户的学生身份扩展资料;无学生身份时为空。 */
private SysUserStudentDto studentInfo;
@@ -99,12 +113,16 @@ public class HardSpuRepertoryVo extends HardSpuRepertoryEntity implements BaseMc
/**
* 设备相关
*/
+ /** 执行库存更新的硬件主键。 */
private Long hardId;
+ /** 执行库存更新的硬件名称。 */
private String hardName;
+ /** 执行库存更新的硬件类型协议编码。 */
private String hardType;
+ /** 执行库存更新的硬件设备编号。 */
private String deviceCode;
}
diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/spu/HardSpuRepertoryServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/spu/HardSpuRepertoryServiceImpl.java
index 500e9b8e0..2360c6a62 100644
--- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/spu/HardSpuRepertoryServiceImpl.java
+++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/spu/HardSpuRepertoryServiceImpl.java
@@ -29,6 +29,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
+import java.util.ArrayList;
import java.util.List;
/**
@@ -49,6 +50,14 @@ public class HardSpuRepertoryServiceImpl extends ServiceImpl scopePageJoin(Page page, HardSpuRepertoryDto dto, DataScope dataScope) {
prepareIdentityQuery(dto);
@@ -233,6 +242,12 @@ public class HardSpuRepertoryServiceImpl extends ServiceImpl list, HardSpuRepertoryDto dto) {
if (ObjectUtil.isEmpty(list)) return;
// 商户
@@ -248,7 +263,11 @@ public class HardSpuRepertoryServiceImpl extends ServiceImpl订单等业务表只保存用户主键和必要快照,身份筛选统一交给 UPMS 数据权限用户接口处理,
+ * Hardware 仅将命中的用户主键并入原有查询,并为当前页批量回填身份展示资料。
+ */
+@Component
+@RequiredArgsConstructor
+public class HardUserIdentitySupport {
+
+ /** 单次调用 UPMS 查询的最大用户数量,控制跨服务响应体和内存占用。 */
+ private static final long USER_BATCH_SIZE = 500L;
+
+ /** 身份条件无匹配用户时使用的无效用户主键,确保下游忽略空集合的 SQL 不会放宽查询。 */
+ private static final Long NO_MATCH_USER_ID = -1L;
+
+ /** UPMS 用户远程服务,负责数据权限、身份筛选及身份资料返回。 */
+ private final RemoteUserService remoteUserService;
+
+ /**
+ * 判断查询对象是否携带实际生效的身份或人脸条件。
+ *
+ * @param query Hardware 业务查询对象;为空表示没有身份条件
+ * @return 至少存在一个有效身份、人脸或身份扩展条件时返回 {@code true}
+ */
+ public boolean hasIdentityQuery(BaseUserIdentityQueryInterface query) {
+ if (query == null) {
+ return false;
+ }
+ return query.getIdentityTypes() != null && !query.getIdentityTypes().isEmpty()
+ || hasStudentQuery(query.getStudentInfo())
+ || hasTeacherQuery(query.getTeacherInfo())
+ || StrUtil.isNotBlank(query.getHasFace());
+ }
+
+ /**
+ * 在现有用户条件范围内查询符合身份条件的用户主键。
+ *
+ * @param query 身份及人脸查询条件
+ * @param userId 页面已有的单用户筛选条件
+ * @param userIds 页面已有的多用户筛选条件
+ * @return 同时符合已有用户范围、当前操作者数据权限和身份条件的用户主键;无匹配时返回无效主键
+ */
+ public List findIdentityUserIds(BaseUserIdentityQueryInterface query, Long userId, List userIds) {
+ List constrainedUserIds = mergeUserConstraints(userId, userIds);
+ if (constrainedUserIds != null && constrainedUserIds.isEmpty()) {
+ return List.of(NO_MATCH_USER_ID);
+ }
+ UserDTO userQuery = buildUserQuery(query, constrainedUserIds);
+ List users = fetchAllUsers(userQuery);
+ if (users.isEmpty()) {
+ return List.of(NO_MATCH_USER_ID);
+ }
+ return users.stream().map(UserVO::getUserId).filter(Objects::nonNull).distinct().toList();
+ }
+
+ /**
+ * 批量补齐 Hardware 列表的用户主资料和身份资料,并按需更新当前用户部门。
+ *
+ * @param rows 当前页 Hardware 业务记录
+ * @param updateDepartment 是否使用 UPMS 当前部门覆盖业务记录中的部门快照
+ * @param 同时实现用户部门和用户身份展示契约的业务 VO 类型
+ */
+ public void fillUserInfo(List rows,
+ boolean updateDepartment) {
+ if (rows == null || rows.isEmpty()) {
+ return;
+ }
+ List userIds = rows.stream().map(BaseUserDeptInterface::getUserId).filter(Objects::nonNull).distinct().toList();
+ if (userIds.isEmpty()) {
+ return;
+ }
+ UserDTO userQuery = new UserDTO();
+ userQuery.setUserIds(userIds);
+ Map userMap = CollectionUtils.convertMap(fetchAllUsers(userQuery), UserVO::getUserId);
+ for (T row : rows) {
+ UserVO user = userMap.get(row.getUserId());
+ if (user == null) {
+ continue;
+ }
+ // 部分历史 VO 显式声明了标准字段,需同时回填旧字段以兼容既有接口与导出。
+ row.setUsername(user.getName());
+ row.setUserMobile(user.getPhone());
+ row.setUserAvatar(user.getAvatar());
+ row.setName(user.getName());
+ row.setPhone(user.getPhone());
+ row.setAvatar(user.getAvatar());
+ if (updateDepartment) {
+ row.setUserDeptId(user.getDeptId());
+ row.setUserDeptName(user.getDeptName());
+ row.setDeptName(user.getDeptName());
+ }
+ row.setIdentityTypes(copyList(user.getIdentityTypes()));
+ row.setStudentInfo(copyStudentInfo(user.getStudentInfo()));
+ row.setTeacherInfo(copyTeacherInfo(user.getTeacherInfo()));
+ }
+ }
+
+ /**
+ * 合并单用户和多用户条件;两者同时存在时取交集,保持原查询的“同时满足”语义。
+ *
+ * @param userId 单用户主键
+ * @param userIds 多用户主键集合
+ * @return 去重后的用户限制;返回 {@code null} 表示未限制用户,空集合表示条件互斥
+ */
+ private List mergeUserConstraints(Long userId, List userIds) {
+ LinkedHashSet constraints = new LinkedHashSet<>();
+ if (userIds != null && !userIds.isEmpty()) {
+ userIds.stream().filter(Objects::nonNull).forEach(constraints::add);
+ }
+ if (userId == null) {
+ return constraints.isEmpty() ? null : new ArrayList<>(constraints);
+ }
+ if (constraints.isEmpty()) {
+ return List.of(userId);
+ }
+ return constraints.contains(userId) ? List.of(userId) : List.of();
+ }
+
+ /**
+ * 把 Hardware 查询契约转换为 UPMS 用户查询对象,并复制可变身份条件。
+ *
+ * @param query Hardware 侧身份查询条件
+ * @param constrainedUserIds 已有用户筛选范围
+ * @return 可独立交给 UPMS 使用的用户查询对象
+ */
+ private UserDTO buildUserQuery(BaseUserIdentityQueryInterface query, List constrainedUserIds) {
+ UserDTO userQuery = new UserDTO();
+ userQuery.setIdentityTypes(copyList(query.getIdentityTypes()));
+ userQuery.setStudentInfo(copyStudentInfo(query.getStudentInfo()));
+ userQuery.setTeacherInfo(copyTeacherInfo(query.getTeacherInfo()));
+ userQuery.setHasFace(query.getHasFace());
+ userQuery.setUserIds(copyList(constrainedUserIds));
+ return userQuery;
+ }
+
+ /**
+ * 分页读取当前操作者权限范围内的全部匹配用户。
+ *
+ * @param query UPMS 用户查询条件
+ * @return 按 UPMS 分页顺序合并的用户列表
+ */
+ private List fetchAllUsers(UserDTO query) {
+ List users = new ArrayList<>();
+ long current = 1L;
+ long total;
+ do {
+ R> response = remoteUserService.getScopeBasicUserPage(current, USER_BATCH_SIZE, query);
+ if (response == null || !response.isOk() || response.getData() == null) {
+ String message = response == null ? null : response.getMsg();
+ throw new IllegalStateException(StrUtil.blankToDefault(message, "查询权限用户及身份信息失败"));
+ }
+ Page page = response.getData();
+ List records = page.getRecords() == null ? List.of() : page.getRecords();
+ users.addAll(records);
+ total = page.getTotal();
+ if (records.isEmpty()) {
+ break;
+ }
+ current++;
+ } while (users.size() < total);
+ return users;
+ }
+
+ /**
+ * 判断学生扩展对象是否包含有效筛选字段。
+ *
+ * @param studentInfo 学生身份扩展条件
+ * @return 学号、学校、班级或监护人手机号任一非空时返回 {@code true}
+ */
+ private boolean hasStudentQuery(SysUserStudentDto studentInfo) {
+ return studentInfo != null && (StrUtil.isNotBlank(studentInfo.getStudentNo())
+ || StrUtil.isNotBlank(studentInfo.getSchoolName())
+ || StrUtil.isNotBlank(studentInfo.getClassName())
+ || StrUtil.isNotBlank(studentInfo.getGuardianPhones()));
+ }
+
+ /**
+ * 判断老师扩展对象是否包含有效筛选字段。
+ *
+ * @param teacherInfo 老师身份扩展条件
+ * @return 老师学号非空时返回 {@code true}
+ */
+ private boolean hasTeacherQuery(SysUserTeacherDto teacherInfo) {
+ return teacherInfo != null && StrUtil.isNotBlank(teacherInfo.getTeacherNo());
+ }
+
+ /**
+ * 复制学生身份资料,避免跨服务响应对象被业务列表意外修改。
+ *
+ * @param source 原学生身份资料
+ * @return 独立副本;源对象为空时返回 {@code null}
+ */
+ private SysUserStudentDto copyStudentInfo(SysUserStudentDto source) {
+ if (source == null) {
+ return null;
+ }
+ SysUserStudentDto target = new SysUserStudentDto();
+ target.setStudentNo(source.getStudentNo());
+ target.setSchoolName(source.getSchoolName());
+ target.setClassName(source.getClassName());
+ target.setGuardianPhones(source.getGuardianPhones());
+ target.setExtJson(source.getExtJson());
+ return target;
+ }
+
+ /**
+ * 复制老师身份资料,保持查询与展示对象相互独立。
+ *
+ * @param source 原老师身份资料
+ * @return 独立副本;源对象为空时返回 {@code null}
+ */
+ private SysUserTeacherDto copyTeacherInfo(SysUserTeacherDto source) {
+ if (source == null) {
+ return null;
+ }
+ SysUserTeacherDto target = new SysUserTeacherDto();
+ target.setTeacherNo(source.getTeacherNo());
+ return target;
+ }
+
+ /**
+ * 复制可变列表并保持空值语义。
+ *
+ * @param source 原列表
+ * @param 列表元素类型
+ * @return 独立列表;源列表为空值时返回 {@code null}
+ */
+ private List copyList(List source) {
+ return source == null ? null : new ArrayList<>(source);
+ }
+}
diff --git a/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/test/java/com/bsd/pjcloud/thirdparty/service/impl/pay/ThirdMchFzRelationServiceImplTest.java b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/test/java/com/bsd/pjcloud/thirdparty/service/impl/pay/ThirdMchFzRelationServiceImplTest.java
new file mode 100644
index 000000000..6a02dc66c
--- /dev/null
+++ b/pjcloud-thirdparty/pjcloud-thirdparty-biz/src/test/java/com/bsd/pjcloud/thirdparty/service/impl/pay/ThirdMchFzRelationServiceImplTest.java
@@ -0,0 +1,366 @@
+package com.bsd.pjcloud.thirdparty.service.impl.pay;
+
+import cn.hutool.json.JSONUtil;
+import com.alipay.api.AlipayClient;
+import com.alipay.api.request.AlipayTradeRoyaltyRelationBatchqueryRequest;
+import com.alipay.api.request.AlipayTradeRoyaltyRelationBindRequest;
+import com.alipay.api.response.AlipayTradeRoyaltyRelationBatchqueryResponse;
+import com.alipay.api.response.AlipayTradeRoyaltyRelationBindResponse;
+import com.baomidou.mybatisplus.core.conditions.Wrapper;
+import com.baomidou.mybatisplus.core.MybatisConfiguration;
+import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
+import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
+import com.bsd.pjcloud.thirdparty.config.AppPlusPayConfig;
+import com.bsd.pjcloud.thirdparty.constant.fz.ThirdMchFzRelationStatusEnum;
+import com.bsd.pjcloud.thirdparty.constant.miniplatform.AppPlusPlatformPayType;
+import com.bsd.pjcloud.thirdparty.entity.pay.ThirdMchFzRelationEntity;
+import com.bsd.pjcloud.thirdparty.mapper.pay.ThirdMchFzRelationMapper;
+import com.bsd.pjcloud.thirdparty.service.ThirdPaymentTaskEnqueuer;
+import com.bsd.pjcloud.wallet.api.entity.mch.MchPaySettingEntity;
+import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
+import com.bsd.pjcloud.wallet.api.vo.MchManageVo;
+import com.bsd.pjcloud.wallet.api.vo.mch.MchPaySettingVo;
+import com.wechat.pay.java.service.profitsharing.ProfitsharingService;
+import com.wechat.pay.java.service.profitsharing.model.AddReceiverResponse;
+import com.wechat.pay.java.service.profitsharing.model.ReceiverRelationType;
+import com.wechat.pay.java.service.profitsharing.model.ReceiverType;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.apache.ibatis.builder.MapperBuilderAssistant;
+import org.springframework.dao.DuplicateKeyException;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+/**
+ * 商户微信、支付宝共用默认平台分佣关系测试。
+ */
+@ExtendWith(MockitoExtension.class)
+class ThirdMchFzRelationServiceImplTest {
+
+ /** 关系事实 Mapper Mock,用于捕获关系快照并模拟唯一键竞争。 */
+ @Mock
+ private ThirdMchFzRelationMapper relationMapper;
+
+ /** 关系任务写入器 Mock,用于区分配置唤醒与支付流量确保任务。 */
+ @Mock
+ private ThirdPaymentTaskEnqueuer taskEnqueuer;
+
+ /** 微信分账 SDK Mock,不发起真实渠道请求。 */
+ @Mock
+ private ProfitsharingService profitsharingService;
+
+ /** 商户内部接口 Mock,用于支付宝 Worker 读取当前授权配置。 */
+ @Mock
+ private RemoteMchService remoteMchService;
+
+ /** 支付宝 SDK 客户端 Mock,用于验证网关成功与业务结果码的组合语义。 */
+ @Mock
+ private AlipayClient alipayClient;
+
+ /** 被测统一分账关系服务。 */
+ private ThirdMchFzRelationServiceImpl service;
+
+ /** 测试租户ID,作为关系唯一键和任务隔离边界。 */
+ private static final Long TENANT_ID = 9L;
+
+ /** 测试商户ID,对应 mch_manage 主键。 */
+ private static final Long MCH_ID = 88L;
+
+ /** 初始化平台默认接收账号与被测服务。 */
+ @BeforeEach
+ void setUp() {
+ TableInfoHelper.initTableInfo(new MapperBuilderAssistant(new MybatisConfiguration(), "test"),
+ ThirdMchFzRelationEntity.class);
+ AppPlusPayConfig config = new AppPlusPayConfig();
+ AppPlusPayConfig.WechatConfig wechat = new AppPlusPayConfig.WechatConfig();
+ wechat.setAppPlatformAppId("wx-app-1001");
+ wechat.setMerchantId("1900000001");
+ wechat.setProfitSharingReceiverAccount("1900000002");
+ wechat.setProfitSharingReceiverName("平台服务商");
+ config.setWechatConfig(wechat);
+
+ AppPlusPayConfig.AliPayConfig alipay = new AppPlusPayConfig.AliPayConfig();
+ alipay.setAppId("2026000000000001");
+ alipay.setAppPrivateKey("test-private-key");
+ alipay.setAlipayPublicKey("test-public-key");
+ alipay.setRoyaltyTransInType("userId");
+ alipay.setRoyaltyTransIn("2088000000000001");
+ alipay.setRoyaltyTransInName("平台支付宝账号");
+ config.setAliPayConfig(alipay);
+ service = new TestableThirdMchFzRelationServiceImpl(relationMapper, taskEnqueuer,
+ profitsharingService, config, remoteMchService, alipayClient);
+ }
+
+ /** 微信和支付宝启用抽佣后分别生成一条默认关系,且请求快照不保存授权 Token。 */
+ @Test
+ void syncDefaultRelationsShouldCreateWechatAndAlipayRelations() {
+ when(relationMapper.selectOne(any(Wrapper.class))).thenReturn(null);
+ MchPaySettingVo wechat = wechatSetting(101L, 3D);
+ MchPaySettingVo alipay = alipaySetting(102L, 5D, "sensitive-auth-token");
+
+ service.syncDefaultRelations(merchant(List.of(wechat, alipay)));
+
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ThirdMchFzRelationEntity.class);
+ verify(relationMapper, times(2)).insert(captor.capture());
+ List relations = captor.getAllValues();
+ ThirdMchFzRelationEntity wechatRelation = relations.stream()
+ .filter(item -> AppPlusPlatformPayType.WECHAT.getType().equals(item.getPlatformType()))
+ .findFirst().orElseThrow();
+ ThirdMchFzRelationEntity alipayRelation = relations.stream()
+ .filter(item -> AppPlusPlatformPayType.ALI_PAY.getType().equals(item.getPlatformType()))
+ .findFirst().orElseThrow();
+ Assertions.assertAll(
+ () -> Assertions.assertEquals("wx-sub-1001", wechatRelation.getChannelSubMchId()),
+ () -> Assertions.assertEquals("1900000002", wechatRelation.getReceiverAccount()),
+ () -> Assertions.assertEquals(ReceiverType.MERCHANT_ID.name(), wechatRelation.getReceiverType()),
+ () -> Assertions.assertEquals(ReceiverRelationType.SERVICE_PROVIDER.name(), wechatRelation.getChannelRelationType()),
+ () -> Assertions.assertEquals("smid-1001", alipayRelation.getChannelSubMchId()),
+ () -> Assertions.assertEquals("2088000000000001", alipayRelation.getReceiverAccount()),
+ () -> Assertions.assertEquals("userId", alipayRelation.getReceiverType()),
+ () -> Assertions.assertFalse(alipayRelation.getBindReq().contains("sensitive-auth-token")),
+ () -> Assertions.assertTrue(alipayRelation.getBindReq().contains("appAuthTokenConfigured")));
+ }
+
+ /** 相同关系身份重复同步时复用既有记录,不重复插入渠道关系事实。 */
+ @Test
+ void syncDefaultRelationsShouldReuseExistingIdentity() {
+ MchPaySettingVo setting = wechatSetting(101L, 3D);
+ when(relationMapper.selectOne(any(Wrapper.class))).thenReturn(null);
+ service.syncDefaultRelations(merchant(List.of(setting)));
+ ArgumentCaptor captor = ArgumentCaptor.forClass(ThirdMchFzRelationEntity.class);
+ verify(relationMapper).insert(captor.capture());
+ ThirdMchFzRelationEntity existing = captor.getValue();
+ existing.setStatus(ThirdMchFzRelationStatusEnum.BOUND.getCode());
+ existing.setBoundTime(LocalDateTime.now());
+
+ clearInvocations(relationMapper, taskEnqueuer);
+ when(relationMapper.selectOne(any(Wrapper.class))).thenReturn(existing);
+ service.syncDefaultRelations(merchant(List.of(setting)));
+
+ verify(relationMapper, never()).insert(any(ThirdMchFzRelationEntity.class));
+ verify(taskEnqueuer, never()).enqueueRelationBind(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt());
+ }
+
+ /** 并发插入命中唯一键时读取胜出记录继续处理,不把正常竞争暴露为配置保存失败。 */
+ @Test
+ void syncDefaultRelationsShouldReuseWinnerAfterDuplicateKey() {
+ ThirdMchFzRelationEntity winner = pendingWechatRelation(7001L);
+ when(relationMapper.selectOne(any(Wrapper.class))).thenReturn(null, winner);
+ doThrow(new DuplicateKeyException("duplicate relation")).when(relationMapper)
+ .insert(any(ThirdMchFzRelationEntity.class));
+
+ service.syncDefaultRelations(merchant(List.of(wechatSetting(101L, 3D))));
+
+ verify(taskEnqueuer).enqueueRelationBind(org.mockito.ArgumentMatchers.eq(TENANT_ID),
+ org.mockito.ArgumentMatchers.eq(7001L), any(), org.mockito.ArgumentMatchers.eq(150));
+ }
+
+ /** 关系尚未绑定时支付入口必须拒绝,并只确保稳定任务存在而不重置既有退避。 */
+ @Test
+ void requireBoundShouldRejectAndEnsureTaskWithoutRearm() {
+ when(relationMapper.selectOne(any(Wrapper.class))).thenReturn(null).thenReturn(null);
+
+ IllegalStateException exception = Assertions.assertThrows(IllegalStateException.class,
+ () -> service.requireBound(wechatSetting(101L, 3D)));
+
+ Assertions.assertTrue(exception.getMessage().contains("尚未绑定"));
+ ArgumentCaptor relationCaptor = ArgumentCaptor.forClass(ThirdMchFzRelationEntity.class);
+ verify(relationMapper).insert(relationCaptor.capture());
+ verify(taskEnqueuer).ensureRelationBind(org.mockito.ArgumentMatchers.eq(TENANT_ID),
+ org.mockito.ArgumentMatchers.eq(relationCaptor.getValue().getId()), any(), org.mockito.ArgumentMatchers.eq(150));
+ verify(taskEnqueuer, never()).enqueueRelationBind(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt());
+ }
+
+ /** 跨服务保存回滚留下的历史已绑定关系应按当前配置恢复,避免支付永久被拒绝。 */
+ @Test
+ void requireBoundShouldRestoreMatchingHistoricalBoundRelation() {
+ ThirdMchFzRelationEntity historical = pendingWechatRelation(7004L);
+ historical.setStatus(ThirdMchFzRelationStatusEnum.SUPERSEDED.getCode());
+ historical.setIsDefault(YesNoEnum.NO.getCode());
+ historical.setBoundTime(LocalDateTime.now());
+ when(relationMapper.selectOne(any(Wrapper.class))).thenReturn(null, historical);
+
+ ThirdMchFzRelationEntity result = service.requireBound(wechatSetting(101L, 3D));
+
+ Assertions.assertSame(historical, result);
+ Assertions.assertEquals(ThirdMchFzRelationStatusEnum.BOUND.getCode(), result.getStatus());
+ Assertions.assertEquals(YesNoEnum.YES.getCode(), result.getIsDefault());
+ verify(taskEnqueuer, never()).ensureRelationBind(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt());
+ verify(taskEnqueuer, never()).enqueueRelationBind(any(), any(), any(), org.mockito.ArgumentMatchers.anyInt());
+ }
+
+ /** 微信返回其他接收方时不能把本地关系标记为已绑定。 */
+ @Test
+ void bindWechatShouldRejectMismatchedResponse() {
+ ThirdMchFzRelationEntity pending = pendingWechatRelation(7002L);
+ ThirdMchFzRelationEntity binding = pendingWechatRelation(7002L);
+ binding.setStatus(ThirdMchFzRelationStatusEnum.BINDING.getCode());
+ when(relationMapper.selectById(7002L)).thenReturn(pending, binding);
+ when(relationMapper.update(any(), any())).thenReturn(1);
+ AddReceiverResponse response = new AddReceiverResponse();
+ response.setSubMchid(binding.getChannelSubMchId());
+ response.setType(ReceiverType.MERCHANT_ID);
+ response.setAccount("different-receiver");
+ response.setRelationType(ReceiverRelationType.SERVICE_PROVIDER);
+ when(profitsharingService.addReceiver(any())).thenReturn(response);
+
+ Assertions.assertFalse(service.bind(7002L));
+ verify(relationMapper, times(2)).update(any(), any());
+ }
+
+ /** 支付宝网关成功但业务 result_code 非 SUCCESS 时不得把关系误记为已绑定。 */
+ @Test
+ void bindAlipayShouldRejectNonSuccessBusinessResult() throws Exception {
+ ThirdMchFzRelationEntity pending = pendingAlipayRelation(7003L);
+ ThirdMchFzRelationEntity binding = pendingAlipayRelation(7003L);
+ binding.setStatus(ThirdMchFzRelationStatusEnum.BINDING.getCode());
+ when(relationMapper.selectById(7003L)).thenReturn(pending, binding);
+ when(relationMapper.update(any(), any())).thenReturn(1);
+ when(remoteMchService.getById(MCH_ID)).thenReturn(com.bsd.pjcloud.common.core.util.R.ok(
+ merchant(List.of(alipaySetting(102L, 5D, "merchant-auth-token")))));
+
+ AlipayTradeRoyaltyRelationBindResponse bindResponse = new AlipayTradeRoyaltyRelationBindResponse();
+ bindResponse.setCode("10000");
+ bindResponse.setResultCode("FAIL");
+ when(alipayClient.execute(any(AlipayTradeRoyaltyRelationBindRequest.class))).thenReturn(bindResponse);
+ AlipayTradeRoyaltyRelationBatchqueryResponse queryResponse = new AlipayTradeRoyaltyRelationBatchqueryResponse();
+ queryResponse.setCode("10000");
+ queryResponse.setResultCode("FAIL");
+ when(alipayClient.execute(any(AlipayTradeRoyaltyRelationBatchqueryRequest.class))).thenReturn(queryResponse);
+
+ Assertions.assertFalse(service.bind(7003L));
+ verify(relationMapper, times(2)).update(any(), any());
+ }
+
+ /** 负数或超过百分之百的抽佣配置必须在关系物化前拒绝。 */
+ @Test
+ void syncDefaultRelationsShouldRejectInvalidRatio() {
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> service.syncDefaultRelations(merchant(List.of(wechatSetting(101L, -1D)))));
+ Assertions.assertThrows(IllegalArgumentException.class,
+ () -> service.syncDefaultRelations(merchant(List.of(alipaySetting(102L, 101D, null)))));
+ verify(relationMapper, never()).insert(any(ThirdMchFzRelationEntity.class));
+ }
+
+ /** 构造商户关系同步快照。 */
+ private MchManageVo merchant(List settings) {
+ MchManageVo merchant = new MchManageVo();
+ merchant.setId(MCH_ID);
+ merchant.setTenantId(TENANT_ID);
+ merchant.setPaySettingList(settings);
+ return merchant;
+ }
+
+ /** 构造启用抽佣的微信子商户支付配置。 */
+ private MchPaySettingVo wechatSetting(Long id, Double ratio) {
+ MchPaySettingEntity.WechatPaySetting detail = new MchPaySettingEntity.WechatPaySetting();
+ detail.setSubMchid("wx-sub-1001");
+ return setting(id, AppPlusPlatformPayType.WECHAT.getType(), ratio, JSONUtil.toJsonStr(detail));
+ }
+
+ /** 构造启用抽佣的支付宝直付通支付配置。 */
+ private MchPaySettingVo alipaySetting(Long id, Double ratio, String appAuthToken) {
+ MchPaySettingEntity.AliPaySetting detail = new MchPaySettingEntity.AliPaySetting();
+ detail.setSubMchid("smid-1001");
+ detail.setAppAuthToken(appAuthToken);
+ return setting(id, AppPlusPlatformPayType.ALI_PAY.getType(), ratio, JSONUtil.toJsonStr(detail));
+ }
+
+ /** 构造关系服务使用的通用支付配置字段。 */
+ private MchPaySettingVo setting(Long id, String platformType, Double ratio, String settingJson) {
+ MchPaySettingVo setting = new MchPaySettingVo();
+ setting.setId(id);
+ setting.setTenantId(TENANT_ID);
+ setting.setMchId(MCH_ID);
+ setting.setPlatformType(platformType);
+ setting.setEnable(YesNoEnum.YES.getCode());
+ setting.setFzRatio(ratio);
+ setting.setSettingJson(settingJson);
+ return setting;
+ }
+
+ /** 构造等待绑定的微信关系事实。 */
+ private ThirdMchFzRelationEntity pendingWechatRelation(Long id) {
+ ThirdMchFzRelationEntity relation = new ThirdMchFzRelationEntity();
+ relation.setId(id);
+ relation.setTenantId(TENANT_ID);
+ relation.setMchId(MCH_ID);
+ relation.setPaySettingId(101L);
+ relation.setPlatformType(AppPlusPlatformPayType.WECHAT.getType());
+ relation.setChannelAppId("wx-app-1001");
+ relation.setChannelSubMchId("wx-sub-1001");
+ relation.setReceiverType(ReceiverType.MERCHANT_ID.name());
+ relation.setReceiverAccount("1900000002");
+ relation.setReceiverName("平台服务商");
+ relation.setChannelRelationType(ReceiverRelationType.SERVICE_PROVIDER.name());
+ relation.setStatus(ThirdMchFzRelationStatusEnum.PENDING_BIND.getCode());
+ relation.setDelFlag("0");
+ return relation;
+ }
+
+ /** 构造等待绑定的支付宝直付通平台分佣关系事实。 */
+ private ThirdMchFzRelationEntity pendingAlipayRelation(Long id) {
+ ThirdMchFzRelationEntity relation = new ThirdMchFzRelationEntity();
+ relation.setId(id);
+ relation.setTenantId(TENANT_ID);
+ relation.setMchId(MCH_ID);
+ relation.setPaySettingId(102L);
+ relation.setPlatformType(AppPlusPlatformPayType.ALI_PAY.getType());
+ relation.setChannelAppId("2026000000000001");
+ relation.setChannelSubMchId("smid-1001");
+ relation.setReceiverType("userId");
+ relation.setReceiverAccount("2088000000000001");
+ relation.setReceiverName("平台支付宝账号");
+ relation.setOutRequestNo("alirel_7003");
+ relation.setStatus(ThirdMchFzRelationStatusEnum.PENDING_BIND.getCode());
+ relation.setDelFlag("0");
+ return relation;
+ }
+
+ /** 固定返回 Mock 支付宝客户端的关系服务测试实现。 */
+ private static final class TestableThirdMchFzRelationServiceImpl extends ThirdMchFzRelationServiceImpl {
+
+ /** 当前测试共享的支付宝 SDK 客户端 Mock。 */
+ private final AlipayClient testAlipayClient;
+
+ /**
+ * 创建可替换支付宝客户端的关系服务。
+ *
+ * @param relationMapper 关系事实 Mapper Mock
+ * @param taskEnqueuer 关系绑定任务写入器 Mock
+ * @param profitsharingService 微信分账 SDK Mock
+ * @param appPlusPayConfig 平台支付配置
+ * @param remoteMchService 商户内部接口 Mock
+ * @param testAlipayClient 支付宝 SDK 客户端 Mock
+ */
+ private TestableThirdMchFzRelationServiceImpl(ThirdMchFzRelationMapper relationMapper,
+ ThirdPaymentTaskEnqueuer taskEnqueuer,
+ ProfitsharingService profitsharingService,
+ AppPlusPayConfig appPlusPayConfig,
+ RemoteMchService remoteMchService,
+ AlipayClient testAlipayClient) {
+ super(relationMapper, taskEnqueuer, profitsharingService, appPlusPayConfig, remoteMchService);
+ this.testAlipayClient = testAlipayClient;
+ }
+
+ /** 返回测试注入的支付宝 SDK 客户端,避免发起真实渠道请求。 */
+ @Override
+ protected AlipayClient client(AppPlusPayConfig.AliPayConfig config) {
+ return testAlipayClient;
+ }
+ }
+}
diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserDeptInterface.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserDeptInterface.java
index fa62123cb..9a5465631 100644
--- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserDeptInterface.java
+++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserDeptInterface.java
@@ -4,9 +4,10 @@ package com.bsd.pjcloud.admin.api.vo.base;
* 用户及其所属部门的通用数据契约。
*
* 历史业务模型使用 {@code userMobile}、{@code userAvatar}、{@code userDeptName} 等字段,
- * 标准别名用于直接适配管理端 {@code UserInfoCell} 组件,同时通过默认方法保持已有实现兼容。
+ * 标准别名用于直接适配管理端 {@code UserInfoCell} 组件,同时通过默认方法保持已有实现兼容。
+ * 本接口继承基础用户契约,使实现类可以直接用于只要求用户主资料的通用装配方法。
*/
-public interface BaseUserDeptInterface {
+public interface BaseUserDeptInterface extends BaseUserInterface, BaseUserPhoneInterface {
/**
* 获取用户主键。
*
@@ -114,6 +115,7 @@ public interface BaseUserDeptInterface {
*
* @return 用户手机号;未绑定时为空
*/
+ @Override
default String getPhone() {
return getUserMobile();
}
@@ -123,6 +125,7 @@ public interface BaseUserDeptInterface {
*
* @param phone 用户手机号,未绑定时可为空
*/
+ @Override
default void setPhone(String phone) {
setUserMobile(phone);
}
diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserIdentityInterface.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserIdentityInterface.java
index b470cb473..cfd87c973 100644
--- a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserIdentityInterface.java
+++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserIdentityInterface.java
@@ -9,9 +9,10 @@ import java.util.List;
* 管理端用户身份信息组件使用的通用数据契约。
*
* 字段与 {@code UserIdentityInfo} 组件的输入保持一致,业务 VO 实现该接口后可直接承载
- * UPMS 批量查询得到的身份类型、学生资料和老师资料。
+ * UPMS 批量查询得到的身份类型、学生资料和老师资料。标准手机号方法继承自
+ * {@link BaseUserPhoneInterface},避免与用户部门接口的历史字段默认适配产生继承冲突。
*/
-public interface BaseUserIdentityInterface {
+public interface BaseUserIdentityInterface extends BaseUserPhoneInterface {
/**
* 获取用户主键,用于批量查询和回填身份资料。
@@ -27,20 +28,6 @@ public interface BaseUserIdentityInterface {
*/
void setUserId(Long userId);
- /**
- * 获取默认用户身份展示的手机号。
- *
- * @return 用户绑定手机号;未绑定时可为空
- */
- String getPhone();
-
- /**
- * 设置默认用户身份展示的手机号。
- *
- * @param phone 用户绑定手机号,未绑定时可为空
- */
- void setPhone(String phone);
-
/**
* 获取用户拥有的身份协议编码。
*
diff --git a/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserPhoneInterface.java b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserPhoneInterface.java
new file mode 100644
index 000000000..56b034e71
--- /dev/null
+++ b/pjcloud-upms/pjcloud-upms-api/src/main/java/com/bsd/pjcloud/admin/api/vo/base/BaseUserPhoneInterface.java
@@ -0,0 +1,24 @@
+package com.bsd.pjcloud.admin.api.vo.base;
+
+/**
+ * 用户标准手机号的公共数据契约。
+ *
+ * 用户部门与用户身份接口都会使用标准手机号。将方法声明收敛到共同父接口,可让部门接口继续
+ * 提供历史字段适配的默认实现,同时避免两个无关接口组成交叉类型时产生默认方法与抽象方法冲突。
+ */
+public interface BaseUserPhoneInterface {
+
+ /**
+ * 获取用户标准手机号。
+ *
+ * @return 用户绑定手机号;未绑定或尚未补充用户资料时可为空
+ */
+ String getPhone();
+
+ /**
+ * 设置用户标准手机号。
+ *
+ * @param phone 用户绑定手机号;未绑定或需要清空展示值时可为空
+ */
+ void setPhone(String phone);
+}
diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletCodeUseDto.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletCodeUseDto.java
index 15ef39720..f74a5a41d 100644
--- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletCodeUseDto.java
+++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/dto/WalletCodeUseDto.java
@@ -1,13 +1,30 @@
package com.bsd.pjcloud.wallet.api.dto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityQueryInterface;
import com.bsd.pjcloud.wallet.api.entity.WalletCodeUseEntity;
import lombok.Data;
import java.util.List;
+/** 二维码使用记录查询条件,支持按申请用户及其身份资料筛选。 */
@Data
-public class WalletCodeUseDto extends WalletCodeUseEntity {
+public class WalletCodeUseDto extends WalletCodeUseEntity implements BaseUserIdentityQueryInterface {
+ /** 身份预筛选后用于二维码记录查询的用户主键集合。 */
private List userIds;
+ /** UPMS 用户身份协议编码集合;为空时不限制身份。 */
+ private List identityTypes;
+
+ /** 学生身份扩展查询条件;对象为空或字段均为空时不限制学生资料。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 老师身份扩展查询条件;对象为空或老师学号为空时不限制老师资料。 */
+ private SysUserTeacherDto teacherInfo;
+
+ /** 人脸录入状态;1 表示已录入,0 表示未录入,空值表示不限制。 */
+ private String hasFace;
+
}
diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/WalletCodeUseVo.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/WalletCodeUseVo.java
index f3a2c34cc..67c734c53 100644
--- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/WalletCodeUseVo.java
+++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/WalletCodeUseVo.java
@@ -1,19 +1,47 @@
package com.bsd.pjcloud.wallet.api.vo;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
+import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
+import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserInterface;
import com.bsd.pjcloud.wallet.api.entity.WalletCodeUseEntity;
import lombok.Data;
-@Data
-public class WalletCodeUseVo extends WalletCodeUseEntity implements BaseUserInterface {
+import java.util.List;
- /**
- * 用户相关
- */
+/** 二维码使用记录展示对象,包含申请用户主资料、部门和身份信息。 */
+@Data
+public class WalletCodeUseVo extends WalletCodeUseEntity implements BaseUserInterface, BaseUserDeptInterface, BaseUserIdentityInterface {
+
+ /** 申请用户姓名,保留历史接口字段名。 */
private String username;
+ /** 申请用户手机号,保留历史接口字段名。 */
private String userMobile;
+ /** 申请用户头像地址,保留历史接口字段名。 */
private String userAvatar;
+ /** 用户标准手机号字段,供统一身份组件展示。 */
+ private String phone;
+
+ /** 用户当前所属部门主键,由 UPMS 用户资料回填。 */
+ private Long userDeptId;
+
+ /** 用户当前所属部门名称,由 UPMS 用户资料回填。 */
+ private String userDeptName;
+
+ /** 用户所属部门展示名称,供统一用户信息组件读取。 */
+ private String deptName;
+
+ /** 用户拥有的 UPMS 身份协议编码集合。 */
+ private List identityTypes;
+
+ /** 用户的学生身份扩展资料;无学生身份时为空。 */
+ private SysUserStudentDto studentInfo;
+
+ /** 用户的老师身份扩展资料;无老师身份时为空。 */
+ private SysUserTeacherDto teacherInfo;
+
}
diff --git a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/WalletManageVo.java b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/WalletManageVo.java
index c84ed329b..40131385e 100644
--- a/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/WalletManageVo.java
+++ b/pjcloud-wallet/pjcloud-wallet-api/src/main/java/com/bsd/pjcloud/wallet/api/vo/WalletManageVo.java
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.wallet.api.vo;
import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseDeptInterface;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.wallet.api.entity.WalletManageEntity;
@@ -13,7 +14,7 @@ import java.util.List;
/** 钱包管理端展示对象,包含公共用户主资料、部门及身份信息。 */
@Data
-public class WalletManageVo extends WalletManageEntity implements BaseUserDeptInterface, BaseUserIdentityInterface, BaseWalletTypeInterface {
+public class WalletManageVo extends WalletManageEntity implements BaseUserDeptInterface, BaseUserIdentityInterface, BaseDeptInterface, BaseWalletTypeInterface {
/**
* 钱包类型
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 a5c6beb68..6f95628fd 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
@@ -2,6 +2,7 @@ package com.bsd.pjcloud.wallet.api.vo.transaction;
import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
+import com.bsd.pjcloud.admin.api.vo.base.BaseDeptInterface;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserDeptInterface;
import com.bsd.pjcloud.admin.api.vo.base.BaseUserIdentityInterface;
import com.bsd.pjcloud.wallet.api.entity.WalletTransactionEntity;
@@ -14,7 +15,7 @@ import java.util.List;
/** 钱包流水管理端展示对象,包含公共用户主资料、部门及身份信息。 */
@Data
-public class WalletTransactionVo extends WalletTransactionEntity implements BaseUserDeptInterface, BaseUserIdentityInterface, BaseWalletTypeInterface, BaseWalletInterface {
+public class WalletTransactionVo extends WalletTransactionEntity implements BaseUserDeptInterface, BaseUserIdentityInterface, BaseDeptInterface, BaseWalletTypeInterface, BaseWalletInterface {
/**
* 字符串金额用于导出
diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletCodeUseServiceImpl.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletCodeUseServiceImpl.java
index 4edfac2e0..6cba878d6 100644
--- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletCodeUseServiceImpl.java
+++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletCodeUseServiceImpl.java
@@ -9,13 +9,10 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.admin.api.dto.UserInfo;
-import com.bsd.pjcloud.admin.api.entity.SysUser;
import com.bsd.pjcloud.admin.api.feign.RemoteFileService;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
-import com.bsd.pjcloud.admin.api.utils.DataUtil;
import com.bsd.pjcloud.common.core.constant.enums.YesNoEnum;
import com.bsd.pjcloud.common.core.util.BeanUtils;
-import com.bsd.pjcloud.common.core.util.CollectionUtils;
import com.bsd.pjcloud.common.core.util.QRCodeImageGenerator;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.hard.api.dto.code.QRCodeExtraDto;
@@ -29,6 +26,7 @@ import com.bsd.pjcloud.wallet.api.vo.WalletCodeUseVo;
import com.bsd.pjcloud.wallet.api.vo.WalletSettingVo;
import com.bsd.pjcloud.wallet.mapper.WalletCodeUseMapper;
import com.bsd.pjcloud.wallet.service.*;
+import com.bsd.pjcloud.wallet.support.WalletUserIdentitySupport;
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.code.QRCodeDto;
@@ -47,7 +45,6 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.time.LocalDateTime;
import java.time.ZoneId;
-import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@@ -75,6 +72,8 @@ public class WalletCodeUseServiceImpl extends ServiceImpl pageJoin(Page page, WalletCodeUseDto dto) {
+ prepareIdentityQuery(dto);
Page list = baseMapper.pageJoin(page, dto);
- if (ObjectUtil.isNotEmpty(list.getRecords())) {
- // 用户
- List userIds = list.getRecords().stream().map(WalletCodeUseVo::getUserId).distinct().toList();
- R> userList = userService.getUserById(new ArrayList<>(userIds));
- if (ObjectUtil.isNotEmpty(userList.getData())) {
- DataUtil.convertUserList(list.getRecords(), CollectionUtils.convertMap(userList.getData(), SysUser::getUserId));
- }
- }
+ userIdentitySupport.fillUserInfo(list.getRecords(), true);
return list;
}
+ /**
+ * 将身份筛选条件转换为最终用户主键集合,复用 Mapper 已有的 {@code userIds} 过滤能力。
+ *
+ * @param dto 二维码使用记录查询条件;处理后身份字段被最终用户主键集合替代
+ */
+ private void prepareIdentityQuery(WalletCodeUseDto dto) {
+ if (!userIdentitySupport.hasIdentityQuery(dto)) {
+ return;
+ }
+ dto.setUserIds(userIdentitySupport.findIdentityUserIds(dto, dto.getUserId(), dto.getUserIds()));
+ dto.setUserId(null);
+ dto.setIdentityTypes(null);
+ dto.setStudentInfo(null);
+ dto.setTeacherInfo(null);
+ dto.setHasFace(null);
+ }
+
@Override
// @GlobalTransactional
@Transactional(rollbackFor = Exception.class)
diff --git a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletManageServiceImpl.java b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletManageServiceImpl.java
index 2f78f9c80..433745e3c 100644
--- a/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletManageServiceImpl.java
+++ b/pjcloud-wallet/pjcloud-wallet-biz/src/main/java/com/bsd/pjcloud/wallet/service/impl/WalletManageServiceImpl.java
@@ -371,7 +371,7 @@ public class WalletManageServiceImpl extends ServiceImpl scopeQbList(WalletManageBalanceDto dto, DataScope dataScope) {
WalletManageDto walletDto = BeanUtils.toBean(dto, WalletManageDto.class);
prepareIdentityQuery(walletDto);
- BeanUtils.copyProperties(walletDto, dto);
+ BeanUtil.copyProperties(walletDto, dto);
List walletList = baseMapper.scopeList(walletDto, dataScope);
if (ObjectUtil.isEmpty(walletList)) return walletList;
walletList.forEach(v -> {