This commit is contained in:
dzc committed 2026-08-25 13:07:03 +08:00
1 parent 890b927354
commit 388ef914fc
16 files changed
+793 -45

No files matched your search

@@ -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<Long> userIds;
/** 目标柜门主键;为空时不限制柜门。 */
private Long cabinetId;
/** UPMS 用户身份协议编码集合;为空表示不限制身份。 */
@@ -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;
/**
* 硬件订单售后详情。
*
* <p>同时保留历史用户、用户部门和用户身份协议,兼容售后详情的旧用户装配逻辑及管理端身份展示。</p>
*/
@Data
public class HardAfterSaleVo extends HardAfterSaleEntity implements BaseUserDeptInterface, BaseUserIdentityInterface, BaseDeptInterface, BaseOrderInterface {
public class HardAfterSaleVo extends HardAfterSaleEntity implements BaseUserInterface, BaseUserDeptInterface, BaseUserIdentityInterface, BaseDeptInterface, BaseOrderInterface {
/**
* 自动退款任务状态;为空表示当前售后仍走人工审核,非空时前端不得展示审核或取消操作。
*/
@@ -30,6 +30,9 @@ public class HardOrderVo extends HardOrderEntity implements BaseUserDeptInterfac
private String userMobile;
/** 用户标准手机号字段,供统一身份组件展示;与历史 userMobile 字段并存。 */
private String phone;
private String userAvatar;
/** 用户身份协议编码集合;没有身份时为空集合或空值。 */
@@ -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<String> identityTypes;
private List<String> 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;
}
@@ -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<HardSpuRepertoryMap
/** 库存上架记录用户身份筛选与展示资料装配器。 */
private final HardUserIdentitySupport userIdentitySupport;
/**
* 分页查询库存更新记录,并补齐上架用户身份及关联展示信息。
*
* @param page 分页参数
* @param dto 库存记录、用户身份及用户范围筛选条件
* @param dataScope 当前管理人员的数据范围
* @return 已完成用户、商户和硬件展示资料装配的分页结果
*/
@Override
public Page<HardSpuRepertoryVo> scopePageJoin(Page page, HardSpuRepertoryDto dto, DataScope dataScope) {
prepareIdentityQuery(dto);
@@ -233,6 +242,12 @@ public class HardSpuRepertoryServiceImpl extends ServiceImpl<HardSpuRepertoryMap
return false;
}
/**
* 批量补齐库存记录的商户及用户展示信息。
*
* @param list 当前页库存更新记录;空列表不触发远程查询
* @param dto 当前查询条件,保留参数以兼容原装配方法契约
*/
private void disposeList(List<HardSpuRepertoryVo> list, HardSpuRepertoryDto dto) {
if (ObjectUtil.isEmpty(list)) return;
// 商户
@@ -248,7 +263,11 @@ public class HardSpuRepertoryServiceImpl extends ServiceImpl<HardSpuRepertoryMap
userIdentitySupport.fillUserInfo(list, true);
}
/** 将库存上架记录的身份条件转换为最终用户主键集合,保持原有 userIds SQL 过滤契约。 */
/**
* 将库存上架记录的身份条件转换为最终用户主键集合,保持原有 userIds SQL 过滤契约。
*
* @param dto 库存记录查询条件;处理后身份字段被最终用户主键集合替代
*/
private void prepareIdentityQuery(HardSpuRepertoryDto dto) {
if (!userIdentitySupport.hasIdentityQuery(dto)) {
return;
@@ -0,0 +1,255 @@
package com.bsd.pjcloud.hard.support;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.admin.api.dto.UserDTO;
import com.bsd.pjcloud.admin.api.dto.user.SysUserStudentDto;
import com.bsd.pjcloud.admin.api.dto.user.SysUserTeacherDto;
import com.bsd.pjcloud.admin.api.feign.RemoteUserService;
import com.bsd.pjcloud.admin.api.vo.UserVO;
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.BaseUserIdentityQueryInterface;
import com.bsd.pjcloud.common.core.util.CollectionUtils;
import com.bsd.pjcloud.common.core.util.R;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* Hardware 后台列表的用户身份查询与展示资料装配器。
*
* <p>订单等业务表只保存用户主键和必要快照,身份筛选统一交给 UPMS 数据权限用户接口处理,
* Hardware 仅将命中的用户主键并入原有查询,并为当前页批量回填身份展示资料。</p>
*/
@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<Long> findIdentityUserIds(BaseUserIdentityQueryInterface query, Long userId, List<Long> userIds) {
List<Long> constrainedUserIds = mergeUserConstraints(userId, userIds);
if (constrainedUserIds != null && constrainedUserIds.isEmpty()) {
return List.of(NO_MATCH_USER_ID);
}
UserDTO userQuery = buildUserQuery(query, constrainedUserIds);
List<UserVO> 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 <T> 同时实现用户部门和用户身份展示契约的业务 VO 类型
*/
public <T extends BaseUserDeptInterface & BaseUserIdentityInterface> void fillUserInfo(List<T> rows,
boolean updateDepartment) {
if (rows == null || rows.isEmpty()) {
return;
}
List<Long> userIds = rows.stream().map(BaseUserDeptInterface::getUserId).filter(Objects::nonNull).distinct().toList();
if (userIds.isEmpty()) {
return;
}
UserDTO userQuery = new UserDTO();
userQuery.setUserIds(userIds);
Map<Long, UserVO> 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<Long> mergeUserConstraints(Long userId, List<Long> userIds) {
LinkedHashSet<Long> 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<Long> 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<UserVO> fetchAllUsers(UserDTO query) {
List<UserVO> users = new ArrayList<>();
long current = 1L;
long total;
do {
R<Page<UserVO>> 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<UserVO> page = response.getData();
List<UserVO> 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 <T> 列表元素类型
* @return 独立列表;源列表为空值时返回 {@code null}
*/
private <T> List<T> copyList(List<T> source) {
return source == null ? null : new ArrayList<>(source);
}
}
@@ -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<ThirdMchFzRelationEntity> captor = ArgumentCaptor.forClass(ThirdMchFzRelationEntity.class);
verify(relationMapper, times(2)).insert(captor.capture());
List<ThirdMchFzRelationEntity> 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<ThirdMchFzRelationEntity> 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<ThirdMchFzRelationEntity> 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<MchPaySettingVo> 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;
}
}
}
@@ -4,9 +4,10 @@ package com.bsd.pjcloud.admin.api.vo.base;
* 用户及其所属部门的通用数据契约。
*
* <p>历史业务模型使用 {@code userMobile}、{@code userAvatar}、{@code userDeptName} 等字段,
* 标准别名用于直接适配管理端 {@code UserInfoCell} 组件,同时通过默认方法保持已有实现兼容。</p>
* 标准别名用于直接适配管理端 {@code UserInfoCell} 组件,同时通过默认方法保持已有实现兼容。
* 本接口继承基础用户契约,使实现类可以直接用于只要求用户主资料的通用装配方法。</p>
*/
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);
}
@@ -9,9 +9,10 @@ import java.util.List;
* 管理端用户身份信息组件使用的通用数据契约。
*
* <p>字段与 {@code UserIdentityInfo} 组件的输入保持一致,业务 VO 实现该接口后可直接承载
* UPMS 批量查询得到的身份类型、学生资料和老师资料。</p>
* UPMS 批量查询得到的身份类型、学生资料和老师资料。标准手机号方法继承自
* {@link BaseUserPhoneInterface},避免与用户部门接口的历史字段默认适配产生继承冲突。</p>
*/
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);
/**
* 获取用户拥有的身份协议编码。
*
@@ -0,0 +1,24 @@
package com.bsd.pjcloud.admin.api.vo.base;
/**
* 用户标准手机号的公共数据契约。
*
* <p>用户部门与用户身份接口都会使用标准手机号。将方法声明收敛到共同父接口,可让部门接口继续
* 提供历史字段适配的默认实现,同时避免两个无关接口组成交叉类型时产生默认方法与抽象方法冲突。</p>
*/
public interface BaseUserPhoneInterface {
/**
* 获取用户标准手机号。
*
* @return 用户绑定手机号;未绑定或尚未补充用户资料时可为空
*/
String getPhone();
/**
* 设置用户标准手机号。
*
* @param phone 用户绑定手机号;未绑定或需要清空展示值时可为空
*/
void setPhone(String phone);
}
@@ -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<Long> userIds;
/** UPMS 用户身份协议编码集合;为空时不限制身份。 */
private List<String> identityTypes;
/** 学生身份扩展查询条件;对象为空或字段均为空时不限制学生资料。 */
private SysUserStudentDto studentInfo;
/** 老师身份扩展查询条件;对象为空或老师学号为空时不限制老师资料。 */
private SysUserTeacherDto teacherInfo;
/** 人脸录入状态;1 表示已录入,0 表示未录入,空值表示不限制。 */
private String hasFace;
}
@@ -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<String> identityTypes;
/** 用户的学生身份扩展资料;无学生身份时为空。 */
private SysUserStudentDto studentInfo;
/** 用户的老师身份扩展资料;无老师身份时为空。 */
private SysUserTeacherDto teacherInfo;
}
@@ -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 {
/**
* 钱包类型
@@ -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 {
/**
* 字符串金额用于导出
@@ -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<WalletCodeUseMapper, W
private final RemoteUserService userService;
private final RemoteFileService remoteFileService;
/** 二维码记录的用户身份筛选与展示资料装配器。 */
private final WalletUserIdentitySupport userIdentitySupport;
/**
* 生成二维码
@@ -185,20 +184,38 @@ public class WalletCodeUseServiceImpl extends ServiceImpl<WalletCodeUseMapper, W
}
/**
* 分页查询二维码使用记录,并补齐申请用户的主资料、部门及身份信息。
*
* @param page 分页参数
* @param dto 二维码记录、用户范围及身份筛选条件
* @return 已完成用户资料装配的二维码使用记录分页
*/
@Override
public Page<WalletCodeUseVo> pageJoin(Page page, WalletCodeUseDto dto) {
prepareIdentityQuery(dto);
Page<WalletCodeUseVo> list = baseMapper.pageJoin(page, dto);
if (ObjectUtil.isNotEmpty(list.getRecords())) {
// 用户
List<Long> userIds = list.getRecords().stream().map(WalletCodeUseVo::getUserId).distinct().toList();
R<List<SysUser>> 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)
@@ -371,7 +371,7 @@ public class WalletManageServiceImpl extends ServiceImpl<WalletManageMapper, Wal
public WalletQbTotalVo qbTotal(WalletManageBalanceDto dto, DataScope dataScope) {
WalletManageDto walletDto = BeanUtils.toBean(dto, WalletManageDto.class);
prepareIdentityQuery(walletDto);
BeanUtils.copyProperties(walletDto, dto);
BeanUtil.copyProperties(walletDto, dto);
new PjcloudDefaultDataScopeHandle(dataScopeService).calcScope(dataScope);
if (ObjectUtil.isNotEmpty(dataScope.getDeptList())) {
dto.setScopeDeptIds(dataScope.getDeptList());
@@ -434,7 +434,7 @@ public class WalletManageServiceImpl extends ServiceImpl<WalletManageMapper, Wal
public List<WalletManageVo> scopeQbList(WalletManageBalanceDto dto, DataScope dataScope) {
WalletManageDto walletDto = BeanUtils.toBean(dto, WalletManageDto.class);
prepareIdentityQuery(walletDto);
BeanUtils.copyProperties(walletDto, dto);
BeanUtil.copyProperties(walletDto, dto);
List<WalletManageVo> walletList = baseMapper.scopeList(walletDto, dataScope);
if (ObjectUtil.isEmpty(walletList)) return walletList;
walletList.forEach(v -> {