فهرست منبع

支付模块重构

skyline 2 ماه پیش
والد
کامیت
68fedb24be
37فایلهای تغییر یافته به همراه4065 افزوده شده و 80 حذف شده
  1. 2 2
      haha-admin-mp/src/utils/config.ts
  2. 9 9
      haha-admin-web/package.json
  3. 18 0
      haha-admin-web/pnpm-lock.yaml
  4. 50 0
      haha-common/src/main/java/com/haha/common/enums/PaymentChannel.java
  5. 34 1
      haha-entity/src/main/java/com/haha/entity/Order.java
  6. 12 48
      haha-miniapp/src/main/java/com/haha/miniapp/controller/CallbackController.java
  7. 366 0
      haha-miniapp/src/main/java/com/haha/miniapp/controller/PayScoreController.java
  8. 408 0
      haha-miniapp/src/main/java/com/haha/miniapp/controller/PaymentController.java
  9. 5 1
      haha-miniapp/src/main/resources/application.yml
  10. 3 0
      haha-miniapp/src/main/resources/cert/apiclient_key.pem
  11. 27 0
      haha-miniapp/src/main/resources/sql/order.sql
  12. 6 0
      haha-service/pom.xml
  13. 40 0
      haha-service/src/main/java/com/haha/service/OrderService.java
  14. 172 19
      haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java
  15. 56 0
      haha-service/src/main/java/com/haha/service/payment/PaymentCallbackResult.java
  16. 67 0
      haha-service/src/main/java/com/haha/service/payment/PaymentChannelFactory.java
  17. 65 0
      haha-service/src/main/java/com/haha/service/payment/PaymentRequest.java
  18. 44 0
      haha-service/src/main/java/com/haha/service/payment/PaymentResult.java
  19. 49 0
      haha-service/src/main/java/com/haha/service/payment/PaymentService.java
  20. 52 0
      haha-service/src/main/java/com/haha/service/payment/PaymentStrategy.java
  21. 48 0
      haha-service/src/main/java/com/haha/service/payment/RefundRequest.java
  22. 32 0
      haha-service/src/main/java/com/haha/service/payment/RefundResult.java
  23. 163 0
      haha-service/src/main/java/com/haha/service/payment/config/WxPayConfig.java
  24. 59 0
      haha-service/src/main/java/com/haha/service/payment/impl/AliCreditPayStrategy.java
  25. 58 0
      haha-service/src/main/java/com/haha/service/payment/impl/AliPayStrategy.java
  26. 391 0
      haha-service/src/main/java/com/haha/service/payment/impl/PaymentServiceImpl.java
  27. 429 0
      haha-service/src/main/java/com/haha/service/payment/impl/WxPayStrategy.java
  28. 68 0
      haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreCallbackResult.java
  29. 20 0
      haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreCancelRequest.java
  30. 74 0
      haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreCompleteRequest.java
  31. 94 0
      haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreCreateRequest.java
  32. 68 0
      haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreResult.java
  33. 91 0
      haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreService.java
  34. 41 0
      haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreStrategy.java
  35. 350 0
      haha-service/src/main/java/com/haha/service/payment/payscore/impl/PayScoreServiceImpl.java
  36. 586 0
      haha-service/src/main/java/com/haha/service/payment/payscore/impl/WxPayScoreStrategy.java
  37. 8 0
      pom.xml

+ 2 - 2
haha-admin-mp/src/utils/config.ts

@@ -3,7 +3,7 @@
  */
 
 // API基础路径
-export const API_BASE_URL = 'http://localhost:8080/api/admin';
+export const API_BASE_URL = 'http://localhost:7077/api/admin';
 
 // 是否使用Mock数据
 export const USE_MOCK = true;
@@ -15,4 +15,4 @@ export const APP_NAME = '哈哈运营平台';
 export const UPLOAD_URL = `${API_BASE_URL}/upload`;
 
 // 图片预览地址前缀
-export const IMAGE_BASE_URL = 'http://localhost:8080';
+export const IMAGE_BASE_URL = 'http://localhost:7077';

+ 9 - 9
haha-admin-web/package.json

@@ -1,12 +1,12 @@
 {
-  "name": "vue-pure-admin",
-  "version": "6.3.0",
+  "name": "fuyu-restaurant-admin",
+  "version": "1.0.0",
   "private": true,
   "type": "module",
   "scripts": {
-    "dev": "NODE_OPTIONS=--max-old-space-size=4096 vite",
+    "dev": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite",
     "serve": "pnpm dev",
-    "build": "rimraf dist && NODE_OPTIONS=--max-old-space-size=8192 vite build && generate-version-file",
+    "build": "rimraf dist && cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build && generate-version-file",
     "build:staging": "rimraf dist && vite build --mode staging",
     "report": "rimraf dist && vite build",
     "preview": "vite preview",
@@ -22,23 +22,22 @@
     "preinstall": "npx only-allow pnpm"
   },
   "keywords": [
-    "vue-pure-admin",
+    "fuyu-restaurant-admin",
     "element-plus",
     "tailwindcss",
-    "pure-admin",
     "typescript",
     "pinia",
     "vue3",
     "vite",
     "esm"
   ],
-  "homepage": "https://github.com/pure-admin/vue-pure-admin",
+  "homepage": "",
   "repository": {
     "type": "git",
-    "url": "git+https://github.com/pure-admin/vue-pure-admin.git"
+    "url": ""
   },
   "bugs": {
-    "url": "https://github.com/pure-admin/vue-pure-admin/issues"
+    "url": ""
   },
   "license": "MIT",
   "author": {
@@ -137,6 +136,7 @@
     "@vitejs/plugin-vue-jsx": "^5.1.2",
     "boxen": "^8.0.1",
     "code-inspector-plugin": "^1.3.0",
+    "cross-env": "^10.1.0",
     "cssnano": "^7.1.2",
     "dagre": "^0.8.5",
     "eslint": "^9.39.1",

+ 18 - 0
haha-admin-web/pnpm-lock.yaml

@@ -273,6 +273,9 @@ importers:
       code-inspector-plugin:
         specifier: ^1.3.0
         version: 1.3.0
+      cross-env:
+        specifier: ^10.1.0
+        version: 10.1.0
       cssnano:
         specifier: ^7.1.2
         version: 7.1.2(postcss@8.5.6)
@@ -725,6 +728,9 @@ packages:
     peerDependencies:
       vue: ^3.2.0
 
+  '@epic-web/invariant@1.0.0':
+    resolution: {integrity: sha512-lrTPqgvfFQtR/eY/qkIzp98OGdNJu0m5ji3q/nJI8v3SXkRKEnWiOxMmbvcSoAIzv/cGiuvRy57k4suKQSAdwA==}
+
   '@esbuild/aix-ppc64@0.24.2':
     resolution: {integrity: sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==}
     engines: {node: '>=18'}
@@ -2580,6 +2586,11 @@ packages:
   cropperjs@1.6.2:
     resolution: {integrity: sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==}
 
+  cross-env@10.1.0:
+    resolution: {integrity: sha512-GsYosgnACZTADcmEyJctkJIoqAhHjttw7RsFrVoJNXbsWWqaq6Ym+7kZjq6mS45O0jij6vtiReppKQEtqWy6Dw==}
+    engines: {node: '>=20'}
+    hasBin: true
+
   cross-spawn@7.0.6:
     resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
     engines: {node: '>= 8'}
@@ -6035,6 +6046,8 @@ snapshots:
     dependencies:
       vue: 3.5.25(typescript@5.9.3)
 
+  '@epic-web/invariant@1.0.0': {}
+
   '@esbuild/aix-ppc64@0.24.2':
     optional: true
 
@@ -7930,6 +7943,11 @@ snapshots:
 
   cropperjs@1.6.2: {}
 
+  cross-env@10.1.0:
+    dependencies:
+      '@epic-web/invariant': 1.0.0
+      cross-spawn: 7.0.6
+
   cross-spawn@7.0.6:
     dependencies:
       path-key: 3.1.1

+ 50 - 0
haha-common/src/main/java/com/haha/common/enums/PaymentChannel.java

@@ -0,0 +1,50 @@
+package com.haha.common.enums;
+
+import lombok.Getter;
+
+/**
+ * 支付渠道枚举
+ */
+@Getter
+public enum PaymentChannel {
+
+    WECHAT("wechat", "微信支付"),
+    ALIPAY("alipay", "支付宝支付"),
+    ALIPAY_CREDIT("alipay_credit", "支付宝信用支付"),
+    BALANCE("balance", "余额支付"),
+    WECHAT_PAYSCORE("wechat_payscore", "微信支付分");
+
+    private final String code;
+    private final String description;
+
+    PaymentChannel(String code, String description) {
+        this.code = code;
+        this.description = description;
+    }
+
+    /**
+     * 根据code获取对应的枚举值
+     * @param code 支付渠道编码
+     * @return 对应的枚举值,未匹配返回null
+     */
+    public static PaymentChannel fromCode(String code) {
+        if (code == null) {
+            return null;
+        }
+        for (PaymentChannel channel : values()) {
+            if (channel.code.equals(code)) {
+                return channel;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 判断code是否有效
+     * @param code 支付渠道编码
+     * @return 是否有效
+     */
+    public static boolean isValid(String code) {
+        return fromCode(code) != null;
+    }
+}

+ 34 - 1
haha-entity/src/main/java/com/haha/entity/Order.java

@@ -32,9 +32,15 @@ public class Order implements Serializable {
 
     private String payStatus;
 
-    @TableField(exist = false)
+    /** 支付方式 */
     private String payType;
 
+    /** 支付渠道(wechat/alipay/alipay_credit/balance) */
+    private String payChannel;
+
+    /** 第三方支付平台交易号 */
+    private String transactionId;
+
     private String videoUrl;
 
     private BigDecimal confidence;
@@ -45,6 +51,33 @@ public class Order implements Serializable {
 
     private LocalDateTime payTime;
 
+    /** 退款状态 */
+    private String refundStatus;
+
+    /** 退款金额 */
+    private BigDecimal refundAmount;
+
+    /** 退款时间 */
+    private LocalDateTime refundTime;
+
+    /** 退款原因 */
+    private String refundReason;
+
+    /** 支付分服务订单号(微信侧) */
+    private String payScoreOrderId;
+
+    /** 支付分订单状态: CREATED/DOING/USER_PAYING/DONE */
+    private String payScoreState;
+
+    /** 支付分服务ID */
+    private String serviceId;
+
+    /** 服务开始时间 */
+    private LocalDateTime serviceStartTime;
+
+    /** 服务结束时间 */
+    private LocalDateTime serviceEndTime;
+
     private Integer status;
 
     @TableField(exist = false)

+ 12 - 48
haha-miniapp/src/main/java/com/haha/miniapp/controller/CallbackController.java

@@ -25,8 +25,6 @@ import org.springframework.web.bind.annotation.RestController;
 import jakarta.servlet.http.HttpServletRequest;
 import java.io.BufferedReader;
 import java.math.BigDecimal;
-import java.math.RoundingMode;
-import java.time.LocalDateTime;
 import java.util.HashMap;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
@@ -352,60 +350,26 @@ public class CallbackController {
             }
 
             // 解析resource_info获取视频URL
-            String videoUrl = null;
             if (resourceInfoStr != null && !resourceInfoStr.isEmpty()) {
                 JSONObject resourceInfo = JSON.parseObject(resourceInfoStr);
-                videoUrl = resourceInfo.getString("video_url");
+                String videoUrl = resourceInfo.getString("video_url");
                 log.info("视频URL: {}", videoUrl);
             }
 
-            // 1. 计算订单金额
-            BigDecimal totalAmount = BigDecimal.ZERO;
-            JSONArray skuList = null;
-            if (skuListStr != null && !skuListStr.isEmpty()) {
-                skuList = JSON.parseArray(skuListStr);
-                if (skuList != null) {
-                    for (int i = 0; i < skuList.size(); i++) {
-                        JSONObject sku = skuList.getJSONObject(i);
-                        BigDecimal price = sku.getBigDecimal("price");
-                        Integer quantity = sku.getInteger("quantity");
-                        if (price != null && quantity != null) {
-                            totalAmount = totalAmount.add(price.multiply(BigDecimal.valueOf(quantity)));
-                        }
-                    }
-                    totalAmount = totalAmount.setScale(2, RoundingMode.HALF_UP);
-                    log.info("订单金额计算完成: {}, 商品数量: {}", totalAmount, skuList.size());
-                }
+            // 解析 confidence
+            BigDecimal confidence = null;
+            if (params.get("confidence") != null) {
+                try {
+                    confidence = new BigDecimal(params.get("confidence").toString());
+                } catch (NumberFormatException ignored) {}
             }
 
-            // 2. 生成订单记录
-            if (userId != null && deviceId != null && totalAmount.compareTo(BigDecimal.ZERO) > 0) {
-                Order order = new Order();
-                order.setActivityId(activityId);
-                order.setUserId(Long.parseLong(userId));
-                order.setDeviceId(deviceId);
-                order.setTotalAmount(totalAmount);
-                order.setItems(skuListStr);
-                order.setStatus(OrderConstants.STATUS_PENDING_PAYMENT);
-                order.setPayStatus(OrderConstants.PAY_STATUS_UNPAID);
-                order.setCreateTime(LocalDateTime.now());
-                if (videoUrl != null) {
-                    order.setVideoUrl(videoUrl);
-                }
-                if (params.get("confidence") != null) {
-                    try {
-                        order.setConfidence(new BigDecimal(params.get("confidence").toString()));
-                    } catch (NumberFormatException ignored) {}
-                }
+            // 委托 Service 层创建订单
+            Order order = orderService.createOrderFromRecognition(
+                activityId, deviceId, userId, skuListStr, resourceInfoStr, confidence);
 
-                boolean saved = orderService.save(order);
-                if (saved) {
-                    log.info("订单创建成功 - 订单ID: {}, 用户: {}, 金额: {}", order.getId(), userId, totalAmount);
-                } else {
-                    log.error("订单创建失败 - 用户: {}, 设备: {}", userId, deviceId);
-                }
-            } else {
-                log.warn("订单创建跳过 - 缺少必要参数: userId={}, deviceId={}, amount={}", userId, deviceId, totalAmount);
+            if (order != null) {
+                log.info("AI识别订单创建成功 - 订单ID: {}", order.getId());
             }
 
             // 3. 优惠信息处理

+ 366 - 0
haha-miniapp/src/main/java/com/haha/miniapp/controller/PayScoreController.java

@@ -0,0 +1,366 @@
+package com.haha.miniapp.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import cn.dev33.satoken.stp.StpUtil;
+import com.alibaba.fastjson2.JSON;
+import com.haha.common.vo.Result;
+import com.haha.entity.Order;
+import com.haha.service.OrderService;
+import com.haha.service.payment.payscore.*;
+import com.haha.service.payment.payscore.PayScoreCreateRequest.PostPayment;
+import com.haha.service.payment.payscore.PayScoreCompleteRequest.PostDiscount;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.BufferedReader;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 微信支付分控制器
+ * 提供支付分服务订单的创建、完结、取消、查询等 API
+ */
+@Slf4j
+@RestController
+@RequestMapping("/payscore")
+@RequiredArgsConstructor
+public class PayScoreController {
+
+    private final PayScoreService payScoreService;
+    private final OrderService orderService;
+
+    /**
+     * 创建支付分服务订单
+     * POST /api/payscore/create
+     *
+     * @param params 请求参数
+     *   - orderId: 订单ID(必填)
+     *   - openId: 用户微信openId(必填)
+     *   - riskFundAmount: 风险金额(可选,默认99元)
+     *   - riskFundName: 风险金名称(可选,DEPOSIT/ADVANCE/CASH_DEPOSIT)
+     * @return 支付分结果(含小程序调起支付分所需的package参数)
+     */
+    @PostMapping("/create")
+    public Result<Map<String, Object>> createPayScoreOrder(@RequestBody Map<String, Object> params) {
+        try {
+            Long userId = StpUtil.getLoginIdAsLong();
+            log.info("用户 {} 请求创建支付分服务订单", userId);
+
+            if (!params.containsKey("orderId")) {
+                return Result.error(400, "参数错误:orderId 不能为空");
+            }
+            if (!params.containsKey("openId")) {
+                return Result.error(400, "参数错误:openId 不能为空");
+            }
+
+            Long orderId = Long.valueOf(params.get("orderId").toString());
+            String openId = params.get("openId").toString();
+
+            Order order = orderService.getById(orderId);
+            if (order == null) {
+                return Result.error(404, "订单不存在");
+            }
+            if (!order.getUserId().equals(userId)) {
+                return Result.error(403, "无权操作该订单");
+            }
+
+            BigDecimal riskFundAmount = null;
+            if (params.containsKey("riskFundAmount")) {
+                riskFundAmount = new BigDecimal(params.get("riskFundAmount").toString());
+            }
+
+            String riskFundName = null;
+            if (params.containsKey("riskFundName")) {
+                riskFundName = params.get("riskFundName").toString();
+            }
+
+            PayScoreResult result;
+            if (riskFundAmount != null || riskFundName != null) {
+                result = payScoreService.createPayScoreOrder(orderId, openId, riskFundAmount, riskFundName);
+            } else {
+                result = payScoreService.createPayScoreOrder(orderId, openId);
+            }
+
+            if (result.isSuccess()) {
+                Map<String, Object> data = new HashMap<>();
+                data.put("outOrderNo", result.getOutOrderNo());
+                data.put("state", result.getState());
+                data.put("package", result.getPackageStr());
+                log.info("创建支付分服务订单成功 - orderId: {}, outOrderNo: {}", orderId, result.getOutOrderNo());
+                return Result.success("创建成功", data);
+            } else {
+                log.warn("创建支付分服务订单失败 - orderId: {}, errorCode: {}, errorMsg: {}", 
+                        orderId, result.getErrorCode(), result.getErrorMsg());
+                return Result.error(500, result.getErrorMsg());
+            }
+
+        } catch (NumberFormatException e) {
+            log.error("参数格式错误", e);
+            return Result.error(400, "参数格式错误");
+        } catch (Exception e) {
+            log.error("创建支付分服务订单失败", e);
+            return Result.error("创建失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 完结支付分服务订单
+     * POST /api/payscore/complete
+     *
+     * @param params 请求参数
+     *   - orderId: 订单ID(必填)
+     *   - totalAmount: 实际扣款总金额(必填)
+     *   - payments: 后付费项目列表(可选)
+     *   - discounts: 商户优惠列表(可选)
+     * @return 支付分结果
+     */
+    @PostMapping("/complete")
+    public Result<Map<String, Object>> completePayScoreOrder(@RequestBody Map<String, Object> params) {
+        try {
+            Long userId = StpUtil.getLoginIdAsLong();
+            log.info("用户 {} 请求完结支付分服务订单", userId);
+
+            if (!params.containsKey("orderId")) {
+                return Result.error(400, "参数错误:orderId 不能为空");
+            }
+            if (!params.containsKey("totalAmount")) {
+                return Result.error(400, "参数错误:totalAmount 不能为空");
+            }
+
+            Long orderId = Long.valueOf(params.get("orderId").toString());
+            BigDecimal totalAmount = new BigDecimal(params.get("totalAmount").toString());
+
+            Order order = orderService.getById(orderId);
+            if (order == null) {
+                return Result.error(404, "订单不存在");
+            }
+            if (!order.getUserId().equals(userId)) {
+                return Result.error(403, "无权操作该订单");
+            }
+
+            List<PostPayment> payments = null;
+            if (params.containsKey("payments")) {
+                payments = JSON.parseArray(params.get("payments").toString(), PostPayment.class);
+            }
+
+            List<PostDiscount> discounts = null;
+            if (params.containsKey("discounts")) {
+                discounts = JSON.parseArray(params.get("discounts").toString(), PostDiscount.class);
+            }
+
+            PayScoreResult result = payScoreService.completePayScoreOrder(orderId, totalAmount, payments, discounts);
+
+            if (result.isSuccess()) {
+                Map<String, Object> data = new HashMap<>();
+                data.put("outOrderNo", result.getOutOrderNo());
+                data.put("state", result.getState());
+                log.info("完结支付分服务订单成功 - orderId: {}, state: {}", orderId, result.getState());
+                return Result.success("完结成功", data);
+            } else {
+                log.warn("完结支付分服务订单失败 - orderId: {}, errorCode: {}, errorMsg: {}", 
+                        orderId, result.getErrorCode(), result.getErrorMsg());
+                return Result.error(500, result.getErrorMsg());
+            }
+
+        } catch (NumberFormatException e) {
+            log.error("参数格式错误", e);
+            return Result.error(400, "参数格式错误");
+        } catch (Exception e) {
+            log.error("完结支付分服务订单失败", e);
+            return Result.error("完结失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 取消支付分服务订单
+     * POST /api/payscore/cancel
+     *
+     * @param params 请求参数
+     *   - orderId: 订单ID(必填)
+     *   - reason: 取消原因(可选)
+     * @return 支付分结果
+     */
+    @PostMapping("/cancel")
+    public Result<Map<String, Object>> cancelPayScoreOrder(@RequestBody Map<String, Object> params) {
+        try {
+            Long userId = StpUtil.getLoginIdAsLong();
+            log.info("用户 {} 请求取消支付分服务订单", userId);
+
+            if (!params.containsKey("orderId")) {
+                return Result.error(400, "参数错误:orderId 不能为空");
+            }
+
+            Long orderId = Long.valueOf(params.get("orderId").toString());
+            String reason = params.containsKey("reason") ? params.get("reason").toString() : "用户取消";
+
+            Order order = orderService.getById(orderId);
+            if (order == null) {
+                return Result.error(404, "订单不存在");
+            }
+            if (!order.getUserId().equals(userId)) {
+                return Result.error(403, "无权操作该订单");
+            }
+
+            PayScoreResult result = payScoreService.cancelPayScoreOrder(orderId, reason);
+
+            if (result.isSuccess()) {
+                Map<String, Object> data = new HashMap<>();
+                data.put("outOrderNo", result.getOutOrderNo());
+                data.put("state", result.getState());
+                log.info("取消支付分服务订单成功 - orderId: {}", orderId);
+                return Result.success("取消成功", data);
+            } else {
+                log.warn("取消支付分服务订单失败 - orderId: {}, errorCode: {}, errorMsg: {}", 
+                        orderId, result.getErrorCode(), result.getErrorMsg());
+                return Result.error(500, result.getErrorMsg());
+            }
+
+        } catch (NumberFormatException e) {
+            log.error("参数格式错误", e);
+            return Result.error(400, "参数格式错误");
+        } catch (Exception e) {
+            log.error("取消支付分服务订单失败", e);
+            return Result.error("取消失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 查询支付分服务订单状态
+     * GET /api/payscore/query/{orderId}
+     *
+     * @param orderId 订单ID
+     * @return 支付分订单状态
+     */
+    @GetMapping("/query/{orderId}")
+    public Result<Map<String, Object>> queryPayScoreOrder(@PathVariable Long orderId) {
+        try {
+            Long userId = StpUtil.getLoginIdAsLong();
+            log.info("用户 {} 查询支付分服务订单状态 - orderId: {}", userId, orderId);
+
+            Order order = orderService.getById(orderId);
+            if (order == null) {
+                return Result.error(404, "订单不存在");
+            }
+            if (!order.getUserId().equals(userId)) {
+                return Result.error(403, "无权访问该订单");
+            }
+
+            if (order.getPayScoreOrderId() == null) {
+                return Result.error(400, "该订单未创建支付分服务订单");
+            }
+
+            PayScoreResult result = payScoreService.queryPayScoreOrder(order.getPayScoreOrderId());
+
+            if (result.isSuccess()) {
+                Map<String, Object> data = new HashMap<>();
+                data.put("outOrderNo", result.getOutOrderNo());
+                data.put("state", result.getState());
+                data.put("totalAmount", result.getTotalAmount());
+                log.info("查询支付分服务订单成功 - orderId: {}, state: {}", orderId, result.getState());
+                return Result.success("查询成功", data);
+            } else {
+                log.warn("查询支付分服务订单失败 - orderId: {}, errorCode: {}, errorMsg: {}", 
+                        orderId, result.getErrorCode(), result.getErrorMsg());
+                return Result.error(500, result.getErrorMsg());
+            }
+
+        } catch (Exception e) {
+            log.error("查询支付分服务订单失败", e);
+            return Result.error("查询失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 同步订单支付分状态
+     * POST /api/payscore/sync/{orderId}
+     *
+     * @param orderId 订单ID
+     * @return 操作结果
+     */
+    @PostMapping("/sync/{orderId}")
+    public Result<Void> syncPayScoreStatus(@PathVariable Long orderId) {
+        try {
+            Long userId = StpUtil.getLoginIdAsLong();
+            log.info("用户 {} 同步支付分状态 - orderId: {}", userId, orderId);
+
+            Order order = orderService.getById(orderId);
+            if (order == null) {
+                return Result.error(404, "订单不存在");
+            }
+            if (!order.getUserId().equals(userId)) {
+                return Result.error(403, "无权操作该订单");
+            }
+
+            boolean success = payScoreService.syncPayScoreStatus(orderId);
+
+            if (success) {
+                log.info("同步支付分状态成功 - orderId: {}", orderId);
+                return Result.success("同步成功", null);
+            } else {
+                return Result.error(500, "同步失败");
+            }
+
+        } catch (Exception e) {
+            log.error("同步支付分状态失败", e);
+            return Result.error("同步失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 支付分回调通知
+     * POST /api/payscore/callback
+     *
+     * 注意:回调接口不需要 token 验证
+     *
+     * @param request HTTP请求
+     * @return 回调响应
+     */
+    @SaIgnore
+    @PostMapping("/callback")
+    public String handleCallback(HttpServletRequest request) {
+        try {
+            log.info("收到支付分回调通知");
+
+            StringBuilder sb = new StringBuilder();
+            try (BufferedReader reader = request.getReader()) {
+                String line;
+                while ((line = reader.readLine()) != null) {
+                    sb.append(line);
+                }
+            }
+            String body = sb.toString();
+
+            Map<String, Object> params = new HashMap<>();
+            params.put("body", body);
+            params.put("Wechatpay-Timestamp", request.getHeader("Wechatpay-Timestamp"));
+            params.put("Wechatpay-Nonce", request.getHeader("Wechatpay-Nonce"));
+            params.put("Wechatpay-Signature", request.getHeader("Wechatpay-Signature"));
+            params.put("Wechatpay-Serial", request.getHeader("Wechatpay-Serial"));
+            params.put("Wechatpay-Signature-Type", request.getHeader("Wechatpay-Signature-Type"));
+
+            log.info("支付分回调请求头: Timestamp={}, Nonce={}, Serial={}", 
+                    params.get("Wechatpay-Timestamp"), 
+                    params.get("Wechatpay-Nonce"), 
+                    params.get("Wechatpay-Serial"));
+
+            PayScoreCallbackResult result = payScoreService.handlePayScoreCallback(params);
+
+            if (result.isSuccess()) {
+                log.info("支付分回调处理成功 - outOrderNo: {}, eventType: {}, state: {}", 
+                        result.getOutOrderNo(), result.getEventType(), result.getState());
+                return "{\"code\": \"SUCCESS\", \"message\": \"成功\"}";
+            } else {
+                log.warn("支付分回调处理失败 - errorMsg: {}", result.getErrorMsg());
+                return "{\"code\": \"FAIL\", \"message\": \"处理失败\"}";
+            }
+
+        } catch (Exception e) {
+            log.error("处理支付分回调失败", e);
+            return "{\"code\": \"FAIL\", \"message\": \"处理失败\"}";
+        }
+    }
+}

+ 408 - 0
haha-miniapp/src/main/java/com/haha/miniapp/controller/PaymentController.java

@@ -0,0 +1,408 @@
+package com.haha.miniapp.controller;
+
+import cn.dev33.satoken.annotation.SaIgnore;
+import com.alibaba.fastjson2.JSON;
+import com.haha.common.enums.PaymentChannel;
+import com.haha.common.vo.Result;
+import com.haha.service.payment.PaymentCallbackResult;
+import com.haha.service.payment.PaymentResult;
+import com.haha.service.payment.PaymentService;
+import com.haha.service.payment.RefundResult;
+import com.haha.service.payment.payscore.PayScoreCallbackResult;
+import com.haha.service.payment.payscore.PayScoreService;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.BufferedReader;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 统一支付控制器
+ * 提供统一的支付API,支持多渠道支付(微信、支付宝等)
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/payment")
+public class PaymentController {
+
+    @Autowired
+    private PaymentService paymentService;
+
+    @Autowired
+    private PayScoreService payScoreService;
+
+    /**
+     * 创建支付
+     * POST /api/payment/create
+     *
+     * 请求参数:
+     * - orderId: 订单ID(必填)
+     * - channel: 支付渠道代码(必填,如 wechat/alipay/alipay_credit/balance)
+     * - openId: 微信openId(微信支付必填)
+     * - buyerId: 支付宝buyerId(支付宝支付必填)
+     *
+     * @param params 支付参数
+     * @return 支付结果
+     */
+    @PostMapping("/create")
+    public Result<PaymentResult> createPayment(@RequestBody Map<String, Object> params) {
+        try {
+            // 1. 提取参数
+            if (!params.containsKey("orderId")) {
+                return Result.error(400, "参数错误:orderId 不能为空");
+            }
+            if (!params.containsKey("channel")) {
+                return Result.error(400, "参数错误:channel 不能为空");
+            }
+
+            Long orderId = Long.valueOf(params.get("orderId").toString());
+            String channelCode = params.get("channel").toString();
+
+            // 2. 转换 channel 字符串为 PaymentChannel 枚举
+            PaymentChannel channel = PaymentChannel.fromCode(channelCode);
+            if (channel == null) {
+                return Result.error(400, "不支持的支付渠道: " + channelCode);
+            }
+
+            // 3. 构建 extra Map
+            Map<String, String> extra = new HashMap<>();
+            if (params.containsKey("openId")) {
+                extra.put("openId", params.get("openId").toString());
+            }
+            if (params.containsKey("buyerId")) {
+                extra.put("buyerId", params.get("buyerId").toString());
+            }
+
+            log.info("创建支付请求 - 订单ID: {}, 渠道: {}", orderId, channel.getDescription());
+
+            // 4. 调用 paymentService.createPayment()
+            PaymentResult result = paymentService.createPayment(orderId, channel, extra);
+
+            // 5. 返回结果
+            if (result.isSuccess()) {
+                log.info("创建支付成功 - 订单ID: {}, 预支付ID: {}", orderId, result.getPrepayId());
+                return Result.success("创建支付成功", result);
+            } else {
+                log.warn("创建支付失败 - 订单ID: {}, 错误: {}", orderId, result.getErrorMsg());
+                return Result.error(500, result.getErrorMsg());
+            }
+
+        } catch (NumberFormatException e) {
+            log.error("参数格式错误", e);
+            return Result.error(400, "参数格式错误:orderId 必须为数字");
+        } catch (Exception e) {
+            log.error("创建支付失败", e);
+            return Result.error("创建支付失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 支付回调(按渠道路由)
+     * POST /api/payment/callback/{channel}
+     *
+     * 注意:回调接口不需要 token 验证,使用 @SaIgnore
+     *
+     * @param channel       支付渠道代码
+     * @param request       HTTP请求
+     * @param requestParams 请求参数(form格式)
+     * @return 回调响应
+     */
+    @SaIgnore
+    @PostMapping("/callback/{channel}")
+    public String handleCallback(
+            @PathVariable String channel,
+            HttpServletRequest request,
+            @RequestParam(required = false) Map<String, String> requestParams) {
+        try {
+            log.info("收到支付回调 - 渠道: {}", channel);
+
+            Map<String, Object> params = new HashMap<>();
+
+            if ("wechat_payscore".equalsIgnoreCase(channel)) {
+                StringBuilder sb = new StringBuilder();
+                try (BufferedReader reader = request.getReader()) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        sb.append(line);
+                    }
+                }
+                String body = sb.toString();
+
+                params.put("body", body);
+                params.put("Wechatpay-Timestamp", request.getHeader("Wechatpay-Timestamp"));
+                params.put("Wechatpay-Nonce", request.getHeader("Wechatpay-Nonce"));
+                params.put("Wechatpay-Signature", request.getHeader("Wechatpay-Signature"));
+                params.put("Wechatpay-Serial", request.getHeader("Wechatpay-Serial"));
+                params.put("Wechatpay-Signature-Type", request.getHeader("Wechatpay-Signature-Type"));
+
+                log.info("微信支付分回调 - Timestamp: {}, Nonce: {}, Serial={}", 
+                        params.get("Wechatpay-Timestamp"), 
+                        params.get("Wechatpay-Nonce"), 
+                        params.get("Wechatpay-Serial"));
+
+                PayScoreCallbackResult result = payScoreService.handlePayScoreCallback(params);
+
+                if (result.isSuccess()) {
+                    log.info("支付分回调处理成功 - outOrderNo: {}, eventType: {}, state: {}", 
+                            result.getOutOrderNo(), result.getEventType(), result.getState());
+                    return "{\"code\": \"SUCCESS\", \"message\": \"成功\"}";
+                } else {
+                    log.warn("支付分回调处理失败 - errorMsg: {}", result.getErrorMsg());
+                    return "{\"code\": \"FAIL\", \"message\": \"处理失败\"}";
+                }
+            }
+
+            if ("wechat".equalsIgnoreCase(channel)) {
+                StringBuilder sb = new StringBuilder();
+                try (BufferedReader reader = request.getReader()) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        sb.append(line);
+                    }
+                }
+                String body = sb.toString();
+                
+                params.put("body", body);
+                params.put("Wechatpay-Timestamp", request.getHeader("Wechatpay-Timestamp"));
+                params.put("Wechatpay-Nonce", request.getHeader("Wechatpay-Nonce"));
+                params.put("Wechatpay-Signature", request.getHeader("Wechatpay-Signature"));
+                params.put("Wechatpay-Serial", request.getHeader("Wechatpay-Serial"));
+                params.put("Wechatpay-Signature-Type", request.getHeader("Wechatpay-Signature-Type"));
+                
+                if (body != null && !body.isEmpty()) {
+                    try {
+                        Map<String, Object> jsonParams = JSON.parseObject(body, Map.class);
+                        params.putAll(jsonParams);
+                    } catch (Exception ignored) {
+                    }
+                }
+                log.info("微信V3回调 - Timestamp: {}, Nonce: {}, Serial: {}", 
+                        params.get("Wechatpay-Timestamp"), 
+                        params.get("Wechatpay-Nonce"), 
+                        params.get("Wechatpay-Serial"));
+            } else {
+                params = parseRequestParams(request, requestParams);
+            }
+            log.info("支付回调参数: {}", JSON.toJSONString(params));
+
+            PaymentChannel paymentChannel = PaymentChannel.fromCode(channel);
+            if (paymentChannel == null) {
+                log.warn("不支持的支付渠道: {}", channel);
+                return buildCallbackErrorResponse(channel);
+            }
+
+            PaymentCallbackResult result = paymentService.handleCallback(paymentChannel, params);
+
+            if (result.isSuccess()) {
+                log.info("支付回调处理成功 - 订单号: {}, 交易号: {}", result.getOrderNo(), result.getTransactionId());
+                return buildCallbackSuccessResponse(paymentChannel);
+            } else {
+                log.warn("支付回调处理失败 - 错误: {}", result.getErrorMsg());
+                return buildCallbackErrorResponse(channel);
+            }
+
+        } catch (Exception e) {
+            log.error("处理支付回调失败", e);
+            return buildCallbackErrorResponse(channel);
+        }
+    }
+
+    /**
+     * 退款
+     * POST /api/payment/refund
+     *
+     * 请求参数:
+     * - orderId: 订单ID(必填)
+     * - reason: 退款原因(可选)
+     *
+     * @param params 退款参数
+     * @return 退款结果
+     */
+    @PostMapping("/refund")
+    public Result<RefundResult> refund(@RequestBody Map<String, Object> params) {
+        try {
+            // 提取 orderId, reason
+            if (!params.containsKey("orderId")) {
+                return Result.error(400, "参数错误:orderId 不能为空");
+            }
+
+            Long orderId = Long.valueOf(params.get("orderId").toString());
+            String reason = params.containsKey("reason") ? params.get("reason").toString() : "用户申请退款";
+
+            log.info("退款请求 - 订单ID: {}, 原因: {}", orderId, reason);
+
+            // 调用 paymentService.refund()
+            RefundResult result = paymentService.refund(orderId, reason);
+
+            if (result.isSuccess()) {
+                log.info("退款成功 - 订单ID: {}, 退款单号: {}", orderId, result.getRefundId());
+                return Result.success("退款成功", result);
+            } else {
+                log.warn("退款失败 - 订单ID: {}, 错误: {}", orderId, result.getErrorMsg());
+                return Result.error(500, result.getErrorMsg());
+            }
+
+        } catch (NumberFormatException e) {
+            log.error("参数格式错误", e);
+            return Result.error(400, "参数格式错误:orderId 必须为数字");
+        } catch (Exception e) {
+            log.error("退款失败", e);
+            return Result.error("退款失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 获取可用支付渠道
+     * GET /api/payment/channels
+     *
+     * @return 支付渠道列表
+     */
+    @GetMapping("/channels")
+    public Result<List<Map<String, String>>> getChannels() {
+        try {
+            // 调用 paymentService.getAvailableChannels()
+            List<PaymentChannel> channels = paymentService.getAvailableChannels();
+
+            // 转换为 [{code: "wechat", name: "微信支付"}, ...] 格式
+            List<Map<String, String>> channelList = channels.stream()
+                    .map(channel -> {
+                        Map<String, String> map = new HashMap<>();
+                        map.put("code", channel.getCode());
+                        map.put("name", channel.getDescription());
+                        return map;
+                    })
+                    .collect(Collectors.toList());
+
+            log.info("获取可用支付渠道 - 数量: {}", channelList.size());
+            return Result.success("获取成功", channelList);
+
+        } catch (Exception e) {
+            log.error("获取支付渠道失败", e);
+            return Result.error("获取支付渠道失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 查询支付状态
+     * GET /api/payment/query/{orderId}
+     *
+     * @param orderId 订单ID
+     * @return 支付状态
+     */
+    @GetMapping("/query/{orderId}")
+    public Result<PaymentResult> queryPayment(@PathVariable Long orderId) {
+        try {
+            log.info("查询支付状态 - 订单ID: {}", orderId);
+
+            // 调用 paymentService.queryPaymentStatus()
+            PaymentResult result = paymentService.queryPaymentStatus(orderId);
+
+            if (result.isSuccess()) {
+                return Result.success("查询成功", result);
+            } else {
+                return Result.error(500, result.getErrorMsg());
+            }
+
+        } catch (Exception e) {
+            log.error("查询支付状态失败", e);
+            return Result.error("查询支付状态失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 解析请求参数,支持 JSON 和 form-urlencoded 两种格式
+     *
+     * @param request       HTTP请求对象
+     * @param requestParams form表单参数
+     * @return 参数Map
+     */
+    private Map<String, Object> parseRequestParams(HttpServletRequest request, Map<String, String> requestParams) {
+        Map<String, Object> params = new HashMap<>();
+
+        String contentType = request.getContentType();
+        log.debug("请求Content-Type: {}", contentType);
+
+        try {
+            // 判断 Content-Type
+            if (contentType != null && contentType.toLowerCase().contains("application/json")) {
+                // JSON 格式:从 request body 读取
+                StringBuilder sb = new StringBuilder();
+                try (BufferedReader reader = request.getReader()) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        sb.append(line);
+                    }
+                }
+                String body = sb.toString();
+                if (body != null && !body.isEmpty()) {
+                    params = JSON.parseObject(body, Map.class);
+                    log.debug("解析JSON参数: {}", params.size());
+                }
+            } else if (contentType != null && contentType.toLowerCase().contains("text/xml")) {
+                // XML 格式(微信支付回调):从 request body 读取原始XML
+                StringBuilder sb = new StringBuilder();
+                try (BufferedReader reader = request.getReader()) {
+                    String line;
+                    while ((line = reader.readLine()) != null) {
+                        sb.append(line);
+                    }
+                }
+                String body = sb.toString();
+                if (body != null && !body.isEmpty()) {
+                    params.put("xmlBody", body);
+                    log.debug("解析XML参数");
+                }
+            } else {
+                // form-urlencoded 格式:使用 @RequestParam 捕获的参数
+                if (requestParams != null && !requestParams.isEmpty()) {
+                    params.putAll(requestParams);
+                    log.debug("解析表单参数: {}", params.size());
+                }
+            }
+        } catch (Exception e) {
+            log.error("解析请求参数失败", e);
+        }
+
+        return params;
+    }
+
+    /**
+     * 构建回调成功响应
+     * 根据不同支付渠道返回不同格式的成功响应
+     *
+     * @param channel 支付渠道
+     * @return 成功响应
+     */
+    private String buildCallbackSuccessResponse(PaymentChannel channel) {
+        if (channel == PaymentChannel.WECHAT) {
+            // 微信支付 V3 返回 JSON 格式
+            return "{\"code\": \"SUCCESS\", \"message\": \"成功\"}";
+        } else if (channel == PaymentChannel.ALIPAY || channel == PaymentChannel.ALIPAY_CREDIT) {
+            // 支付宝返回 "success" 字符串
+            return "success";
+        } else {
+            // 其他渠道默认返回 "success"
+            return "success";
+        }
+    }
+
+    /**
+     * 构建回调失败响应
+     *
+     * @param channelCode 支付渠道代码
+     * @return 失败响应
+     */
+    private String buildCallbackErrorResponse(String channelCode) {
+        if ("wechat".equalsIgnoreCase(channelCode)) {
+            // 微信支付 V3 返回 JSON 格式
+            return "{\"code\": \"FAIL\", \"message\": \"处理失败\"}";
+        } else {
+            return "fail";
+        }
+    }
+}

+ 5 - 1
haha-miniapp/src/main/resources/application.yml

@@ -80,7 +80,11 @@ wechat:
     mch-id: 1888888888
     mch-key: 88888888888888888888888888888888
     v3-api-key: 88888888888888888888888888888888
-    notify-url: http://localhost:7077/api/callback/wechat/pay
+    notify-url: http://localhost:7077/api/payment/callback/wechat
+    private-key-path: classpath:cert/apiclient_key.pem
+    merchant-serial-number: YOUR_MERCHANT_SERIAL_NUMBER
+    service-id: YOUR_PAY_SCORE_SERVICE_ID
+    pay-score-notify-url: http://localhost:7077/api/payment/callback/wechat_payscore
   # 小程序配置
   miniapp:
     app-id: your_wechat_miniapp_appid

+ 3 - 0
haha-miniapp/src/main/resources/cert/apiclient_key.pem

@@ -0,0 +1,3 @@
+-----BEGIN PRIVATE KEY-----
+PLACEHOLDER_REPLACE_WITH_YOUR_REAL_MERCHANT_PRIVATE_KEY
+-----END PRIVATE KEY-----

+ 27 - 0
haha-miniapp/src/main/resources/sql/order.sql

@@ -79,3 +79,30 @@ INSERT IGNORE INTO `t_order` (`order_no`, `out_trade_no`, `haha_order_no`, `user
     ('ORD202601252110347510', 'OUT202601252110347258', 'HAHA202601252110347241', 10000, 'DEVICE_004', 88.0, 'REFUND', NULL, NULL, '[{"name": "雪碧", "price": 3.5, "quantity": 3}, {"name": "坚果", "price": 12.0, "quantity": 3}, {"name": "面包", "price": 7.5, "quantity": 3}, {"name": "饮料", "price": 5.0, "quantity": 2}, {"name": "口香糖", "price": 3.0, "quantity": 3}]', 3, '2026-01-23 22:28:34', NULL),
     ('ORD202601252110342013', 'OUT202601252110348634', 'HAHA202601252110345165', 10000, 'DEVICE_003', 54.5, 'UNPAID', 'https://example.com/video/ORD202601252110342013.mp4', 80.9, '[{"name": "巧克力", "price": 8.0, "quantity": 3}, {"name": "可乐", "price": 3.5, "quantity": 1}, {"name": "饮料", "price": 5.0, "quantity": 3}, {"name": "薯片", "price": 6.0, "quantity": 2}]', 1, '2026-01-02 17:53:34', NULL),
     ('ORD202601252110346533', 'OUT202601252110341183', NULL, 10000, 'DEVICE_003', 58.0, 'REFUND', NULL, NULL, '[{"name": "巧克力", "price": 8.0, "quantity": 1}, {"name": "巧克力", "price": 8.0, "quantity": 1}, {"name": "矿泉水", "price": 2.0, "quantity": 3}, {"name": "坚果", "price": 12.0, "quantity": 3}]', 3, '2026-01-12 07:50:34', NULL);
+
+-- ============================================
+-- 支付系统多渠道重构 - 新增字段
+-- ============================================
+ALTER TABLE `t_order`
+ADD COLUMN `pay_channel` VARCHAR(20) DEFAULT NULL COMMENT '支付渠道(wechat/alipay/alipay_credit/balance)' AFTER `pay_status`,
+ADD COLUMN `pay_type` VARCHAR(50) DEFAULT NULL COMMENT '支付方式描述' AFTER `pay_channel`,
+ADD COLUMN `transaction_id` VARCHAR(100) DEFAULT NULL COMMENT '第三方支付平台交易号' AFTER `pay_type`,
+ADD COLUMN `refund_status` VARCHAR(20) DEFAULT NULL COMMENT '退款状态' AFTER `pay_time`,
+ADD COLUMN `refund_amount` DECIMAL(10,2) DEFAULT NULL COMMENT '退款金额' AFTER `refund_status`,
+ADD COLUMN `refund_time` DATETIME DEFAULT NULL COMMENT '退款时间' AFTER `refund_amount`,
+ADD COLUMN `refund_reason` VARCHAR(255) DEFAULT NULL COMMENT '退款原因' AFTER `refund_time`;
+
+-- 新增索引
+ALTER TABLE `t_order`
+ADD INDEX `idx_pay_channel` (`pay_channel`),
+ADD INDEX `idx_transaction_id` (`transaction_id`);
+
+-- =============================================
+-- 支付分(先享后付)相关字段
+-- =============================================
+ALTER TABLE t_order ADD COLUMN pay_score_order_id VARCHAR(64) DEFAULT NULL COMMENT '支付分服务订单号';
+ALTER TABLE t_order ADD COLUMN pay_score_state VARCHAR(20) DEFAULT NULL COMMENT '支付分订单状态';
+ALTER TABLE t_order ADD COLUMN service_id VARCHAR(64) DEFAULT NULL COMMENT '支付分服务ID';
+ALTER TABLE t_order ADD COLUMN service_start_time DATETIME DEFAULT NULL COMMENT '服务开始时间';
+ALTER TABLE t_order ADD COLUMN service_end_time DATETIME DEFAULT NULL COMMENT '服务结束时间';
+ALTER TABLE t_order ADD INDEX idx_pay_score_state (pay_score_state);

+ 6 - 0
haha-service/pom.xml

@@ -64,6 +64,12 @@
             <groupId>org.springframework.security</groupId>
             <artifactId>spring-security-crypto</artifactId>
         </dependency>
+
+        <!-- 微信支付 V3 官方 SDK -->
+        <dependency>
+            <groupId>com.github.wechatpay-apiv3</groupId>
+            <artifactId>wechatpay-java</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

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

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.haha.entity.Order;
 
+import java.math.BigDecimal;
 import java.util.List;
 import java.util.Map;
 
@@ -55,4 +56,43 @@ public interface OrderService extends IService<Order> {
     Order getOrderByOutTradeNo(String outTradeNo);
     Order getOrderBydeviceId(String deviceId);
     boolean updatePayStatus(String orderNo, String payStatus);
+
+    /**
+     * 根据AI识别结果创建订单
+     *
+     * @param activityId 活动ID
+     * @param deviceId 设备ID
+     * @param userId 用户ID
+     * @param skuListStr SKU列表JSON字符串
+     * @param resourceInfoStr 资源信息JSON字符串
+     * @param confidence 识别置信度
+     * @return 创建的订单,如果无需创建返回null
+     */
+    Order createOrderFromRecognition(String activityId, String deviceId, String userId,
+                                      String skuListStr, String resourceInfoStr, BigDecimal confidence);
+
+    /**
+     * 创建订单并初始化支付分服务订单
+     *
+     * @param activityId 活动ID
+     * @param deviceId 设备ID
+     * @param userId 用户ID
+     * @param skuListStr SKU列表JSON字符串
+     * @param resourceInfoStr 资源信息JSON字符串
+     * @param confidence 识别置信度
+     * @param openId 用户微信openId(用于支付分)
+     * @return 创建的订单
+     */
+    Order createOrderWithPayScore(String activityId, String deviceId, String userId,
+                                   String skuListStr, String resourceInfoStr, BigDecimal confidence,
+                                   String openId);
+
+    /**
+     * 完结订单并触发支付分扣款
+     *
+     * @param orderId 订单ID
+     * @param totalAmount 最终金额
+     * @return 是否成功
+     */
+    boolean completeOrderWithPayScore(Long orderId, BigDecimal totalAmount);
 }

+ 172 - 19
haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

@@ -1,10 +1,14 @@
 package com.haha.service.impl;
 
+import com.alibaba.fastjson2.JSON;
+import com.alibaba.fastjson2.JSONArray;
+import com.alibaba.fastjson2.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.haha.common.constant.OrderConstants;
+import com.haha.common.enums.PaymentChannel;
 import com.haha.common.exception.BusinessException;
 import com.haha.common.utils.EntityLabelUtils;
 import com.haha.entity.Device;
@@ -14,8 +18,14 @@ import com.haha.mapper.DeviceMapper;
 import com.haha.mapper.OrderMapper;
 import com.haha.mapper.ShopMapper;
 import com.haha.service.OrderService;
+import com.haha.service.payment.PaymentService;
+import com.haha.service.payment.RefundResult;
+import com.haha.service.payment.payscore.PayScoreResult;
+import com.haha.service.payment.payscore.PayScoreService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
@@ -35,6 +45,14 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
 
     private final DeviceMapper deviceMapper;
     private final ShopMapper shopMapper;
+    
+    @Autowired
+    @Lazy
+    private PaymentService paymentService;
+
+    @Autowired
+    @Lazy
+    private PayScoreService payScoreService;
 
     @Override
     public IPage<Order> getPage(int page, int pageSize, String orderNo, String deviceId, 
@@ -145,20 +163,13 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean refund(Long orderId, String reason) {
-        Order order = this.getById(orderId);
-        if (order == null) {
-            throw new BusinessException(404, "订单不存在");
-        }
-
-        // 检查订单状态
-        if (!OrderConstants.PAY_STATUS_PAID.equals(order.getPayStatus())) {
-            throw new BusinessException(400, "只有已支付的订单才能退款");
-        }
-
-        log.info("订单退款: orderId={}, orderNo={}, reason={}", orderId, order.getOrderNo(), reason);
-
-        // 更新支付状态为已退款
-        return this.updatePayStatus(order.getOrderNo(), OrderConstants.PAY_STATUS_REFUND);
+        // 委托给 PaymentService 处理退款
+        // PaymentService.refund() 内部已经处理了:
+        // - 订单状态校验
+        // - 调用第三方退款 API
+        // - 更新退款状态和支付状态
+        RefundResult result = paymentService.refund(orderId, reason);
+        return result.isSuccess();
     }
 
     @Override
@@ -195,6 +206,67 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
         return lambdaUpdate().eq(Order::getOrderNo, orderNo).set(Order::getPayStatus, payStatus).update();
     }
 
+    @Override
+    @Transactional
+    public Order createOrderFromRecognition(String activityId, String deviceId, String userId,
+                                             String skuListStr, String resourceInfoStr, BigDecimal confidence) {
+        // 1. 计算订单金额(从 skuListStr 解析 JSONArray,遍历计算 price * quantity)
+        BigDecimal totalAmount = BigDecimal.ZERO;
+        JSONArray skuList = null;
+        if (skuListStr != null && !skuListStr.isEmpty()) {
+            skuList = JSON.parseArray(skuListStr);
+            if (skuList != null) {
+                for (int i = 0; i < skuList.size(); i++) {
+                    JSONObject sku = skuList.getJSONObject(i);
+                    BigDecimal price = sku.getBigDecimal("price");
+                    Integer quantity = sku.getInteger("quantity");
+                    if (price != null && quantity != null) {
+                        totalAmount = totalAmount.add(price.multiply(BigDecimal.valueOf(quantity)));
+                    }
+                }
+                totalAmount = totalAmount.setScale(2, RoundingMode.HALF_UP);
+            }
+        }
+
+        // 2. 解析视频URL
+        String videoUrl = null;
+        if (resourceInfoStr != null && !resourceInfoStr.isEmpty()) {
+            JSONObject resourceInfo = JSON.parseObject(resourceInfoStr);
+            videoUrl = resourceInfo.getString("video_url");
+        }
+
+        // 3. 创建订单(仅在有用户、设备和金额时)
+        if (userId != null && deviceId != null && totalAmount.compareTo(BigDecimal.ZERO) > 0) {
+            Order order = new Order();
+            order.setActivityId(activityId);
+            order.setUserId(Long.parseLong(userId));
+            order.setDeviceId(deviceId);
+            order.setTotalAmount(totalAmount);
+            order.setItems(skuListStr);
+            order.setStatus(OrderConstants.STATUS_PENDING_PAYMENT);
+            order.setPayStatus(OrderConstants.PAY_STATUS_UNPAID);
+            order.setCreateTime(LocalDateTime.now());
+            if (videoUrl != null) {
+                order.setVideoUrl(videoUrl);
+            }
+            if (confidence != null) {
+                order.setConfidence(confidence);
+            }
+
+            boolean saved = this.save(order);
+            if (saved) {
+                log.info("订单创建成功 - 订单ID: {}, 用户: {}, 金额: {}", order.getId(), userId, totalAmount);
+                return order;
+            } else {
+                log.error("订单创建失败 - 用户: {}, 设备: {}", userId, deviceId);
+                return null;
+            }
+        } else {
+            log.warn("订单创建跳过 - 缺少必要参数: userId={}, deviceId={}, amount={}", userId, deviceId, totalAmount);
+            return null;
+        }
+    }
+
     /**
      * 填充订单标签字段
      */
@@ -208,11 +280,18 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             order.setStatusLabel(statusLabel.getLabel());
         }
         
-        // 推断支付方式(数据库暂无pay_type字段,根据outTradeNo推断)
-        if (order.getOutTradeNo() != null && !order.getOutTradeNo().isEmpty()) {
-            order.setPayType("wechat"); // 有支付订单号默认为微信支付
-        } else if ("paid".equals(order.getPayStatus())) {
-            order.setPayType("free"); // 已支付但无支付订单号,可能是免费订单
+        // 如果 payType 为空但 payChannel 有值,从 payChannel 获取描述
+        if (order.getPayType() == null || order.getPayType().isEmpty()) {
+            if (order.getPayChannel() != null && !order.getPayChannel().isEmpty()) {
+                PaymentChannel channel = PaymentChannel.fromCode(order.getPayChannel());
+                if (channel != null) {
+                    order.setPayType(channel.getDescription());
+                }
+            } else if (order.getOutTradeNo() != null && !order.getOutTradeNo().isEmpty()) {
+                order.setPayType("微信支付"); // 兼容旧数据
+            } else if ("PAID".equals(order.getPayStatus())) {
+                order.setPayType("免费");
+            }
         }
         
         // 根据 deviceId 查询设备信息,获取门店名称
@@ -236,4 +315,78 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             }
         }
     }
+
+    @Override
+    @Transactional
+    public Order createOrderWithPayScore(String activityId, String deviceId, String userId,
+                                          String skuListStr, String resourceInfoStr, BigDecimal confidence,
+                                          String openId) {
+        Order order = createOrderFromRecognition(activityId, deviceId, userId, 
+                skuListStr, resourceInfoStr, confidence);
+        
+        if (order == null) {
+            return null;
+        }
+
+        if (openId == null || openId.isEmpty()) {
+            log.warn("[支付分集成] openId为空,跳过支付分服务订单创建 - orderId: {}", order.getId());
+            return order;
+        }
+
+        try {
+            PayScoreResult result = payScoreService.createPayScoreOrder(order.getId(), openId);
+            
+            if (result.isSuccess()) {
+                log.info("[支付分集成] 支付分服务订单创建成功 - orderId: {}, outOrderNo: {}", 
+                        order.getId(), result.getOutOrderNo());
+                order.setPayScoreOrderId(result.getOutOrderNo());
+                order.setPayScoreState(result.getState());
+                order.setPayChannel("wechat_payscore");
+                order.setPayType("微信支付分");
+                this.updateById(order);
+            } else {
+                log.warn("[支付分集成] 支付分服务订单创建失败 - orderId: {}, errorCode: {}, errorMsg: {}", 
+                        order.getId(), result.getErrorCode(), result.getErrorMsg());
+            }
+        } catch (Exception e) {
+            log.error("[支付分集成] 支付分服务订单创建异常 - orderId: {}", order.getId(), e);
+        }
+
+        return order;
+    }
+
+    @Override
+    @Transactional
+    public boolean completeOrderWithPayScore(Long orderId, BigDecimal totalAmount) {
+        Order order = this.getById(orderId);
+        if (order == null) {
+            log.error("[支付分集成] 订单不存在 - orderId: {}", orderId);
+            return false;
+        }
+
+        if (order.getPayScoreOrderId() == null) {
+            log.warn("[支付分集成] 订单未创建支付分服务订单,直接更新状态 - orderId: {}", orderId);
+            order.setTotalAmount(totalAmount);
+            order.setPayStatus(OrderConstants.PAY_STATUS_PAID);
+            order.setStatus(OrderConstants.STATUS_COMPLETED);
+            order.setPayTime(LocalDateTime.now());
+            return this.updateById(order);
+        }
+
+        try {
+            PayScoreResult result = payScoreService.completePayScoreOrder(orderId, totalAmount, null);
+            
+            if (result.isSuccess()) {
+                log.info("[支付分集成] 支付分完结成功 - orderId: {}, state: {}", orderId, result.getState());
+                return true;
+            } else {
+                log.error("[支付分集成] 支付分完结失败 - orderId: {}, errorCode: {}, errorMsg: {}", 
+                        orderId, result.getErrorCode(), result.getErrorMsg());
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("[支付分集成] 支付分完结异常 - orderId: {}", orderId, e);
+            return false;
+        }
+    }
 }

+ 56 - 0
haha-service/src/main/java/com/haha/service/payment/PaymentCallbackResult.java

@@ -0,0 +1,56 @@
+package com.haha.service.payment;
+
+import com.haha.common.enums.PaymentChannel;
+import lombok.Builder;
+import lombok.Data;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * 支付回调结果DTO
+ */
+@Data
+@Builder
+public class PaymentCallbackResult {
+    
+    /**
+     * 是否成功
+     */
+    private boolean success;
+    
+    /**
+     * 本地订单号
+     */
+    private String orderNo;
+    
+    /**
+     * 第三方交易号
+     */
+    private String transactionId;
+    
+    /**
+     * 支付金额
+     */
+    private BigDecimal amount;
+    
+    /**
+     * 支付时间
+     */
+    private LocalDateTime payTime;
+    
+    /**
+     * 支付渠道
+     */
+    private PaymentChannel channel;
+    
+    /**
+     * 原始回调数据
+     */
+    private Map<String, Object> rawData;
+    
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+}

+ 67 - 0
haha-service/src/main/java/com/haha/service/payment/PaymentChannelFactory.java

@@ -0,0 +1,67 @@
+package com.haha.service.payment;
+
+import com.haha.common.enums.PaymentChannel;
+import jakarta.annotation.PostConstruct;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.util.EnumMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 支付渠道工厂类
+ * 负责管理和分发支付策略
+ */
+@Slf4j
+@Component
+public class PaymentChannelFactory {
+    
+    @Autowired
+    private List<PaymentStrategy> strategies;
+    
+    private final Map<PaymentChannel, PaymentStrategy> strategyMap = new EnumMap<>(PaymentChannel.class);
+    
+    /**
+     * 初始化:注册所有支付策略
+     */
+    @PostConstruct
+    public void init() {
+        for (PaymentStrategy strategy : strategies) {
+            strategyMap.put(strategy.getChannel(), strategy);
+            log.info("注册支付策略: {} -> {}", strategy.getChannel().getCode(), strategy.getClass().getSimpleName());
+        }
+    }
+    
+    /**
+     * 获取指定渠道的支付策略
+     * @param channel 支付渠道
+     * @return 支付策略实例
+     * @throws IllegalArgumentException 如果渠道不支持
+     */
+    public PaymentStrategy getStrategy(PaymentChannel channel) {
+        PaymentStrategy strategy = strategyMap.get(channel);
+        if (strategy == null) {
+            throw new IllegalArgumentException("不支持的支付渠道: " + channel.getCode());
+        }
+        return strategy;
+    }
+    
+    /**
+     * 获取所有已注册的支付渠道
+     * @return 已注册的支付渠道列表
+     */
+    public List<PaymentChannel> getAvailableChannels() {
+        return List.copyOf(strategyMap.keySet());
+    }
+    
+    /**
+     * 判断是否支持某个渠道
+     * @param channel 支付渠道
+     * @return 是否支持
+     */
+    public boolean isChannelSupported(PaymentChannel channel) {
+        return strategyMap.containsKey(channel);
+    }
+}

+ 65 - 0
haha-service/src/main/java/com/haha/service/payment/PaymentRequest.java

@@ -0,0 +1,65 @@
+package com.haha.service.payment;
+
+import com.haha.common.enums.PaymentChannel;
+import lombok.Builder;
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.Map;
+
+/**
+ * 支付请求DTO
+ */
+@Data
+@Builder
+public class PaymentRequest {
+    
+    /**
+     * 本地订单ID
+     */
+    private Long orderId;
+    
+    /**
+     * 本地订单号
+     */
+    private String orderNo;
+    
+    /**
+     * 支付金额
+     */
+    private BigDecimal amount;
+    
+    /**
+     * 用户ID
+     */
+    private Long userId;
+    
+    /**
+     * 支付渠道
+     */
+    private PaymentChannel channel;
+    
+    /**
+     * 订单描述
+     */
+    private String description;
+    
+    /**
+     * 微信openId(微信支付必填)
+     */
+    private String openId;
+    
+    /**
+     * 支付宝buyerId(支付宝支付必填)
+     */
+    private String buyerId;
+    
+    /**
+     * 回调通知URL
+     */
+    private String notifyUrl;
+    
+    /**
+     * 扩展参数
+     */
+    private Map<String, String> extraParams;
+}

+ 44 - 0
haha-service/src/main/java/com/haha/service/payment/PaymentResult.java

@@ -0,0 +1,44 @@
+package com.haha.service.payment;
+
+import com.haha.common.enums.PaymentChannel;
+import lombok.Builder;
+import lombok.Data;
+import java.util.Map;
+
+/**
+ * 支付结果DTO
+ */
+@Data
+@Builder
+public class PaymentResult {
+    
+    /**
+     * 是否成功
+     */
+    private boolean success;
+    
+    /**
+     * 支付渠道
+     */
+    private PaymentChannel channel;
+    
+    /**
+     * 预支付ID(微信:prepay_id,支付宝:trade_no)
+     */
+    private String prepayId;
+    
+    /**
+     * 错误码
+     */
+    private String errorCode;
+    
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+    
+    /**
+     * 支付参数(返回给前端调起支付用)
+     */
+    private Map<String, String> payParams;
+}

+ 49 - 0
haha-service/src/main/java/com/haha/service/payment/PaymentService.java

@@ -0,0 +1,49 @@
+package com.haha.service.payment;
+
+import com.haha.common.enums.PaymentChannel;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 支付业务门面服务
+ * 上层代码统一通过此接口调用支付功能,无需关心具体支付渠道实现
+ */
+public interface PaymentService {
+    
+    /**
+     * 创建支付
+     * @param orderId 本地订单ID
+     * @param channel 支付渠道
+     * @param extra 扩展参数(如openId, buyerId等)
+     * @return 支付结果(含前端调起支付所需参数)
+     */
+    PaymentResult createPayment(Long orderId, PaymentChannel channel, Map<String, String> extra);
+    
+    /**
+     * 退款
+     * @param orderId 本地订单ID
+     * @param reason 退款原因
+     * @return 退款结果
+     */
+    RefundResult refund(Long orderId, String reason);
+    
+    /**
+     * 处理支付回调
+     * @param channel 支付渠道
+     * @param params 回调参数
+     * @return 回调处理结果
+     */
+    PaymentCallbackResult handleCallback(PaymentChannel channel, Map<String, Object> params);
+    
+    /**
+     * 查询支付状态
+     * @param orderId 本地订单ID
+     * @return 支付结果
+     */
+    PaymentResult queryPaymentStatus(Long orderId);
+    
+    /**
+     * 获取当前可用的支付渠道列表
+     */
+    List<PaymentChannel> getAvailableChannels();
+}

+ 52 - 0
haha-service/src/main/java/com/haha/service/payment/PaymentStrategy.java

@@ -0,0 +1,52 @@
+package com.haha.service.payment;
+
+import com.haha.common.enums.PaymentChannel;
+import java.util.Map;
+
+/**
+ * 支付策略接口
+ * 所有支付渠道必须实现此接口
+ */
+public interface PaymentStrategy {
+    
+    /**
+     * 获取当前策略对应的支付渠道
+     * @return 支付渠道
+     */
+    PaymentChannel getChannel();
+    
+    /**
+     * 创建支付(统一下单)
+     * @param request 支付请求
+     * @return 支付结果
+     */
+    PaymentResult createPayment(PaymentRequest request);
+    
+    /**
+     * 申请退款
+     * @param request 退款请求
+     * @return 退款结果
+     */
+    RefundResult refund(RefundRequest request);
+    
+    /**
+     * 解析支付回调通知
+     * @param params 回调参数
+     * @return 回调解析结果
+     */
+    PaymentCallbackResult parseCallback(Map<String, Object> params);
+    
+    /**
+     * 验证回调签名
+     * @param params 回调参数
+     * @return 签名是否有效
+     */
+    boolean verifyCallback(Map<String, Object> params);
+    
+    /**
+     * 查询支付订单状态
+     * @param outTradeNo 商户订单号
+     * @return 支付结果
+     */
+    PaymentResult queryPayment(String outTradeNo);
+}

+ 48 - 0
haha-service/src/main/java/com/haha/service/payment/RefundRequest.java

@@ -0,0 +1,48 @@
+package com.haha.service.payment;
+
+import lombok.Builder;
+import lombok.Data;
+import java.math.BigDecimal;
+
+/**
+ * 退款请求DTO
+ */
+@Data
+@Builder
+public class RefundRequest {
+    
+    /**
+     * 本地订单ID
+     */
+    private Long orderId;
+    
+    /**
+     * 本地订单号
+     */
+    private String orderNo;
+    
+    /**
+     * 第三方交易号
+     */
+    private String transactionId;
+    
+    /**
+     * 退款金额
+     */
+    private BigDecimal refundAmount;
+    
+    /**
+     * 订单总金额
+     */
+    private BigDecimal totalAmount;
+    
+    /**
+     * 退款原因
+     */
+    private String reason;
+    
+    /**
+     * 退款单号(系统生成)
+     */
+    private String refundNo;
+}

+ 32 - 0
haha-service/src/main/java/com/haha/service/payment/RefundResult.java

@@ -0,0 +1,32 @@
+package com.haha.service.payment;
+
+import lombok.Builder;
+import lombok.Data;
+
+/**
+ * 退款结果DTO
+ */
+@Data
+@Builder
+public class RefundResult {
+    
+    /**
+     * 是否成功
+     */
+    private boolean success;
+    
+    /**
+     * 退款单号
+     */
+    private String refundId;
+    
+    /**
+     * 错误码
+     */
+    private String errorCode;
+    
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+}

+ 163 - 0
haha-service/src/main/java/com/haha/service/payment/config/WxPayConfig.java

@@ -0,0 +1,163 @@
+package com.haha.service.payment.config;
+
+import com.wechat.pay.java.core.Config;
+import com.wechat.pay.java.core.RSAAutoCertificateConfig;
+import com.wechat.pay.java.core.notification.NotificationConfig;
+import com.wechat.pay.java.core.notification.NotificationParser;
+import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
+import com.wechat.pay.java.service.refund.RefundService;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+
+import jakarta.annotation.PostConstruct;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+/**
+ * 微信支付 V3 SDK 配置
+ * 
+ * 通过 wechat.pay.mch-id 配置项控制是否启用
+ * 创建 RSAAutoCertificateConfig(自动管理平台证书)和核心服务 Bean
+ */
+@Slf4j
+@Data
+@Configuration
+@ConfigurationProperties(prefix = "wechat.pay")
+@ConditionalOnProperty(name = "wechat.pay.mch-id")
+public class WxPayConfig {
+
+    /** 小程序/公众号 AppID */
+    private String appId;
+    
+    /** 商户号 */
+    private String mchId;
+    
+    /** API V2 密钥(旧版,兼容保留) */
+    private String mchKey;
+    
+    /** API V3 密钥 */
+    private String v3ApiKey;
+    
+    /** 支付结果回调通知地址 */
+    private String notifyUrl;
+    
+    /** 商户API私钥文件路径(支持 classpath: 前缀) */
+    private String privateKeyPath;
+    
+    /** 商户API证书序列号 */
+    private String merchantSerialNumber;
+
+    /** 支付分服务ID */
+    private String serviceId;
+
+    /** 支付分回调通知地址 */
+    private String payScoreNotifyUrl;
+
+    @Autowired
+    private ResourceLoader resourceLoader;
+    
+    /**
+     * 微信支付 SDK 核心配置
+     * 使用 RSAAutoCertificateConfig 自动管理平台证书
+     */
+    @Bean
+    public Config wxPaySdkConfig() {
+        log.info("初始化微信支付 V3 SDK 配置 - 商户号: {}, AppID: {}", mchId, appId);
+        
+        try {
+            // 读取私钥内容
+            String privateKeyContent = loadPrivateKey();
+            
+            Config config = new RSAAutoCertificateConfig.Builder()
+                    .merchantId(mchId)
+                    .privateKey(privateKeyContent)
+                    .merchantSerialNumber(merchantSerialNumber)
+                    .apiV3Key(v3ApiKey)
+                    .build();
+            
+            log.info("微信支付 V3 SDK 配置初始化成功");
+            return config;
+        } catch (Exception e) {
+            log.error("微信支付 V3 SDK 配置初始化失败: {}", e.getMessage());
+            log.warn("微信支付功能将不可用,请检查配置参数和证书文件");
+            // 返回 null,下游 Bean 创建时会跳过
+            return null;
+        }
+    }
+    
+    /**
+     * JSAPI 支付服务(含扩展功能:一步下单+生成支付参数)
+     */
+    @Bean
+    public JsapiServiceExtension jsapiServiceExtension(Config wxPaySdkConfig) {
+        if (wxPaySdkConfig == null) {
+            log.warn("微信支付 SDK 配置未初始化,JSAPI 服务不可用");
+            return null;
+        }
+        return new JsapiServiceExtension.Builder().config(wxPaySdkConfig).build();
+    }
+    
+    /**
+     * 退款服务
+     */
+    @Bean
+    public RefundService wxRefundService(Config wxPaySdkConfig) {
+        if (wxPaySdkConfig == null) {
+            log.warn("微信支付 SDK 配置未初始化,退款服务不可用");
+            return null;
+        }
+        return new RefundService.Builder().config(wxPaySdkConfig).build();
+    }
+    
+    /**
+     * 回调通知解析器(含签名验证+AES-256-GCM 解密)
+     */
+    @Bean
+    public NotificationParser wxNotificationParser(Config wxPaySdkConfig) {
+        if (wxPaySdkConfig == null) {
+            log.warn("微信支付 SDK 配置未初始化,回调通知解析不可用");
+            return null;
+        }
+        // RSAAutoCertificateConfig 实现了 NotificationConfig 接口
+        return new NotificationParser((NotificationConfig) wxPaySdkConfig);
+    }
+    
+    /**
+     * 读取商户私钥文件内容
+     */
+    private String loadPrivateKey() throws IOException {
+        Resource resource = resourceLoader.getResource(privateKeyPath);
+        if (!resource.exists()) {
+            throw new IOException("商户私钥文件不存在: " + privateKeyPath);
+        }
+        String content = new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
+        log.info("成功读取商户私钥文件: {}", privateKeyPath);
+        return content;
+    }
+    
+    /**
+     * 微信支付HTTP客户端(用于支付分等SDK未封装的API调用)
+     * 自动处理请求签名和响应验签
+     */
+    @Bean
+    @ConditionalOnProperty(name = "wechat.pay.mch-id")
+    public com.wechat.pay.java.core.http.HttpClient wxPayHttpClient(
+            @Qualifier("wxPaySdkConfig") com.wechat.pay.java.core.Config wxPaySdkConfig) {
+        return new com.wechat.pay.java.core.http.DefaultHttpClientBuilder()
+                .config(wxPaySdkConfig)
+                .build();
+    }
+
+    @PostConstruct
+    public void logConfig() {
+        log.info("微信支付配置加载完成 - AppID: {}, 商户号: {}, 回调地址: {}", appId, mchId, notifyUrl);
+    }
+}

+ 59 - 0
haha-service/src/main/java/com/haha/service/payment/impl/AliCreditPayStrategy.java

@@ -0,0 +1,59 @@
+package com.haha.service.payment.impl;
+
+import com.haha.common.enums.PaymentChannel;
+import com.haha.service.payment.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 支付宝信用支付策略(骨架实现)
+ * 
+ * 芝麻信用先享后付模式
+ * 通过配置 alipay.credit.enabled=true 启用
+ * 待真正接入支付宝 SDK 后完善具体实现
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(name = "alipay.credit.enabled", havingValue = "true", matchIfMissing = false)
+public class AliCreditPayStrategy implements PaymentStrategy {
+    
+    @Override
+    public PaymentChannel getChannel() {
+        return PaymentChannel.ALIPAY_CREDIT;
+    }
+    
+    @Override
+    public PaymentResult createPayment(PaymentRequest request) {
+        log.info("支付宝信用支付 - 创建支付请求: 订单号={}, 金额={}, 用户={}", 
+            request.getOrderNo(), request.getAmount(), request.getUserId());
+        throw new UnsupportedOperationException("支付宝信用支付暂未接入,请等待后续版本支持");
+    }
+    
+    @Override
+    public RefundResult refund(RefundRequest request) {
+        log.info("支付宝信用支付 - 退款请求: 订单号={}, 退款金额={}", 
+            request.getOrderNo(), request.getRefundAmount());
+        throw new UnsupportedOperationException("支付宝信用支付退款暂未接入,请等待后续版本支持");
+    }
+    
+    @Override
+    public PaymentCallbackResult parseCallback(Map<String, Object> params) {
+        log.info("支付宝信用支付 - 解析回调通知: {}", params);
+        throw new UnsupportedOperationException("支付宝信用支付回调解析暂未接入");
+    }
+    
+    @Override
+    public boolean verifyCallback(Map<String, Object> params) {
+        log.info("支付宝信用支付 - 验证回调签名");
+        throw new UnsupportedOperationException("支付宝信用支付签名验证暂未接入");
+    }
+    
+    @Override
+    public PaymentResult queryPayment(String outTradeNo) {
+        log.info("支付宝信用支付 - 查询支付状态: outTradeNo={}", outTradeNo);
+        throw new UnsupportedOperationException("支付宝信用支付订单查询暂未接入");
+    }
+}

+ 58 - 0
haha-service/src/main/java/com/haha/service/payment/impl/AliPayStrategy.java

@@ -0,0 +1,58 @@
+package com.haha.service.payment.impl;
+
+import com.haha.common.enums.PaymentChannel;
+import com.haha.service.payment.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 支付宝普通支付策略(骨架实现)
+ * 
+ * 通过配置 alipay.enabled=true 启用
+ * 待真正接入支付宝 SDK 后完善具体实现
+ */
+@Slf4j
+@Component
+@ConditionalOnProperty(name = "alipay.enabled", havingValue = "true", matchIfMissing = false)
+public class AliPayStrategy implements PaymentStrategy {
+    
+    @Override
+    public PaymentChannel getChannel() {
+        return PaymentChannel.ALIPAY;
+    }
+    
+    @Override
+    public PaymentResult createPayment(PaymentRequest request) {
+        log.info("支付宝普通支付 - 创建支付请求: 订单号={}, 金额={}, 用户={}", 
+            request.getOrderNo(), request.getAmount(), request.getUserId());
+        throw new UnsupportedOperationException("支付宝普通支付暂未接入,请等待后续版本支持");
+    }
+    
+    @Override
+    public RefundResult refund(RefundRequest request) {
+        log.info("支付宝普通支付 - 退款请求: 订单号={}, 退款金额={}", 
+            request.getOrderNo(), request.getRefundAmount());
+        throw new UnsupportedOperationException("支付宝退款暂未接入,请等待后续版本支持");
+    }
+    
+    @Override
+    public PaymentCallbackResult parseCallback(Map<String, Object> params) {
+        log.info("支付宝普通支付 - 解析回调通知: {}", params);
+        throw new UnsupportedOperationException("支付宝回调解析暂未接入");
+    }
+    
+    @Override
+    public boolean verifyCallback(Map<String, Object> params) {
+        log.info("支付宝普通支付 - 验证回调签名");
+        throw new UnsupportedOperationException("支付宝签名验证暂未接入");
+    }
+    
+    @Override
+    public PaymentResult queryPayment(String outTradeNo) {
+        log.info("支付宝普通支付 - 查询支付状态: outTradeNo={}", outTradeNo);
+        throw new UnsupportedOperationException("支付宝订单查询暂未接入");
+    }
+}

+ 391 - 0
haha-service/src/main/java/com/haha/service/payment/impl/PaymentServiceImpl.java

@@ -0,0 +1,391 @@
+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.OrderService;
+import com.haha.service.payment.*;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 支付业务门面服务实现
+ * 统一处理支付相关业务逻辑,协调订单服务和支付策略
+ */
+@Slf4j
+@Service
+public class PaymentServiceImpl implements PaymentService {
+    
+    @Autowired
+    private PaymentChannelFactory channelFactory;
+    
+    @Autowired
+    private OrderService orderService;
+    
+    @Override
+    public PaymentResult createPayment(Long orderId, PaymentChannel channel, Map<String, String> extra) {
+        log.info("创建支付请求: orderId={}, channel={}", orderId, channel);
+        
+        // 1. 参数校验
+        if (orderId == null) {
+            log.error("创建支付失败: 订单ID不能为空");
+            return PaymentResult.builder()
+                    .success(false)
+                    .errorCode("INVALID_PARAM")
+                    .errorMsg("订单ID不能为空")
+                    .build();
+        }
+        if (channel == null) {
+            log.error("创建支付失败: 支付渠道不能为空, orderId={}", orderId);
+            return PaymentResult.builder()
+                    .success(false)
+                    .errorCode("INVALID_PARAM")
+                    .errorMsg("支付渠道不能为空")
+                    .build();
+        }
+        
+        // 2. 查询订单
+        Order order = orderService.getById(orderId);
+        if (order == null) {
+            log.error("创建支付失败: 订单不存在, orderId={}", orderId);
+            return PaymentResult.builder()
+                    .success(false)
+                    .errorCode("ORDER_NOT_FOUND")
+                    .errorMsg("订单不存在")
+                    .build();
+        }
+        
+        // 3. 校验订单状态(必须是待支付状态)
+        if (!OrderConstants.PAY_STATUS_UNPAID.equals(order.getPayStatus())) {
+            log.error("创建支付失败: 订单状态不正确, orderId={}, payStatus={}", orderId, order.getPayStatus());
+            return PaymentResult.builder()
+                    .success(false)
+                    .errorCode("INVALID_ORDER_STATUS")
+                    .errorMsg("订单状态不正确,只有待支付订单可以发起支付")
+                    .build();
+        }
+        
+        // 4. 获取支付策略
+        PaymentStrategy strategy;
+        try {
+            strategy = channelFactory.getStrategy(channel);
+        } catch (IllegalArgumentException e) {
+            log.error("创建支付失败: 不支持的支付渠道, channel={}", channel);
+            return PaymentResult.builder()
+                    .success(false)
+                    .errorCode("UNSUPPORTED_CHANNEL")
+                    .errorMsg("不支持的支付渠道: " + channel.getCode())
+                    .build();
+        }
+        
+        // 5. 构建支付请求
+        PaymentRequest request = PaymentRequest.builder()
+                .orderId(orderId)
+                .orderNo(order.getOrderNo())
+                .amount(order.getTotalAmount())
+                .userId(order.getUserId())
+                .channel(channel)
+                .description("哈哈零售-订单支付")
+                .openId(extra != null ? extra.get("openId") : null)
+                .buyerId(extra != null ? extra.get("buyerId") : null)
+                .extraParams(extra)
+                .build();
+        
+        log.info("调用支付策略创建支付: orderNo={}, amount={}", order.getOrderNo(), order.getTotalAmount());
+        
+        // 6. 调用策略创建支付
+        PaymentResult result = strategy.createPayment(request);
+        
+        // 7. 如果成功,更新订单的支付渠道信息
+        if (result.isSuccess()) {
+            order.setPayChannel(channel.getCode());
+            order.setPayType(channel.getDescription());
+            orderService.updateById(order);
+            log.info("支付创建成功: orderId={}, channel={}", orderId, channel);
+        } else {
+            log.warn("支付创建失败: orderId={}, errorCode={}, errorMsg={}", 
+                    orderId, result.getErrorCode(), result.getErrorMsg());
+        }
+        
+        return result;
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public RefundResult refund(Long orderId, String reason) {
+        log.info("发起退款请求: orderId={}, reason={}", orderId, reason);
+        
+        // 1. 参数校验
+        if (orderId == null) {
+            log.error("退款失败: 订单ID不能为空");
+            return RefundResult.builder()
+                    .success(false)
+                    .errorCode("INVALID_PARAM")
+                    .errorMsg("订单ID不能为空")
+                    .build();
+        }
+        
+        // 2. 查询订单
+        Order order = orderService.getById(orderId);
+        if (order == null) {
+            log.error("退款失败: 订单不存在, orderId={}", orderId);
+            return RefundResult.builder()
+                    .success(false)
+                    .errorCode("ORDER_NOT_FOUND")
+                    .errorMsg("订单不存在")
+                    .build();
+        }
+        
+        // 3. 校验订单状态(必须是已支付)
+        if (!OrderConstants.PAY_STATUS_PAID.equals(order.getPayStatus())) {
+            log.error("退款失败: 订单状态不正确, orderId={}, payStatus={}", orderId, order.getPayStatus());
+            return RefundResult.builder()
+                    .success(false)
+                    .errorCode("INVALID_ORDER_STATUS")
+                    .errorMsg("订单状态不正确,只有已支付订单可以退款")
+                    .build();
+        }
+        
+        // 4. 根据订单的 payChannel 获取支付渠道
+        PaymentChannel channel = PaymentChannel.fromCode(order.getPayChannel());
+        
+        // 5. 如果 channel 为 null 或 BALANCE,直接更新退款状态(余额退款不经过第三方)
+        if (channel == null || channel == PaymentChannel.BALANCE) {
+            log.info("余额退款,直接更新订单状态: orderId={}", orderId);
+            updateOrderRefundStatus(order, reason, null);
+            return RefundResult.builder()
+                    .success(true)
+                    .refundId("BALANCE_REFUND_" + orderId)
+                    .build();
+        }
+        
+        // 6. 获取支付策略
+        PaymentStrategy strategy;
+        try {
+            strategy = channelFactory.getStrategy(channel);
+        } catch (IllegalArgumentException e) {
+            log.error("退款失败: 不支持的支付渠道, channel={}", channel);
+            return RefundResult.builder()
+                    .success(false)
+                    .errorCode("UNSUPPORTED_CHANNEL")
+                    .errorMsg("不支持的支付渠道: " + channel.getCode())
+                    .build();
+        }
+        
+        // 7. 构建退款请求
+        String refundNo = generateRefundNo(order.getOrderNo());
+        RefundRequest refundRequest = RefundRequest.builder()
+                .orderId(orderId)
+                .orderNo(order.getOrderNo())
+                .transactionId(order.getTransactionId())
+                .refundAmount(order.getTotalAmount())
+                .totalAmount(order.getTotalAmount())
+                .reason(reason)
+                .refundNo(refundNo)
+                .build();
+        
+        log.info("调用支付策略执行退款: orderNo={}, refundNo={}, amount={}", 
+                order.getOrderNo(), refundNo, order.getTotalAmount());
+        
+        // 8. 调用策略退款
+        RefundResult result = strategy.refund(refundRequest);
+        
+        // 9. 更新订单退款状态
+        if (result.isSuccess()) {
+            updateOrderRefundStatus(order, reason, result.getRefundId());
+            log.info("退款成功: orderId={}, refundId={}", orderId, result.getRefundId());
+        } else {
+            log.warn("退款失败: orderId={}, errorCode={}, errorMsg={}", 
+                    orderId, result.getErrorCode(), result.getErrorMsg());
+        }
+        
+        return result;
+    }
+    
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PaymentCallbackResult handleCallback(PaymentChannel channel, Map<String, Object> params) {
+        log.info("处理支付回调: channel={}", channel);
+        
+        // 1. 参数校验
+        if (channel == null) {
+            log.error("处理回调失败: 支付渠道不能为空");
+            return PaymentCallbackResult.builder()
+                    .success(false)
+                    .errorMsg("支付渠道不能为空")
+                    .build();
+        }
+        if (params == null || params.isEmpty()) {
+            log.error("处理回调失败: 回调参数不能为空, channel={}", channel);
+            return PaymentCallbackResult.builder()
+                    .success(false)
+                    .errorMsg("回调参数不能为空")
+                    .build();
+        }
+        
+        // 2. 获取策略
+        PaymentStrategy strategy;
+        try {
+            strategy = channelFactory.getStrategy(channel);
+        } catch (IllegalArgumentException e) {
+            log.error("处理回调失败: 不支持的支付渠道, channel={}", channel);
+            return PaymentCallbackResult.builder()
+                    .success(false)
+                    .errorMsg("不支持的支付渠道: " + channel.getCode())
+                    .build();
+        }
+        
+        // 3. 验证签名
+        // 注意:对于微信支付 V3,verifyCallback 始终返回 true,
+        // 因为 V3 SDK 的签名验证由 NotificationParser 在 parseCallback 中自动完成。
+        // 如果签名验证失败,parseCallback 会返回 success=false。
+        if (!strategy.verifyCallback(params)) {
+            log.error("处理回调失败: 签名验证失败, channel={}", channel);
+            return PaymentCallbackResult.builder()
+                    .success(false)
+                    .errorMsg("签名验证失败")
+                    .build();
+        }
+        
+        log.info("回调签名验证通过: channel={}", channel);
+        
+        // 4. 解析回调(对于微信 V3,此步骤内部包含 SDK 自动签名验证)
+        PaymentCallbackResult result = strategy.parseCallback(params);
+        
+        // 5. 如果解析成功,更新订单状态
+        // 对于微信 V3,如果 SDK 签名验证失败,parseCallback 返回 success=false,不会进入此分支
+        if (result.isSuccess()) {
+            String orderNo = result.getOrderNo();
+            log.info("回调解析成功,更新订单状态: orderNo={}, transactionId={}", orderNo, result.getTransactionId());
+            
+            // 更新支付状态
+            boolean updated = orderService.updatePayStatus(orderNo, OrderConstants.PAY_STATUS_PAID);
+            if (!updated) {
+                log.warn("更新订单支付状态失败: orderNo={}", orderNo);
+            }
+            
+            // 更新 transactionId 和 payTime
+            Order order = orderService.getOrderByOutTradeNo(orderNo);
+            if (order == null) {
+                // 尝试通过 orderNo 查找
+                order = orderService.lambdaQuery()
+                        .eq(Order::getOrderNo, orderNo)
+                        .one();
+            }
+            
+            if (order != null) {
+                order.setTransactionId(result.getTransactionId());
+                order.setPayTime(result.getPayTime() != null ? result.getPayTime() : LocalDateTime.now());
+                orderService.updateById(order);
+                log.info("订单更新成功: orderId={}, transactionId={}, payTime={}", 
+                        order.getId(), order.getTransactionId(), order.getPayTime());
+            } else {
+                log.error("订单不存在,无法更新transactionId: orderNo={}", orderNo);
+            }
+        } else {
+            log.warn("回调解析失败: channel={}, errorMsg={}", channel, result.getErrorMsg());
+        }
+        
+        return result;
+    }
+    
+    @Override
+    public PaymentResult queryPaymentStatus(Long orderId) {
+        log.info("查询支付状态: orderId={}", orderId);
+        
+        // 1. 参数校验
+        if (orderId == null) {
+            log.error("查询支付状态失败: 订单ID不能为空");
+            return PaymentResult.builder()
+                    .success(false)
+                    .errorCode("INVALID_PARAM")
+                    .errorMsg("订单ID不能为空")
+                    .build();
+        }
+        
+        // 2. 查询订单
+        Order order = orderService.getById(orderId);
+        if (order == null) {
+            log.error("查询支付状态失败: 订单不存在, orderId={}", orderId);
+            return PaymentResult.builder()
+                    .success(false)
+                    .errorCode("ORDER_NOT_FOUND")
+                    .errorMsg("订单不存在")
+                    .build();
+        }
+        
+        // 3. 获取支付渠道
+        PaymentChannel channel = PaymentChannel.fromCode(order.getPayChannel());
+        if (channel == null) {
+            log.warn("订单无支付渠道信息,返回本地状态: orderId={}, payStatus={}", orderId, order.getPayStatus());
+            boolean isPaid = OrderConstants.PAY_STATUS_PAID.equals(order.getPayStatus());
+            return PaymentResult.builder()
+                    .success(isPaid)
+                    .errorCode(isPaid ? null : "UNPAID")
+                    .errorMsg(isPaid ? null : "订单未支付")
+                    .build();
+        }
+        
+        // 4. 如果是余额支付,直接返回本地状态
+        if (channel == PaymentChannel.BALANCE) {
+            boolean isPaid = OrderConstants.PAY_STATUS_PAID.equals(order.getPayStatus());
+            log.info("余额支付,返回本地状态: orderId={}, isPaid={}", orderId, isPaid);
+            return PaymentResult.builder()
+                    .success(isPaid)
+                    .channel(channel)
+                    .errorCode(isPaid ? null : "UNPAID")
+                    .errorMsg(isPaid ? null : "订单未支付")
+                    .build();
+        }
+        
+        // 5. 调用策略查询
+        PaymentStrategy strategy;
+        try {
+            strategy = channelFactory.getStrategy(channel);
+        } catch (IllegalArgumentException e) {
+            log.error("查询支付状态失败: 不支持的支付渠道, channel={}", channel);
+            return PaymentResult.builder()
+                    .success(false)
+                    .errorCode("UNSUPPORTED_CHANNEL")
+                    .errorMsg("不支持的支付渠道: " + channel.getCode())
+                    .build();
+        }
+        
+        log.info("调用支付策略查询状态: orderNo={}, channel={}", order.getOrderNo(), channel);
+        PaymentResult result = strategy.queryPayment(order.getOrderNo());
+        result.setChannel(channel);
+        
+        return result;
+    }
+    
+    @Override
+    public List<PaymentChannel> getAvailableChannels() {
+        log.debug("获取可用支付渠道列表");
+        return channelFactory.getAvailableChannels();
+    }
+    
+    /**
+     * 更新订单退款状态
+     */
+    private void updateOrderRefundStatus(Order order, String reason, String refundId) {
+        order.setRefundStatus("REFUNDED");
+        order.setRefundAmount(order.getTotalAmount());
+        order.setRefundTime(LocalDateTime.now());
+        order.setRefundReason(reason);
+        order.setPayStatus(OrderConstants.PAY_STATUS_REFUND);
+        orderService.updateById(order);
+    }
+    
+    /**
+     * 生成退款单号
+     */
+    private String generateRefundNo(String orderNo) {
+        return "RF" + orderNo + System.currentTimeMillis();
+    }
+}

+ 429 - 0
haha-service/src/main/java/com/haha/service/payment/impl/WxPayStrategy.java

@@ -0,0 +1,429 @@
+package com.haha.service.payment.impl;
+
+import com.haha.common.enums.PaymentChannel;
+import com.haha.service.payment.*;
+import com.haha.service.payment.config.WxPayConfig;
+import com.wechat.pay.java.core.exception.HttpException;
+import com.wechat.pay.java.core.exception.MalformedMessageException;
+import com.wechat.pay.java.core.exception.ServiceException;
+import com.wechat.pay.java.core.exception.ValidationException;
+import com.wechat.pay.java.core.notification.NotificationParser;
+import com.wechat.pay.java.core.notification.RequestParam;
+import com.wechat.pay.java.service.payments.jsapi.JsapiServiceExtension;
+import com.wechat.pay.java.service.payments.jsapi.model.Amount;
+import com.wechat.pay.java.service.payments.jsapi.model.Payer;
+import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest;
+import com.wechat.pay.java.service.payments.jsapi.model.PrepayWithRequestPaymentResponse;
+import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByOutTradeNoRequest;
+import com.wechat.pay.java.service.payments.model.Transaction;
+import com.wechat.pay.java.service.refund.RefundService;
+import com.wechat.pay.java.service.refund.model.AmountReq;
+import com.wechat.pay.java.service.refund.model.CreateRequest;
+import com.wechat.pay.java.service.refund.model.Refund;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 微信支付策略实现
+ * 使用微信支付 V3 官方 SDK (wechatpay-java)
+ */
+@Slf4j
+@Component
+public class WxPayStrategy implements PaymentStrategy {
+
+    @Autowired(required = false)
+    private JsapiServiceExtension jsapiServiceExtension;
+
+    @Autowired(required = false)
+    private RefundService wxRefundService;
+
+    @Autowired(required = false)
+    private NotificationParser wxNotificationParser;
+
+    @Autowired(required = false)
+    private WxPayConfig wxPayConfig;
+
+    @Override
+    public PaymentChannel getChannel() {
+        return PaymentChannel.WECHAT;
+    }
+
+    @Override
+    public PaymentResult createPayment(PaymentRequest request) {
+        log.info("[微信支付] 创建支付订单 - 订单号: {}, 金额: {}, 用户ID: {}, openId: {}",
+                request.getOrderNo(), request.getAmount(), request.getUserId(), request.getOpenId());
+
+        // 1. 检查服务可用性
+        if (jsapiServiceExtension == null) {
+            log.error("[微信支付] JSAPI 服务未初始化,请检查微信支付配置");
+            return PaymentResult.builder()
+                    .success(false)
+                    .channel(PaymentChannel.WECHAT)
+                    .errorCode("SERVICE_UNAVAILABLE")
+                    .errorMsg("微信支付服务未初始化,请检查配置")
+                    .build();
+        }
+
+        if (wxPayConfig == null) {
+            log.error("[微信支付] 支付配置未初始化");
+            return error("CONFIG_UNAVAILABLE", "微信支付配置未初始化");
+        }
+
+        // 2. 参数校验
+        if (request.getOpenId() == null || request.getOpenId().isEmpty()) {
+            log.error("[微信支付] 创建支付失败 - openId为空");
+            return error("PARAM_ERROR", "微信支付必须提供openId");
+        }
+
+        // 3. 构建下单请求
+        PrepayRequest prepayRequest = new PrepayRequest();
+        prepayRequest.setAppid(wxPayConfig.getAppId());
+        prepayRequest.setMchid(wxPayConfig.getMchId());
+        prepayRequest.setDescription(request.getDescription() != null ? request.getDescription() : "哈哈零售-订单支付");
+        prepayRequest.setOutTradeNo(request.getOrderNo());
+        prepayRequest.setNotifyUrl(request.getNotifyUrl() != null ? request.getNotifyUrl() : wxPayConfig.getNotifyUrl());
+
+        // 设置金额(元转分)
+        Amount amount = new Amount();
+        amount.setTotal(request.getAmount().multiply(BigDecimal.valueOf(100)).intValue());
+        amount.setCurrency("CNY");
+        prepayRequest.setAmount(amount);
+
+        // 设置支付者
+        Payer payer = new Payer();
+        payer.setOpenid(request.getOpenId());
+        prepayRequest.setPayer(payer);
+
+        log.info("[微信支付] 统一下单参数: appId={}, mchId={}, outTradeNo={}, totalFee={}分, openId={}, notifyUrl={}",
+                wxPayConfig.getAppId(), wxPayConfig.getMchId(), request.getOrderNo(),
+                amount.getTotal(), request.getOpenId(), prepayRequest.getNotifyUrl());
+
+        // 4. 调用 SDK(一步完成下单+生成支付参数)
+        try {
+            PrepayWithRequestPaymentResponse response = jsapiServiceExtension.prepayWithRequestPayment(prepayRequest);
+
+            // 5. 封装返回结果
+            Map<String, String> payParams = new HashMap<>();
+            payParams.put("appId", response.getAppId());
+            payParams.put("timeStamp", response.getTimeStamp());
+            payParams.put("nonceStr", response.getNonceStr());
+            payParams.put("package", response.getPackageVal());
+            payParams.put("signType", response.getSignType());
+            payParams.put("paySign", response.getPaySign());
+
+            String prepayId = extractPrepayId(response.getPackageVal());
+            log.info("[微信支付] 统一下单成功 - prepayId: {}, timeStamp: {}", prepayId, response.getTimeStamp());
+
+            return PaymentResult.builder()
+                    .success(true)
+                    .channel(PaymentChannel.WECHAT)
+                    .prepayId(prepayId)
+                    .payParams(payParams)
+                    .build();
+
+        } catch (HttpException e) {
+            log.error("[微信支付] 统一下单HTTP异常: {}", e.getMessage(), e);
+            return error("NETWORK_ERROR", "网络请求异常: " + e.getMessage());
+        } catch (ServiceException e) {
+            log.error("[微信支付] 统一下单业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
+            return error(e.getErrorCode(), e.getErrorMessage());
+        } catch (MalformedMessageException e) {
+            log.error("[微信支付] 统一下单消息解析异常: {}", e.getMessage(), e);
+            return error("MESSAGE_ERROR", "消息解析异常: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("[微信支付] 统一下单未知异常", e);
+            return error("UNKNOWN_ERROR", "下单失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public RefundResult refund(RefundRequest request) {
+        log.info("[微信支付] 申请退款 - 订单号: {}, 交易号: {}, 退款单号: {}, 退款金额: {}, 订单总金额: {}, 退款原因: {}",
+                request.getOrderNo(), request.getTransactionId(), request.getRefundNo(),
+                request.getRefundAmount(), request.getTotalAmount(), request.getReason());
+
+        // 检查服务可用性
+        if (wxRefundService == null) {
+            log.error("[微信支付] 退款服务未初始化,请检查微信支付配置");
+            return RefundResult.builder()
+                    .success(false)
+                    .errorCode("SERVICE_UNAVAILABLE")
+                    .errorMsg("退款服务未初始化,请检查配置")
+                    .build();
+        }
+
+        try {
+            CreateRequest createRequest = new CreateRequest();
+
+            // 设置原交易信息(优先使用 transactionId)
+            if (request.getTransactionId() != null && !request.getTransactionId().isEmpty()) {
+                createRequest.setTransactionId(request.getTransactionId());
+            } else {
+                createRequest.setOutTradeNo(request.getOrderNo());
+            }
+
+            // 设置退款单号
+            createRequest.setOutRefundNo(request.getRefundNo());
+            
+            // 设置退款原因
+            if (request.getReason() != null && !request.getReason().isEmpty()) {
+                createRequest.setReason(request.getReason());
+            }
+
+            // 设置退款金额
+            AmountReq amountReq = new AmountReq();
+            amountReq.setRefund(request.getRefundAmount().multiply(BigDecimal.valueOf(100)).longValue());
+            amountReq.setTotal(request.getTotalAmount().multiply(BigDecimal.valueOf(100)).longValue());
+            amountReq.setCurrency("CNY");
+            createRequest.setAmount(amountReq);
+
+            log.info("[微信支付] 退款参数: transactionId={}, outTradeNo={}, outRefundNo={}, refundFee={}分, totalFee={}分",
+                    createRequest.getTransactionId(), createRequest.getOutTradeNo(),
+                    createRequest.getOutRefundNo(), amountReq.getRefund(), amountReq.getTotal());
+
+            Refund refund = wxRefundService.create(createRequest);
+
+            log.info("[微信支付] 退款申请成功 - refundId: {}, status: {}", refund.getRefundId(), refund.getStatus());
+
+            return RefundResult.builder()
+                    .success(true)
+                    .refundId(refund.getRefundId())
+                    .build();
+
+        } catch (ServiceException e) {
+            log.error("[微信支付] 退款业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
+            return RefundResult.builder()
+                    .success(false)
+                    .errorCode(e.getErrorCode())
+                    .errorMsg(e.getErrorMessage())
+                    .build();
+        } catch (HttpException e) {
+            log.error("[微信支付] 退款HTTP异常: {}", e.getMessage(), e);
+            return RefundResult.builder()
+                    .success(false)
+                    .errorCode("NETWORK_ERROR")
+                    .errorMsg("网络请求异常: " + e.getMessage())
+                    .build();
+        } catch (Exception e) {
+            log.error("[微信支付] 退款未知异常", e);
+            return RefundResult.builder()
+                    .success(false)
+                    .errorCode("UNKNOWN_ERROR")
+                    .errorMsg("退款失败: " + e.getMessage())
+                    .build();
+        }
+    }
+
+    @Override
+    public boolean verifyCallback(Map<String, Object> params) {
+        // 微信 V3 SDK 的 NotificationParser.parse() 在解析时自动完成签名验证
+        // 验签逻辑已内置于 parseCallback 中,此处返回 true 允许进入解析流程
+        // 如果签名验证失败,parseCallback 中 SDK 会抛出 ValidationException
+        log.debug("[微信支付] V3 签名验证由 SDK NotificationParser 在 parseCallback 中自动完成");
+        return true;
+    }
+
+    @Override
+    public PaymentCallbackResult parseCallback(Map<String, Object> params) {
+        log.info("[微信支付] 解析回调通知 - 参数Keys: {}", params.keySet());
+
+        // 检查服务可用性
+        if (wxNotificationParser == null) {
+            log.error("[微信支付] 回调解析服务未初始化,请检查微信支付配置");
+            return PaymentCallbackResult.builder()
+                    .success(false)
+                    .channel(PaymentChannel.WECHAT)
+                    .errorMsg("回调解析服务未初始化,请检查配置")
+                    .build();
+        }
+
+        try {
+            // 从 params 中提取微信 V3 回调所需的信息
+            String timestamp = (String) params.get("Wechatpay-Timestamp");
+            String nonce = (String) params.get("Wechatpay-Nonce");
+            String signature = (String) params.get("Wechatpay-Signature");
+            String serial = (String) params.get("Wechatpay-Serial");
+            String signType = (String) params.getOrDefault("Wechatpay-Signature-Type", "WECHATPAY2-SHA256-RSA2048");
+            String body = (String) params.get("body");
+
+            log.debug("[微信支付] 回调请求头: timestamp={}, nonce={}, serial={}, signType={}",
+                    timestamp, nonce, serial, signType);
+
+            // 构建 RequestParam
+            RequestParam requestParam = new RequestParam.Builder()
+                    .serialNumber(serial)
+                    .nonce(nonce)
+                    .timestamp(timestamp)
+                    .signature(signature)
+                    .signType(signType)
+                    .body(body)
+                    .build();
+
+            // SDK 自动完成签名验证 + AES-256-GCM 解密
+            Transaction transaction = wxNotificationParser.parse(requestParam, Transaction.class);
+
+            log.info("[微信支付] 回调解析成功 - outTradeNo: {}, transactionId: {}, tradeState: {}",
+                    transaction.getOutTradeNo(), transaction.getTransactionId(), transaction.getTradeState());
+
+            // 转换支付时间
+            LocalDateTime payTime = null;
+            if (transaction.getSuccessTime() != null) {
+                // 微信返回的时间格式: 2018-06-08T10:34:56+08:00 (ISO 8601)
+                try {
+                    OffsetDateTime offsetDateTime = OffsetDateTime.parse(transaction.getSuccessTime(), DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+                    payTime = offsetDateTime.toLocalDateTime();
+                } catch (Exception e) {
+                    log.warn("[微信支付] 支付时间解析失败: {}, 使用当前时间", transaction.getSuccessTime());
+                    payTime = LocalDateTime.now();
+                }
+            }
+
+            // 转换金额(分转元)
+            BigDecimal amount = BigDecimal.ZERO;
+            if (transaction.getAmount() != null && transaction.getAmount().getTotal() != null) {
+                amount = BigDecimal.valueOf(transaction.getAmount().getTotal())
+                        .divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
+            }
+
+            // 判断交易状态
+            boolean isSuccess = Transaction.TradeStateEnum.SUCCESS.equals(transaction.getTradeState());
+
+            log.info("[微信支付] 回调处理完成 - 订单号: {}, 交易号: {}, 金额: {}, 支付时间: {}, 成功: {}",
+                    transaction.getOutTradeNo(), transaction.getTransactionId(), amount, payTime, isSuccess);
+
+            return PaymentCallbackResult.builder()
+                    .success(isSuccess)
+                    .orderNo(transaction.getOutTradeNo())
+                    .transactionId(transaction.getTransactionId())
+                    .amount(amount)
+                    .payTime(payTime)
+                    .channel(PaymentChannel.WECHAT)
+                    .rawData(params)
+                    .build();
+
+        } catch (ValidationException e) {
+            log.error("[微信支付] 回调签名验证失败: {}", e.getMessage(), e);
+            return PaymentCallbackResult.builder()
+                    .success(false)
+                    .channel(PaymentChannel.WECHAT)
+                    .rawData(params)
+                    .errorMsg("签名验证失败: " + e.getMessage())
+                    .build();
+        } catch (MalformedMessageException e) {
+            log.error("[微信支付] 回调消息解析失败: {}", e.getMessage(), e);
+            return PaymentCallbackResult.builder()
+                    .success(false)
+                    .channel(PaymentChannel.WECHAT)
+                    .rawData(params)
+                    .errorMsg("消息解析失败: " + e.getMessage())
+                    .build();
+        } catch (Exception e) {
+            log.error("[微信支付] 回调处理未知异常", e);
+            return PaymentCallbackResult.builder()
+                    .success(false)
+                    .channel(PaymentChannel.WECHAT)
+                    .rawData(params)
+                    .errorMsg("回调处理失败: " + e.getMessage())
+                    .build();
+        }
+    }
+
+    @Override
+    public PaymentResult queryPayment(String outTradeNo) {
+        log.info("[微信支付] 查询订单状态 - 订单号: {}", outTradeNo);
+
+        // 检查服务可用性
+        if (jsapiServiceExtension == null) {
+            log.error("[微信支付] JSAPI 服务未初始化,请检查微信支付配置");
+            return PaymentResult.builder()
+                    .success(false)
+                    .channel(PaymentChannel.WECHAT)
+                    .errorCode("SERVICE_UNAVAILABLE")
+                    .errorMsg("支付查询服务未初始化,请检查配置")
+                    .build();
+        }
+
+        if (wxPayConfig == null) {
+            return error("CONFIG_UNAVAILABLE", "微信支付配置未初始化");
+        }
+
+        try {
+            QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest();
+            request.setMchid(wxPayConfig.getMchId());
+            request.setOutTradeNo(outTradeNo);
+
+            Transaction transaction = jsapiServiceExtension.queryOrderByOutTradeNo(request);
+
+            boolean isSuccess = Transaction.TradeStateEnum.SUCCESS.equals(transaction.getTradeState());
+
+            Map<String, String> payParams = new HashMap<>();
+            payParams.put("tradeState", transaction.getTradeState().name());
+            if (transaction.getTradeStateDesc() != null) {
+                payParams.put("tradeStateDesc", transaction.getTradeStateDesc());
+            }
+            if (transaction.getTransactionId() != null) {
+                payParams.put("transactionId", transaction.getTransactionId());
+            }
+
+            log.info("[微信支付] 订单查询完成 - outTradeNo: {}, tradeState: {}, transactionId: {}",
+                    outTradeNo, transaction.getTradeState(), transaction.getTransactionId());
+
+            return PaymentResult.builder()
+                    .success(isSuccess)
+                    .channel(PaymentChannel.WECHAT)
+                    .payParams(payParams)
+                    .build();
+
+        } catch (ServiceException e) {
+            log.error("[微信支付] 订单查询业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
+            return error(e.getErrorCode(), e.getErrorMessage());
+        } catch (HttpException e) {
+            log.error("[微信支付] 订单查询HTTP异常: {}", e.getMessage(), e);
+            return error("NETWORK_ERROR", "网络请求异常: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("[微信支付] 订单查询未知异常", e);
+            return error("UNKNOWN_ERROR", "查询失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 私有辅助方法 ====================
+
+    /**
+     * 从 packageVal 中提取 prepayId
+     * packageVal 格式: "prepay_id=xxx"
+     */
+    private String extractPrepayId(String packageVal) {
+        if (packageVal != null && packageVal.startsWith("prepay_id=")) {
+            return packageVal.substring("prepay_id=".length());
+        }
+        return packageVal;
+    }
+
+    /**
+     * 构建错误的支付结果
+     */
+    private PaymentResult error(String code, String msg) {
+        return PaymentResult.builder()
+                .success(false)
+                .channel(PaymentChannel.WECHAT)
+                .errorCode(code)
+                .errorMsg(msg)
+                .build();
+    }
+
+    /**
+     * 构建错误的支付结果(使用默认错误码)
+     */
+    private PaymentResult error(String msg) {
+        return error("ERROR", msg);
+    }
+}

+ 68 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreCallbackResult.java

@@ -0,0 +1,68 @@
+package com.haha.service.payment.payscore;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 支付分 - 回调解析结果
+ */
+@Data
+public class PayScoreCallbackResult {
+    
+    /**
+     * 是否成功解析
+     */
+    private boolean success;
+    
+    /**
+     * 事件类型: PAYSCORE.USER_CONFIRMED / PAYSCORE.USER_PAID
+     */
+    private String eventType;
+    
+    /**
+     * 商户服务订单号
+     */
+    private String outOrderNo;
+    
+    /**
+     * 服务订单状态
+     */
+    private String state;
+    
+    /**
+     * 总金额(元)
+     */
+    private BigDecimal totalAmount;
+    
+    /**
+     * 用户OpenID
+     */
+    private String openId;
+    
+    /**
+     * 支付完成时间
+     */
+    private LocalDateTime payTime;
+    
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+    
+    public static PayScoreCallbackResult success(String eventType, String outOrderNo, String state) {
+        PayScoreCallbackResult result = new PayScoreCallbackResult();
+        result.setSuccess(true);
+        result.setEventType(eventType);
+        result.setOutOrderNo(outOrderNo);
+        result.setState(state);
+        return result;
+    }
+    
+    public static PayScoreCallbackResult fail(String errorMsg) {
+        PayScoreCallbackResult result = new PayScoreCallbackResult();
+        result.setSuccess(false);
+        result.setErrorMsg(errorMsg);
+        return result;
+    }
+}

+ 20 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreCancelRequest.java

@@ -0,0 +1,20 @@
+package com.haha.service.payment.payscore;
+
+import lombok.Data;
+
+/**
+ * 支付分 - 取消服务订单请求
+ */
+@Data
+public class PayScoreCancelRequest {
+    
+    /**
+     * 商户服务订单号
+     */
+    private String outOrderNo;
+    
+    /**
+     * 取消原因
+     */
+    private String reason;
+}

+ 74 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreCompleteRequest.java

@@ -0,0 +1,74 @@
+package com.haha.service.payment.payscore;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 支付分 - 完结服务订单请求
+ */
+@Data
+public class PayScoreCompleteRequest {
+    
+    /**
+     * 商户服务订单号
+     */
+    private String outOrderNo;
+    
+    /**
+     * 总金额(元) - 最终收款总金额
+     */
+    private BigDecimal totalAmount;
+    
+    /**
+     * 后付费项目(实际付费明细)
+     */
+    private List<PayScoreCreateRequest.PostPayment> postPayments;
+    
+    /**
+     * 商户优惠列表
+     */
+    private List<PostDiscount> postDiscounts;
+    
+    /**
+     * 服务结束时间(yyyyMMddHHmmss)
+     */
+    private String endTime;
+    
+    /**
+     * 服务开始设备ID
+     */
+    private String startDeviceId;
+    
+    /**
+     * 服务结束设备ID
+     */
+    private String endDeviceId;
+    
+    /**
+     * 商户优惠
+     */
+    @Data
+    public static class PostDiscount {
+        
+        /**
+         * 优惠名称
+         */
+        private String name;
+        
+        /**
+         * 优惠说明
+         */
+        private String description;
+        
+        /**
+         * 优惠金额(元)
+         */
+        private BigDecimal amount;
+        
+        /**
+         * 优惠数量
+         */
+        private Integer count;
+    }
+}

+ 94 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreCreateRequest.java

@@ -0,0 +1,94 @@
+package com.haha.service.payment.payscore;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.List;
+
+/**
+ * 支付分 - 创建服务订单请求
+ */
+@Data
+public class PayScoreCreateRequest {
+    
+    /**
+     * 商户服务订单号
+     */
+    private String outOrderNo;
+    
+    /**
+     * 服务信息描述(不超过20字符)
+     */
+    private String serviceIntroduction;
+    
+    /**
+     * 用户OpenID
+     */
+    private String openId;
+    
+    /**
+     * 风险金名称: DEPOSIT(押金)/ADVANCE(预付款)/CASH_DEPOSIT(保证金)
+     */
+    private String riskFundName;
+    
+    /**
+     * 风险金额(元)
+     */
+    private BigDecimal riskFundAmount;
+    
+    /**
+     * 后付费项目列表
+     */
+    private List<PostPayment> postPayments;
+    
+    /**
+     * 服务开始时间(yyyyMMddHHmmss 或 OnAccept)
+     */
+    private String startTime;
+    
+    /**
+     * 服务结束时间(yyyyMMddHHmmss)
+     */
+    private String endTime;
+    
+    /**
+     * 服务开始设备ID(充电桩SN,必传)
+     */
+    private String startDeviceId;
+    
+    /**
+     * 服务结束设备ID
+     */
+    private String endDeviceId;
+    
+    /**
+     * 商户自定义数据(需urlencode,不超过256字符)
+     */
+    private String attach;
+    
+    /**
+     * 后付费项目
+     */
+    @Data
+    public static class PostPayment {
+        
+        /**
+         * 付费名称(不超过20字符)
+         */
+        private String name;
+        
+        /**
+         * 付费金额(元),大于等于0
+         */
+        private BigDecimal amount;
+        
+        /**
+         * 付费说明(不超过30字符)
+         */
+        private String description;
+        
+        /**
+         * 付费数量 [1,100]
+         */
+        private Integer count;
+    }
+}

+ 68 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreResult.java

@@ -0,0 +1,68 @@
+package com.haha.service.payment.payscore;
+
+import lombok.Data;
+import java.math.BigDecimal;
+import java.util.Map;
+
+/**
+ * 支付分 - 统一响应结果
+ */
+@Data
+public class PayScoreResult {
+    
+    /**
+     * 是否成功
+     */
+    private boolean success;
+    
+    /**
+     * 商户服务订单号
+     */
+    private String outOrderNo;
+    
+    /**
+     * 服务订单状态: CREATED/DOING/USER_PAYING/DONE/REVOKED
+     */
+    private String state;
+    
+    /**
+     * 小程序调起支付分确认页的package参数
+     */
+    private String packageStr;
+    
+    /**
+     * 总金额(元)
+     */
+    private BigDecimal totalAmount;
+    
+    /**
+     * 错误码
+     */
+    private String errorCode;
+    
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+    
+    /**
+     * 原始响应数据
+     */
+    private Map<String, Object> rawData;
+    
+    public static PayScoreResult success(String outOrderNo, String state) {
+        PayScoreResult result = new PayScoreResult();
+        result.setSuccess(true);
+        result.setOutOrderNo(outOrderNo);
+        result.setState(state);
+        return result;
+    }
+    
+    public static PayScoreResult fail(String errorCode, String errorMsg) {
+        PayScoreResult result = new PayScoreResult();
+        result.setSuccess(false);
+        result.setErrorCode(errorCode);
+        result.setErrorMsg(errorMsg);
+        return result;
+    }
+}

+ 91 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreService.java

@@ -0,0 +1,91 @@
+package com.haha.service.payment.payscore;
+
+import com.haha.service.payment.payscore.PayScoreCreateRequest.PostPayment;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 支付分业务服务接口
+ * 封装支付分相关的业务逻辑,协调订单服务和支付分策略
+ */
+public interface PayScoreService {
+
+    /**
+     * 创建支付分服务订单
+     * 
+     * @param orderId 本地订单ID
+     * @param openId 用户微信openId
+     * @return 支付分结果(含小程序调起支付分所需的package参数)
+     */
+    PayScoreResult createPayScoreOrder(Long orderId, String openId);
+
+    /**
+     * 创建支付分服务订单(带风险金配置)
+     * 
+     * @param orderId 本地订单ID
+     * @param openId 用户微信openId
+     * @param riskFundAmount 风险金额
+     * @param riskFundName 风险金名称(DEPOSIT/ADVANCE/CASH_DEPOSIT)
+     * @return 支付分结果
+     */
+    PayScoreResult createPayScoreOrder(Long orderId, String openId, 
+            BigDecimal riskFundAmount, String riskFundName);
+
+    /**
+     * 完结支付分服务订单(触发自动扣款)
+     * 
+     * @param orderId 本地订单ID
+     * @param totalAmount 实际扣款总金额
+     * @param payments 后付费项目列表
+     * @return 支付分结果
+     */
+    PayScoreResult completePayScoreOrder(Long orderId, BigDecimal totalAmount, 
+            List<PostPayment> payments);
+
+    /**
+     * 完结支付分服务订单(带优惠)
+     * 
+     * @param orderId 本地订单ID
+     * @param totalAmount 实际扣款总金额
+     * @param payments 后付费项目列表
+     * @param discounts 商户优惠列表
+     * @return 支付分结果
+     */
+    PayScoreResult completePayScoreOrder(Long orderId, BigDecimal totalAmount, 
+            List<PostPayment> payments, List<PayScoreCompleteRequest.PostDiscount> discounts);
+
+    /**
+     * 取消支付分服务订单
+     * 
+     * @param orderId 本地订单ID
+     * @param reason 取消原因
+     * @return 支付分结果
+     */
+    PayScoreResult cancelPayScoreOrder(Long orderId, String reason);
+
+    /**
+     * 查询支付分服务订单状态
+     * 
+     * @param outOrderNo 商户服务订单号
+     * @return 支付分结果
+     */
+    PayScoreResult queryPayScoreOrder(String outOrderNo);
+
+    /**
+     * 处理支付分回调通知
+     * 
+     * @param params 回调参数(含请求头和请求体)
+     * @return 回调处理结果
+     */
+    PayScoreCallbackResult handlePayScoreCallback(Map<String, Object> params);
+
+    /**
+     * 同步订单支付分状态
+     * 从微信侧查询最新状态并更新本地订单
+     * 
+     * @param orderId 本地订单ID
+     * @return 是否同步成功
+     */
+    boolean syncPayScoreStatus(Long orderId);
+}

+ 41 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/PayScoreStrategy.java

@@ -0,0 +1,41 @@
+package com.haha.service.payment.payscore;
+
+import com.haha.common.enums.PaymentChannel;
+import java.util.Map;
+
+/**
+ * 支付分策略接口
+ * 支付分(先享后付)与普通即时支付生命周期不同,独立定义策略接口
+ */
+public interface PayScoreStrategy {
+
+    /**
+     * 获取支持的支付渠道
+     */
+    PaymentChannel getChannel();
+
+    /**
+     * 创建支付分服务订单
+     */
+    PayScoreResult createServiceOrder(PayScoreCreateRequest request);
+
+    /**
+     * 完结服务订单(触发自动扣款)
+     */
+    PayScoreResult completeServiceOrder(PayScoreCompleteRequest request);
+
+    /**
+     * 取消服务订单
+     */
+    PayScoreResult cancelServiceOrder(PayScoreCancelRequest request);
+
+    /**
+     * 查询服务订单
+     */
+    PayScoreResult queryServiceOrder(String outOrderNo);
+
+    /**
+     * 解析支付分回调通知
+     */
+    PayScoreCallbackResult parseCallback(Map<String, Object> params);
+}

+ 350 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/impl/PayScoreServiceImpl.java

@@ -0,0 +1,350 @@
+package com.haha.service.payment.payscore.impl;
+
+import com.haha.common.constant.OrderConstants;
+import com.haha.entity.Order;
+import com.haha.service.OrderService;
+import com.haha.service.payment.config.WxPayConfig;
+import com.haha.service.payment.payscore.*;
+import com.haha.service.payment.payscore.PayScoreCreateRequest.PostPayment;
+import com.haha.service.payment.payscore.PayScoreCompleteRequest.PostDiscount;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 支付分业务服务实现
+ * 封装支付分相关的业务逻辑,协调订单服务和支付分策略
+ */
+@Slf4j
+@Service
+public class PayScoreServiceImpl implements PayScoreService {
+
+    @Autowired(required = false)
+    private PayScoreStrategy payScoreStrategy;
+
+    @Autowired
+    private OrderService orderService;
+
+    @Autowired(required = false)
+    private WxPayConfig wxPayConfig;
+
+    private static final BigDecimal DEFAULT_RISK_FUND_AMOUNT = new BigDecimal("99.00");
+    private static final String DEFAULT_RISK_FUND_NAME = "DEPOSIT";
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PayScoreResult createPayScoreOrder(Long orderId, String openId) {
+        return createPayScoreOrder(orderId, openId, DEFAULT_RISK_FUND_AMOUNT, DEFAULT_RISK_FUND_NAME);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PayScoreResult createPayScoreOrder(Long orderId, String openId, 
+            BigDecimal riskFundAmount, String riskFundName) {
+        log.info("[支付分服务] 创建服务订单 - orderId: {}, openId: {}, riskFund: {}元", 
+                orderId, openId, riskFundAmount);
+
+        PayScoreResult checkResult = checkServiceAvailable();
+        if (checkResult != null) {
+            return checkResult;
+        }
+
+        Order order = validateAndGetOrder(orderId);
+
+        if (order.getPayScoreOrderId() != null && !"REVOKED".equals(order.getPayScoreState())) {
+            log.warn("[支付分服务] 订单已存在支付分订单 - orderId: {}, payScoreState: {}", 
+                    orderId, order.getPayScoreState());
+            return PayScoreResult.fail("ORDER_EXISTS", "该订单已创建支付分服务订单");
+        }
+
+        PayScoreCreateRequest request = new PayScoreCreateRequest();
+        request.setOutOrderNo(generateOutOrderNo(orderId));
+        request.setServiceIntroduction("充电服务");
+        request.setOpenId(openId);
+        request.setRiskFundName(riskFundName != null ? riskFundName : DEFAULT_RISK_FUND_NAME);
+        request.setRiskFundAmount(riskFundAmount != null ? riskFundAmount : DEFAULT_RISK_FUND_AMOUNT);
+        request.setStartTime("OnAccept");
+        request.setStartDeviceId(order.getDeviceId());
+
+        if (order.getTotalAmount() != null && order.getTotalAmount().compareTo(BigDecimal.ZERO) > 0) {
+            PostPayment payment = new PostPayment();
+            payment.setName("充电费用");
+            payment.setAmount(order.getTotalAmount());
+            payment.setDescription("预估费用");
+            request.setPostPayments(Collections.singletonList(payment));
+        }
+
+        PayScoreResult result = payScoreStrategy.createServiceOrder(request);
+
+        if (result.isSuccess()) {
+            order.setPayScoreOrderId(result.getOutOrderNo());
+            order.setPayScoreState(result.getState());
+            if (wxPayConfig != null) {
+                order.setServiceId(wxPayConfig.getServiceId());
+            }
+            order.setServiceStartTime(LocalDateTime.now());
+            order.setPayChannel("wechat_payscore");
+            order.setPayType("微信支付分");
+            orderService.updateById(order);
+
+            log.info("[支付分服务] 创建服务订单成功 - orderId: {}, outOrderNo: {}, state: {}", 
+                    orderId, result.getOutOrderNo(), result.getState());
+        } else {
+            log.error("[支付分服务] 创建服务订单失败 - orderId: {}, errorCode: {}, errorMsg: {}", 
+                    orderId, result.getErrorCode(), result.getErrorMsg());
+        }
+
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PayScoreResult completePayScoreOrder(Long orderId, BigDecimal totalAmount, 
+            List<PostPayment> payments) {
+        return completePayScoreOrder(orderId, totalAmount, payments, null);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PayScoreResult completePayScoreOrder(Long orderId, BigDecimal totalAmount, 
+            List<PostPayment> payments, List<PostDiscount> discounts) {
+        log.info("[支付分服务] 完结服务订单 - orderId: {}, totalAmount: {}元", orderId, totalAmount);
+
+        PayScoreResult checkResult = checkServiceAvailable();
+        if (checkResult != null) {
+            return checkResult;
+        }
+
+        Order order = validateAndGetOrder(orderId);
+
+        if (order.getPayScoreOrderId() == null) {
+            log.error("[支付分服务] 订单未创建支付分服务订单 - orderId: {}", orderId);
+            return PayScoreResult.fail("ORDER_NOT_CREATED", "该订单未创建支付分服务订单");
+        }
+
+        if ("DONE".equals(order.getPayScoreState()) || "REVOKED".equals(order.getPayScoreState())) {
+            log.warn("[支付分服务] 订单已完结或已取消 - orderId: {}, payScoreState: {}", 
+                    orderId, order.getPayScoreState());
+            return PayScoreResult.fail("ORDER_COMPLETED", "该订单已完结或已取消");
+        }
+
+        PayScoreCompleteRequest request = new PayScoreCompleteRequest();
+        request.setOutOrderNo(order.getPayScoreOrderId());
+        request.setTotalAmount(totalAmount);
+        request.setPostPayments(payments);
+        request.setPostDiscounts(discounts);
+        request.setEndTime(formatDateTime(LocalDateTime.now()));
+        request.setStartDeviceId(order.getDeviceId());
+
+        PayScoreResult result = payScoreStrategy.completeServiceOrder(request);
+
+        if (result.isSuccess()) {
+            order.setPayScoreState(result.getState() != null ? result.getState() : "USER_PAYING");
+            order.setServiceEndTime(LocalDateTime.now());
+            order.setTotalAmount(totalAmount);
+            orderService.updateById(order);
+
+            log.info("[支付分服务] 完结服务订单成功 - orderId: {}, state: {}", 
+                    orderId, result.getState());
+        } else {
+            log.error("[支付分服务] 完结服务订单失败 - orderId: {}, errorCode: {}, errorMsg: {}", 
+                    orderId, result.getErrorCode(), result.getErrorMsg());
+        }
+
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PayScoreResult cancelPayScoreOrder(Long orderId, String reason) {
+        log.info("[支付分服务] 取消服务订单 - orderId: {}, reason: {}", orderId, reason);
+
+        PayScoreResult checkResult = checkServiceAvailable();
+        if (checkResult != null) {
+            return checkResult;
+        }
+
+        Order order = validateAndGetOrder(orderId);
+
+        if (order.getPayScoreOrderId() == null) {
+            log.error("[支付分服务] 订单未创建支付分服务订单 - orderId: {}", orderId);
+            return PayScoreResult.fail("ORDER_NOT_CREATED", "该订单未创建支付分服务订单");
+        }
+
+        if ("DONE".equals(order.getPayScoreState()) || "REVOKED".equals(order.getPayScoreState())) {
+            log.warn("[支付分服务] 订单已完结或已取消 - orderId: {}, payScoreState: {}", 
+                    orderId, order.getPayScoreState());
+            return PayScoreResult.fail("ORDER_COMPLETED", "该订单已完结或已取消");
+        }
+
+        PayScoreCancelRequest request = new PayScoreCancelRequest();
+        request.setOutOrderNo(order.getPayScoreOrderId());
+        request.setReason(reason != null ? reason : "用户取消");
+
+        PayScoreResult result = payScoreStrategy.cancelServiceOrder(request);
+
+        if (result.isSuccess()) {
+            order.setPayScoreState("REVOKED");
+            order.setStatus(OrderConstants.STATUS_CANCELLED);
+            orderService.updateById(order);
+
+            log.info("[支付分服务] 取消服务订单成功 - orderId: {}", orderId);
+        } else {
+            log.error("[支付分服务] 取消服务订单失败 - orderId: {}, errorCode: {}, errorMsg: {}", 
+                    orderId, result.getErrorCode(), result.getErrorMsg());
+        }
+
+        return result;
+    }
+
+    @Override
+    public PayScoreResult queryPayScoreOrder(String outOrderNo) {
+        log.info("[支付分服务] 查询服务订单 - outOrderNo: {}", outOrderNo);
+        
+        PayScoreResult checkResult = checkServiceAvailable();
+        if (checkResult != null) {
+            return checkResult;
+        }
+        
+        return payScoreStrategy.queryServiceOrder(outOrderNo);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public PayScoreCallbackResult handlePayScoreCallback(Map<String, Object> params) {
+        log.info("[支付分服务] 处理回调通知");
+
+        if (payScoreStrategy == null) {
+            log.error("[支付分服务] 支付分策略未初始化,请检查微信支付配置");
+            return PayScoreCallbackResult.fail("支付分服务未配置");
+        }
+
+        PayScoreCallbackResult result = payScoreStrategy.parseCallback(params);
+
+        if (!result.isSuccess()) {
+            log.error("[支付分服务] 回调解析失败 - errorMsg: {}", result.getErrorMsg());
+            return result;
+        }
+
+        String outOrderNo = result.getOutOrderNo();
+        String eventType = result.getEventType();
+        String state = result.getState();
+
+        log.info("[支付分服务] 回调解析成功 - outOrderNo: {}, eventType: {}, state: {}", 
+                outOrderNo, eventType, state);
+
+        Order order = orderService.lambdaQuery()
+                .eq(Order::getPayScoreOrderId, outOrderNo)
+                .one();
+
+        if (order == null) {
+            log.error("[支付分服务] 未找到对应订单 - payScoreOrderId: {}", outOrderNo);
+            return result;
+        }
+
+        if ("PAYSCORE.USER_CONFIRMED".equals(eventType)) {
+            order.setPayScoreState("DOING");
+            log.info("[支付分服务] 用户确认使用服务 - orderId: {}", order.getId());
+        } else if ("PAYSCORE.USER_PAID".equals(eventType)) {
+            order.setPayScoreState("DONE");
+            order.setPayStatus(OrderConstants.PAY_STATUS_PAID);
+            order.setStatus(OrderConstants.STATUS_COMPLETED);
+            order.setPayTime(result.getPayTime() != null ? result.getPayTime() : LocalDateTime.now());
+            if (result.getTotalAmount() != null) {
+                order.setTotalAmount(result.getTotalAmount());
+            }
+            log.info("[支付分服务] 用户完成支付 - orderId: {}, amount: {}元", 
+                    order.getId(), result.getTotalAmount());
+        }
+
+        orderService.updateById(order);
+
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean syncPayScoreStatus(Long orderId) {
+        log.info("[支付分服务] 同步订单状态 - orderId: {}", orderId);
+
+        PayScoreResult checkResult = checkServiceAvailable();
+        if (checkResult != null) {
+            return false;
+        }
+
+        Order order = validateAndGetOrder(orderId);
+
+        if (order.getPayScoreOrderId() == null) {
+            log.warn("[支付分服务] 订单未创建支付分服务订单,无法同步 - orderId: {}", orderId);
+            return false;
+        }
+
+        PayScoreResult result = payScoreStrategy.queryServiceOrder(order.getPayScoreOrderId());
+
+        if (!result.isSuccess()) {
+            log.error("[支付分服务] 查询服务订单失败 - orderId: {}, errorCode: {}", 
+                    orderId, result.getErrorCode());
+            return false;
+        }
+
+        String newState = result.getState();
+        String oldState = order.getPayScoreState();
+
+        if (newState != null && !newState.equals(oldState)) {
+            order.setPayScoreState(newState);
+
+            if ("DONE".equals(newState)) {
+                order.setPayStatus(OrderConstants.PAY_STATUS_PAID);
+                order.setStatus(OrderConstants.STATUS_COMPLETED);
+                if (result.getTotalAmount() != null) {
+                    order.setTotalAmount(result.getTotalAmount());
+                }
+            } else if ("REVOKED".equals(newState)) {
+                order.setStatus(OrderConstants.STATUS_CANCELLED);
+            }
+
+            orderService.updateById(order);
+            log.info("[支付分服务] 订单状态已更新 - orderId: {}, oldState: {}, newState: {}", 
+                    orderId, oldState, newState);
+        }
+
+        return true;
+    }
+
+    private PayScoreResult checkServiceAvailable() {
+        if (payScoreStrategy == null) {
+            log.error("[支付分服务] 支付分策略未初始化,请检查微信支付配置");
+            return PayScoreResult.fail("SERVICE_UNAVAILABLE", "支付分服务未配置");
+        }
+        return null;
+    }
+
+    private Order validateAndGetOrder(Long orderId) {
+        if (orderId == null) {
+            throw new IllegalArgumentException("订单ID不能为空");
+        }
+
+        Order order = orderService.getById(orderId);
+        if (order == null) {
+            throw new IllegalArgumentException("订单不存在: " + orderId);
+        }
+
+        return order;
+    }
+
+    private String generateOutOrderNo(Long orderId) {
+        return "PS" + System.currentTimeMillis() + orderId;
+    }
+
+    private String formatDateTime(LocalDateTime dateTime) {
+        return dateTime.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
+    }
+}

+ 586 - 0
haha-service/src/main/java/com/haha/service/payment/payscore/impl/WxPayScoreStrategy.java

@@ -0,0 +1,586 @@
+package com.haha.service.payment.payscore.impl;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.haha.common.enums.PaymentChannel;
+import com.haha.service.payment.config.WxPayConfig;
+import com.haha.service.payment.payscore.*;
+import com.wechat.pay.java.core.exception.HttpException;
+import com.wechat.pay.java.core.exception.MalformedMessageException;
+import com.wechat.pay.java.core.exception.ServiceException;
+import com.wechat.pay.java.core.exception.ValidationException;
+import com.wechat.pay.java.core.http.HttpClient;
+import com.wechat.pay.java.core.http.HttpMethod;
+import com.wechat.pay.java.core.http.HttpRequest;
+import com.wechat.pay.java.core.http.HttpResponse;
+import com.wechat.pay.java.core.http.JsonRequestBody;
+import com.wechat.pay.java.core.http.MediaType;
+import com.wechat.pay.java.core.notification.NotificationParser;
+import com.wechat.pay.java.core.notification.RequestParam;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDateTime;
+import java.time.OffsetDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 微信支付分策略实现
+ * 使用微信支付 V3 SDK core 模块的 HttpClient 发送自定义请求
+ */
+@Slf4j
+@Component
+public class WxPayScoreStrategy implements PayScoreStrategy {
+
+    private static final String PAY_SCORE_BASE_URL = "https://api.mch.weixin.qq.com/v3/payscore/serviceorder";
+
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    @Autowired(required = false)
+    private HttpClient wxPayHttpClient;
+
+    @Autowired(required = false)
+    private NotificationParser wxNotificationParser;
+
+    @Autowired(required = false)
+    private WxPayConfig wxPayConfig;
+
+    @Override
+    public PaymentChannel getChannel() {
+        return PaymentChannel.WECHAT;
+    }
+
+    @Override
+    public PayScoreResult createServiceOrder(PayScoreCreateRequest request) {
+        log.info("[微信支付分] 创建服务订单 - 订单号: {}, 设备: {}", request.getOutOrderNo(), request.getStartDeviceId());
+
+        // 空检查
+        PayScoreResult checkResult = checkServiceAvailable();
+        if (checkResult != null) {
+            return checkResult;
+        }
+
+        try {
+            // 构建请求体 JSON
+            ObjectNode body = objectMapper.createObjectNode();
+            body.put("out_order_no", request.getOutOrderNo());
+            body.put("appid", wxPayConfig.getAppId());
+            body.put("service_id", wxPayConfig.getServiceId());
+            body.put("service_introduction", request.getServiceIntroduction());
+            body.put("notify_url", wxPayConfig.getPayScoreNotifyUrl());
+            body.put("need_user_confirm", true);
+
+            // 后付费项目
+            if (request.getPostPayments() != null && !request.getPostPayments().isEmpty()) {
+                ArrayNode postPayments = objectMapper.createArrayNode();
+                for (PayScoreCreateRequest.PostPayment payment : request.getPostPayments()) {
+                    ObjectNode paymentObj = objectMapper.createObjectNode();
+                    paymentObj.put("name", payment.getName());
+                    paymentObj.put("amount", yuanToFen(payment.getAmount()));
+                    if (payment.getDescription() != null) {
+                        paymentObj.put("description", payment.getDescription());
+                    }
+                    if (payment.getCount() != null) {
+                        paymentObj.put("count", payment.getCount());
+                    }
+                    postPayments.add(paymentObj);
+                }
+                body.set("post_payments", postPayments);
+            }
+
+            // 风险金
+            ObjectNode riskFund = objectMapper.createObjectNode();
+            riskFund.put("name", request.getRiskFundName() != null ? request.getRiskFundName() : "DEPOSIT");
+            riskFund.put("amount", yuanToFen(request.getRiskFundAmount()));
+            body.set("risk_fund", riskFund);
+
+            // 服务时间段
+            ObjectNode timeRange = objectMapper.createObjectNode();
+            timeRange.put("start_time", request.getStartTime() != null ? request.getStartTime() : "OnAccept");
+            if (request.getEndTime() != null) {
+                timeRange.put("end_time", request.getEndTime());
+            }
+            body.set("time_range", timeRange);
+
+            // 服务位置(设备)
+            if (request.getStartDeviceId() != null) {
+                ObjectNode location = objectMapper.createObjectNode();
+                location.put("start_location", request.getStartDeviceId());
+                if (request.getEndDeviceId() != null) {
+                    location.put("end_location", request.getEndDeviceId());
+                }
+                body.set("location", location);
+            }
+
+            // 附加数据
+            if (request.getAttach() != null) {
+                body.put("attach", request.getAttach());
+            }
+
+            // 用户标识(如果需要免确认,可传入 openid)
+            if (request.getOpenId() != null) {
+                body.put("openid", request.getOpenId());
+            }
+
+            String requestBody = objectMapper.writeValueAsString(body);
+            log.info("[微信支付分] 创建订单请求体: {}", requestBody);
+
+            // 发送 POST 请求
+            HttpRequest httpRequest = new HttpRequest.Builder()
+                    .httpMethod(HttpMethod.POST)
+                    .url(PAY_SCORE_BASE_URL)
+                    .body(new JsonRequestBody.Builder().body(requestBody).build())
+                    .addHeader("Content-Type", MediaType.APPLICATION_JSON.getValue())
+                    .addHeader("Accept", MediaType.APPLICATION_JSON.getValue())
+                    .build();
+
+            HttpResponse<String> response = wxPayHttpClient.execute(httpRequest, String.class);
+
+            // 解析响应
+            String responseBody = response.getServiceResponse();
+            log.info("[微信支付分] 创建订单响应: body={}", responseBody);
+
+            JsonNode respJson = objectMapper.readTree(responseBody);
+
+            String outOrderNo = getJsonString(respJson, "out_order_no");
+            String state = getJsonString(respJson, "state");
+            String packageStr = getJsonString(respJson, "package");
+
+            PayScoreResult result = PayScoreResult.success(outOrderNo, state);
+            result.setPackageStr(packageStr);
+
+            Map<String, Object> rawData = new HashMap<>();
+            rawData.put("response", responseBody);
+            result.setRawData(rawData);
+
+            log.info("[微信支付分] 创建订单成功 - 订单号: {}, 状态: {}, package: {}", outOrderNo, state, packageStr);
+            return result;
+
+        } catch (HttpException e) {
+            log.error("[微信支付分] 创建订单HTTP异常: {}", e.getMessage(), e);
+            return PayScoreResult.fail("NETWORK_ERROR", "网络请求异常: " + e.getMessage());
+        } catch (ServiceException e) {
+            log.error("[微信支付分] 创建订单业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
+            return PayScoreResult.fail(e.getErrorCode(), e.getErrorMessage());
+        } catch (MalformedMessageException e) {
+            log.error("[微信支付分] 创建订单消息解析异常: {}", e.getMessage(), e);
+            return PayScoreResult.fail("MESSAGE_ERROR", "消息解析异常: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("[微信支付分] 创建订单未知异常", e);
+            return PayScoreResult.fail("UNKNOWN_ERROR", "创建订单失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public PayScoreResult completeServiceOrder(PayScoreCompleteRequest request) {
+        log.info("[微信支付分] 完结服务订单 - 订单号: {}, 总金额: {}元", request.getOutOrderNo(), request.getTotalAmount());
+
+        // 空检查
+        PayScoreResult checkResult = checkServiceAvailable();
+        if (checkResult != null) {
+            return checkResult;
+        }
+
+        try {
+            // 构建请求体 JSON
+            ObjectNode body = objectMapper.createObjectNode();
+            body.put("appid", wxPayConfig.getAppId());
+            body.put("service_id", wxPayConfig.getServiceId());
+
+            // 后付费项目(实际收费)
+            if (request.getPostPayments() != null && !request.getPostPayments().isEmpty()) {
+                ArrayNode postPayments = objectMapper.createArrayNode();
+                for (PayScoreCreateRequest.PostPayment payment : request.getPostPayments()) {
+                    ObjectNode paymentObj = objectMapper.createObjectNode();
+                    paymentObj.put("name", payment.getName());
+                    paymentObj.put("amount", yuanToFen(payment.getAmount()));
+                    if (payment.getDescription() != null) {
+                        paymentObj.put("description", payment.getDescription());
+                    }
+                    if (payment.getCount() != null) {
+                        paymentObj.put("count", payment.getCount());
+                    }
+                    postPayments.add(paymentObj);
+                }
+                body.set("post_payments", postPayments);
+            }
+
+            // 商户优惠
+            if (request.getPostDiscounts() != null && !request.getPostDiscounts().isEmpty()) {
+                ArrayNode postDiscounts = objectMapper.createArrayNode();
+                for (PayScoreCompleteRequest.PostDiscount discount : request.getPostDiscounts()) {
+                    ObjectNode discountObj = objectMapper.createObjectNode();
+                    discountObj.put("name", discount.getName());
+                    if (discount.getDescription() != null) {
+                        discountObj.put("description", discount.getDescription());
+                    }
+                    if (discount.getAmount() != null) {
+                        discountObj.put("amount", yuanToFen(discount.getAmount()));
+                    }
+                    if (discount.getCount() != null) {
+                        discountObj.put("count", discount.getCount());
+                    }
+                    postDiscounts.add(discountObj);
+                }
+                body.set("post_discounts", postDiscounts);
+            }
+
+            // 总金额
+            body.put("total_amount", yuanToFen(request.getTotalAmount()));
+
+            // 服务时间段
+            if (request.getEndTime() != null) {
+                ObjectNode timeRange = objectMapper.createObjectNode();
+                timeRange.put("end_time", request.getEndTime());
+                body.set("time_range", timeRange);
+            }
+
+            // 服务位置(设备)
+            if (request.getStartDeviceId() != null || request.getEndDeviceId() != null) {
+                ObjectNode location = objectMapper.createObjectNode();
+                if (request.getStartDeviceId() != null) {
+                    location.put("start_location", request.getStartDeviceId());
+                }
+                if (request.getEndDeviceId() != null) {
+                    location.put("end_location", request.getEndDeviceId());
+                }
+                body.set("location", location);
+            }
+
+            String requestBody = objectMapper.writeValueAsString(body);
+            String url = PAY_SCORE_BASE_URL + "/" + request.getOutOrderNo() + "/complete";
+            log.info("[微信支付分] 完结订单请求: url={}, body={}", url, requestBody);
+
+            // 发送 POST 请求
+            HttpRequest httpRequest = new HttpRequest.Builder()
+                    .httpMethod(HttpMethod.POST)
+                    .url(url)
+                    .body(new JsonRequestBody.Builder().body(requestBody).build())
+                    .addHeader("Content-Type", MediaType.APPLICATION_JSON.getValue())
+                    .addHeader("Accept", MediaType.APPLICATION_JSON.getValue())
+                    .build();
+
+            HttpResponse<String> response = wxPayHttpClient.execute(httpRequest, String.class);
+
+            // 解析响应体
+            String responseBody = response.getServiceResponse();
+            log.info("[微信支付分] 完结订单响应: body={}", responseBody);
+
+            if (responseBody != null && !responseBody.isEmpty()) {
+                JsonNode respJson = objectMapper.readTree(responseBody);
+                String outOrderNo = getJsonString(respJson, "out_order_no");
+                String state = getJsonString(respJson, "state");
+                log.info("[微信支付分] 完结订单成功 - 订单号: {}, 状态: {}", outOrderNo, state);
+                return PayScoreResult.success(outOrderNo != null ? outOrderNo : request.getOutOrderNo(), 
+                        state != null ? state : "USER_PAYING");
+            }
+
+            log.info("[微信支付分] 完结订单成功 - 订单号: {}", request.getOutOrderNo());
+            return PayScoreResult.success(request.getOutOrderNo(), "USER_PAYING");
+
+        } catch (HttpException e) {
+            log.error("[微信支付分] 完结订单HTTP异常: {}", e.getMessage(), e);
+            return PayScoreResult.fail("NETWORK_ERROR", "网络请求异常: " + e.getMessage());
+        } catch (ServiceException e) {
+            log.error("[微信支付分] 完结订单业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
+            return PayScoreResult.fail(e.getErrorCode(), e.getErrorMessage());
+        } catch (MalformedMessageException e) {
+            log.error("[微信支付分] 完结订单消息解析异常: {}", e.getMessage(), e);
+            return PayScoreResult.fail("MESSAGE_ERROR", "消息解析异常: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("[微信支付分] 完结订单未知异常", e);
+            return PayScoreResult.fail("UNKNOWN_ERROR", "完结订单失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public PayScoreResult cancelServiceOrder(PayScoreCancelRequest request) {
+        log.info("[微信支付分] 取消服务订单 - 订单号: {}, 原因: {}", request.getOutOrderNo(), request.getReason());
+
+        // 空检查
+        PayScoreResult checkResult = checkServiceAvailable();
+        if (checkResult != null) {
+            return checkResult;
+        }
+
+        try {
+            // 构建请求体 JSON
+            ObjectNode body = objectMapper.createObjectNode();
+            body.put("appid", wxPayConfig.getAppId());
+            body.put("service_id", wxPayConfig.getServiceId());
+            body.put("reason", request.getReason() != null ? request.getReason() : "用户取消");
+
+            String requestBody = objectMapper.writeValueAsString(body);
+            String url = PAY_SCORE_BASE_URL + "/" + request.getOutOrderNo() + "/cancel";
+            log.info("[微信支付分] 取消订单请求: url={}, body={}", url, requestBody);
+
+            // 发送 POST 请求
+            HttpRequest httpRequest = new HttpRequest.Builder()
+                    .httpMethod(HttpMethod.POST)
+                    .url(url)
+                    .body(new JsonRequestBody.Builder().body(requestBody).build())
+                    .addHeader("Content-Type", MediaType.APPLICATION_JSON.getValue())
+                    .addHeader("Accept", MediaType.APPLICATION_JSON.getValue())
+                    .build();
+
+            HttpResponse<String> response = wxPayHttpClient.execute(httpRequest, String.class);
+
+            // 解析响应体
+            String responseBody = response.getServiceResponse();
+            log.info("[微信支付分] 取消订单响应: body={}", responseBody);
+
+            if (responseBody != null && !responseBody.isEmpty()) {
+                JsonNode respJson = objectMapper.readTree(responseBody);
+                String outOrderNo = getJsonString(respJson, "out_order_no");
+                String state = getJsonString(respJson, "state");
+                log.info("[微信支付分] 取消订单成功 - 订单号: {}, 状态: {}", outOrderNo, state);
+                return PayScoreResult.success(outOrderNo != null ? outOrderNo : request.getOutOrderNo(), 
+                        state != null ? state : "REVOKED");
+            }
+
+            log.info("[微信支付分] 取消订单成功 - 订单号: {}", request.getOutOrderNo());
+            return PayScoreResult.success(request.getOutOrderNo(), "REVOKED");
+
+        } catch (HttpException e) {
+            log.error("[微信支付分] 取消订单HTTP异常: {}", e.getMessage(), e);
+            return PayScoreResult.fail("NETWORK_ERROR", "网络请求异常: " + e.getMessage());
+        } catch (ServiceException e) {
+            log.error("[微信支付分] 取消订单业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
+            return PayScoreResult.fail(e.getErrorCode(), e.getErrorMessage());
+        } catch (MalformedMessageException e) {
+            log.error("[微信支付分] 取消订单消息解析异常: {}", e.getMessage(), e);
+            return PayScoreResult.fail("MESSAGE_ERROR", "消息解析异常: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("[微信支付分] 取消订单未知异常", e);
+            return PayScoreResult.fail("UNKNOWN_ERROR", "取消订单失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public PayScoreResult queryServiceOrder(String outOrderNo) {
+        log.info("[微信支付分] 查询服务订单 - 订单号: {}", outOrderNo);
+
+        // 空检查
+        PayScoreResult checkResult = checkServiceAvailable();
+        if (checkResult != null) {
+            return checkResult;
+        }
+
+        try {
+            // 构建查询 URL
+            String url = PAY_SCORE_BASE_URL 
+                    + "?out_order_no=" + outOrderNo 
+                    + "&service_id=" + wxPayConfig.getServiceId() 
+                    + "&appid=" + wxPayConfig.getAppId();
+            log.info("[微信支付分] 查询订单请求: url={}", url);
+
+            // 发送 GET 请求
+            HttpRequest httpRequest = new HttpRequest.Builder()
+                    .httpMethod(HttpMethod.GET)
+                    .url(url)
+                    .addHeader("Accept", MediaType.APPLICATION_JSON.getValue())
+                    .build();
+
+            HttpResponse<String> response = wxPayHttpClient.execute(httpRequest, String.class);
+
+            // 解析响应
+            String responseBody = response.getServiceResponse();
+            log.info("[微信支付分] 查询订单响应: body={}", responseBody);
+
+            JsonNode respJson = objectMapper.readTree(responseBody);
+
+            String respOutOrderNo = getJsonString(respJson, "out_order_no");
+            String state = getJsonString(respJson, "state");
+
+            PayScoreResult result = PayScoreResult.success(respOutOrderNo, state);
+
+            // 解析总金额
+            if (respJson.has("total_amount") && !respJson.get("total_amount").isNull()) {
+                int totalAmountFen = respJson.get("total_amount").asInt();
+                result.setTotalAmount(fenToYuan(totalAmountFen));
+            }
+
+            Map<String, Object> rawData = new HashMap<>();
+            rawData.put("response", responseBody);
+            result.setRawData(rawData);
+
+            log.info("[微信支付分] 查询订单成功 - 订单号: {}, 状态: {}, 金额: {}元", 
+                    respOutOrderNo, state, result.getTotalAmount());
+            return result;
+
+        } catch (HttpException e) {
+            log.error("[微信支付分] 查询订单HTTP异常: {}", e.getMessage(), e);
+            return PayScoreResult.fail("NETWORK_ERROR", "网络请求异常: " + e.getMessage());
+        } catch (ServiceException e) {
+            log.error("[微信支付分] 查询订单业务异常: code={}, msg={}", e.getErrorCode(), e.getErrorMessage(), e);
+            return PayScoreResult.fail(e.getErrorCode(), e.getErrorMessage());
+        } catch (MalformedMessageException e) {
+            log.error("[微信支付分] 查询订单消息解析异常: {}", e.getMessage(), e);
+            return PayScoreResult.fail("MESSAGE_ERROR", "消息解析异常: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("[微信支付分] 查询订单未知异常", e);
+            return PayScoreResult.fail("UNKNOWN_ERROR", "查询订单失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public PayScoreCallbackResult parseCallback(Map<String, Object> params) {
+        log.info("[微信支付分] 解析回调通知 - 参数Keys: {}", params.keySet());
+
+        // 检查服务可用性
+        if (wxNotificationParser == null) {
+            log.error("[微信支付分] 回调解析服务未初始化,请检查微信支付配置");
+            return PayScoreCallbackResult.fail("回调解析服务未初始化,请检查配置");
+        }
+
+        try {
+            // 从 params 中提取微信 V3 回调所需的信息
+            String timestamp = (String) params.get("Wechatpay-Timestamp");
+            String nonce = (String) params.get("Wechatpay-Nonce");
+            String signature = (String) params.get("Wechatpay-Signature");
+            String serial = (String) params.get("Wechatpay-Serial");
+            String signType = (String) params.getOrDefault("Wechatpay-Signature-Type", "WECHATPAY2-SHA256-RSA2048");
+            String body = (String) params.get("body");
+
+            log.debug("[微信支付分] 回调请求头: timestamp={}, nonce={}, serial={}, signType={}",
+                    timestamp, nonce, serial, signType);
+
+            // 构建 RequestParam
+            RequestParam requestParam = new RequestParam.Builder()
+                    .serialNumber(serial)
+                    .nonce(nonce)
+                    .timestamp(timestamp)
+                    .signature(signature)
+                    .signType(signType)
+                    .body(body)
+                    .build();
+
+            // SDK 自动完成签名验证 + AES-256-GCM 解密
+            // 支付分回调数据结构与普通支付不同,使用 String.class 获取解密后的 JSON
+            String decryptedJson = wxNotificationParser.parse(requestParam, String.class);
+            log.info("[微信支付分] 回调解密数据: {}", decryptedJson);
+
+            // 解析 JSON
+            JsonNode dataJson = objectMapper.readTree(decryptedJson);
+
+            // 从外层 body 获取 event_type(回调通知外层结构)
+            String eventType = null;
+            try {
+                JsonNode outerBody = objectMapper.readTree(body);
+                eventType = getJsonString(outerBody, "event_type");
+            } catch (Exception e) {
+                log.warn("[微信支付分] 解析外层body获取event_type失败: {}", e.getMessage());
+            }
+
+            // 如果外层没有,尝试从解密数据中获取
+            if (eventType == null && dataJson.has("event_type")) {
+                eventType = getJsonString(dataJson, "event_type");
+            }
+
+            String outOrderNo = getJsonString(dataJson, "out_order_no");
+            String state = getJsonString(dataJson, "state");
+            String openId = getJsonString(dataJson, "openid");
+
+            PayScoreCallbackResult result = PayScoreCallbackResult.success(eventType, outOrderNo, state);
+            result.setOpenId(openId);
+
+            // 解析总金额
+            if (dataJson.has("total_amount") && !dataJson.get("total_amount").isNull()) {
+                int totalAmountFen = dataJson.get("total_amount").asInt();
+                result.setTotalAmount(fenToYuan(totalAmountFen));
+            }
+
+            // 解析支付时间
+            if (dataJson.has("pay_succ_time") && !dataJson.get("pay_succ_time").isNull()) {
+                String paySuccTime = dataJson.get("pay_succ_time").asText();
+                result.setPayTime(parsePayTime(paySuccTime));
+            }
+
+            log.info("[微信支付分] 回调解析成功 - eventType: {}, outOrderNo: {}, state: {}, openId: {}, amount: {}元",
+                    eventType, outOrderNo, state, openId, result.getTotalAmount());
+            return result;
+
+        } catch (ValidationException e) {
+            log.error("[微信支付分] 回调签名验证失败: {}", e.getMessage(), e);
+            return PayScoreCallbackResult.fail("签名验证失败: " + e.getMessage());
+        } catch (MalformedMessageException e) {
+            log.error("[微信支付分] 回调消息解析失败: {}", e.getMessage(), e);
+            return PayScoreCallbackResult.fail("消息解析失败: " + e.getMessage());
+        } catch (Exception e) {
+            log.error("[微信支付分] 回调处理未知异常", e);
+            return PayScoreCallbackResult.fail("回调处理失败: " + e.getMessage());
+        }
+    }
+
+    // ==================== 私有辅助方法 ====================
+
+    /**
+     * 检查服务可用性
+     */
+    private PayScoreResult checkServiceAvailable() {
+        if (wxPayHttpClient == null) {
+            log.error("[微信支付分] HTTP客户端未初始化,请检查微信支付配置");
+            return PayScoreResult.fail("SERVICE_UNAVAILABLE", "支付分服务未配置,HTTP客户端未初始化");
+        }
+        if (wxPayConfig == null) {
+            log.error("[微信支付分] 支付配置未初始化");
+            return PayScoreResult.fail("CONFIG_UNAVAILABLE", "支付分服务未配置");
+        }
+        if (wxPayConfig.getServiceId() == null || wxPayConfig.getServiceId().isEmpty()) {
+            log.error("[微信支付分] 支付分服务ID未配置");
+            return PayScoreResult.fail("CONFIG_ERROR", "支付分服务ID未配置");
+        }
+        return null;
+    }
+
+    /**
+     * 元转分
+     */
+    private int yuanToFen(BigDecimal yuan) {
+        if (yuan == null) {
+            return 0;
+        }
+        return yuan.multiply(BigDecimal.valueOf(100)).intValue();
+    }
+
+    /**
+     * 分转元
+     */
+    private BigDecimal fenToYuan(int fen) {
+        return BigDecimal.valueOf(fen).divide(BigDecimal.valueOf(100), 2, RoundingMode.HALF_UP);
+    }
+
+    /**
+     * 安全获取 JSON 字符串值
+     */
+    private String getJsonString(JsonNode json, String key) {
+        if (json.has(key) && !json.get(key).isNull()) {
+            return json.get(key).asText();
+        }
+        return null;
+    }
+
+    /**
+     * 解析支付时间
+     */
+    private LocalDateTime parsePayTime(String timeStr) {
+        if (timeStr == null || timeStr.isEmpty()) {
+            return null;
+        }
+        try {
+            // 微信返回的时间格式: 2018-06-08T10:34:56+08:00 (ISO 8601)
+            OffsetDateTime offsetDateTime = OffsetDateTime.parse(timeStr, DateTimeFormatter.ISO_OFFSET_DATE_TIME);
+            return offsetDateTime.toLocalDateTime();
+        } catch (Exception e) {
+            log.warn("[微信支付分] 支付时间解析失败: {}, 使用当前时间", timeStr);
+            return LocalDateTime.now();
+        }
+    }
+}

+ 8 - 0
pom.xml

@@ -34,6 +34,7 @@
         <aspectj.version>1.9.22</aspectj.version>
         <okhttp.version>4.12.0</okhttp.version>
         <lombok.version>1.18.36</lombok.version>
+        <wechatpay.version>0.2.17</wechatpay.version>
     </properties>
 
     <dependencyManagement>
@@ -128,6 +129,13 @@
                 <artifactId>haha-sdk</artifactId>
                 <version>${project.version}</version>
             </dependency>
+
+            <!-- 微信支付 V3 官方 SDK -->
+            <dependency>
+                <groupId>com.github.wechatpay-apiv3</groupId>
+                <artifactId>wechatpay-java</artifactId>
+                <version>${wechatpay.version}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>