Explorar el Código

退款处理优化

skyline hace 4 semanas
padre
commit
f82b368608

+ 9 - 1
haha-admin/src/main/java/com/haha/admin/config/MybatisPlusConfig.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.DbType;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.core.config.GlobalConfig;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -26,15 +27,22 @@ public class MybatisPlusConfig {
     }
 
     /**
-     * 分页插件配置
+     * MyBatis-Plus 插件配置
+     * 包含分页插件和乐观锁插件
      */
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+
+        // 分页插件
         PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
         paginationInterceptor.setMaxLimit(500L);
         paginationInterceptor.setOverflow(false);
         interceptor.addInnerInterceptor(paginationInterceptor);
+
+        // 乐观锁插件(自动处理 @Version 注解)
+        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
+
         return interceptor;
     }
 }

+ 35 - 6
haha-admin/src/main/java/com/haha/admin/controller/RefundApplicationController.java

@@ -14,6 +14,8 @@ import com.haha.entity.Order;
 import com.haha.entity.OrderGoods;
 import com.haha.service.OrderGoodsService;
 import com.haha.service.OrderService;
+import com.haha.service.payment.PaymentService;
+import com.haha.service.payment.RefundResult;
 import lombok.Data;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -24,6 +26,10 @@ import java.util.stream.Collectors;
 
 /**
  * 退款申请管理控制器
+ *
+ * 负责退款申请的查询、详情查看、审核等操作。
+ * 审核通过时,同步调用支付渠道退款API(微信/支付宝等),
+ * 而非仅更新数据库状态。
  */
 @Slf4j
 @RestController
@@ -33,6 +39,7 @@ public class RefundApplicationController {
 
     private final OrderService orderService;
     private final OrderGoodsService orderGoodsService;
+    private final PaymentService paymentService;
 
     /**
      * 分页查询退款申请列表
@@ -90,6 +97,11 @@ public class RefundApplicationController {
 
     /**
      * 审核退款申请
+     *
+     * 审核通过时,同步调用 PaymentService.refund() 执行真实的第三方退款操作
+     * (微信退款API / 余额退还等),而非仅更新数据库状态。
+     * 仅当第三方退款成功时,数据库状态才会标记为 REFUNDED。
+     * 审核拒绝时仅更新数据库状态,不涉及第三方调用。
      */
     @RequirePermission("order:update")
     @Log(module = "退款管理", operation = OperationType.UPDATE, summary = "审核退款申请")
@@ -100,12 +112,29 @@ public class RefundApplicationController {
             return Result.error(404, "退款申请不存在");
         }
 
-        order.setRefundStatus(dto.isApproved() ? "REFUNDED" : "REJECTED");
-        order.setRefundReason(dto.getRemark());
-        orderService.updateById(order);
-
-        log.info("退款申请审核完成 - orderId: {}, approved: {}, remark: {}", id, dto.isApproved(), dto.getRemark());
-        return Result.success(dto.isApproved() ? "退款申请已通过" : "退款申请已拒绝", null);
+        if (dto.isApproved()) {
+            // 审核通过:调用真实退款API,PaymentService.refund() 内部会:
+            // 1. 调用微信/支付宝退款接口
+            // 2. 余额支付则退回到用户账户余额
+            // 3. 更新订单退款状态为 REFUNDED
+            log.info("退款申请审核通过,开始执行退款 - orderId: {}, reason: {}", id, dto.getRemark());
+            RefundResult result = paymentService.refund(id, dto.getRemark() != null ? dto.getRemark() : "管理员审核退款");
+
+            if (!result.isSuccess()) {
+                log.error("退款执行失败 - orderId: {}, errorCode: {}, errorMsg: {}",
+                        id, result.getErrorCode(), result.getErrorMsg());
+                return Result.error("退款失败:" + result.getErrorMsg());
+            }
+            log.info("退款执行成功 - orderId: {}, refundId: {}", id, result.getRefundId());
+            return Result.success("退款成功", null);
+        } else {
+            // 审核拒绝:仅更新数据库状态
+            order.setRefundStatus("REJECTED");
+            order.setRefundReason(dto.getRemark());
+            orderService.updateById(order);
+            log.info("退款申请已拒绝 - orderId: {}, remark: {}", id, dto.getRemark());
+            return Result.success("退款申请已拒绝", null);
+        }
     }
 
     /**

+ 5 - 1
haha-common/src/main/java/com/haha/common/enums/DeviceAlertType.java

@@ -1,10 +1,14 @@
 package com.haha.common.enums;
 
+/**
+ * 设备告警类型枚举
+ */
 public enum DeviceAlertType {
 
     OFFLINE(1, "设备离线"),
     WEAK_SIGNAL(2, "信号弱"),
-    BACK_ONLINE(3, "恢复上线");
+    BACK_ONLINE(3, "恢复上线"),
+    ABNORMAL_ORDER(4, "异常订单");
 
     private final int code;
     private final String description;

+ 5 - 4
haha-entity/src/main/java/com/haha/entity/Order.java

@@ -1,9 +1,6 @@
 package com.haha.entity;
 
-import com.baomidou.mybatisplus.annotation.IdType;
-import com.baomidou.mybatisplus.annotation.TableField;
-import com.baomidou.mybatisplus.annotation.TableId;
-import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.annotation.*;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
@@ -19,6 +16,10 @@ public class Order implements Serializable {
     @TableId(type = IdType.ASSIGN_ID)
     private Long id;
 
+    /** 乐观锁版本号 */
+    @Version
+    private Integer version = 1;
+
     private String orderNo;
 
     private String orderName;

+ 10 - 1
haha-miniapp/src/main/java/com/haha/miniapp/config/MybatisPlusConfig.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.annotation.DbType;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.core.config.GlobalConfig;
 import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
+import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
 import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -26,15 +27,23 @@ public class MybatisPlusConfig {
     }
 
     /**
-     * 分页插件配置
+     * MyBatis-Plus 插件配置
+     * 包含分页插件和乐观锁插件
      */
     @Bean
     public MybatisPlusInterceptor mybatisPlusInterceptor() {
         MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
+
+        // 分页插件
         PaginationInnerInterceptor paginationInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
         paginationInterceptor.setMaxLimit(500L);
         paginationInterceptor.setOverflow(false);
         interceptor.addInnerInterceptor(paginationInterceptor);
+
+        // 乐观锁插件(自动处理 @Version 注解)
+        // updateById/update 时会自动校验 version = oldVersion 并自增 version = oldVersion + 1
+        interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
+
         return interceptor;
     }
 }

+ 10 - 0
haha-service/src/main/java/com/haha/service/DeviceAlertService.java

@@ -31,4 +31,14 @@ public interface DeviceAlertService extends IService<DeviceAlertRecord> {
      * @param deviceId 设备编号
      */
     void processBackOnlineNotify(String deviceId);
+
+    /**
+     * 处理异常订单告警(如IN类型-异物放入订单)
+     * 当AI识别到用户向柜内放入异物而非正常取出商品时,触发此告警
+     *
+     * @param deviceId     设备编号
+     * @param orderId      订单ID
+     * @param alertContent 告警内容描述
+     */
+    void processAbnormalOrderAlert(String deviceId, Long orderId, String alertContent);
 }

+ 40 - 0
haha-service/src/main/java/com/haha/service/impl/DeviceAlertServiceImpl.java

@@ -259,6 +259,46 @@ public class DeviceAlertServiceImpl extends ServiceImpl<DeviceAlertRecordMapper,
         stringRedisTemplate.opsForValue().set(key, "1", properties.getCooldownMinutes(), TimeUnit.MINUTES);
     }
 
+    @Override
+    public void processAbnormalOrderAlert(String deviceId, Long orderId, String alertContent) {
+        try {
+            if (!properties.isEnabled()) {
+                log.debug("[设备告警] 告警功能已禁用,跳过异常订单告警 - deviceId: {}", deviceId);
+                return;
+            }
+
+            // 查询设备信息
+            Device device = deviceService.getDeviceBySn(deviceId);
+            if (device == null) {
+                log.warn("[设备告警] 设备不存在,跳过异常订单告警 - deviceId: {}", deviceId);
+                return;
+            }
+
+            String deviceName = device.getName() != null ? device.getName() : deviceId;
+            String shopName = getShopName(device.getShopId());
+            LocalDateTime alertTime = LocalDateTime.now();
+
+            // 构建告警记录
+            DeviceAlertRecord record = new DeviceAlertRecord();
+            record.setDeviceId(deviceId);
+            record.setShopId(device.getShopId());
+            record.setDeviceName(deviceName);
+            record.setAlertType(DeviceAlertType.ABNORMAL_ORDER.getCode());
+            record.setAlertLevel(ALERT_LEVEL_NORMAL); // 普通级别,管理员可在后台审核
+            record.setAlertContent(String.format("订单ID:%d - %s,设备[%s](%s),门店:%s",
+                    orderId, alertContent, deviceName, deviceId, shopName));
+            record.setTriggerSource("SYSTEM");
+            record.setAlertTime(alertTime);
+
+            this.save(record);
+            log.info("[设备告警] 异常订单告警已记录 - deviceId: {}, orderId: {}, content: {}",
+                    deviceId, orderId, alertContent);
+
+        } catch (Exception e) {
+            log.error("[设备告警] 处理异常订单告警异常 - deviceId: {}, orderId: {}", deviceId, orderId, e);
+        }
+    }
+
     /**
      * 获取门店名称
      */

+ 120 - 5
haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

@@ -37,11 +37,16 @@ import org.springframework.transaction.annotation.Transactional;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
 import java.time.LocalDateTime;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
 
 @Slf4j
 @Service
@@ -91,6 +96,12 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
 
     @Override
     public void handleMessage(Map<String, Object> params) {
+        // 签名验证:确保回调来自哈哈平台
+        if (!validateSign(params)) {
+            log.warn("消息回调签名验证失败,忽略该通知");
+            return;
+        }
+
         String notifyType = extractParam(params, "notify_type");
         if (notifyType == null || notifyType.isEmpty()) {
             log.warn("消息回调缺少 notify_type 参数");
@@ -390,6 +401,17 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
             order.setOrderType(recognizeType);
             orderService.updateById(order);
             log.info("订单类型设置成功 - orderId: {}, orderType: {}", order.getId(), recognizeType);
+
+            // IN类型(异物放入)触发告警,提醒管理员审核
+            if ("IN".equals(recognizeType)) {
+                String alertContent = String.format("AI识别到异物放入(IN类型),订单号:%s", order.getOrderNo());
+                log.warn("[异常订单] {} - orderId: {}", alertContent, order.getId());
+                try {
+                    deviceAlertService.processAbnormalOrderAlert(deviceId, order.getId(), alertContent);
+                } catch (Exception e) {
+                    log.error("[异常订单] 触发告警失败 - orderId: {}", order.getId(), e);
+                }
+            }
         }
 
         // 更新订单信息
@@ -446,6 +468,12 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void handleOrderCallback(Map<String, Object> params) {
+        // 签名验证:确保订单回调来自哈哈平台
+        if (!validateSign(params)) {
+            log.warn("订单回调签名验证失败,忽略该通知");
+            return;
+        }
+
         try {
             String orderId = extractParam(params, "order_id");
             String deviceId = extractParam(params, "device_id");
@@ -500,10 +528,86 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
     public boolean validateSign(Map<String, Object> params) {
         String receivedSign = extractParam(params, "sign");
         if (receivedSign == null || receivedSign.isEmpty()) {
-            log.warn("签名参数为空");
+            log.warn("签名参数为空,回调来源可能未经验证");
             return false;
         }
-        return true;
+
+        if (appSecret == null || appSecret.isEmpty()) {
+            log.error("appSecret 未配置,无法验证签名");
+            return false;
+        }
+
+        try {
+            // 标准 HMAC-SHA256 签名验证算法:
+            // 1. 排除 sign、sign_type 等签名相关字段
+            // 2. 剩余参数按 key 字典序升序排列
+            // 3. 拼接为 key1=value1&key2=value2&...&keyN=valueN 格式
+            // 4. 使用 appSecret 作为密钥计算 HMAC-SHA256
+            // 5. 将结果转换为小写十六进制字符串,与 sign 参数比对
+            //
+            // 注意:如果哈哈平台的签名算法不同(如 MD5、拼接方式差异),
+            // 请根据平台文档调整下面的实现。
+
+            String signContent = buildSignContent(params);
+            String computedSign = hmacSha256(signContent, appSecret);
+
+            boolean isValid = computedSign.equalsIgnoreCase(receivedSign);
+            if (!isValid) {
+                log.warn("签名验证失败 - 计算签名: {}, 接收签名: {}, 签名内容: {}",
+                        computedSign, receivedSign, signContent);
+            } else {
+                log.debug("签名验证通过");
+            }
+            return isValid;
+
+        } catch (NoSuchAlgorithmException e) {
+            log.error("HMAC-SHA256 算法不可用", e);
+            return false;
+        } catch (InvalidKeyException e) {
+            log.error("签名密钥无效", e);
+            return false;
+        } catch (Exception e) {
+            log.error("签名验证异常", e);
+            return false;
+        }
+    }
+
+    /**
+     * 构建待签名字符串
+     * 将所有参数(排除 sign, sign_type)按 key 字典序排序,
+     * 拼接为 key=value 格式,以 & 连接
+     */
+    private String buildSignContent(Map<String, Object> params) {
+        return params.entrySet().stream()
+                .filter(e -> !"sign".equals(e.getKey()) && !"sign_type".equals(e.getKey()))
+                .filter(e -> e.getValue() != null)
+                .sorted(Map.Entry.comparingByKey())
+                .map(e -> e.getKey() + "=" + e.getValue().toString())
+                .collect(Collectors.joining("&"));
+    }
+
+    /**
+     * HMAC-SHA256 计算
+     */
+    private String hmacSha256(String data, String key)
+            throws NoSuchAlgorithmException, InvalidKeyException {
+        Mac mac = Mac.getInstance("HmacSHA256");
+        SecretKeySpec secretKeySpec = new SecretKeySpec(
+                key.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
+        mac.init(secretKeySpec);
+        byte[] result = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));
+        return bytesToHex(result);
+    }
+
+    /**
+     * 字节数组转小写十六进制字符串
+     */
+    private String bytesToHex(byte[] bytes) {
+        StringBuilder sb = new StringBuilder();
+        for (byte b : bytes) {
+            sb.append(String.format("%02x", b));
+        }
+        return sb.toString();
     }
 
     private void saveDeviceStatusToRedis(String deviceId, String activityId, String doorStatus, String openType, String outUserId) {
@@ -681,6 +785,17 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
             order.setOrderType("OUT");
         }
 
+        // IN类型订单触发告警(基于订单名称判断)
+        if ("IN".equals(order.getOrderType())) {
+            String alertContent = String.format("订单回调识别为IN类型(异物放入),订单名称:%s", orderName);
+            log.warn("[异常订单] {} - orderId: {}, orderNo: {}", alertContent, order.getId(), orderId);
+            try {
+                deviceAlertService.processAbnormalOrderAlert(order.getDeviceId(), order.getId(), alertContent);
+            } catch (Exception e) {
+                log.error("[异常订单] 触发告警失败 - orderId: {}", order.getId(), e);
+            }
+        }
+
         if (orderDetail != null) {
             order.setItems(orderDetail);
         }

+ 29 - 3
haha-service/src/main/java/com/haha/service/payment/impl/PaymentServiceImpl.java

@@ -3,6 +3,7 @@ package com.haha.service.payment.impl;
 import com.haha.common.constant.OrderConstants;
 import com.haha.common.enums.PaymentChannel;
 import com.haha.entity.Order;
+import com.haha.service.AccountService;
 import com.haha.service.OrderService;
 import com.haha.service.payment.*;
 import lombok.extern.slf4j.Slf4j;
@@ -29,6 +30,9 @@ public class PaymentServiceImpl implements PaymentService {
     
     @Autowired
     private OrderService orderService;
+
+    @Autowired
+    private AccountService accountService;
     
     @Override
     public PaymentResult createPayment(Long orderId, PaymentChannel channel, Map<String, String> extra) {
@@ -195,13 +199,14 @@ public class PaymentServiceImpl implements PaymentService {
                     .build();
         }
         
-        // 8. 构建退款请求
+        // 8. 构建退款请求(使用实付金额 paidAmount,避免多退优惠券部分)
+        BigDecimal actualRefundAmount = order.getPaidAmount() != null ? order.getPaidAmount() : order.getTotalAmount();
         String refundNo = generateRefundNo(order.getOrderNo());
         RefundRequest refundRequest = RefundRequest.builder()
                 .orderId(orderId)
                 .orderNo(order.getOrderNo())
                 .transactionId(order.getTransactionId())
-                .refundAmount(order.getTotalAmount())
+                .refundAmount(actualRefundAmount)
                 .totalAmount(order.getTotalAmount())
                 .reason(reason)
                 .refundNo(refundNo)
@@ -464,14 +469,35 @@ public class PaymentServiceImpl implements PaymentService {
 
     /**
      * 更新订单退款状态
+     *
+     * 处理逻辑:
+     * 1. 如果是余额支付,同步将退款金额退还到用户账户余额
+     * 2. 退款金额使用实付金额 (paidAmount),而非订单总金额 (totalAmount)
+     * 3. 更新订单退款相关字段
      */
     private void updateOrderRefundStatus(Order order, String reason, String refundId) {
+        BigDecimal refundAmount = order.getPaidAmount() != null ? order.getPaidAmount() : order.getTotalAmount();
+
+        // 如果是余额支付,将退款金额退还到用户账户
+        if (PaymentChannel.BALANCE.getCode().equals(order.getPayChannel())) {
+            boolean balanceUpdated = accountService.updateBalance(order.getUserId(), refundAmount);
+            if (balanceUpdated) {
+                log.info("余额退款 - 用户 {} 余额已增加: {}元, orderId={}",
+                        order.getUserId(), refundAmount, order.getId());
+            } else {
+                log.error("余额退款失败 - 用户 {} 余额退还失败: {}元, orderId={}",
+                        order.getUserId(), refundAmount, order.getId());
+            }
+        }
+
         order.setRefundStatus("REFUNDED");
-        order.setRefundAmount(order.getTotalAmount());
+        order.setRefundAmount(refundAmount);
         order.setRefundTime(LocalDateTime.now());
         order.setRefundReason(reason);
         order.setPayStatus(OrderConstants.PAY_STATUS_REFUND);
         orderService.updateById(order);
+        log.info("订单退款状态已更新 - orderId={}, refundAmount={}, payStatus={}",
+                order.getId(), refundAmount, OrderConstants.PAY_STATUS_REFUND);
     }
     
     /**