diff --git a/db/20260908_智能柜手动结算权限待添加.txt b/db/20260908_智能柜手动结算权限待添加.txt new file mode 100644 index 000000000..49c60dd4c --- /dev/null +++ b/db/20260908_智能柜手动结算权限待添加.txt @@ -0,0 +1,49 @@ +================================================================ +智能柜订单手动结算(2026-09-08)—— 权限 手动添加清单 +================================================================ +说明:第三方(旷世)识别不可用后,智能柜免密订单(payType=9 + 设备类型4) +需要在售后订单详情页人工核对视频后手动结算/取消。以下权限需在 pjcloudx +主库 sys_menu 手动添加(菜单管理界面配置或执行文末幂等 SQL)。 + +---------------------------------------------------------------- +一、按钮权限(sys_menu,menu_type='2') +---------------------------------------------------------------- +挂载位置建议:挂在「售后订单详情」可访问的角色/菜单下(订单详情页为 +动态路由 order-refund,无独立菜单,建议挂在 hard 订单相关父菜单下, +与 hard_hardwareOrder_view 同级或同父)。 + +1. hard_hardwareOrder_manualSettle 订单-智能柜手动结算 + (售后订单详情页「手动结算」区块:手动结算 + 未拿商品取消订单,共用一个权限) + +---------------------------------------------------------------- +二、页面菜单 +---------------------------------------------------------------- +无新增页面(功能内嵌在已有售后订单详情页 order-refund 中),无需配菜单。 + +---------------------------------------------------------------- +三、字典 +---------------------------------------------------------------- +无新增字典。 + +---------------------------------------------------------------- +四、附:可直接执行的幂等 SQL(按需使用,执行前请备份) +---------------------------------------------------------------- +-- 按钮权限(挂在 hard_hardwareOrder_view 所在的父菜单下) +SET @menu_id_base = (SELECT GREATEST(COALESCE(MAX(`menu_id`), 0), 2026090801000000000) FROM `pjcloudx`.`sys_menu`); +INSERT INTO `pjcloudx`.`sys_menu` +(`menu_id`, `name`, `permission`, `parent_id`, `visible`, `sort_order`, `keep_alive`, `embedded`, `menu_type`, `create_by`, `create_time`, `del_flag`, `tenant_id`) +SELECT @menu_id_base + ROW_NUMBER() OVER (ORDER BY p.`tenant_id`, p.`menu_id`), + '智能柜手动结算', 'hard_hardwareOrder_manualSettle', p.`menu_id`, '1', 20, '0', '0', '2', 'system', NOW(), '0', p.`tenant_id` +FROM `pjcloudx`.`sys_menu` p +WHERE p.`permission` = 'hard_hardwareOrder_view' AND p.`del_flag` = '0' + AND NOT EXISTS ( + SELECT 1 FROM `pjcloudx`.`sys_menu` e + WHERE e.`tenant_id` = p.`tenant_id` AND e.`permission` = 'hard_hardwareOrder_manualSettle' AND e.`del_flag` = '0' + ); +SET @menu_id_base = NULL; + +---------------------------------------------------------------- +五、数据库迁移 +---------------------------------------------------------------- +无(复用 hard_door_flow.skuCallbackVo / skuCallbackTime 字段记录手动操作留痕, +不涉及表结构变更,无需执行任何 SQL)。 diff --git a/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardDoorManualSettleDto.java b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardDoorManualSettleDto.java new file mode 100644 index 000000000..451932475 --- /dev/null +++ b/pjcloud-hardware/pjcloud-hardware-api/src/main/java/com/bsd/pjcloud/hard/api/dto/order/HardDoorManualSettleDto.java @@ -0,0 +1,29 @@ +package com.bsd.pjcloud.hard.api.dto.order; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.util.List; + +/** + * 智能柜订单手动结算(第三方识别不可用时,人工核对视频后结算) + */ +@Data +public class HardDoorManualSettleDto { + + @Schema(description = "订单id", requiredMode = Schema.RequiredMode.REQUIRED) + private Long orderId; + + @Schema(description = "手动选择的商品列表", requiredMode = Schema.RequiredMode.REQUIRED) + private List items; + + @Data + public static class Item { + + @Schema(description = "智能柜商品id(hard_spu主键)", requiredMode = Schema.RequiredMode.REQUIRED) + private Long productId; + + @Schema(description = "数量", requiredMode = Schema.RequiredMode.REQUIRED) + private Integer productCount; + } +} diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java index 2cb12191f..02efec9d2 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/controller/order/HardOrderController.java @@ -30,6 +30,7 @@ import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum; import com.bsd.pjcloud.hard.api.dto.app.HardAppCreateOrderScancode; import com.bsd.pjcloud.hard.api.dto.hard.CurrentUserHardTypeInfoDto; import com.bsd.pjcloud.hard.api.dto.hard.HardOfflinePayDto; +import com.bsd.pjcloud.hard.api.dto.order.HardDoorManualSettleDto; import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; import com.bsd.pjcloud.hard.api.dto.order.HardOrderPreferencesDto; import com.bsd.pjcloud.hard.api.dto.order.act.HardCreateActOrderDto; @@ -324,6 +325,24 @@ public class HardOrderController { return R.ok(); } + @Operation(summary = "智能柜订单手动结算", description = "第三方识别不可用时,人工核对视频后结算智能柜订单") + @PostMapping("/manual-settle") + @PreAuthorize("@pms.hasPermission('hard_hardwareOrder_manualSettle')") + public R manualSettle(@RequestBody HardDoorManualSettleDto dto) { + PjcloudUser user = SecurityUtils.getUser(); + hardwareOrderService.manualSettleHardDoorOrder(dto, user.getUsername()); + return R.ok(); + } + + @Operation(summary = "智能柜订单手动取消", description = "智能柜订单用户未拿商品时手动取消订单") + @PostMapping("/manual-cancel") + @PreAuthorize("@pms.hasPermission('hard_hardwareOrder_manualSettle')") + public R manualCancel(@RequestBody HardDoorManualSettleDto dto) { + PjcloudUser user = SecurityUtils.getUser(); + hardwareOrderService.manualCancelHardDoorOrder(dto.getOrderId(), user.getUsername()); + return R.ok(); + } + @GetMapping("/export") @PreAuthorize("@pms.hasPermission('hard_hardwareOrder_export')") public void export(HttpServletResponse response, HardOrderDto dto) throws IOException { diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java index e56753472..6190b25f3 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/HardOrderService.java @@ -15,6 +15,7 @@ import com.bsd.pjcloud.hard.api.dto.hard.HandCancelCodeDto; import com.bsd.pjcloud.hard.api.dto.hard.HardCreateOrderDto; import com.bsd.pjcloud.hard.api.dto.hard.HardDoneHardQrCodeScanOrderDto; import com.bsd.pjcloud.hard.api.dto.order.HardCreateCarDto; +import com.bsd.pjcloud.hard.api.dto.order.HardDoorManualSettleDto; import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; import com.bsd.pjcloud.hard.api.dto.order.act.HardCreateActOrderDto; import com.bsd.pjcloud.hard.api.dto.order.app.HardAppCreateFarmerOrderDto; @@ -119,6 +120,16 @@ public interface HardOrderService extends IService { */ Long cancelHardDoorOrder(Long orderId); + /** + * 手动结算智能柜订单(第三方识别不可用时,人工核对视频后结算) + */ + Long manualSettleHardDoorOrder(HardDoorManualSettleDto dto, String operator); + + /** + * 手动取消智能柜订单(用户未拿商品) + */ + Long manualCancelHardDoorOrder(Long orderId, String operator); + /** * 快速扣款 */ diff --git a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java index 10606711a..275e3b856 100644 --- a/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java +++ b/pjcloud-hardware/pjcloud-hardware-biz/src/main/java/com/bsd/pjcloud/hard/service/impl/order/HardOrderServiceImpl.java @@ -29,6 +29,7 @@ import com.bsd.pjcloud.common.data.datascope.DataScope; import com.bsd.pjcloud.common.security.service.PjcloudUser; import com.bsd.pjcloud.hard.api.constant.HardProductNatureEnum; import com.bsd.pjcloud.hard.api.constant.HardTypeEnum; +import com.bsd.pjcloud.hard.api.constant.flow.HardDoorFlowEnum; import com.bsd.pjcloud.hard.api.constant.order.HardOrderStatusEnum; import com.bsd.pjcloud.hard.api.constant.order.HardOrderTypeEnum; import com.bsd.pjcloud.hard.api.constant.order.HardPayTypeEnum; @@ -39,6 +40,7 @@ import com.bsd.pjcloud.hard.api.dto.hard.HardDoneHardQrCodeScanOrderDto; import com.bsd.pjcloud.hard.api.dto.hard.HandCancelCodeDto; import com.bsd.pjcloud.hard.api.dto.hard.HardCreateOrderDto; import com.bsd.pjcloud.hard.api.dto.order.HardCreateCarDto; +import com.bsd.pjcloud.hard.api.dto.order.HardDoorManualSettleDto; import com.bsd.pjcloud.hard.api.dto.order.HardOrderDto; import com.bsd.pjcloud.hard.api.dto.order.act.HardCreateActOrderDto; import com.bsd.pjcloud.hard.api.dto.order.app.HardAppCreateFarmerOrderDto; @@ -49,6 +51,8 @@ import com.bsd.pjcloud.hard.api.dto.spu.DeductionSpuStockDto; import com.bsd.pjcloud.hard.api.dto.wechat.WechantReverseDto; import com.bsd.pjcloud.hard.api.dto.wechat.WechatMicropayDto; import com.bsd.pjcloud.hard.api.entity.HardApptEntity; +import com.bsd.pjcloud.hard.api.entity.HardAuthEntity; +import com.bsd.pjcloud.hard.api.entity.HardDoorFlowEntity; import com.bsd.pjcloud.hard.api.entity.HardThirdMiOrderEntity; import com.bsd.pjcloud.hard.api.entity.manager.HardManagerEntity; import com.bsd.pjcloud.hard.api.entity.order.HardCarEntity; @@ -60,6 +64,7 @@ import com.bsd.pjcloud.hard.api.vo.manager.HardManagerVo; import com.bsd.pjcloud.hard.api.vo.order.HardCarVo; import com.bsd.pjcloud.hard.api.vo.order.HardCreateHardQrCodeScanOrderVo; import com.bsd.pjcloud.hard.api.vo.order.HardOrderVo; +import com.bsd.pjcloud.hard.api.vo.spu.HardSpuJoinVo; import com.bsd.pjcloud.hard.api.vo.wechat.WechatMicropayVo; import com.bsd.pjcloud.hard.mapper.HardThirdMiOrderMapper; import com.bsd.pjcloud.hard.mapper.order.HardOrderMapper; @@ -109,6 +114,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.TimeUnit; @@ -134,6 +140,9 @@ public class HardOrderServiceImpl extends ServiceImpl carListDto) { + HardTypeEnum sysType = HardTypeEnum.valueOfByType(order.getSysType()); + HardOrderTypeEnum orderType = HardOrderTypeEnum.valueOfByType(order.getOrderType()); // 获取商品价格 Integer totalPrice = hardCarService.computerCarTotalPrice(sysType, orderType, carListDto); // 添加购物车 - Long parentCarId = hardCarService.createCard(dto.getUserId(), dto.getMerchantId(), sysType, orderType, carListDto); + Long parentCarId = hardCarService.createCard(order.getUserId(), order.getMerchantId(), sysType, orderType, carListDto); // 获取商品描述 String subject = hardCarService.computerCarSubject(sysType, orderType, carListDto); // 更新订单信息 HardOrderEntity updateObj = HardOrderEntity.builder() - .id(dto.getId()) + .id(order.getId()) .parentCarId(parentCarId) .totalPrice(totalPrice) .payPrice(totalPrice) @@ -1128,7 +1144,7 @@ public class HardOrderServiceImpl extends ServiceImpl spuJoinList = hardSpuJoinService.getSpuList(flow.getHardDoorId(), flow.getCabinetNo()); + List joinSpuIds = ObjectUtil.isEmpty(spuJoinList) ? List.of() : spuJoinList.stream() + .filter(v -> ObjectUtil.isNotEmpty(v.getSpuDetail())) + .map(v -> v.getSpuDetail().getId()) + .toList(); + for (HardDoorManualSettleDto.Item item : dto.getItems()) { + if (ObjectUtil.isEmpty(item.getProductId()) || ObjectUtil.isEmpty(item.getProductCount()) || item.getProductCount() <= 0) { + throw new RuntimeException("手动结算失败,商品数量必须大于0"); + } + if (!joinSpuIds.contains(item.getProductId())) { + throw new RuntimeException("手动结算失败,商品不在该柜配置内"); + } + } + // 构建购物车 + List carListDto = new ArrayList<>(); + for (HardDoorManualSettleDto.Item item : dto.getItems()) { + HardCreateCarDto entity = new HardCreateCarDto(); + entity.setProductId(item.getProductId()); + entity.setProductNature(HardProductNatureEnum.DEFAULT); + entity.setProductCount(item.getProductCount()); + carListDto.add(entity); + } + // 结算(算价→写购物车→更新订单→扣库存→支付) + Long orderId = settleHardDoorOrder(order, carListDto); + // 复查订单状态,游客免密订单支付宝扣款失败时回滚本地数据 + HardOrderEntity settleOrder = baseMapper.selectById(orderId); + if (ObjectUtil.isEmpty(settleOrder) || !ObjectUtil.equals(settleOrder.getOrderStatus(), HardOrderStatusEnum.PAY.getType())) { + throw new RuntimeException("手动结算失败,订单支付未成功(支付宝免密扣款失败)"); + } + // 更新流程 + HardDoorFlowEntity flowUpdate = HardDoorFlowEntity.builder() + .id(flow.getId()) + .flowStatus(HardDoorFlowEnum.FLOW_PAY_ORDER.getType()) + .payTime(LocalDateTime.now()) + .skuCallbackTime(LocalDateTime.now()) + .build(); + if (ObjectUtil.isEmpty(flow.getSkuCallbackVo())) { + flowUpdate.setSkuCallbackVo(buildManualDoorFlowVo(dto, operator, false)); + } + hardDoorFlowService.updateById(flowUpdate); + // 完成授权流程 + if (ObjectUtil.isNotEmpty(flow.getAuthId())) { + HardAuthEntity authUpdate = HardAuthEntity.builder() + .id(flow.getAuthId()) + .isDone(YesNoEnum.YES.getCode()) + .isDoneTime(LocalDateTime.now()) + .build(); + hardAuthService.updateById(authUpdate); + } + log.info("|{}|智能柜订单手动结算成功orderId|{}|operator|{}|items|{}|", TAG, orderId, operator, JSONUtil.toJsonStr(dto.getItems())); + return orderId; + } + + /** + * 手动取消智能柜订单(用户未拿商品) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public Long manualCancelHardDoorOrder(Long orderId, String operator) { + if (ObjectUtil.isEmpty(orderId)) { + throw new RuntimeException("手动取消失败,参数错误"); + } + // 查询订单 + HardOrderEntity order = baseMapper.selectById(orderId); + if (ObjectUtil.isEmpty(order)) { + throw new RuntimeException("手动取消失败,订单不存在"); + } + if (!ObjectUtil.equals(order.getPayType(), HardPayTypeEnum.HARD_DOOR_FREE_PAY.getType()) + || !ObjectUtil.equals(order.getSysType(), HardTypeEnum.DOOR.getType())) { + throw new RuntimeException("手动取消失败,订单不是智能柜免密支付订单"); + } + if (!ObjectUtil.equals(order.getOrderStatus(), HardOrderStatusEnum.CREATE.getType())) { + throw new RuntimeException("手动取消失败,订单不是待结算状态"); + } + // 取消订单 + Long cancelId = cancelHardDoorOrder(orderId); + if (ObjectUtil.isEmpty(cancelId)) { + throw new RuntimeException("手动取消失败,订单取消未成功"); + } + // 更新流程 + HardDoorFlowVo flow = hardDoorFlowService.getByOrderId(orderId); + if (ObjectUtil.isNotEmpty(flow)) { + HardDoorFlowEntity flowUpdate = HardDoorFlowEntity.builder() + .id(flow.getId()) + .flowStatus(HardDoorFlowEnum.FLOW_CALLBACK_RECOGNITION.getType()) + .skuCallbackTime(LocalDateTime.now()) + .build(); + if (ObjectUtil.isEmpty(flow.getSkuCallbackVo())) { + HardDoorManualSettleDto manualDto = new HardDoorManualSettleDto(); + manualDto.setOrderId(orderId); + flowUpdate.setSkuCallbackVo(buildManualDoorFlowVo(manualDto, operator, true)); + } + hardDoorFlowService.updateById(flowUpdate); + } + log.info("|{}|智能柜订单手动取消成功orderId|{}|operator|{}|", TAG, orderId, operator); + return cancelId; + } + + /** + * 构建智能柜手动结算/取消的流程留痕(写入 skuCallbackVo) + */ + private String buildManualDoorFlowVo(HardDoorManualSettleDto dto, String operator, Boolean isCancel) { + Map manualVo = new LinkedHashMap<>(); + manualVo.put("manual", true); + manualVo.put("manualCancel", isCancel); + manualVo.put("operator", operator); + manualVo.put("time", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + if (ObjectUtil.isNotEmpty(dto)) { + manualVo.put("orderId", dto.getOrderId()); + manualVo.put("items", dto.getItems()); + } + return JSONUtil.toJsonStr(manualVo); + } + @Override @Transactional(rollbackFor = Exception.class) public void fastReduceBalance(HardOrderEntity order) {