Explorar o código

会员管理代码

wwh hai 1 semana
pai
achega
8f34542e57

+ 45 - 1
baqing-shop/src/main/java/com/ruoyi/web/modules/order/service/impl/OrderReviewAppServiceImpl.java

@@ -183,6 +183,9 @@ public class OrderReviewAppServiceImpl implements IOrderReviewAppService
183 183
         {
184 184
             return Collections.emptyList();
185 185
         }
186
+        List<Long> orderIds = reviews.stream().map(BizGoodsReview::getOrderId).distinct()
187
+                .collect(Collectors.toList());
188
+        Map<Long, List<BizOrderItem>> itemsByOrderId = loadItemsByOrderIds(orderIds);
186 189
         List<OrderReviewListRowVO> rows = new ArrayList<>(reviews.size());
187 190
         for (BizGoodsReview review : reviews)
188 191
         {
@@ -205,19 +208,60 @@ public class OrderReviewAppServiceImpl implements IOrderReviewAppService
205 208
                 row.setCreateTime(order.getCreateTime());
206 209
             }
207 210
             row.setReviewStatus(OrderAppConstants.REVIEW_STATUS_DONE);
208
-            row.setFirstItem(orderAppSupport.toItemSummary(toItemFromReview(review)));
211
+            BizOrderItem item = resolveReviewItem(review, itemsByOrderId);
212
+            row.setFirstItem(orderAppSupport.toItemSummary(item));
209 213
             row.setItemCount(1);
210 214
             rows.add(row);
211 215
         }
212 216
         return rows;
213 217
     }
214 218
 
219
+    private Map<Long, List<BizOrderItem>> loadItemsByOrderIds(List<Long> orderIds)
220
+    {
221
+        if (orderIds == null || orderIds.isEmpty())
222
+        {
223
+            return Collections.emptyMap();
224
+        }
225
+        List<BizOrderItem> allItems = orderItemMapper.selectByOrderIds(orderIds);
226
+        Map<Long, List<BizOrderItem>> map = new HashMap<>();
227
+        if (allItems == null)
228
+        {
229
+            return map;
230
+        }
231
+        for (BizOrderItem item : allItems)
232
+        {
233
+            map.computeIfAbsent(item.getOrderId(), key -> new ArrayList<>()).add(item);
234
+        }
235
+        return map;
236
+    }
237
+
238
+    private BizOrderItem resolveReviewItem(BizGoodsReview review, Map<Long, List<BizOrderItem>> itemsByOrderId)
239
+    {
240
+        List<BizOrderItem> items = itemsByOrderId.get(review.getOrderId());
241
+        if (items != null && !items.isEmpty())
242
+        {
243
+            if (review.getOrderItemId() != null)
244
+            {
245
+                for (BizOrderItem item : items)
246
+                {
247
+                    if (review.getOrderItemId().equals(item.getItemId()))
248
+                    {
249
+                        return item;
250
+                    }
251
+                }
252
+            }
253
+            return items.get(0);
254
+        }
255
+        return toItemFromReview(review);
256
+    }
257
+
215 258
     private BizOrderItem toItemFromReview(BizGoodsReview review)
216 259
     {
217 260
         BizOrderItem item = new BizOrderItem();
218 261
         item.setGoodsId(review.getGoodsId());
219 262
         item.setGoodsImage(review.getGoodsMainPic());
220 263
         item.setGoodsSpec(review.getGoodsSpec());
264
+        item.setQuantity(1);
221 265
         return item;
222 266
     }
223 267
 

+ 1 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/pay/service/impl/WeChatPayServiceImpl.java

@@ -53,6 +53,7 @@ public class WeChatPayServiceImpl implements IWeChatPayService
53 53
         vo.setOrderNo(order.getOrderNo());
54 54
         if (!payProperties.isRealPayEnabled())
55 55
         {
56
+            ensurePayRecord(order, memberId);
56 57
             vo.setMock(true);
57 58
             return vo;
58 59
         }

+ 57 - 0
baqing-shop/src/test/java/com/ruoyi/web/modules/order/service/OrderReviewAppServiceImplTest.java

@@ -1,12 +1,15 @@
1 1
 package com.ruoyi.web.modules.order.service;
2 2
 
3 3
 import static org.junit.jupiter.api.Assertions.assertEquals;
4
+import static org.junit.jupiter.api.Assertions.assertNotNull;
4 5
 import static org.junit.jupiter.api.Assertions.assertThrows;
5 6
 import static org.mockito.ArgumentMatchers.any;
6 7
 import static org.mockito.Mockito.never;
7 8
 import static org.mockito.Mockito.verify;
8 9
 import static org.mockito.Mockito.when;
10
+import java.math.BigDecimal;
9 11
 import java.util.Collections;
12
+import java.util.Date;
10 13
 import org.junit.jupiter.api.Test;
11 14
 import org.junit.jupiter.api.extension.ExtendWith;
12 15
 import org.mockito.InjectMocks;
@@ -17,6 +20,7 @@ import com.ruoyi.web.modules.account.domain.BizMember;
17 20
 import com.ruoyi.web.modules.account.facade.IMemberFacade;
18 21
 import com.ruoyi.web.modules.order.constant.OrderAppConstants;
19 22
 import com.ruoyi.web.modules.order.constant.OrderConstants;
23
+import com.ruoyi.web.modules.order.domain.BizGoodsReview;
20 24
 import com.ruoyi.web.modules.order.domain.BizOrder;
21 25
 import com.ruoyi.web.modules.order.domain.BizOrderItem;
22 26
 import com.ruoyi.web.modules.order.dto.OrderReviewSubmitDTO;
@@ -25,6 +29,8 @@ import com.ruoyi.web.modules.order.mapper.BizOrderItemMapper;
25 29
 import com.ruoyi.web.modules.order.mapper.BizOrderMapper;
26 30
 import com.ruoyi.web.modules.order.service.impl.OrderReviewAppServiceImpl;
27 31
 import com.ruoyi.web.modules.order.support.OrderAppSupport;
32
+import com.ruoyi.web.modules.order.vo.OrderAppItemSummaryVO;
33
+import com.ruoyi.web.modules.order.vo.OrderReviewListRowVO;
28 34
 
29 35
 @ExtendWith(MockitoExtension.class)
30 36
 class OrderReviewAppServiceImplTest
@@ -51,6 +57,57 @@ class OrderReviewAppServiceImplTest
51 57
     @InjectMocks
52 58
     private OrderReviewAppServiceImpl reviewAppService;
53 59
 
60
+    @Test
61
+    void list_done_usesOrderItemSnapshot()
62
+    {
63
+        when(memberFacade.isMemberEnabled(MEMBER_ID)).thenReturn(true);
64
+        BizGoodsReview review = new BizGoodsReview();
65
+        review.setReviewId(1L);
66
+        review.setOrderId(ORDER_ID);
67
+        review.setOrderItemId(11L);
68
+        review.setGoodsId(2L);
69
+        review.setScore(5);
70
+        review.setContent("很好");
71
+        review.setCreateTime(new Date());
72
+        review.setGoodsMainPic("review-pic.jpg");
73
+        review.setGoodsSpec("review-spec");
74
+        when(reviewMapper.selectMemberList(MEMBER_ID)).thenReturn(Collections.singletonList(review));
75
+
76
+        BizOrder order = new BizOrder();
77
+        order.setOrderId(ORDER_ID);
78
+        order.setOrderNo("NO001");
79
+        order.setOrderStatus(OrderConstants.STATUS_COMPLETED);
80
+        order.setShopId(10L);
81
+        order.setShopName("牛牛店铺");
82
+        order.setPayAmount(new BigDecimal("99.00"));
83
+        when(orderMapper.selectByIdAndMember(ORDER_ID, MEMBER_ID)).thenReturn(order);
84
+        when(orderAppSupport.toStatusText(OrderConstants.STATUS_COMPLETED)).thenReturn("交易成功");
85
+
86
+        BizOrderItem item = new BizOrderItem();
87
+        item.setItemId(11L);
88
+        item.setOrderId(ORDER_ID);
89
+        item.setGoodsId(2L);
90
+        item.setGoodsName("测试商品");
91
+        item.setGoodsSpec("批次:2024春季");
92
+        item.setGoodsImage("order-pic.jpg");
93
+        item.setUnitPrice(new BigDecimal("99.00"));
94
+        item.setQuantity(1);
95
+        when(orderItemMapper.selectByOrderIds(Collections.singletonList(ORDER_ID)))
96
+                .thenReturn(Collections.singletonList(item));
97
+
98
+        OrderAppItemSummaryVO summary = new OrderAppItemSummaryVO();
99
+        summary.setGoodsName("测试商品");
100
+        summary.setUnitPrice(new BigDecimal("99.00"));
101
+        summary.setQuantity(1);
102
+        when(orderAppSupport.toItemSummary(item)).thenReturn(summary);
103
+
104
+        OrderReviewListRowVO row = reviewAppService.list(MEMBER_ID, OrderAppConstants.REVIEW_TAB_DONE).get(0);
105
+        assertNotNull(row.getFirstItem());
106
+        assertEquals("测试商品", row.getFirstItem().getGoodsName());
107
+        assertEquals(new BigDecimal("99.00"), row.getFirstItem().getUnitPrice());
108
+        verify(orderAppSupport).toItemSummary(item);
109
+    }
110
+
54 111
     @Test
55 112
     void submit_success()
56 113
     {

+ 3 - 0
baqing-shop/src/test/java/com/ruoyi/web/modules/pay/service/impl/WeChatPayServiceImplTest.java

@@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
5 5
 import static org.junit.jupiter.api.Assertions.assertTrue;
6 6
 import static org.mockito.ArgumentMatchers.any;
7 7
 import static org.mockito.ArgumentMatchers.eq;
8
+import static org.mockito.ArgumentMatchers.any;
8 9
 import static org.mockito.Mockito.never;
9 10
 import static org.mockito.Mockito.verify;
10 11
 import static org.mockito.Mockito.when;
@@ -69,11 +70,13 @@ class WeChatPayServiceImplTest
69 70
     void createJsapiPay_mockModeWithoutOpenid_succeeds()
70 71
     {
71 72
         when(payProperties.isRealPayEnabled()).thenReturn(false);
73
+        when(payRecordMapper.selectLatestByOrderId(order.getOrderId())).thenReturn(null);
72 74
 
73 75
         WeChatPayParamsVO vo = weChatPayService.createJsapiPay(200L, order, null);
74 76
 
75 77
         assertTrue(vo.isMock());
76 78
         assertEquals(order.getOrderId(), vo.getOrderId());
79
+        verify(payRecordMapper).insert(any());
77 80
         verify(shopWechatPayService, never()).resolvePayMchId(any());
78 81
         verify(paySupport, never()).createJsapiPrepayId(any(), any(), any(), any(), any(), any());
79 82
     }

+ 1 - 1
doc/平台后台/支付管理/支付流程.md

@@ -13,7 +13,7 @@
13 13
 |----|:----:|------|
14 14
 | 平台代收 | ✓ | 所有订单走 `wechat.pay.mch-id` |
15 15
 | 会员 `wx_openid` 绑定 | 生产 ✓ / Mock 跳过 | 生产微信支付前置;**Mock 不要求绑定**(H5 浏览器联调) |
16
-| JSAPI / Mock 落单 / 回调 | ✓ | 见 §8、§9 |
16
+| JSAPI / Mock 落单 / 回调 | ✓ | 见 §8、§9;**BUY_NOW 与多商品共用** `/api/order/{id}/pay` |
17 17
 | 商家入驻申请 | — | **暂不涉及** |
18 18
 | 平台入驻审核 | — | **暂不涉及** |
19 19
 | 按店铺 `pay_mode` 路由 | — | **暂不涉及**;`resolvePayMchId` 对 C 端等效恒为平台号 |

+ 12 - 7
doc/消费者APP/确认订单页(单商品)(取消支付前)/确认订单页(单商品)前端技术方案.md

@@ -1,9 +1,9 @@
1 1
 # 确认订单页(单商品)— 前端技术方案(C 端 · shop-app)
2 2
 
3
-> **依据:** 《确认订单页(单商品)功能需求.md》v1.0、《确认订单页(单商品)技术方案.md》v1.0  
4
-> **关联:** 《商品详情内页前端技术方案》v1.0、《我的服务前端技术方案》、《确认订单页(多商品)技术方案》v1.1(接口同源)  
5
-> **范围:** 消费者 APP **`shop-app`** 立即购买确认页、支付结果交互;**不** 改后端、**不** 实现购物车合单页、**不** 实现我的订单列表。  
6
-> **实现状态:** 页面与 API 已落地(v1.1);支付为 **Mock 弹窗** + `pay`/`cancel` 接口,待联调
3
+> **依据:** 《确认订单页(单商品)功能需求.md》v1.0、《确认订单页(单商品)技术方案.md》v1.2  
4
+> **关联:** 《商品详情内页前端技术方案》v1.0、《确认订单页(多商品)技术方案》v1.3(接口同源)  
5
+> **范围:** 消费者 APP **`shop-app`** 立即购买确认页、支付结果交互;**不** 实现购物车合单页、我的订单列表。  
6
+> **实现状态:** 页面与 API 已落地;支付走 **`POST /api/order/{id}/pay`**(默认 **Mock 同步落单**,无需绑 openid)
7 7
 
8 8
 ---
9 9
 
@@ -100,10 +100,14 @@ export const PAGE_PAY_RESULT = '/subpackage/order/pay-result'
100 100
 
101 101
 | 方法 | HTTP | 路径 | 说明 |
102 102
 |------|------|------|------|
103
-| `payOrder(orderId)` | POST | `/api/order/{orderId}/pay` | v1 Mock 直接成功 |
103
+| `payOrder(orderId)` | POST | `/api/order/{orderId}/pay` | Mock:`data.mock=true` 已落单;生产:返回 `payParams` |
104 104
 | `cancelPay(orderId)` | POST | `/api/order/{orderId}/pay/cancel` | 用户取消收银台 |
105 105
 | `getOrderDetail(orderId)` | GET | `/api/order/{orderId}` | 结果页 |
106 106
 
107
+**Mock 响应示例:** `{ orderId, orderNo, mock: true }` — 前端直接跳支付成功页,**无需** `wx.bind`。
108
+
109
+**生产(待联调):** 须先绑 openid → 调 `payOrder` → `WeixinJSBridge` 调起 → 成功查详情 / 取消调 `cancelPay`。
110
+
107 111
 ### 4.3 地址(复用)
108 112
 
109 113
 | 方法 | HTTP | 路径 |
@@ -285,7 +289,7 @@ async function doBuyNow() {
285 289
 
286 290
 | 项 | 说明 |
287 291
 |----|------|
288
-| 微信 JSAPI 真实唤起 | Mock pay 接口 |
292
+| 微信 H5 真实调起(WeixinJSBridge) | 后端已就绪;前端待联调 |
289 293
 | 我的订单列表续付 | 另册 |
290 294
 | 购物车多商品确认页 | 多商品前端专册 |
291 295
 | 多规格 SKU 矩阵 | 商品 SKU 落地后扩展 |
@@ -296,9 +300,10 @@ async function doBuyNow() {
296 300
 
297 301
 | 版本 | 说明 |
298 302
 |------|------|
303
+| **v1.2** | 对齐后端微信支付 v1.2;Mock pay 说明、生产联调前置 |
299 304
 | **v1.0** | 首版:BUY_NOW 确认页路由、checkout/order API、详情跳转、支付取消闭环 |
300 305
 | **v1.1** | 落地 checkout/pay-result 页面与组件;规格展示复用 `parseCartSpecText` |
301 306
 
302 307
 ---
303 308
 
304
-*文档版本:v1.1 · 工程目录 `shop-app` · 不修改《确认订单页(单商品)技术方案.md》(后端方案)*
309
+*文档版本:v1.2 · 工程目录 `shop-app` · 后端见《确认订单页(单商品)技术方案.md》v1.2*

+ 89 - 58
doc/消费者APP/确认订单页(单商品)(取消支付前)/确认订单页(单商品)技术方案.md

@@ -1,9 +1,9 @@
1 1
 # 确认订单页(单商品)— 技术方案(C 端)
2 2
 
3 3
 > **依据:** 《确认订单页(单商品)功能需求.md》v1.0  
4
-> **关联:** 《确认订单页(多商品)技术方案》v1.1(**共用 checkout/order 模块**)、平台《订单管理技术方案》v1.0.2、《商品管理功能需求》v1.3.3、《店铺管理技术方案》v1.3.6、《会员管理技术方案》v1.3、《关联需求分析.md》v1.6 §11;C 端《商品详情内页技术方案》v1.4、《我的服务技术方案》v1.1;商家《运费模版技术方案》v1.1、《店铺商品列表技术方案》v1.6  
5
-> **范围:** C 端 **`source=BUY_NOW`** 确认页 preview/submit + **`/api/order/**`** 支付/关单;写入 **`biz_order` / `biz_order_item`**(**单行**);**不含** 购物车路径、平台履约、微信 SDK 生产对接
6
-> **原则:** **复用** 多商品 checkout 同一套 Service/表;**支付成功** 扣库存;**待支付不扣**;v1 **统一规格**;**不删购物车**(不经 cart)。
4
+> **关联:** 《确认订单页(多商品)技术方案》v1.3(**共用 checkout/order/pay 模块**)、《微信支付入驻与对接技术方案》v1.1、《支付流程.md》v1.3;C 端《商品详情内页技术方案》v1.4  
5
+> **范围:** C 端 **`source=BUY_NOW`** 确认页 preview/submit + **`/api/order/**`** 微信支付/关单;写入 **`biz_order` / `biz_order_item`**(**单行**);**不含** 购物车路径、C 端 H5 真实调起支付
6
+> **原则:** **复用** 多商品 checkout/order/pay;**支付成功** 扣库存;**待支付不扣**;**不删购物车**(`cart_item_id=NULL`)。
7 7
 
8 8
 ---
9 9
 
@@ -17,56 +17,46 @@
17 17
 | 鉴权 | 须 **会员 Token**(`MemberWebConfig` → `/api/checkout/**`、`/api/order/**`) |
18 18
 | 订单号 | `O` + `yyyyMMdd` + 6 位序号(与平台订单方案一致) |
19 19
 | 支付超时 | `sys_config`:`order.pay.timeout.minutes`,默认 **1440** |
20
-| 支付 v1 | **微信支付 Mock**;生产对接 **另册** |
20
+| 支付 v1.2 | **微信 JSAPI**;`wechat.pay.mock=true` 开发直付;生产见《微信支付入驻与对接技术方案》 |
21
+| 结算 | **平台代收**(本期不涉及商户入驻路由) |
21 22
 | 定时关单 | **`OrderPayTimeoutJob`**(O8) |
22 23
 
23 24
 ### 1.1 与多商品方案的关系
24 25
 
25 26
 | 维度 | 多商品专册 | **本册(单商品)** |
26 27
 |------|------------|-------------------|
27
-| 后端模块 | `com.ruoyi.web.modules.order` checkout + order | **同一套**,不另建包 |
28
+| 后端模块 | `order` checkout + order + **`pay`** | **同一套**,不另建包 |
28 29
 | `source` | `CART` / `BUY_NOW` | **固定 `BUY_NOW`** |
29 30
 | 行数 | ≥1 | **强制 1 行** |
30 31
 | 购物车 | prepare + pay 后删行 | **不涉及** |
31 32
 | 运费 | 多行聚合 | **单行简化**(§2.4) |
32 33
 | 前端页 | 可共用 UI | **独立路由** + 详情跳转(见前端技术方案) |
33 34
 
34
-> **实现策略:** 在 `CheckoutAppServiceImpl` 内对 `source=BUY_NOW` 增加 **`items.size()==1`** 校验;其余 preview/submit/pay 逻辑 **与多商品共用**。
35
+> **实现策略:** `CheckoutAppServiceImpl` 对 `source=BUY_NOW` 校验 **`items.size()==1`**;preview/submit/pay **与多商品共用**;pay 落单走 **`OrderPaySuccessServiceImpl.completeWeChatPay`**。
35 36
 
36
-### 1.2 模块落位(共用 · 待建部分
37
+### 1.2 模块落位(已实现
37 38
 
38 39
 ```text
39
-baqing-shop/src/main/java/com/ruoyi/web/modules/order/
40
-├── controller/
41
-│   ├── CheckoutAppController.java        # /api/checkout/**  preview/submit
42
-│   └── OrderAppController.java           # /api/order/**      pay/cancel/detail
43
-├── service/
44
-│   ├── ICheckoutAppService.java
45
-│   ├── impl/CheckoutAppServiceImpl.java  # BUY_NOW 单行分支
46
-│   ├── IOrderAppService.java
47
-│   └── impl/OrderAppServiceImpl.java
48
-├── support/
49
-│   ├── OrderSupport.java                 # 已有
50
-│   └── CheckoutSupport.java              # 地址/四条件/单行运费
51
-├── constant/CheckoutConstants.java
52
-├── dto/CheckoutPreviewDTO.java、CheckoutSubmitDTO.java …
53
-└── vo/CheckoutPreviewVO.java、CheckoutSubmitResultVO.java …
54
-
55
-account/(已有)
56
-├── GET  /api/member/address/list
57
-└── BizMemberAddressMapper
58
-
59
-goods/(已有)
60
-├── IGoodsPurchaseFacade.canPurchase
61
-├── BizGoodsMapper、BizGoodsFreightMapper
62
-└── BizGoodsServiceSnapshotMapper
63
-
64
-freight/(已实现)
65
-├── IFreightTemplateFacade.calcFreight / buildFreightDesc
66
-└── FreightCalcSupport
67
-
68
-home/(已有 · 上游)
69
-└── GET /api/goods/{goodsId}/can-purchase   # 详情立即购买前置
40
+baqing-shop/src/main/java/com/ruoyi/web/modules/
41
+├── order/
42
+│   ├── controller/
43
+│   │   ├── CheckoutAppController.java        # /api/checkout/**  preview/submit
44
+│   │   └── OrderAppController.java           # /api/order/**      pay/cancel/detail
45
+│   ├── service/
46
+│   │   ├── ICheckoutAppService / CheckoutAppServiceImpl   # BUY_NOW 单行分支
47
+│   │   ├── IOrderAppService / OrderAppServiceImpl
48
+│   │   └── IOrderPaySuccessService / OrderPaySuccessServiceImpl
49
+│   ├── support/CheckoutSupport.java
50
+│   └── constant/CheckoutConstants.java       # SOURCE_BUY_NOW 等
51
+└── pay/
52
+    ├── service/IWeChatPayService / WeChatPayServiceImpl
53
+    ├── controller/WeChatPayNotifyController  # POST /api/pay/wechat/notify
54
+    ├── support/WeChatPaySupport              # JSAPI V3 + 回调验签
55
+    └── domain/BizPayRecord                   # biz_pay_record
56
+
57
+account/
58
+├── POST /api/member/wx/bind                  # 生产绑 openid(Mock 可跳过)
59
+└── biz_member.wx_openid
70 60
 ```
71 61
 
72 62
 ### 1.3 协作链(立即购买 · 本册主路径)
@@ -79,7 +69,8 @@ home/(已有 · 上游)
79 69
     → POST /api/checkout/submit   { source: "BUY_NOW", addressId, items: [单行+remark] }
80 70
         → INSERT biz_order(0) + biz_order_item ×1(cart_item_id=NULL)
81 71
     → POST /api/order/{orderId}/pay
82
-        → deductStock ×1 → status=1
72
+        → [Mock] ensurePayRecord + completeWeChatPay(扣库存、status=1)
73
+        → [生产] JSAPI 下单 → payParams → 微信回调 → completeWeChatPay
83 74
     → 或 POST /api/order/{orderId}/pay/cancel → status=4, close_type=2
84 75
 
85 76
 biz_member_address ──► consignee_* 快照
@@ -124,6 +115,8 @@ WHERE goods_id = #{goodsId} AND stock >= #{qty} AND del_flag = '0'
124 115
 | `biz_order_item` | **1 条** 明细快照 | `sql/biz_order.sql` + `sql/biz_order_item_checkout.sql` |
125 116
 | `biz_member_address` | 读地址;快照写主表 | `sql/biz_member.sql` |
126 117
 | `biz_goods` / `biz_goods_freight` | 价/库/运费 | 已有 |
118
+| `biz_pay_record` | 支付流水;`out_trade_no=order_no` | [`sql/biz_pay_record.sql`](../../../sql/biz_pay_record.sql) |
119
+| `biz_member.wx_openid` | 生产 JSAPI payer.openid | [`sql/biz_member.sql`](../../../sql/biz_member.sql) |
127 120
 | `biz_goods_service_snapshot` | 预览 `serviceDesc` | 已有 |
128 121
 
129 122
 **BUY_NOW 特征字段:**
@@ -277,19 +270,37 @@ CheckoutSupport.calcFreightSingle(goodsId, quantity, provinceId, goodsAmount)
277 270
 
278 271
 ---
279 272
 
280
-## 4. C 端接口 · `/api/order`(支付 · 取消支付前)
273
+## 4. C 端接口 · `/api/order`(微信支付 · 取消支付前)
281 274
 
282
-与多商品 **共用**;BUY_NOW 差异:**pay 成功后不调用 `ICartFacade`**。
275
+与多商品 **共用** `OrderAppController` / `IWeChatPayService`;BUY_NOW 差异:**`cart_item_id=NULL`,pay 成功后不删购物车行**。
283 276
 
284
-### 4.1 支付 `POST /api/order/{orderId}/pay`
277
+### 4.1 接口一览
278
+
279
+| 方法 | 路径 | 说明 |
280
+|------|------|------|
281
+| POST | `/api/order/{orderId}/pay` | 发起支付 → `WeChatPayParamsVO` |
282
+| POST | `/api/order/{orderId}/pay/cancel` | 用户取消 → 已关闭 |
283
+| GET | `/api/order/{orderId}` | 支付结果页 / 详情 |
284
+| POST | `/api/member/wx/bind` | 生产绑 openid(Mock **不要求**) |
285
+| POST | `/api/pay/wechat/notify` | 微信回调(Anonymous) |
286
+
287
+### 4.2 支付 `POST /api/order/{orderId}/pay`
285 288
 
286 289
 | 项 | 说明 |
287 290
 |----|------|
288
-| 前置 | `order_status=0` 且未超时 |
289
-| 事务 | **单行** `deductStock` → `order_status=1, pay_status=1, pay_time, pay_type=1` |
290
-| 购物车 | **跳过** removeItems |
291
+| 归属 | `member_id` 校验 |
292
+| 前置 | `order_status=0` 且 `now < pay_expire_time` |
293
+| Mock | `wechat.pay.mock=true` → 写 `biz_pay_record` + **同步** `completeWeChatPay`;返回 `{ mock: true }`;**无需 openid** |
294
+| 生产 | 须 `wx_openid` → JSAPI 统一下单 → 返回 **payParams**;**回调**落单 |
295
+| 落单 | `OrderPaySuccessServiceImpl.completeWeChatPay`:单行 `deductStock` → `order_status=1` |
296
+| 购物车 | `cart_item_id` 均为 null → **不调用** `ICartFacade.removeItems` |
297
+| 库存失败 | 整单失败;订单 **保持待支付** |
298
+
299
+**响应 `WeChatPayParamsVO`:** `orderId`、`orderNo`、`mock`、`payParams`(生产:`appId/timeStamp/nonceStr/package/signType/paySign`)。
291 300
 
292
-### 4.2 取消支付 `POST /api/order/{orderId}/pay/cancel`
301
+> 详见《微信支付入驻与对接技术方案》v1.1、《支付流程.md》§8。
302
+
303
+### 4.3 取消支付 `POST /api/order/{orderId}/pay/cancel`
293 304
 
294 305
 | 项 | 说明 |
295 306
 |----|------|
@@ -297,7 +308,7 @@ CheckoutSupport.calcFreightSingle(goodsId, quantity, provinceId, goodsAmount)
297 308
 | 动作 | `order_status=4, close_type=2` |
298 309
 | 库存 | 不扣,无需回滚 |
299 310
 
300
-### 4.3 订单详情 `GET /api/order/{orderId}`
311
+### 4.4 订单详情 `GET /api/order/{orderId}`
301 312
 
302 313
 支付结果页展示:`orderNo`、`payAmount`、`items[0]`、`payExpireTime`(待支付时剩余秒数)。
303 314
 
@@ -317,14 +328,19 @@ CheckoutAppController
317 328
         → BizOrderMapper.insert + BizOrderItemMapper.insert
318 329
 
319 330
 OrderAppController
320
-    → IOrderAppService.pay / cancelPay
321
-        → deductStock (1 row)
322
-        → 不调用 ICartFacade
331
+    → IOrderAppService.createPay / cancelPay
332
+        → IWeChatPayService.createJsapiPay
333
+        → [Mock] OrderPaySuccessServiceImpl.completeWeChatPay
334
+WeChatPayNotifyController
335
+    → IWeChatPayService.handleNotify → completeWeChatPay
323 336
 ```
324 337
 
325 338
 | 要点 | 说明 |
326 339
 |------|------|
327
-| specText | BUY_NOW:入参优先;空则读 `biz_goods_attr`(attr_type=2)拼接,仍无则「默认」 |
340
+| 单行扣库存 | `completeWeChatPay` 遍历 1 条 `biz_order_item` |
341
+| 不删购物车 | BUY_NOW 明细 `cart_item_id=NULL`,`removeItems` 列表为空 |
342
+| Mock 流水 | `WeChatPayServiceImpl` Mock 分支 **先** `ensurePayRecord` 再返回 |
343
+| specText | BUY_NOW:入参优先;空则读 `biz_goods_attr` 拼接 |
328 344
 | 服务快照 | `biz_goods_service_snapshot` → `service_desc` |
329 345
 | 地址快照 | `consignee_address = regionName + " " + detailAddress` |
330 346
 | 防重复提交 | submit 中 **禁用连点**(前端)+ 可选服务端 member 级短锁 |
@@ -370,11 +386,12 @@ public static final BigDecimal DEFAULT_KG_PER_PIECE = BigDecimal.ONE;
370 386
 | COS-R1 备注 | `buyer_remark` |
371 387
 | COS-S2 提交不扣库存 | submit 无 stock UPDATE |
372 388
 | COS-S3 不经购物车 | 无 cart 协作 |
373
-| COS10 支付扣库存 | pay → deductStock |
389
+| COS10 支付扣库存 | `completeWeChatPay` → deductStock |
374 390
 | COS11 取消关闭 | pay/cancel → close_type=2 |
375 391
 | COS12 超时 | `pay_expire_time` + Job |
376 392
 | COS13 四条件 | preview + submit |
377 393
 | GD12 立即购买 | `source=BUY_NOW` |
394
+| COS8 微信支付 | `IWeChatPayService` + Mock/生产双模式 |
378 395
 
379 396
 ---
380 397
 
@@ -393,16 +410,27 @@ public static final BigDecimal DEFAULT_KG_PER_PIECE = BigDecimal.ONE;
393 410
 | T9 | 支付后库存不足并发 | pay 失败;订单仍待支付 |
394 411
 | T10 | 停业/下架 submit | 400;无订单 |
395 412
 
413
+**自动化测试(已实现):**
414
+
415
+| 类 | 覆盖 |
416
+|----|------|
417
+| `CheckoutBuyNowAppServiceImplTest` | preview/submit BUY_NOW 校验 |
418
+| `BuyNowPayFlowTest` | Mock pay 闭环;无 openid;不删购物车 |
419
+| `OrderAppServiceImplTest` | createPay BUY_NOW 跳过 cart |
420
+| `WeChatPayServiceImplTest` | Mock/生产 openid、pay_record |
421
+| `OrderPaySuccessServiceImplTest` | completeWeChatPay 幂等/扣库存 |
422
+
396 423
 ---
397 424
 
398 425
 ## 10. 实现分期
399 426
 
400 427
 | 阶段 | 内容 | 状态 |
401 428
 |------|------|------|
402
-| **v1.0** | checkout BUY_NOW preview/submit;order pay/cancel;`biz_order_item` 扩展字段 | **待建** |
403
-| **v1.0·运费** | `calcFreightSingle` + `biz_goods_freight` + Facade | **待建**(Facade **已有**) |
404
-| **v1.1** | 详情 `doBuyNow` 联调;支付成功页 | **待建**(前端) |
405
-| **v1.2** | 微信生产 SDK | 另册 |
429
+| **v1.0** | BUY_NOW preview/submit;order pay/cancel | **已实现** |
430
+| **v1.0·运费** | `calcFreight` + `biz_goods_freight` | **已实现** |
431
+| **v1.2·支付** | 微信 JSAPI + `biz_pay_record` + Mock/生产 + 回调 | **已实现** |
432
+| **v1.1** | 详情联调;支付成功页 | **前端已实现**(Mock pay) |
433
+| **v1.3** | H5 公众号 OAuth + 真实调起支付 | **待联调** |
406 434
 
407 435
 ---
408 436
 
@@ -411,7 +439,9 @@ public static final BigDecimal DEFAULT_KG_PER_PIECE = BigDecimal.ONE;
411 439
 | 项 | 说明 |
412 440
 |----|------|
413 441
 | 购物车路径 | 《确认订单页(多商品)技术方案》 |
442
+| 商户微信支付入驻 | 本期平台代收;见《支付流程.md》§0 |
414 443
 | 我的订单续付列表 | 另册 |
444
+| H5 外链 MWEB 支付 | 未实现 |
415 445
 | 优惠券/满减 | 需求未要求 |
416 446
 | 确认页 Redis 会话 | 可选;本期 **无** |
417 447
 
@@ -421,8 +451,9 @@ public static final BigDecimal DEFAULT_KG_PER_PIECE = BigDecimal.ONE;
421 451
 
422 452
 | 版本 | 说明 |
423 453
 |------|------|
424
-| **v1.0** | 首版:BUY_NOW 单行 preview/submit/pay/cancel;共用 order 模块;单行运费;与功能需求 v1.0 对齐 |
454
+| **v1.2** | 接入微信支付:共用 pay 模块、Mock 写 pay_record、BuyNowPayFlowTest;对齐多商品 v1.2 |
455
+| **v1.0** | 首版:BUY_NOW preview/submit/pay/cancel;单行运费 |
425 456
 
426 457
 ---
427 458
 
428
-*文档版本:v1.0 · 关联《确认订单页(单商品)功能需求.md》v1.0、《确认订单页(多商品)技术方案.md》v1.1、《订单管理技术方案.md》v1.0.2、《商品详情内页技术方案.md》v1.4 · DDL:[`sql/biz_order.sql`](../../../sql/biz_order.sql)、[`sql/biz_order_item_checkout.sql`](../../../sql/biz_order_item_checkout.sql)、[`sql/biz_goods_freight.sql`](../../../sql/biz_goods_freight.sql)*
459
+*文档版本:v1.2 · 关联《确认订单页(单商品)功能需求.md》v1.0、《确认订单页(多商品)技术方案.md》v1.3、《微信支付入驻与对接技术方案.md》v1.1 · DDL:[`sql/biz_order.sql`](../../../sql/biz_order.sql)、[`sql/biz_pay_record.sql`](../../../sql/biz_pay_record.sql)、[`sql/biz_member.sql`](../../../sql/biz_member.sql)*