feat:智能柜手动结算

This commit is contained in:
dzc committed 2026-09-08 15:57:47 +08:00
1 parent 1ecbcf540c
commit 1c86c227ee
5 files changed
+274 -6

No files matched your search

@@ -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)。
@@ -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<Item> 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;
}
}
@@ -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 {
@@ -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<HardOrderEntity> {
*/
Long cancelHardDoorOrder(Long orderId);
/**
* 手动结算智能柜订单(第三方识别不可用时,人工核对视频后结算)
*/
Long manualSettleHardDoorOrder(HardDoorManualSettleDto dto, String operator);
/**
* 手动取消智能柜订单(用户未拿商品)
*/
Long manualCancelHardDoorOrder(Long orderId, String operator);
/**
* 快速扣款
*/
@@ -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<HardOrderMapper, HardOrder
private final HardDoorFlowService hardDoorFlowService;
private final HardBroadcastService hardBroadcastService;
private final HardSpuService hardSpuService;
private final HardSpuJoinService hardSpuJoinService;
@Lazy
private final HardAuthService hardAuthService;
private final HardApptService hardApptService;
private final WechatPayService wechatPayService;
private final AlipayService alipayService;
@@ -1107,17 +1116,24 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
carListDto.add(entity);
});
}
HardTypeEnum sysType = HardTypeEnum.valueOfByType(dto.getSysType());
HardOrderTypeEnum orderType = HardOrderTypeEnum.valueOfByType(dto.getOrderType());
return settleHardDoorOrder(dto, carListDto);
}
/**
* 智能柜订单结算公共流程:算价→写购物车→更新订单→扣库存→支付
*/
private Long settleHardDoorOrder(HardOrderEntity order, List<HardCreateCarDto> 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<HardOrderMapper, HardOrder
HardOrderEntity orderObj = baseMapper.selectById(updateObj.getId());
// 扣减库存
getSelf().fastenerInventory(orderObj, YesNoEnum.YES);
if (ObjectUtil.equals(dto.getOrderType(), HardOrderTypeEnum.TOC_VISITOR.getType())) {
if (ObjectUtil.equals(order.getOrderType(), HardOrderTypeEnum.TOC_VISITOR.getType())) {
HardThirdMiOrderEntity thirdOrder = hardThirdMiOrderMapper.getByOrderId(orderObj.getId());
if (ObjectUtil.isEmpty(thirdOrder)) {
log.error("|{}|智能柜订单结算,第三方免密订单不存在orderId|{}|", TAG, orderObj.getId());
@@ -1136,7 +1152,7 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
boolean isPay = miniAlipayService.miPay(thirdOrder, totalPrice);
if (isPay) {
// 更新订单状态
getSelf().orderSuccess(dto, false);
getSelf().orderSuccess(order, false);
}
}
}
@@ -1175,6 +1191,150 @@ public class HardOrderServiceImpl extends ServiceImpl<HardOrderMapper, HardOrder
return updateObj.getId();
}
/**
* 手动结算智能柜订单(第三方识别不可用时,人工核对视频后结算)
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Long manualSettleHardDoorOrder(HardDoorManualSettleDto dto, String operator) {
if (ObjectUtil.isEmpty(dto) || ObjectUtil.isEmpty(dto.getOrderId()) || ObjectUtil.isEmpty(dto.getItems())) {
throw new RuntimeException("手动结算失败,参数错误");
}
// 查询订单
HardOrderEntity order = baseMapper.selectById(dto.getOrderId());
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("手动结算失败,订单不是待结算状态");
}
// 查询流程
HardDoorFlowVo flow = hardDoorFlowService.getByOrderId(order.getId());
if (ObjectUtil.isEmpty(flow)) {
throw new RuntimeException("手动结算失败,智能柜流程不存在");
}
if (ObjectUtil.isNotEmpty(flow.getPayTime())) {
throw new RuntimeException("手动结算失败,订单已结算");
}
// 校验商品是否在该柜配置内
List<HardSpuJoinVo> spuJoinList = hardSpuJoinService.getSpuList(flow.getHardDoorId(), flow.getCabinetNo());
List<Long> 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<HardCreateCarDto> 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<String, Object> 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) {