fix:设备查询菜品频繁问题

fix:退看流水对账问题
This commit is contained in:
dzc committed 2026-09-02 12:52:32 +08:00
1 parent a3c89be742
commit 2a9457e7a7
9 files changed
+222 -70

No files matched your search

@@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.canteen.dto.CanteenSpuUpdateStockReqDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuAll;
import com.bsd.pjcloud.canteen.dto.DailyMenuAvailabilityCheckDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
import com.bsd.pjcloud.canteen.vo.APPOnlineSpuVO;
import com.bsd.pjcloud.canteen.vo.app.CanteenOnlineSpuAppVO;
@@ -96,4 +97,14 @@ public interface RemoteDailyMenuService {
@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "merchantId") Long merchantId);
/**
* 按闭区间查询收银终端多日菜谱,返回结构与单日接口完全一致。
*
* @param request 日期范围、可选筛选条件和收银终端所属商户
* @return 指定日期范围内的每日菜谱列表;无可售菜品时 data 为空列表
*/
@PostMapping("/canteenDailyMenu/ad/getCanteenListByDateRange")
R<List<DailyMenuAppVO>> getCanteenListByDateRange(
@RequestBody @Validated DailyMenuDateRangeQueryDTO request);
}
@@ -0,0 +1,38 @@
package com.bsd.pjcloud.canteen.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.time.LocalDate;
/**
* 收银终端按日期范围查询每日菜谱的请求参数。
*/
@Data
@Schema(description = "收银终端每日菜谱日期范围查询参数")
public class DailyMenuDateRangeQueryDTO {
/** 菜谱起始日期,查询闭区间包含当天。 */
@NotNull(message = "菜谱开始日期不能为空")
@Schema(description = "菜谱起始日期,格式为 yyyy-MM-dd", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDate startDate;
/** 菜谱结束日期,查询闭区间包含当天。 */
@NotNull(message = "菜谱结束日期不能为空")
@Schema(description = "菜谱结束日期,格式为 yyyy-MM-dd", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDate endDate;
/** 餐段协议编码;为空时查询全部餐段。 */
@Schema(description = "餐段编码;为空表示全部餐段")
private Integer hourType;
/** 菜品分类 ID;为空时查询全部分类。 */
@Schema(description = "菜品分类 ID;为空表示全部分类")
private Long categoryId;
/** 收银终端所属商户 ID,用于隔离不同商户的每日菜谱。 */
@NotNull(message = "每日菜谱商户不能为空")
@Schema(description = "收银终端所属商户 ID", requiredMode = Schema.RequiredMode.REQUIRED)
private Long merchantId;
}
@@ -12,6 +12,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuCVDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuAll;
import com.bsd.pjcloud.canteen.dto.DailyMenuAvailabilityCheckDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
import com.bsd.pjcloud.canteen.service.CanteenSpuService;
@@ -308,6 +309,19 @@ public class CanteenDailyMenuController {
return R.ok(canteenDailyMenuService.getCanteenMenuListScope(day, hourType, categoryId, merchantId));
}
/**
* 按闭区间查询收银终端多日菜谱,返回字段及套餐组成与单日接口保持一致。
*
* @param request 日期范围、可选筛选条件和收银终端所属商户
* @return 指定日期范围内的每日菜谱列表;无数据时返回空列表
*/
@PostMapping("/ad/getCanteenListByDateRange")
@SysLog("按日期范围查询每日菜品列表")
public R<List<DailyMenuAppVO>> getCanteenListByDateRange(
@Validated @RequestBody DailyMenuDateRangeQueryDTO request) {
return R.ok(canteenDailyMenuService.getCanteenMenuListByDateRange(request));
}
// --------------APP---------------
@@ -1,6 +1,7 @@
package com.bsd.pjcloud.canteen.mapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
import com.bsd.pjcloud.canteen.vo.DailyMenuAdminVO;
import com.bsd.pjcloud.canteen.vo.DailyMenuVO;
@@ -34,6 +35,16 @@ public interface CanteenDailyMenuMapper extends PjcloudBaseMapper<CanteenDailyMe
List<DailyMenuAppVO> getCanteenMenuList(@Param("day") String day, @Param("hourType") Integer hourType,
@Param("categoryId") Long categoryId, @Param("merchantId") Long merchantId, DataScope dataScope);
/**
* 按闭区间查询收银终端多日菜谱展示数据,字段与单日查询保持一致。
*
* @param request 日期范围、可选筛选条件和商户 ID
* @param dataScope 当前数据权限范围
* @return 按日期、餐段和商品排序的菜谱列表,套餐明细由 Service 批量补充
*/
List<DailyMenuAppVO> getCanteenMenuListByDateRange(@Param("request") DailyMenuDateRangeQueryDTO request,
DataScope dataScope);
Page<DailyMenuAppVO> selectTimeFoodPage(@Param("page")Page page, DailyMenuDTO dailyMenuDTO);
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.service.IService;
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuCVDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuAll;
import com.bsd.pjcloud.canteen.dto.DailyMenuAvailabilityCheckDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
import com.bsd.pjcloud.canteen.entity.CanteenDailyMenuEntity;
import com.bsd.pjcloud.canteen.vo.DailyMenuAdminVO;
@@ -62,6 +63,14 @@ public interface CanteenDailyMenuService extends IService<CanteenDailyMenuEntity
*/
List<DailyMenuAppVO> getCanteenMenuListScope(String day, Integer hourType, Long categoryId, Long merchantId);
/**
* 按闭区间查询收银终端多日菜谱,并补充与单日接口一致的套餐组成数据。
*
* @param request 日期范围、可选筛选条件和商户 ID
* @return 日期升序排列的菜谱展示字段列表
*/
List<DailyMenuAppVO> getCanteenMenuListByDateRange(DailyMenuDateRangeQueryDTO request);
Page selectTimeFoodPage(Page page, DailyMenuDTO dailyMenuDTO);
/**
@@ -10,6 +10,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bsd.pjcloud.canteen.dto.CanteenDailyMenuCVDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuAll;
import com.bsd.pjcloud.canteen.dto.DailyMenuAvailabilityCheckDTO;
import com.bsd.pjcloud.canteen.dto.DailyMenuDateRangeQueryDTO;
import com.bsd.pjcloud.canteen.constant.DailyMenuAvailabilityLock;
import com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO;
import com.bsd.pjcloud.canteen.entity.CanteenSpuEntity;
@@ -344,6 +345,28 @@ public class CanteenDailyMenuServiceImpl extends ServiceImpl<CanteenDailyMenuMap
return list;
}
/**
* 按闭区间查询收银终端多日菜谱,并批量补充套餐组成以避免逐条查询。
*
* @param request 日期范围、可选筛选条件和收银终端所属商户
* @return 日期升序排列的可售菜谱列表;单品的套餐组成固定为空列表
*/
@Override
public List<DailyMenuAppVO> getCanteenMenuListByDateRange(DailyMenuDateRangeQueryDTO request) {
if (request == null || request.getStartDate() == null || request.getEndDate() == null) {
throw new IllegalArgumentException("菜谱开始日期与结束日期不能为空");
}
if (request.getMerchantId() == null) {
throw new IllegalArgumentException("每日菜谱商户不能为空");
}
if (request.getEndDate().isBefore(request.getStartDate())) {
throw new IllegalArgumentException("菜谱结束日期不能早于开始日期");
}
List<DailyMenuAppVO> list = baseMapper.getCanteenMenuListByDateRange(request, DataScope.of());
fillDailyMenuAppCombo(list);
return list;
}
@Override
public Page selectTimeFoodPage(Page page, DailyMenuDTO dailyMenuDTO) {
Page<DailyMenuAppVO> pageData = baseMapper.selectTimeFoodPage(page, dailyMenuDTO);
@@ -111,6 +111,48 @@
</where>
order by sp.sort asc
</select>
<select id="getCanteenMenuListByDateRange" resultMap="adDailyMenuAppVO">
SELECT
t.id,
t.start_time,
t.merchant_id,
sp.id AS spu_id,
sp.name,
sp.price,
sp.market_price,
sp.pic_url,
sp.slider_pic_urls,
sp.introduction,
sp.description,
sp.stock,
sp.sales_count,
sp.sort,
sp.is_show,
c.id AS category_id,
c.name AS category_name,
IFNULL(sp.spu_type, 1) AS spu_type,
t.hour_type,
<!-- 数据权限拦截器会在当前查询外层按部门和创建人过滤,必须在临时结果集中透传这两个字段。 -->
t.dept_id,
t.create_by
FROM canteen_daily_menu t
LEFT JOIN canteen_spu sp
on t.spu_id=sp.id
LEFT JOIN canteen_category c
on sp.category_id = c.id AND c.del_flag = 0
<where>
sp.del_flag = 0 and t.del_flag = 0
AND t.start_time BETWEEN #{request.startDate} AND #{request.endDate}
<if test="request.categoryId != null">
AND sp.category_id = #{request.categoryId}
</if>
<if test="request.hourType != null">
AND t.hour_type = #{request.hourType}
</if>
AND t.merchant_id = #{request.merchantId}
</where>
order by t.start_time asc, t.hour_type asc, sp.sort asc
</select>
<select id="selectTimeFoodPage" resultType="com.bsd.pjcloud.canteen.vo.app.DailyMenuAppVO"
parameterType="com.bsd.pjcloud.canteen.dto.app.DailyMenuDTO">
SELECT m.id,
@@ -409,7 +409,7 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
}
/**
* 按用户部门汇总钱包操作、硬件消费、已完成退款和净消费数据。
* 按用户部门汇总钱包操作、硬件消费、退款流水和净消费数据。
*
* @param dto 当前用户权限范围内的部门、商户、时间和钱包类型筛选条件
* @return 部门交易统计;筛选范围内没有部门时返回空列表
@@ -432,28 +432,25 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
buildWalletStatisticsDto(dto, WalletBizEnum.UPDATE.getCode(), WalletTransactionSumAndCountDto.TYPE_INCOME));
List<WalletTransactionSumAndCountVo> walletOnceReduceList = queryWalletStatistics(
buildWalletStatisticsDto(dto, WalletBizEnum.UPDATE.getCode(), WalletTransactionSumAndCountDto.TYPE_EXPENSE));
List<WalletTransactionSumAndCountVo> walletRefundList = queryWalletStatistics(
buildWalletStatisticsDto(dto, WalletBizEnum.REFUND.getCode(), null));
List<HardOrderDepartmentStatisticsDetail> payDetails =
hardOrderMapper.listPayStatisticsDetails(buildOrderStatisticsDto(dto, false));
List<HardOrderDepartmentStatisticsDetail> refundDetails =
hardOrderMapper.listRefundStatisticsDetails(buildOrderStatisticsDto(dto, true));
hardOrderMapper.listPayStatisticsDetails(buildPayStatisticsDto(dto));
Map<String, List<HardOrderDepartmentStatisticsDetail>> detailsByBizType = new LinkedHashMap<>();
detailsByBizType.put(WalletBizEnum.PAYMENT.getCode(), payDetails);
detailsByBizType.put(WalletBizEnum.REFUND.getCode(), refundDetails);
Map<String, Map<String, WalletTransactionDepartmentSnapshotVo>> transactionSnapshotMaps =
queryTransactionDepartmentSnapshotMaps(detailsByBizType);
List<HardOrderSumAndCountVo> payList = aggregateByTransactionDepartment(payDetails,
transactionSnapshotMaps.getOrDefault(WalletBizEnum.PAYMENT.getCode(), Map.of()), dto, false);
List<HardOrderSumAndCountVo> refundList = aggregateByTransactionDepartment(refundDetails,
transactionSnapshotMaps.getOrDefault(WalletBizEnum.REFUND.getCode(), Map.of()), dto, true);
List<HardOrderSumAndCountVo> payList = aggregatePaymentsByTransactionDepartment(payDetails,
transactionSnapshotMaps.getOrDefault(WalletBizEnum.PAYMENT.getCode(), Map.of()), dto);
Map<Long, WalletTransactionSumAndCountVo> walletBatchMap = indexWalletStatistics(walletBatchList);
Map<Long, WalletTransactionSumAndCountVo> walletReduceMap = indexWalletStatistics(walletReduceList);
Map<Long, WalletTransactionSumAndCountVo> walletReturnCanteenMap = indexWalletStatistics(walletReturnCanteenList);
Map<Long, WalletTransactionSumAndCountVo> walletOnceBatchMap = indexWalletStatistics(walletOnceBatchList);
Map<Long, WalletTransactionSumAndCountVo> walletOnceReduceMap = indexWalletStatistics(walletOnceReduceList);
Map<Long, WalletTransactionSumAndCountVo> walletRefundMap = indexWalletStatistics(walletRefundList);
Map<Long, HardOrderSumAndCountVo> payMap = indexOrderStatistics(payList);
Map<Long, HardOrderSumAndCountVo> refundMap = indexOrderStatistics(refundList);
List<HardDeptTotalVo> resultList = new ArrayList<>(deptList.size());
for (SysDept dept : deptList) {
@@ -488,7 +485,7 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
ObjectUtil.defaultIfNull(walletOnceReduce.getSum(), 0L))));
statistics.setWalletOnceReduceCount(ObjectUtil.defaultIfNull(walletOnceReduce.getCount(), 0L));
}
applyOrderStatistics(statistics, payMap.get(dept.getDeptId()), refundMap.get(dept.getDeptId()));
applyOrderStatistics(statistics, payMap.get(dept.getDeptId()), walletRefundMap.get(dept.getDeptId()));
resultList.add(statistics);
}
return resultList;
@@ -519,7 +516,7 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
}
/**
* 构建钱包部门统计条件,保证五类钱包统计使用完全一致的权限、时间和钱包类型范围。
* 构建钱包部门统计条件,保证六类钱包统计使用完全一致的权限、时间和钱包类型范围。
*
* @param dto 部门统计页面筛选条件
* @param bizType 钱包流水业务类型编码
@@ -553,46 +550,37 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
}
/**
* 构建硬件订单部门统计条件。退款按售后完成时间统计,并覆盖支付、核销及整单退款状态,
* 因为部分退款不会把原订单状态改成整单退款。
* 构建硬件订单支付部门统计条件。订单金额仍保留非钱包支付能力,钱包支付会在后续按流水历史部门重新归属。
*
* @param dto 部门统计页面筛选条件
* @param refund 是否查询已完成退款
* @return 支付或退款汇总条件
* @return 支付明细查询条件
*/
private HardOrderSumAndCountDto buildOrderStatisticsDto(HardDeptTotalDto dto, boolean refund) {
private HardOrderSumAndCountDto buildPayStatisticsDto(HardDeptTotalDto dto) {
HardOrderSumAndCountDto orderDto = new HardOrderSumAndCountDto();
orderDto.setUserDeptIds(dto.getDeptIds());
orderDto.setIsInclude(dto.getIsInclude());
orderDto.setOrderStatusCodes(HardOrderStatusEnum.getPayPrice());
orderDto.setWalletTypeId(dto.getWalletTypeId());
orderDto.setMchIds(dto.getMchIds());
if (refund) {
orderDto.setRefundStatusCodes(List.of(HardSaleStatusEnum.COMPLETE.getStatus()));
orderDto.setRefundTimeList(dto.getCreateTimeList());
} else {
orderDto.setCreateTimeList(dto.getCreateTimeList());
}
orderDto.setCreateTimeList(dto.getCreateTimeList());
return orderDto;
}
/**
* 将订单或售后明细按钱包流水发生时保存的部门重新归集,并在重新归属后应用页面部门条件。
* 将订单支付明细按钱包流水发生时保存的部门重新归集,并在重新归属后应用页面部门条件。
*
* <p>支付统计中,现金、第三方支付或历史异常记录没有对应钱包流水时,继续使用订单创建时保存的
* 部门快照,避免丢失原有非钱包交易能力。退款统计则与退款记录页面一致,只接受真实 REFUND
* 钱包流水,不允许把没有资金流水的已完成售后回退计入。</p>
* <p>现金、第三方支付或历史异常记录没有对应钱包流水时,继续使用订单创建时保存的部门快照,
* 避免丢失原有非钱包交易能力。退款不再进入本方法,统一由 Wallet 按 REFUND 流水直接汇总。</p>
*
* @param details 符合订单状态、商户和时间条件的支付或售后明细
* @param transactionSnapshotMap 当前业务类型以业务号为键的钱包流水快照
* @param details 符合订单状态、商户和时间条件的支付明细
* @param transactionSnapshotMap 以订单业务号为键的钱包支付流水快照
* @param dto 页面部门包含或排除条件
* @param transactionOnly 是否只统计真实存在的钱包流水,并以钱包流水金额为准
* @return 按最终历史部门归集后的金额和次数
*/
private List<HardOrderSumAndCountVo> aggregateByTransactionDepartment(
private List<HardOrderSumAndCountVo> aggregatePaymentsByTransactionDepartment(
List<HardOrderDepartmentStatisticsDetail> details,
Map<String, WalletTransactionDepartmentSnapshotVo> transactionSnapshotMap,
HardDeptTotalDto dto, boolean transactionOnly) {
HardDeptTotalDto dto) {
if (ObjectUtil.isEmpty(details)) {
return List.of();
}
@@ -604,10 +592,6 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
continue;
}
WalletTransactionDepartmentSnapshotVo snapshot = transactionSnapshotMap.get(detail.getBizId());
if (transactionOnly && (snapshot == null || snapshot.getPrice() == null)) {
// 退款记录以真实 REFUND 钱包流水为准,已完成但没有资金流水的售后单不能回退计入。
continue;
}
Long departmentId = snapshot == null ? detail.getDeptId() : snapshot.getDeptId();
if (departmentId == null || !matchesDepartmentFilter(departmentId, departmentFilter, dto.getIsInclude())) {
continue;
@@ -619,8 +603,7 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
value.setCount(0L);
return value;
});
long amount = transactionOnly
? Math.abs(snapshot.getPrice()) : ObjectUtil.defaultIfNull(detail.getSum(), 0L);
long amount = ObjectUtil.defaultIfNull(detail.getSum(), 0L);
aggregate.setSum(Math.addExact(aggregate.getSum(), amount));
aggregate.setCount(Math.addExact(aggregate.getCount(), ObjectUtil.defaultIfNull(detail.getCount(), 0L)));
}
@@ -628,14 +611,14 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
}
/**
* 将支付和退款明细整理成受控查询组,并用一次或极少数几次 Wallet 调用查询全部流水历史部门。
* 将需要历史归属的交易明细整理成受控查询组,并用一次或极少数几次 Wallet 调用查询流水历史部门。
*
* <p>单个 SQL 查询组最多一万条,单次远程请求最多十组。常规报表只产生一次远程调用,
* 极端大数据量会自动拆分远程请求,避免请求体或 SQL 参数失控。远程失败时终止统计,
* 禁止静默回退造成历史部门和订单部门混合口径。</p>
*
* @param detailsByBizType 以钱包业务类型编码为键的支付订单或退款售后明细
* @return 第一层以业务类型编码为键、第二层以订单或售后单 ID 为键的钱包流水快照
* @param detailsByBizType 以钱包业务类型编码为键的待归属业务明细;部门页当前只传入支付订单
* @return 第一层以业务类型编码为键、第二层以业务 ID 为键的钱包流水快照
* @throws IllegalStateException 钱包历史部门查询失败时抛出
*/
private Map<String, Map<String, WalletTransactionDepartmentSnapshotVo>> queryTransactionDepartmentSnapshotMaps(
@@ -740,7 +723,7 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
}
/**
* 将订单统计按部门建立索引,供支付和退款结果按相同键合并。
* 将订单支付统计按部门建立索引,供支付结果与 Wallet 退款结果按相同键合并。
*
* @param statisticsList 订单部门统计列表
* @return 以部门 ID 为键的统计索引
@@ -758,13 +741,17 @@ public class HardOrderTotalServiceImpl implements HardOrderTotalService {
}
/**
* 合并部门的订单支付与已完成退款,并按原接口口径计算净金额和净次数。
* 合并部门的订单支付与 Wallet 退款流水,并计算净金额和净次数。
*
* <p>退款金额保留 REFUND 流水原始正负号,退款次数按流水条数统计,因此退款到账、退回审核冲正和
* 充值退款扣回与退款记录页面保持完全相同的流水口径。</p>
*
* @param target 部门统计结果
* @param pay 支付统计,允许为空
* @param refund 已完成售后退款统计,允许为空
* @param refund Wallet REFUND 流水统计,允许为空
*/
private void applyOrderStatistics(HardDeptTotalVo target, HardOrderSumAndCountVo pay, HardOrderSumAndCountVo refund) {
private void applyOrderStatistics(HardDeptTotalVo target, HardOrderSumAndCountVo pay,
WalletTransactionSumAndCountVo refund) {
long payPrice = pay == null ? 0L : ObjectUtil.defaultIfNull(pay.getSum(), 0L);
long payCount = pay == null ? 0L : ObjectUtil.defaultIfNull(pay.getCount(), 0L);
long refundPrice = refund == null ? 0L : ObjectUtil.defaultIfNull(refund.getSum(), 0L);
@@ -13,9 +13,11 @@ import com.bsd.pjcloud.hard.service.HardManagerService;
import com.bsd.pjcloud.wallet.api.constant.WalletBizEnum;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotBatchDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionDepartmentSnapshotDto;
import com.bsd.pjcloud.wallet.api.dto.WalletTransactionSumAndCountDto;
import com.bsd.pjcloud.wallet.api.feign.RemoteMchService;
import com.bsd.pjcloud.wallet.api.feign.RemoteWalletTransactionService;
import com.bsd.pjcloud.wallet.api.vo.WalletTransactionDepartmentSnapshotVo;
import com.bsd.pjcloud.wallet.api.vo.WalletTransactionSumAndCountVo;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
@@ -28,6 +30,7 @@ import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -54,7 +57,7 @@ class HardOrderTotalServiceImplTest {
@Mock
private RemoteMchService remoteMchService;
/** 钱包流水远程服务,分别提供普通钱包汇总和交易历史部门快照。 */
/** 钱包流水远程服务,提供普通钱包汇总、REFUND 汇总和支付历史部门快照。 */
@Mock
private RemoteWalletTransactionService remoteWalletTransactionService;
@@ -66,7 +69,7 @@ class HardOrderTotalServiceImplTest {
private HardOrderTotalServiceImpl service;
/**
* 构造真实汇总服务,并让与本用例无关的五类钱包充值统计返回空集合。
* 构造真实汇总服务,并让未单独配置的 Wallet 部门统计返回空集合。
*/
@BeforeEach
void setUp() {
@@ -76,7 +79,8 @@ class HardOrderTotalServiceImplTest {
}
/**
* 验证排除测试部门时以钱包流水部门为准:支付保留非钱包订单快照,退款只统计真实 REFUND 流水。
* 验证排除测试部门时以钱包流水部门为准:支付保留非钱包订单快照,退款直接采用全部 REFUND 流水。
* Wallet 返回的退款净额与条数已经包含分批退款、退回审核冲正和充值退款扣回,Hardware 不得再次按售后状态筛选。
*/
@Test
void pageListDeptFiltersAfterApplyingTransactionDepartmentSnapshot() {
@@ -85,18 +89,12 @@ class HardOrderTotalServiceImplTest {
detail("101", 11L, 9L, 100L),
detail("102", 12L, 1L, 200L),
detail("103", null, 1L, 50L)));
when(hardOrderMapper.listRefundStatisticsDetails(any(HardOrderSumAndCountDto.class))).thenReturn(List.of(
detail("101", 21L, 9L, 35L),
// 同一订单分批退款会产生不同售后单 ID,必须各自计入金额和次数。
detail("104", 21L, 9L, 25L),
detail("102", 22L, 1L, 40L),
detail("203", null, 1L, 10L)));
when(remoteWalletTransactionService.sumAndCount(argThat(walletQuery -> walletQuery != null
&& List.of(WalletBizEnum.REFUND.getCode()).equals(walletQuery.getBizTypes()))))
.thenReturn(R.ok(List.of(walletStatistics(1L, -9L, 3L))));
when(remoteWalletTransactionService.listDepartmentSnapshotsBatch(any())).thenReturn(R.ok(List.of(
snapshot(WalletBizEnum.PAYMENT.getCode(), "101", 11L, 1L, -100L),
snapshot(WalletBizEnum.PAYMENT.getCode(), "102", 12L, 9L, -200L),
snapshot(WalletBizEnum.REFUND.getCode(), "101", 21L, 1L, 30L),
snapshot(WalletBizEnum.REFUND.getCode(), "104", 21L, 1L, 20L),
snapshot(WalletBizEnum.REFUND.getCode(), "102", 22L, 9L, 40L))));
snapshot(WalletBizEnum.PAYMENT.getCode(), "102", 12L, 9L, -200L))));
HardDeptTotalDto query = new HardDeptTotalDto();
query.setDeptIds(List.of(9L));
@@ -108,32 +106,51 @@ class HardOrderTotalServiceImplTest {
HardDeptTotalVo normalDepartment = result.get(0);
assertEquals(BigDecimal.valueOf(150L), normalDepartment.getOrderPayPrice());
assertEquals(2L, normalDepartment.getOrderPayCount());
assertEquals(BigDecimal.valueOf(50L), normalDepartment.getOrderRefundPrice());
assertEquals(2L, normalDepartment.getOrderRefundCont());
assertEquals(BigDecimal.valueOf(100L), normalDepartment.getOrderProfitPrice());
assertEquals(0L, normalDepartment.getOrderProfitCont());
assertEquals(BigDecimal.valueOf(-9L), normalDepartment.getOrderRefundPrice());
assertEquals(3L, normalDepartment.getOrderRefundCont());
assertEquals(BigDecimal.valueOf(159L), normalDepartment.getOrderProfitPrice());
assertEquals(-1L, normalDepartment.getOrderProfitCont());
ArgumentCaptor<WalletTransactionDepartmentSnapshotBatchDto> captor =
ArgumentCaptor.forClass(WalletTransactionDepartmentSnapshotBatchDto.class);
verify(remoteWalletTransactionService).listDepartmentSnapshotsBatch(captor.capture());
verify(remoteWalletTransactionService, never()).listDepartmentSnapshots(any());
verify(hardOrderMapper, never()).listRefundStatisticsDetails(any(HardOrderSumAndCountDto.class));
verify(remoteWalletTransactionService).sumAndCount(argThat(walletQuery -> walletQuery != null
&& List.of(WalletBizEnum.REFUND.getCode()).equals(walletQuery.getBizTypes())
&& walletQuery.getType() == null
&& List.of(9L).equals(walletQuery.getDeptIds())
&& Boolean.FALSE.equals(walletQuery.getIsInclude())));
List<WalletTransactionDepartmentSnapshotDto> queryGroups = captor.getValue().getQueries();
assertEquals(2, queryGroups.size());
assertEquals(1, queryGroups.size());
assertEquals(WalletBizEnum.PAYMENT.getCode(), queryGroups.get(0).getBizType());
assertEquals(List.of("101", "102"), queryGroups.get(0).getBizIds());
assertEquals(List.of(11L, 12L), queryGroups.get(0).getWalletIds());
assertEquals(WalletBizEnum.REFUND.getCode(), queryGroups.get(1).getBizType());
assertEquals(List.of("101", "104", "102"), queryGroups.get(1).getBizIds());
assertEquals(List.of(21L, 22L), queryGroups.get(1).getWalletIds());
}
/**
* 创建一条订单或售后金额明细,部门表示原订单创建时的回退快照。
* 创建 Wallet 已按流水历史部门聚合的金额和条数。
*
* @param bizId 订单 ID 或售后单 ID 字符串
* @param departmentId 流水发生时部门 ID
* @param amount REFUND 流水正负金额之和,单位分
* @param count REFUND 流水条数,包含正向退款与负向冲正或扣回
* @return Wallet 部门流水统计
*/
private WalletTransactionSumAndCountVo walletStatistics(Long departmentId, Long amount, Long count) {
WalletTransactionSumAndCountVo statistics = new WalletTransactionSumAndCountVo();
statistics.setDeptId(departmentId);
statistics.setSum(amount);
statistics.setCount(count);
return statistics;
}
/**
* 创建一条订单支付金额明细,部门表示原订单创建时的回退快照。
*
* @param bizId 订单 ID 字符串
* @param walletId 原订单绑定的钱包 ID;非钱包交易为空
* @param departmentId 原订单部门 ID
* @param amount 支付或退款金额,单位分
* @param amount 支付金额,单位分
* @return 单笔待归属统计明细
*/
private HardOrderDepartmentStatisticsDetail detail(String bizId, Long walletId, Long departmentId, Long amount) {
@@ -150,7 +167,7 @@ class HardOrderTotalServiceImplTest {
* 创建钱包流水发生时固化的部门快照。
*
* @param bizType 钱包流水业务类型持久化编码
* @param bizId 订单 ID 或售后单 ID 字符串
* @param bizId 订单 ID 字符串
* @param walletId 产生流水的钱包 ID
* @param departmentId 流水创建时部门 ID
* @param price 钱包流水实际发生金额,单位分