Explorar el Código

会员管理代码

wwh hace 1 semana
padre
commit
24b7a955f2

+ 10 - 51
baqing-shop/src/main/java/com/ruoyi/web/modules/cart/service/impl/CartAppServiceImpl.java

@@ -6,9 +6,7 @@ import java.util.ArrayList;
6 6
 import java.util.Collections;
7 7
 import java.util.Comparator;
8 8
 import java.util.Date;
9
-import java.util.HashMap;
10 9
 import java.util.List;
11
-import java.util.Map;
12 10
 import org.springframework.beans.factory.annotation.Autowired;
13 11
 import org.springframework.stereotype.Service;
14 12
 import org.springframework.transaction.annotation.Transactional;
@@ -33,8 +31,8 @@ import com.ruoyi.web.modules.cart.vo.CartCheckoutPrepareVO;
33 31
 import com.ruoyi.web.modules.cart.vo.CartCleanInvalidResultVO;
34 32
 import com.ruoyi.web.modules.cart.vo.CartItemAddResultVO;
35 33
 import com.ruoyi.web.modules.cart.vo.CartItemVO;
34
+import com.ruoyi.web.modules.cart.vo.CartItemVO;
36 35
 import com.ruoyi.web.modules.cart.vo.CartListVO;
37
-import com.ruoyi.web.modules.cart.vo.CartShopGroupVO;
38 36
 import com.ruoyi.web.modules.category.facade.ICategoryFacade;
39 37
 import com.ruoyi.web.modules.goods.domain.BizGoods;
40 38
 import com.ruoyi.web.modules.goods.domain.BizGoodsAttr;
@@ -76,61 +74,18 @@ public class CartAppServiceImpl implements ICartAppService
76 74
         if (rows == null || rows.isEmpty())
77 75
         {
78 76
             CartListVO empty = new CartListVO();
79
-            empty.setGroups(Collections.emptyList());
77
+            empty.setItems(Collections.emptyList());
80 78
             empty.setCheckedSummary(emptySummary());
81 79
             return empty;
82 80
         }
83
-        Map<Long, CartShopGroupVO> groupMap = new HashMap<>();
84
-        Map<Long, Date> shopLastUpdate = new HashMap<>();
85
-        List<CartItemVO> allItems = new ArrayList<>();
81
+        List<CartItemVO> items = new ArrayList<>();
86 82
         for (CartItemRowDTO row : rows)
87 83
         {
88
-            CartItemVO item = toCartItemVO(row);
89
-            allItems.add(item);
90
-            CartShopGroupVO group = groupMap.get(row.getShopId());
91
-            if (group == null)
92
-            {
93
-                group = new CartShopGroupVO();
94
-                group.setShopId(row.getShopId());
95
-                group.setShopName(StringUtils.isNotEmpty(row.getShopName()) ? row.getShopName() : "");
96
-                group.setShopAvatar(row.getShopAvatar());
97
-                group.setShopStatus(row.getShopStatus());
98
-                groupMap.put(row.getShopId(), group);
99
-            }
100
-            group.getItems().add(item);
101
-            Date updateTime = row.getUpdateTime();
102
-            Date last = shopLastUpdate.get(row.getShopId());
103
-            if (updateTime != null && (last == null || updateTime.after(last)))
104
-            {
105
-                shopLastUpdate.put(row.getShopId(), updateTime);
106
-            }
107
-        }
108
-        List<Long> shopIds = new ArrayList<>(groupMap.keySet());
109
-        shopIds.sort((a, b) -> {
110
-            Date da = shopLastUpdate.get(a);
111
-            Date db = shopLastUpdate.get(b);
112
-            if (da == null && db == null)
113
-            {
114
-                return 0;
115
-            }
116
-            if (da == null)
117
-            {
118
-                return 1;
119
-            }
120
-            if (db == null)
121
-            {
122
-                return -1;
123
-            }
124
-            return db.compareTo(da);
125
-        });
126
-        List<CartShopGroupVO> groups = new ArrayList<>();
127
-        for (Long shopId : shopIds)
128
-        {
129
-            groups.add(groupMap.get(shopId));
84
+            items.add(toCartItemVO(row));
130 85
         }
131 86
         CartListVO vo = new CartListVO();
132
-        vo.setGroups(groups);
133
-        vo.setCheckedSummary(buildCheckedSummary(allItems));
87
+        vo.setItems(items);
88
+        vo.setCheckedSummary(buildCheckedSummary(items));
134 89
         return vo;
135 90
     }
136 91
 
@@ -365,6 +320,10 @@ public class CartAppServiceImpl implements ICartAppService
365 320
         vo.setPurchasable(purchasable);
366 321
         vo.setInvalidType(invalidType);
367 322
         vo.setInvalidMsg(CartInvalidSupport.resolveInvalidMsg(invalidType, null));
323
+        vo.setShopId(row.getShopId());
324
+        vo.setShopName(StringUtils.isNotEmpty(row.getShopName()) ? row.getShopName() : "");
325
+        vo.setShopAvatar(row.getShopAvatar());
326
+        vo.setShopStatus(row.getShopStatus());
368 327
         return vo;
369 328
     }
370 329
 

+ 48 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/cart/vo/CartItemVO.java

@@ -31,6 +31,14 @@ public class CartItemVO
31 31
 
32 32
     private String invalidMsg;
33 33
 
34
+    private Long shopId;
35
+
36
+    private String shopName;
37
+
38
+    private String shopAvatar;
39
+
40
+    private String shopStatus;
41
+
34 42
     public Long getCartItemId()
35 43
     {
36 44
         return cartItemId;
@@ -150,4 +158,44 @@ public class CartItemVO
150 158
     {
151 159
         this.invalidMsg = invalidMsg;
152 160
     }
161
+
162
+    public Long getShopId()
163
+    {
164
+        return shopId;
165
+    }
166
+
167
+    public void setShopId(Long shopId)
168
+    {
169
+        this.shopId = shopId;
170
+    }
171
+
172
+    public String getShopName()
173
+    {
174
+        return shopName;
175
+    }
176
+
177
+    public void setShopName(String shopName)
178
+    {
179
+        this.shopName = shopName;
180
+    }
181
+
182
+    public String getShopAvatar()
183
+    {
184
+        return shopAvatar;
185
+    }
186
+
187
+    public void setShopAvatar(String shopAvatar)
188
+    {
189
+        this.shopAvatar = shopAvatar;
190
+    }
191
+
192
+    public String getShopStatus()
193
+    {
194
+        return shopStatus;
195
+    }
196
+
197
+    public void setShopStatus(String shopStatus)
198
+    {
199
+        this.shopStatus = shopStatus;
200
+    }
153 201
 }

+ 6 - 5
baqing-shop/src/main/java/com/ruoyi/web/modules/cart/vo/CartListVO.java

@@ -8,18 +8,19 @@ import java.util.List;
8 8
  */
9 9
 public class CartListVO
10 10
 {
11
-    private List<CartShopGroupVO> groups;
11
+    /** 购物车条目列表(按 cartItemId 倒序;每条含店铺信息) */
12
+    private List<CartItemVO> items;
12 13
 
13 14
     private CartCheckedSummaryVO checkedSummary;
14 15
 
15
-    public List<CartShopGroupVO> getGroups()
16
+    public List<CartItemVO> getItems()
16 17
     {
17
-        return groups;
18
+        return items;
18 19
     }
19 20
 
20
-    public void setGroups(List<CartShopGroupVO> groups)
21
+    public void setItems(List<CartItemVO> items)
21 22
     {
22
-        this.groups = groups;
23
+        this.items = items;
23 24
     }
24 25
 
25 26
     public CartCheckedSummaryVO getCheckedSummary()

+ 5 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/order/service/impl/SellerAftersaleServiceImpl.java

@@ -17,6 +17,7 @@ import com.ruoyi.web.modules.order.mapper.BizOrderAftersaleMapper;
17 17
 import com.ruoyi.web.modules.order.mapper.BizOrderMapper;
18 18
 import com.ruoyi.web.modules.order.service.ISellerAftersaleService;
19 19
 import com.ruoyi.web.modules.order.support.OrderAppSupport;
20
+import com.ruoyi.web.modules.pay.service.IOrderAftersaleRefundService;
20 21
 import com.ruoyi.web.modules.order.vo.SellerAftersaleDetailVO;
21 22
 import com.ruoyi.web.modules.order.vo.SellerAftersaleListRowVO;
22 23
 
@@ -32,6 +33,9 @@ public class SellerAftersaleServiceImpl implements ISellerAftersaleService
32 33
     @Autowired
33 34
     private OrderAppSupport orderAppSupport;
34 35
 
36
+    @Autowired
37
+    private IOrderAftersaleRefundService orderAftersaleRefundService;
38
+
35 39
     @Override
36 40
     public List<SellerAftersaleListRowVO> selectSellerList(Long shopId, SellerAftersaleQuery query)
37 41
     {
@@ -90,6 +94,7 @@ public class SellerAftersaleServiceImpl implements ISellerAftersaleService
90 94
             throw new ServiceException(OrderAppConstants.MSG_AFTERSALE_ALREADY_FINISHED);
91 95
         }
92 96
         orderMapper.updateForAftersaleClose(aftersale.getOrderId(), shopId, OrderConstants.CLOSE_TYPE_REFUND, operator);
97
+        orderAftersaleRefundService.refundOnAftersaleFinish(aftersale);
93 98
     }
94 99
 
95 100
     private void enrichListRow(SellerAftersaleListRowVO row)

+ 18 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/pay/constant/PayConstants.java

@@ -53,4 +53,22 @@ public final class PayConstants
53 53
     public static final String MSG_APPLY_NOT_PENDING = "当前申请不在审核中";
54 54
 
55 55
     public static final String MSG_APPLY_REJECT_REMARK_REQUIRED = "请填写驳回原因";
56
+
57
+    public static final String REFUND_STATUS_PENDING = "0";
58
+
59
+    public static final String REFUND_STATUS_SUCCESS = "1";
60
+
61
+    public static final String REFUND_STATUS_FAILED = "2";
62
+
63
+    public static final String REFUND_STATUS_PROCESSING = "3";
64
+
65
+    public static final String REFUND_NO_PREFIX = "RF";
66
+
67
+    public static final String MSG_REFUND_PAY_NOT_FOUND = "未找到可退款的支付记录";
68
+
69
+    public static final String MSG_REFUND_AMOUNT_INVALID = "退款金额无效";
70
+
71
+    public static final String MSG_REFUND_EXCEED = "退款金额超出可退余额";
72
+
73
+    public static final String MSG_REFUND_FAILED = "微信退款失败,请稍后重试";
56 74
 }

+ 45 - 0
baqing-shop/src/main/java/com/ruoyi/web/modules/pay/support/WeChatPaySupport.java

@@ -44,6 +44,8 @@ public class WeChatPaySupport
44 44
 {
45 45
     private static final String JSAPI_URL = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi";
46 46
 
47
+    private static final String REFUND_URL = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds";
48
+
47 49
     private static final String CODE2SESSION_URL = "https://api.weixin.qq.com/sns/jscode2session";
48 50
 
49 51
     private final WeChatPayProperties payProperties;
@@ -109,6 +111,49 @@ public class WeChatPaySupport
109 111
         return result.getString("prepay_id");
110 112
     }
111 113
 
114
+    /**
115
+     * 微信 V3 境内退款(同步返回 SUCCESS / PROCESSING 视为受理成功)
116
+     *
117
+     * @return 微信 refund_id
118
+     */
119
+    public String createDomesticRefund(String transactionId, String outRefundNo, BigDecimal totalAmount,
120
+            BigDecimal refundAmount, String reason, String mchId)
121
+    {
122
+        assertPayConfigReady();
123
+        if (StringUtils.isEmpty(transactionId) || StringUtils.isEmpty(outRefundNo))
124
+        {
125
+            throw new ServiceException(PayConstants.MSG_REFUND_FAILED);
126
+        }
127
+        int totalFen = totalAmount.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).intValueExact();
128
+        int refundFen = refundAmount.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).intValueExact();
129
+        if (refundFen <= 0 || refundFen > totalFen)
130
+        {
131
+            throw new ServiceException(PayConstants.MSG_REFUND_AMOUNT_INVALID);
132
+        }
133
+        JSONObject amount = new JSONObject();
134
+        amount.put("refund", refundFen);
135
+        amount.put("total", totalFen);
136
+        amount.put("currency", "CNY");
137
+        JSONObject payload = new JSONObject();
138
+        payload.put("transaction_id", transactionId);
139
+        payload.put("out_refund_no", outRefundNo);
140
+        payload.put("reason", StringUtils.isEmpty(reason) ? "售后退款" : reason);
141
+        payload.put("amount", amount);
142
+        String body = payload.toJSONString();
143
+        String response = postSigned(REFUND_URL, body, mchId);
144
+        JSONObject result = JSON.parseObject(response);
145
+        if (result == null || StringUtils.isEmpty(result.getString("refund_id")))
146
+        {
147
+            throw new ServiceException(PayConstants.MSG_REFUND_FAILED);
148
+        }
149
+        String status = result.getString("status");
150
+        if (!"SUCCESS".equals(status) && !"PROCESSING".equals(status))
151
+        {
152
+            throw new ServiceException(PayConstants.MSG_REFUND_FAILED);
153
+        }
154
+        return result.getString("refund_id");
155
+    }
156
+
112 157
     public Map<String, String> buildJsapiPayParams(String prepayId)
113 158
     {
114 159
         String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);

+ 1 - 1
baqing-shop/src/main/resources/mapper/cart/CartAppMapper.xml

@@ -29,7 +29,7 @@
29 29
     <select id="selectItemRowsByMemberId" resultType="com.ruoyi.web.modules.cart.dto.CartItemRowDTO">
30 30
         <include refid="cartItemSelect"/>
31 31
         where c.member_id = #{memberId}
32
-        order by c.update_time desc, c.cart_item_id desc
32
+        order by c.cart_item_id desc
33 33
     </select>
34 34
 
35 35
     <select id="selectItemRowsByIdsAndMember" resultType="com.ruoyi.web.modules.cart.dto.CartItemRowDTO">

+ 2 - 2
baqing-shop/src/test/java/com/ruoyi/web/modules/cart/controller/CartAppControllerTest.java

@@ -78,7 +78,7 @@ class CartAppControllerTest
78 78
     {
79 79
         MemberContext.setMemberId(MEMBER_ID);
80 80
         CartListVO list = new CartListVO();
81
-        list.setGroups(Collections.emptyList());
81
+        list.setItems(Collections.emptyList());
82 82
         CartCheckedSummaryVO summary = new CartCheckedSummaryVO();
83 83
         summary.setCheckedCount(0);
84 84
         summary.setCheckedQuantity(0);
@@ -89,7 +89,7 @@ class CartAppControllerTest
89 89
         mockMvc.perform(get("/api/cart"))
90 90
                 .andExpect(status().isOk())
91 91
                 .andExpect(jsonPath("$.code").value(200))
92
-                .andExpect(jsonPath("$.data.groups").isArray());
92
+                .andExpect(jsonPath("$.data.items").isArray());
93 93
 
94 94
         verify(cartAppService).list(MEMBER_ID);
95 95
     }

+ 8 - 5
baqing-shop/src/test/java/com/ruoyi/web/modules/cart/service/CartAppServiceImplTest.java

@@ -76,21 +76,24 @@ class CartAppServiceImplTest
76 76
 
77 77
         CartListVO vo = cartAppService.list(MEMBER_ID);
78 78
 
79
-        assertEquals(0, vo.getGroups().size());
79
+        assertEquals(0, vo.getItems().size());
80 80
         assertEquals(0, vo.getCheckedSummary().getCheckedCount());
81 81
     }
82 82
 
83 83
     @Test
84
-    void list_groupsByShop()
84
+    void list_returnsFlatItemsWithShop()
85 85
     {
86 86
         when(memberFacade.isMemberEnabled(MEMBER_ID)).thenReturn(true);
87
-        when(cartAppMapper.selectItemRowsByMemberId(MEMBER_ID)).thenReturn(Arrays.asList(cartRow(1L, 101L),
88
-                cartRow(2L, 102L)));
87
+        when(cartAppMapper.selectItemRowsByMemberId(MEMBER_ID)).thenReturn(Arrays.asList(cartRow(2L, 102L),
88
+                cartRow(1L, 101L)));
89 89
         when(categoryFacade.isCategoryVisible(10L)).thenReturn(true);
90 90
 
91 91
         CartListVO vo = cartAppService.list(MEMBER_ID);
92 92
 
93
-        assertEquals(2, vo.getGroups().size());
93
+        assertEquals(2, vo.getItems().size());
94
+        assertEquals(2L, vo.getItems().get(0).getCartItemId().longValue());
95
+        assertEquals(102L, vo.getItems().get(0).getShopId().longValue());
96
+        assertEquals("测试店", vo.getItems().get(0).getShopName());
94 97
         assertEquals(2, vo.getCheckedSummary().getCheckedCount());
95 98
     }
96 99
 

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

@@ -23,6 +23,7 @@ import com.ruoyi.web.modules.order.mapper.BizOrderAftersaleMapper;
23 23
 import com.ruoyi.web.modules.order.mapper.BizOrderMapper;
24 24
 import com.ruoyi.web.modules.order.service.impl.SellerAftersaleServiceImpl;
25 25
 import com.ruoyi.web.modules.order.support.OrderAppSupport;
26
+import com.ruoyi.web.modules.pay.service.IOrderAftersaleRefundService;
26 27
 import com.ruoyi.web.modules.order.vo.SellerAftersaleDetailVO;
27 28
 import com.ruoyi.web.modules.order.vo.SellerAftersaleListRowVO;
28 29
 
@@ -42,6 +43,9 @@ class SellerAftersaleServiceImplTest
42 43
     @Mock
43 44
     private OrderAppSupport orderAppSupport;
44 45
 
46
+    @Mock
47
+    private IOrderAftersaleRefundService orderAftersaleRefundService;
48
+
45 49
     @InjectMocks
46 50
     private SellerAftersaleServiceImpl sellerAftersaleService;
47 51
 
@@ -93,6 +97,7 @@ class SellerAftersaleServiceImplTest
93 97
 
94 98
         verify(aftersaleMapper).updateForFinish(any(BizOrderAftersale.class));
95 99
         verify(orderMapper).updateForAftersaleClose(500L, SHOP_ID, OrderConstants.CLOSE_TYPE_REFUND, "seller01");
100
+        verify(orderAftersaleRefundService).refundOnAftersaleFinish(aftersale);
96 101
     }
97 102
 
98 103
     @Test

+ 20 - 5
doc/店铺后台/售后管理/售后管理技术方案.md

@@ -2,8 +2,8 @@
2 2
 
3 3
 > **依据:** 《售后管理功能需求.md》v1.0  
4 4
 > **关联:** C 端《我的订单技术方案》v1.2;平台《订单管理技术方案》v1.0.2、《订单管理功能需求.md》v1.0.1、《关联需求分析.md》v1.6;商家《全部订单技术方案》v1.0.2、《发货管理技术方案》v1.0、《商家端店铺上下文接口说明》  
5
-> **范围:** 商家端 **订单管理 · 售后管理** — 当前店铺售后列表、检索、详情、**处理完结**;**不含** C 端提交售后(已有 `OrderAftersaleAppController`)、退款原路退回、平台仲裁。  
6
-> **原则:** **无新建表**;读写 **`biz_order_aftersale`**;`shop_id = X-Shop-Id` 隔离;与 C 端 **同表同源**;完结 **更新** `process_result / aftersale_status / finish_time`;**不** 改订单主状态、**不** 自动退款/回库存
5
+> **范围:** 商家端 **订单管理 · 售后管理** — 当前店铺售后列表、检索、详情、**处理完结**;**不含** C 端提交售后(已有 `OrderAftersaleAppController`)、平台仲裁。**退款** 在商家 **处理完结** 时 **自动原路退回**(见 §8.5)。
6
+> **原则:** 读写 **`biz_order_aftersale`**、**`biz_refund_record`**;`shop_id = X-Shop-Id` 隔离;与 C 端 **同表同源**;完结 **更新** `process_result / aftersale_status / finish_time` 并 **关单**、**触发退款**
7 7
 
8 8
 ---
9 9
 
@@ -246,19 +246,32 @@ LEFT JOIN biz_order_item oi ON a.order_item_id = oi.item_id
246 246
 |------|------|
247 247
 | 前置 | `shop_id` 匹配;`aftersale_status = '1'` |
248 248
 | 校验 | `processResult` 非空;长度 ≤1000 |
249
-| 效果 | `UPDATE` `process_result, aftersale_status='2', finish_time=NOW(), update_time=NOW()` |
249
+| 效果 | `UPDATE` 售后完结;订单 **关单**(`order_status→4`,`close_type=4`);**自动退款** |
250 250
 | 幂等 | 已完结再次调用 → 业务错误「售后已完结,不可重复处理」 |
251
-| 订单/库存/退款 | **不** 触发 |
251
+| 退款 | 调用 `OrderAftersaleRefundServiceImpl.refundOnAftersaleFinish`;金额=`apply_amount`;Mock 模式直成功;真实模式调微信 V3 `/v3/refund/domestic/refunds` |
252
+| 库存 | **不** 自动回滚 |
252 253
 
253 254
 ```text
254 255
 商家点击「处理售后」
255 256
     → PUT /finish
256 257
     → SellerAftersaleServiceImpl.finish
257 258
     → BizOrderAftersaleMapper.updateForFinish
259
+    → BizOrderMapper.updateForAftersaleClose
260
+    → OrderAftersaleRefundServiceImpl.refundOnAftersaleFinish
258 261
     → C 端 GET detail 自动读到 processResult + FINISHED
259 262
 ```
260 263
 
261
-### 3.5 业务错误 msg
264
+### 3.5 自动退款(`biz_refund_record`)
265
+
266
+| 项 | 说明 |
267
+|----|------|
268
+| 触发 | 商家 **处理完结** 成功后 **同步** 发起 |
269
+| 金额 | 售后 `apply_amount`;须 ≤ 支付成功金额且 ≤ 订单可退余额 |
270
+| 幂等 | `uk_aftersale_id`;同一售后仅退款一次 |
271
+| Mock | `wechat.pay.mock=true` 或 `enabled=false` 时写入 `MOCK_RF{aftersale_no}` |
272
+| 失败 | 抛业务异常,**整笔 finish 事务回滚**(售后/关单/退款均不生效) |
273
+
274
+### 3.6 业务错误 msg
262 275
 
263 276
 | 场景 | msg |
264 277
 |------|-----|
@@ -266,6 +279,8 @@ LEFT JOIN biz_order_item oi ON a.order_item_id = oi.item_id
266 279
 | 越权/不存在 | 售后单不存在或无权访问(`OrderAppConstants.MSG_AFTERSALE_NOT_FOUND`) |
267 280
 | 非进行中 | 售后已完结,不可重复处理 |
268 281
 | 处理结果为空 | 请填写处理结果 |
282
+| 无可退支付 | 未找到可退款的支付记录 |
283
+| 退款失败 | 微信退款失败,请稍后重试 |
269 284
 
270 285
 ---
271 286
 

+ 9 - 1
doc/消费者APP/我的订单/我的订单前端技术方案.md

@@ -152,6 +152,7 @@ onShow → getOrderBadges()
152 152
 onLoad(query.tab)
153 153
     → listOrders({ tab, pageNum, pageSize })
154 154
     → mapOrderListRow(row)
155
+        → card.statusText:有 aftersaleStatus 时展示「售后处理中/售后已完成」,否则 orderStatusText
155 156
         → card.items[] 逐行渲染 OrderGoodsRow
156 157
         → 已完成:行内 reviewStatus=PENDING →「评价」;DONE →「查看评价」
157 158
     → 底部 OrderActionBar:过滤 REVIEW / VIEW_REVIEW,仅 PAY / 确认收货 / 售后 / 再买一单
@@ -206,9 +207,14 @@ review-edit?orderId=&orderItemId=
206 207
 ## 6. 展示映射 `orderDisplay.js`
207 208
 
208 209
 - `mapOrderListRow`:`items[]` 全量映射;`firstItem = items[0]` 兼容
210
+- **列表状态文案:**
211
+  - 透传 `orderStatusText`(订单主状态)
212
+  - 透传 `aftersaleStatus`(`null` / `1` / `2`)
213
+  - `statusText`(卡片展示):`aftersaleStatus === '1'` → **售后处理中**;`'2'` → **售后已完成**;否则 **orderStatusText**
209 214
 - 单行含 `reviewStatus`(`PENDING` / `DONE` / `NONE`)、`orderItemId`
210 215
 - `footerActions(actions)`:过滤 `REVIEW`、`VIEW_REVIEW`
211 216
 - `showItemReview(orderId, orderItemId)`:弹窗展示已评内容(`orderAction.js`)
217
+- 售后专区列表/详情仍用 `mapAftersaleStatusText`(商家处理中 / 售后完结),与 **订单列表** 文案区分
212 218
 
213 219
 ---
214 220
 
@@ -234,6 +240,7 @@ review-edit?orderId=&orderItemId=
234 240
 | F6 | 底部无整单「评价」按钮 |
235 241
 | F7 | 评价提交后该行变「查看评价」 |
236 242
 | F8 | 售后提交后进进行中列表 |
243
+| F9 | 列表存在 aftersaleStatus=1 → 卡片状态「售后处理中」;=2 →「售后已完成」;null → orderStatusText |
237 244
 
238 245
 ---
239 246
 
@@ -241,10 +248,11 @@ review-edit?orderId=&orderItemId=
241 248
 
242 249
 | 版本 | 说明 |
243 250
 |------|------|
251
+| **v1.3** | 订单列表 `aftersaleStatus` 前端映射 statusText;与后端 orderStatusText 分离 |
244 252
 | **v1.2** | 列表/详情/评价列表:`items[]` 全量;行内评价;底部 actions 去评价 |
245 253
 | **v1.0** | 首版:路由、API、页面流程、与后端接口对齐 |
246 254
 | **v1.1** | 落地 list/detail/review/aftersale 分包页、个人中心订单入口与角标、公共组件 |
247 255
 
248 256
 ---
249 257
 
250
-*文档版本:v1.2 · 关联《我的订单功能需求.md》v1.1、《我的订单技术方案.md》v1.2*
258
+*文档版本:v1.3 · 关联《我的订单功能需求.md》v1.2、《我的订单技术方案.md》v1.3*

+ 4 - 2
doc/消费者APP/我的订单/我的订单功能需求.md

@@ -220,7 +220,7 @@
220 220
 |------|------|
221 221
 | 店铺 | **店铺头像、名称**(可读实时档或下单快照,以前台为准) |
222 222
 | 商品摘要 | **全部商品行**:主图、名称、规格、单价、数量;已完成订单 **行内**「评价/查看评价」 |
223
-| 订单状态 | 待付款 / 待发货 / 待收货 / 交易成功 / 已关闭 等 **C 端文案** |
223
+| 订单状态 | **主状态**(待付款/待发货/待收货/交易成功/已关闭);**存在售后时** 卡片状态区优先展示 **售后处理中**(进行中)或 **售后已完成**(已完结),以前台映射为准 |
224 224
 | 应付/实付 | **实付金额**(含运费);待支付展示 **应付总额** |
225 225
 | 下单时间 | |
226 226
 | 操作按钮 | 卡片底部:**去支付、确认收货、申请售后、再次购买**;**评价在行内**(非底部整单按钮) |
@@ -232,6 +232,7 @@
232 232
 | **MO-L1** | 默认 **不展示** 他人订单、平台已删除订单 |
233 233
 | **MO-L2** | 支持下拉 **刷新**;分页加载(交互以前台为准) |
234 234
 | **MO-L3** | 空列表展示 **空态提示** 与 **去逛逛** 引导 |
235
+| **MO-L4** | 接口返回 `aftersaleStatus` 时,列表卡片 **优先展示售后状态文案**(处理中/已完成);**不改变** 订单主状态字段语义 |
235 236
 
236 237
 ---
237 238
 
@@ -575,8 +576,9 @@
575 576
 | 版本 | 说明 |
576 577
 |------|------|
577 578
 | **v1.1** | 列表全量商品行;评价改 **一行一评**;行内评价按钮;底部不含整单评价 |
579
+| **v1.2** | 列表返回售后状态 `aftersaleStatus`;卡片 **售后文案由 C 端前台展示**(MO-L4) |
578 580
 | **v1.0** | 首版定稿:订单列表/详情、角标、待支付续付、确认收货、评价、售后;与平台订单 O3/O7/O8/O13 及确认订单/会员/商品边界对齐;草稿保持不变 |
579 581
 
580 582
 ---
581 583
 
582
-*文档版本:v1.1(定稿)· 关联《我的订单功能需求-草稿.md》、《订单管理功能需求.md》v1.0.1、《会员管理功能需求.md》v1.1、《确认订单页(多商品)功能需求.md》v1.1、《确认订单页(单商品)功能需求.md》v1.0、《购物车功能需求.md》v1.0、《商品详情内页功能需求.md》v1.0、《我的服务功能需求.md》v1.1、《关联需求分析.md》v1.6 · 草稿保持不变。*
584
+*文档版本:v1.2(定稿)· 关联《我的订单功能需求-草稿.md》、《订单管理功能需求.md》v1.0.1、《会员管理功能需求.md》v1.1、《确认订单页(多商品)功能需求.md》v1.1、《确认订单页(单商品)功能需求.md》v1.0、《购物车功能需求.md》v1.0、《商品详情内页功能需求.md》v1.0、《我的服务功能需求.md》v1.1、《关联需求分析.md》v1.6 · 草稿保持不变。*

+ 2 - 2
doc/消费者APP/购物车/购物车前端技术方案.md

@@ -12,7 +12,7 @@
12 12
 | 项 | 说明 |
13 13
 |----|------|
14 14
 | 框架 | uni-app **Vue 3** + **uview-plus** |
15
-| 请求 | `@/utils/request`;列表为 `AjaxResult.data`(`groups` + `checkedSummary`) |
15
+| 请求 | `@/utils/request`;列表为 `AjaxResult.data`(`items` + `checkedSummary`) |
16 16
 | 鉴权 | **全部** `/api/cart/**` 须会员 Token |
17 17
 | 页面位置 | **主包 Tab** `pages/cart/index`(与 `pages.json` tabBar 一致) |
18 18
 | 规格 v1 | 统一规格;加购 `specKey` 不传, `specText` 由详情拼接或「默认」 |
@@ -143,7 +143,7 @@ onShow
143 143
 
144 144
 | 函数 | 用途 |
145 145
 |------|------|
146
-| `mapCartList` | `data` → `groups` + `checkedSummary` |
146
+| `mapCartList` | `data.items` → 页面 `items`;`groupCartItemsByShop` 生成 UI 用 `groups` + `checkedSummary` |
147 147
 | `hasInvalidCartItems` | 是否显示「清理失效」 |
148 148
 | `getCheckedPurchasableIds` | 去结算入参 |
149 149
 | `getCheckedShopIds` | 跨店校验 |

+ 9 - 19
doc/消费者APP/购物车/购物车技术方案.md

@@ -68,7 +68,7 @@ category/(已有 · 协作)
68 68
         → 四条件校验 + 同规格合并累加
69 69
 
70 70
 【本模块】
71
-    GET  /api/cart                         → 按店分组列表 + 失效标签
71
+    GET  /api/cart                         → 条目列表(cartItemId 倒序,含店铺)+ 勾选合计
72 72
     PUT  /api/cart/items/{id}/quantity     → 改数量(≤库存)
73 73
     PUT  /api/cart/items/checked           → 批量更新勾选
74 74
     DELETE /api/cart/items/{id}            → 移出
@@ -201,7 +201,7 @@ CREATE TABLE IF NOT EXISTS `biz_member_cart_item` (
201 201
 
202 202
 | 方法 | 路径 | 说明 |
203 203
 |------|------|------|
204
-| GET | `/api/cart` | 购物车列表(按店分组) |
204
+| GET | `/api/cart` | 购物车条目列表 |
205 205
 | POST | `/api/cart/items` | **加购**(详情页调用) |
206 206
 | PUT | `/api/cart/items/{cartItemId}/quantity` | 修改数量 |
207 207
 | PUT | `/api/cart/items/checked` | 批量更新勾选 |
@@ -229,18 +229,9 @@ CREATE TABLE IF NOT EXISTS `biz_member_cart_item` (
229 229
 
230 230
 | 字段 | 类型 | 说明 |
231 231
 |------|------|------|
232
-| groups | array | 按店分组,见下 |
232
+| items | array | **购物车条目列表**(主结构),见下 |
233 233
 | checkedSummary | object | 当前 **已勾选且可购** 行合计(前端底栏参考) |
234 234
 
235
-**`groups[]` → `CartShopGroupVO`:**
236
-
237
-| 字段 | 类型 | 说明 |
238
-|------|------|------|
239
-| shopId | long | 进店参数 |
240
-| shopName | string | 实时 |
241
-| shopStatus | string | `0` 开业 / `1` 停业 |
242
-| items | array | `CartItemVO[]` |
243
-
244 235
 **`items[]` → `CartItemVO`:**
245 236
 
246 237
 | 字段 | 类型 | 说明 |
@@ -257,15 +248,14 @@ CREATE TABLE IF NOT EXISTS `biz_member_cart_item` (
257 248
 | purchasable | boolean | 是否可勾选结算 |
258 249
 | invalidType | string | §2.4 枚举;可购时为 `NONE` |
259 250
 | invalidMsg | string | 前端 Toast/标签;可购时 null |
251
+| shopId | long | 所属店铺 |
252
+| shopName | string | 店铺名称(实时) |
253
+| shopAvatar | string | 店铺头像 |
254
+| shopStatus | string | `0` 开业 / `1` 停业 |
260 255
 
261
-**排序:**
262
-
263
-| 维度 | 规则 |
264
-|------|------|
265
-| 店组 | `MAX(update_time) DESC` |
266
-| 组内 | `update_time DESC` |
256
+**排序:** `cart_item_id DESC`(最新加购/合并行在前)。
267 257
 
268
-**空态:** `groups=[]` → 前端「购物车是空的」。
258
+**空态:** `items=[]` → 前端「购物车是空的」。
269 259
 
270 260
 **`checkedSummary`:**
271 261
 

+ 29 - 0
sql/biz_refund_record.sql

@@ -0,0 +1,29 @@
1
+-- =============================================================================
2
+-- 退款流水 biz_refund_record
3
+-- 用途:售后完结后原路退款(微信 V3);幂等键 aftersale_id
4
+-- 前置:biz_order_aftersale.sql、biz_pay_record.sql
5
+-- =============================================================================
6
+
7
+CREATE TABLE IF NOT EXISTS `biz_refund_record` (
8
+  `refund_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '退款记录ID',
9
+  `aftersale_id` bigint(20) NOT NULL COMMENT '售后ID(biz_order_aftersale.aftersale_id)',
10
+  `aftersale_no` varchar(32) NOT NULL COMMENT '售后编号',
11
+  `order_id` bigint(20) NOT NULL COMMENT '订单ID',
12
+  `order_no` varchar(32) NOT NULL COMMENT '订单编号',
13
+  `pay_id` bigint(20) NOT NULL COMMENT '支付流水ID(biz_pay_record.pay_id)',
14
+  `member_id` bigint(20) NOT NULL COMMENT '会员ID',
15
+  `shop_id` bigint(20) NOT NULL COMMENT '店铺ID',
16
+  `out_refund_no` varchar(64) NOT NULL COMMENT '商户退款单号(RF+售后编号)',
17
+  `wx_refund_id` varchar(64) DEFAULT NULL COMMENT '微信退款单号',
18
+  `refund_amount` decimal(12,2) NOT NULL COMMENT '退款金额(元)',
19
+  `refund_status` char(1) NOT NULL DEFAULT '0' COMMENT '0待退款 1成功 2失败 3处理中',
20
+  `fail_reason` varchar(256) DEFAULT NULL COMMENT '失败原因',
21
+  `notify_time` datetime DEFAULT NULL COMMENT '退款完成时间',
22
+  `create_time` datetime NOT NULL COMMENT '创建时间',
23
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
24
+  PRIMARY KEY (`refund_id`),
25
+  UNIQUE KEY `uk_aftersale_id` (`aftersale_id`),
26
+  UNIQUE KEY `uk_out_refund_no` (`out_refund_no`),
27
+  KEY `idx_order_id` (`order_id`),
28
+  KEY `idx_refund_status` (`refund_status`)
29
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='售后退款流水';