feat:退款详情优化

This commit is contained in:
dzc committed 2026-08-27 15:36:41 +08:00
1 parent 440cffb3d5
commit f4ff6bd3a3
6 files changed
+148 -1

No files matched your search

@@ -6,6 +6,7 @@ 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.HardAfterSaleCarEntity;
import com.bsd.pjcloud.hard.api.entity.order.HardAfterSaleEntity;
import com.bsd.pjcloud.hard.api.vo.base.BaseOrderInterface;
import lombok.Data;
@@ -99,4 +100,9 @@ public class HardAfterSaleVo extends HardAfterSaleEntity implements BaseUserInte
private Integer orderPayPrice;
private LocalDateTime orderPayTime;
/**
* 当前售后单申请时保存的商品快照,用于审批详情展示本次退款商品及份数;历史未保存明细时为空列表。
*/
private List<HardAfterSaleCarEntity> carList;
}
@@ -8,11 +8,14 @@ import com.bsd.pjcloud.common.core.constant.enums.MerchantTypeEnum;
import com.bsd.pjcloud.common.core.util.R;
import com.bsd.pjcloud.common.excel.annotation.ResponseExcel;
import com.bsd.pjcloud.common.log.annotation.SysLog;
import com.bsd.pjcloud.common.security.service.PjcloudUser;
import com.bsd.pjcloud.common.security.util.SecurityUtils;
import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum;
import com.bsd.pjcloud.hard.api.dto.order.HardCanteenUserApptQueryDto;
import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenProductTotalVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptOrderVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptStatisticsSummaryVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptStatisticsVo;
import com.bsd.pjcloud.hard.api.vo.order.excel.HardCanteenUserApptExcelVo;
@@ -91,6 +94,37 @@ public class HardCanteenApptTotalController {
return R.ok(canteenApptTotalService.productReservTotal(page, dto, null));
}
/**
* 查询 App 当前可管理食堂范围内的预约菜品及点餐人员统计。
*
* <p>食堂主键不信任客户端输入,而是根据当前商户管理员重新计算;服务层只统计有效预约订单,
* 并按已完成退款扣减后的净菜品份数聚合。</p>
*
* @param query 预约用餐日期范围和餐段筛选条件
* @return 当前账号可管理食堂内的菜品和点餐人员汇总;没有可管理食堂时返回空汇总
*/
@Operation(summary = "App预约菜品人员统计", description = "按当前账号可管理食堂统计预约菜品和点餐人员净份数")
@ApiCryptoAes
@PostMapping("/app/user-reserv-statistics")
public R<HardCanteenUserApptStatisticsSummaryVo> appUserReservationStatistics(
@Valid @RequestBody HardCanteenUserApptQueryDto query) {
PjcloudUser user = SecurityUtils.getUser();
if (user == null || user.getId() == null) {
throw new IllegalStateException("未获取到当前登录用户");
}
R<List<Long>> merchantResponse = remoteMchService
.getSelfMchIdList(user.getId(), List.of(MerchantTypeEnum.CANTEEN));
if (merchantResponse == null || !merchantResponse.isOk()) {
String message = merchantResponse == null ? null : merchantResponse.getMsg();
throw new IllegalStateException(ObjectUtil.isEmpty(message) ? "查询可管理食堂范围失败" : message);
}
List<Long> merchantIds = merchantResponse.getData();
if (ObjectUtil.isEmpty(merchantIds)) {
return R.ok(new HardCanteenUserApptStatisticsSummaryVo());
}
return R.ok(canteenApptTotalService.appUserReservationStatistics(query, merchantIds));
}
/* ************************************* end app end ************************************* */
/**
@@ -136,6 +136,13 @@ public class HardCarController {
}
/* ************************************* end app end ************************************* */
/**
* 分页查询订单原购物车商品,并补充已完成退款、申请中退款和当前剩余份数。
*
* @param page 分页参数
* @param orderId 原订单主键
* @return 原购物车商品分页;订单不存在或未关联购物车时返回空数据
*/
@GetMapping("/order-id")
@Operation(summary = "根据订单id查询购物车")
public R<Page<HardCarVo>> orderId(@ParameterObject Page page, @RequestParam Long orderId) {
@@ -144,7 +151,9 @@ public class HardCarController {
if (ObjectUtil.isEmpty(order) || ObjectUtil.isEmpty(order.getParentCarId())) return R.ok(null);
HardCarDto dto = new HardCarDto();
dto.setParentCarId(order.getParentCarId());
return R.ok(hardCarService.pageJoin(page, dto));
Page<HardCarVo> carPage = hardCarService.pageJoin(page, dto);
hardAfterSaleCarService.fillRefundQuantityStats(order.getParentCarId(), carPage.getRecords());
return R.ok(carPage);
}
@GetMapping("/refund-car")
@@ -6,6 +6,7 @@ import com.bsd.pjcloud.hard.api.dto.order.HardCanteenUserApptQueryDto;
import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenProductTotalVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptOrderVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptStatisticsSummaryVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptVo;
import com.bsd.pjcloud.hard.api.vo.order.HardCanteenUserApptStatisticsVo;
@@ -53,4 +54,14 @@ public interface HardCanteenApptTotalService {
*/
HardCanteenUserApptStatisticsVo userReservationStatistics(HardCanteenUserApptQueryDto query);
/**
* 按 App 当前账号可管理的食堂范围统计有效预约菜品及点餐人员。
*
* @param query 预约用餐日期范围和餐段筛选条件
* @param merchantIds 服务端根据当前账号解析出的可管理食堂主键集合
* @return 已完成退款扣减后的菜品和点餐人员汇总;食堂范围为空时返回空汇总
*/
HardCanteenUserApptStatisticsSummaryVo appUserReservationStatistics(
HardCanteenUserApptQueryDto query, List<Long> merchantIds);
}
@@ -466,6 +466,12 @@ public class HardAfterSaleServiceImpl extends ServiceImpl<HardAfterSaleMapper, H
return true;
}
/**
* 查询订单最新一笔售后详情,并装配该售后单申请时保存的商品快照。
*
* @param dto 售后查询条件,当前详情入口使用订单主键
* @return 最新售后详情;订单没有售后记录时返回 {@code null}
*/
@Override
public HardAfterSaleVo join(HardAfterSaleDto dto) {
List<HardAfterSaleEntity> list = baseMapper.selectList(new LambdaQueryWrapper<HardAfterSaleEntity>()
@@ -475,6 +481,8 @@ public class HardAfterSaleServiceImpl extends ServiceImpl<HardAfterSaleMapper, H
if (ObjectUtil.isEmpty(list)) return null;
HardAfterSaleVo detail = BeanUtils.toBean(list.stream().findFirst().get(), HardAfterSaleVo.class);
if (ObjectUtil.isNotEmpty(detail)) {
// 审批必须查看当前售后单自己的商品快照,不能用订单原购物车数量代替本次退款数量。
detail.setCarList(hardAfterSaleCarService.getByRefundId(detail.getId()));
// 用户
if (ObjectUtil.isNotEmpty(detail.getUserId())) {
R<UserInfo> user = userService.getInnerByUserId(detail.getUserId());
@@ -299,6 +299,85 @@ public class HardCanteenApptTotalServiceImpl implements HardCanteenApptTotalServ
return result;
}
/**
* 按 App 当前账号可管理食堂统计预约菜品及点餐人员。
*
* <p>与 PC 人员预约统计不同,App 商户视角以可管理食堂中的有效订单为主数据源,
* 再按订单用户主键批量补齐姓名和部门,避免商户账号的组织数据权限漏掉实际点餐人员。</p>
*
* @param query 预约用餐日期范围和餐段筛选条件
* @param merchantIds 服务端解析出的可管理食堂主键集合,禁止使用客户端自行声明的范围
* @return 完成退款扣减后的菜品及点餐人员汇总
*/
@Override
public HardCanteenUserApptStatisticsSummaryVo appUserReservationStatistics(
HardCanteenUserApptQueryDto query, List<Long> merchantIds) {
validateQuery(query);
List<Long> scopedMerchantIds = merchantIds == null
? List.of() : merchantIds.stream().filter(Objects::nonNull).distinct().toList();
if (scopedMerchantIds.isEmpty()) {
return new HardCanteenUserApptStatisticsSummaryVo();
}
HardOrderDto orderQuery = buildReservationOrderQuery(query, null);
orderQuery.setMerchantId(null);
orderQuery.setMchIds(scopedMerchantIds);
List<HardOrderVo> orders = retainEffectiveReservationOrders(
hardOrderMapper.scopeList(orderQuery, null), query);
if (orders.isEmpty()) {
return new HardCanteenUserApptStatisticsSummaryVo();
}
List<Long> userIds = orders.stream().map(HardOrderVo::getUserId)
.filter(Objects::nonNull).distinct().toList();
List<UserVO> users = fetchUsersByIds(userIds);
List<HardCanteenUserApptVo> rows = buildRows(users, query, getQueryDates(query), orders).stream()
.filter(row -> row.getOrders() != null && !row.getOrders().isEmpty()).toList();
Map<Long, HardOrderVo> orderMap = orders.stream().filter(order -> order.getId() != null)
.collect(Collectors.toMap(HardOrderVo::getId, Function.identity(), (left, right) -> left));
Map<Long, List<HardCarEntity>> carMap = fetchReservationCars(orders, query);
StatisticsBucket bucket = new StatisticsBucket();
rows.forEach(row -> bucket.addRow(row, orderMap, carMap));
return bucket.toSummary();
}
/**
* 按订单用户主键批量读取不受商户账号组织数据权限裁剪的用户展示摘要。
*
* <p>订单范围已先由可管理食堂限定,因此这里只补齐这些订单关联人员的姓名、账号和部门;
* 若历史用户已删除,则保留一个未命名占位,确保对应订单和菜品份数不会从统计中消失。</p>
*
* @param userIds 可管理食堂有效预约订单中的去重用户主键
* @return 与输入主键顺序一致的用户摘要列表
*/
private List<UserVO> fetchUsersByIds(List<Long> userIds) {
if (userIds.isEmpty()) {
return List.of();
}
Map<Long, UserVO> userMap = new LinkedHashMap<>();
for (int start = 0; start < userIds.size(); start += ORDER_USER_BATCH_SIZE) {
int end = Math.min(start + ORDER_USER_BATCH_SIZE, userIds.size());
List<Long> batchUserIds = new ArrayList<>(userIds.subList(start, end));
R<List<UserVO>> response = remoteUserService.getUserDeptByIds(batchUserIds);
if (response == null || !response.isOk() || response.getData() == null) {
String message = response == null ? null : response.getMsg();
throw new IllegalStateException(StrUtil.isBlank(message) ? "查询点餐人员信息失败" : message);
}
response.getData().stream().filter(user -> user.getUserId() != null)
.forEach(user -> userMap.putIfAbsent(user.getUserId(), user));
}
return userIds.stream().map(userId -> {
UserVO user = userMap.get(userId);
if (user != null) {
return user;
}
UserVO fallback = new UserVO();
fallback.setUserId(userId);
fallback.setName("未命名用户");
return fallback;
}).toList();
}
/**
* 校验必填条件和前后端状态协议值,防止未知值被静默当成“全部”。
*