Procházet zdrojové kódy

增加与ai大模型对话

wwh před 6 dny
rodič
revize
7f1ef69337

+ 24 - 4
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/EmploymentOrderController.java

@@ -10,6 +10,7 @@ import com.huimv.employment.service.order.dto.OrderStartWorkRequest;
10 10
 import com.huimv.employment.service.order.dto.OrderStartWorkResponse;
11 11
 import com.huimv.employment.service.settlement.SettlementService;
12 12
 import com.huimv.employment.service.settlement.dto.OrderStartSettlementRequest;
13
+import com.huimv.employment.service.settlement.dto.SettlementConfirmPayRequest;
13 14
 import com.huimv.employment.service.settlement.dto.SettlementSummaryResponse;
14 15
 import io.swagger.v3.oas.annotations.Operation;
15 16
 import io.swagger.v3.oas.annotations.Parameter;
@@ -26,13 +27,13 @@ import org.springframework.web.bind.annotation.RestController;
26 27
 /**
27 28
  * 企业端用工订单 API。
28 29
  * <p>
29
- * 节点推进相关:确认开工(→ work_started)、发起结算(→ settlement_pending)
30
- * 均需企业 JWT;订单须归属当前登录企业。
30
+ * 节点推进:确认开工(→ work_started)、发起结算(→ settlement_pending)
31
+ * 确认并支付(→ settlement_paid)。均需企业 JWT;订单须归属当前登录企业。
31 32
  * </p>
32 33
  */
33 34
 @RestController
34 35
 @RequestMapping("/api/v1/mp/orders")
35
-@Tag(name = "用工订单", description = "企业确认开工、发起结算等")
36
+@Tag(name = "用工订单", description = "企业确认开工、发起结算、确认支付等")
36 37
 @SecurityRequirement(name = "Authorization")
37 38
 public class EmploymentOrderController {
38 39
 
@@ -78,7 +79,7 @@ public class EmploymentOrderController {
78 79
 
79 80
     @GetMapping("/{orderId}/settlement")
80 81
     @Operation(summary = "查询订单结算单",
81
-            description = "返回本单当前有效结算单(非 cancelled)及明细。",
82
+            description = "返回本单当前有效结算单(非 cancelled)及明细、支付信息。",
82 83
             parameters = {
83 84
                     @Parameter(name = "orderId", in = ParameterIn.PATH, required = true,
84 85
                             description = "fe_employment_order.id")
@@ -106,6 +107,25 @@ public class EmploymentOrderController {
106 107
         return R.ok(settlementService.startSettlement(LoginUserHolder.require().getUserId(), orderId, request));
107 108
     }
108 109
 
110
+    @PostMapping("/{orderId}/confirm-pay-settlement")
111
+    @Operation(summary = "确认结算并支付",
112
+            description = "企业确认结算单并完成支付(MVP Mock 即时成功)。"
113
+                    + "结算单 pending→paid,写入 fe_payment_record,订单 settlement_pending→settlement_paid。"
114
+                    + "已支付幂等返回。body.payment_channel 可选 wechat/alipay/bank,默认 wechat。",
115
+            parameters = {
116
+                    @Parameter(name = "orderId", in = ParameterIn.PATH, required = true,
117
+                            description = "fe_employment_order.id")
118
+            })
119
+    public R<SettlementSummaryResponse> confirmPaySettlement(
120
+            @PathVariable("orderId") Long orderId,
121
+            @RequestBody(required = false) SettlementConfirmPayRequest request) {
122
+        requireEnterprise();
123
+        if (request == null) {
124
+            request = new SettlementConfirmPayRequest();
125
+        }
126
+        return R.ok(settlementService.confirmAndPay(LoginUserHolder.require().getUserId(), orderId, request));
127
+    }
128
+
109 129
     /** 本 Controller 接口仅企业用户可访问。 */
110 130
     private static LoginUser requireEnterprise() {
111 131
         LoginUser loginUser = LoginUserHolder.require();

+ 159 - 0
huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/entity/FePaymentRecord.java

@@ -0,0 +1,159 @@
1
+package com.huimv.employment.dao.entity;
2
+
3
+import com.baomidou.mybatisplus.annotation.IdType;
4
+import com.baomidou.mybatisplus.annotation.TableId;
5
+import com.baomidou.mybatisplus.annotation.TableName;
6
+
7
+import java.math.BigDecimal;
8
+import java.time.LocalDateTime;
9
+
10
+/**
11
+ * 支付/打款记录 {@code fe_payment_record}。
12
+ * <p>企业确认结算并支付时创建;MVP 为 Mock 即时成功,对接真实支付后由回调更新状态。</p>
13
+ */
14
+@TableName("fe_payment_record")
15
+public class FePaymentRecord {
16
+
17
+    @TableId(type = IdType.AUTO)
18
+    private Long id;
19
+
20
+    private String paymentNo;
21
+
22
+    private Long settlementId;
23
+
24
+    private Long enterpriseId;
25
+
26
+    /** wechat / alipay / bank */
27
+    private String paymentChannel;
28
+
29
+    private BigDecimal paymentAmount;
30
+
31
+    /** pending / success / failed / refunded */
32
+    private String paymentStatus;
33
+
34
+    private String externalTradeNo;
35
+
36
+    private Long payerUserId;
37
+
38
+    private LocalDateTime paidAt;
39
+
40
+    private String receiptUrl;
41
+
42
+    private String failReason;
43
+
44
+    private LocalDateTime createTime;
45
+
46
+    private LocalDateTime updateTime;
47
+
48
+    public Long getId() {
49
+        return id;
50
+    }
51
+
52
+    public void setId(Long id) {
53
+        this.id = id;
54
+    }
55
+
56
+    public String getPaymentNo() {
57
+        return paymentNo;
58
+    }
59
+
60
+    public void setPaymentNo(String paymentNo) {
61
+        this.paymentNo = paymentNo;
62
+    }
63
+
64
+    public Long getSettlementId() {
65
+        return settlementId;
66
+    }
67
+
68
+    public void setSettlementId(Long settlementId) {
69
+        this.settlementId = settlementId;
70
+    }
71
+
72
+    public Long getEnterpriseId() {
73
+        return enterpriseId;
74
+    }
75
+
76
+    public void setEnterpriseId(Long enterpriseId) {
77
+        this.enterpriseId = enterpriseId;
78
+    }
79
+
80
+    public String getPaymentChannel() {
81
+        return paymentChannel;
82
+    }
83
+
84
+    public void setPaymentChannel(String paymentChannel) {
85
+        this.paymentChannel = paymentChannel;
86
+    }
87
+
88
+    public BigDecimal getPaymentAmount() {
89
+        return paymentAmount;
90
+    }
91
+
92
+    public void setPaymentAmount(BigDecimal paymentAmount) {
93
+        this.paymentAmount = paymentAmount;
94
+    }
95
+
96
+    public String getPaymentStatus() {
97
+        return paymentStatus;
98
+    }
99
+
100
+    public void setPaymentStatus(String paymentStatus) {
101
+        this.paymentStatus = paymentStatus;
102
+    }
103
+
104
+    public String getExternalTradeNo() {
105
+        return externalTradeNo;
106
+    }
107
+
108
+    public void setExternalTradeNo(String externalTradeNo) {
109
+        this.externalTradeNo = externalTradeNo;
110
+    }
111
+
112
+    public Long getPayerUserId() {
113
+        return payerUserId;
114
+    }
115
+
116
+    public void setPayerUserId(Long payerUserId) {
117
+        this.payerUserId = payerUserId;
118
+    }
119
+
120
+    public LocalDateTime getPaidAt() {
121
+        return paidAt;
122
+    }
123
+
124
+    public void setPaidAt(LocalDateTime paidAt) {
125
+        this.paidAt = paidAt;
126
+    }
127
+
128
+    public String getReceiptUrl() {
129
+        return receiptUrl;
130
+    }
131
+
132
+    public void setReceiptUrl(String receiptUrl) {
133
+        this.receiptUrl = receiptUrl;
134
+    }
135
+
136
+    public String getFailReason() {
137
+        return failReason;
138
+    }
139
+
140
+    public void setFailReason(String failReason) {
141
+        this.failReason = failReason;
142
+    }
143
+
144
+    public LocalDateTime getCreateTime() {
145
+        return createTime;
146
+    }
147
+
148
+    public void setCreateTime(LocalDateTime createTime) {
149
+        this.createTime = createTime;
150
+    }
151
+
152
+    public LocalDateTime getUpdateTime() {
153
+        return updateTime;
154
+    }
155
+
156
+    public void setUpdateTime(LocalDateTime updateTime) {
157
+        this.updateTime = updateTime;
158
+    }
159
+}

+ 9 - 0
huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/mapper/FePaymentRecordMapper.java

@@ -0,0 +1,9 @@
1
+package com.huimv.employment.dao.mapper;
2
+
3
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4
+import com.huimv.employment.dao.entity.FePaymentRecord;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface FePaymentRecordMapper extends BaseMapper<FePaymentRecord> {
9
+}

+ 196 - 4
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/settlement/SettlementService.java

@@ -7,6 +7,7 @@ import com.huimv.employment.dao.entity.FeDraftCostSnapshot;
7 7
 import com.huimv.employment.dao.entity.FeEmploymentDraft;
8 8
 import com.huimv.employment.dao.entity.FeEmploymentOrder;
9 9
 import com.huimv.employment.dao.entity.FeEnterprise;
10
+import com.huimv.employment.dao.entity.FePaymentRecord;
10 11
 import com.huimv.employment.dao.entity.FeServicePlan;
11 12
 import com.huimv.employment.dao.entity.FeSettlementFee;
12 13
 import com.huimv.employment.dao.entity.FeSettlementItem;
@@ -15,6 +16,7 @@ import com.huimv.employment.dao.entity.FeWorkerRegistration;
15 16
 import com.huimv.employment.dao.mapper.FeDraftCostSnapshotMapper;
16 17
 import com.huimv.employment.dao.mapper.FeEmploymentDraftMapper;
17 18
 import com.huimv.employment.dao.mapper.FeEmploymentOrderMapper;
19
+import com.huimv.employment.dao.mapper.FePaymentRecordMapper;
18 20
 import com.huimv.employment.dao.mapper.FeServicePlanMapper;
19 21
 import com.huimv.employment.dao.mapper.FeSettlementFeeMapper;
20 22
 import com.huimv.employment.dao.mapper.FeSettlementItemMapper;
@@ -22,6 +24,7 @@ import com.huimv.employment.dao.mapper.FeSettlementOrderMapper;
22 24
 import com.huimv.employment.dao.mapper.FeWorkerRegistrationMapper;
23 25
 import com.huimv.employment.service.enterprise.EnterpriseService;
24 26
 import com.huimv.employment.service.settlement.dto.OrderStartSettlementRequest;
27
+import com.huimv.employment.service.settlement.dto.SettlementConfirmPayRequest;
25 28
 import com.huimv.employment.service.settlement.dto.SettlementFeeResponse;
26 29
 import com.huimv.employment.service.settlement.dto.SettlementItemResponse;
27 30
 import com.huimv.employment.service.settlement.dto.SettlementSummaryResponse;
@@ -44,11 +47,10 @@ import java.util.concurrent.ThreadLocalRandom;
44 47
 /**
45 48
  * 结算服务(方案 A:企业主动发起)。
46 49
  * <p>
47
- * 流程:订单处于 {@code work_started} → 企业调用发起结算 →
48
- * 生成 {@code fe_settlement_order}(含按人明细、费用分项)→
49
- * 订单推进至 {@code settlement_pending}(待确认结算)。
50
+ * 流程:{@code work_started} → 发起结算 → {@code settlement_pending} →
51
+ * 确认并支付(Mock)→ {@code settlement_paid}。
50 52
  * </p>
51
- * <p>MVP 无考勤:金额按日薪×天数或方案成本快照估算;确认支付另接口处理。</p>
53
+ * <p>MVP 无考勤、无真实支付通道:金额按日薪/方案快照估算,支付即时成功。</p>
52 54
  */
53 55
 @Service
54 56
 public class SettlementService {
@@ -59,19 +61,37 @@ public class SettlementService {
59 61
     private static final String STEP_WORK_STARTED = "work_started";
60 62
     /** 订单步骤:待结算(发起结算成功后的目标步骤) */
61 63
     private static final String STEP_SETTLEMENT_PENDING = "settlement_pending";
64
+    /** 订单步骤:已支付 */
65
+    private static final String STEP_SETTLEMENT_PAID = "settlement_paid";
62 66
     /** 登记状态:已签完合同,可纳入结算 */
63 67
     private static final String REG_COMPLETED = "completed";
64 68
     /** 结算单状态:待企业确认 */
65 69
     private static final String STATUS_PENDING = "pending";
70
+    /** 结算单状态:已确认待支付 */
71
+    private static final String STATUS_CONFIRMED = "confirmed";
72
+    /** 结算单状态:支付中 */
73
+    private static final String STATUS_PAYING = "paying";
74
+    /** 结算单状态:已支付 */
75
+    private static final String STATUS_PAID = "paid";
66 76
     /** 结算单状态:已取消(不视为有效结算单) */
67 77
     private static final String STATUS_CANCELLED = "cancelled";
78
+    /** 明细状态:已支付 */
79
+    private static final String ITEM_STATUS_PAID = "paid";
80
+    /** 支付状态:成功 */
81
+    private static final String PAY_STATUS_SUCCESS = "success";
68 82
     /** 订单业务状态:结算中 */
69 83
     private static final String ORDER_STATUS_SETTLING = "settling";
84
+    /** 订单业务状态:已支付 */
85
+    private static final String ORDER_STATUS_PAID = "paid";
86
+    private static final String CHANNEL_WECHAT = "wechat";
87
+    private static final String CHANNEL_ALIPAY = "alipay";
88
+    private static final String CHANNEL_BANK = "bank";
70 89
     private static final DateTimeFormatter SETTLEMENT_NO_DAY = DateTimeFormatter.ofPattern("yyyyMMdd");
71 90
 
72 91
     private final FeSettlementOrderMapper feSettlementOrderMapper;
73 92
     private final FeSettlementItemMapper feSettlementItemMapper;
74 93
     private final FeSettlementFeeMapper feSettlementFeeMapper;
94
+    private final FePaymentRecordMapper fePaymentRecordMapper;
75 95
     private final FeEmploymentOrderMapper feEmploymentOrderMapper;
76 96
     private final FeEmploymentDraftMapper feEmploymentDraftMapper;
77 97
     private final FeWorkerRegistrationMapper feWorkerRegistrationMapper;
@@ -82,6 +102,7 @@ public class SettlementService {
82 102
     public SettlementService(FeSettlementOrderMapper feSettlementOrderMapper,
83 103
                              FeSettlementItemMapper feSettlementItemMapper,
84 104
                              FeSettlementFeeMapper feSettlementFeeMapper,
105
+                             FePaymentRecordMapper fePaymentRecordMapper,
85 106
                              FeEmploymentOrderMapper feEmploymentOrderMapper,
86 107
                              FeEmploymentDraftMapper feEmploymentDraftMapper,
87 108
                              FeWorkerRegistrationMapper feWorkerRegistrationMapper,
@@ -91,6 +112,7 @@ public class SettlementService {
91 112
         this.feSettlementOrderMapper = feSettlementOrderMapper;
92 113
         this.feSettlementItemMapper = feSettlementItemMapper;
93 114
         this.feSettlementFeeMapper = feSettlementFeeMapper;
115
+        this.fePaymentRecordMapper = fePaymentRecordMapper;
94 116
         this.feEmploymentOrderMapper = feEmploymentOrderMapper;
95 117
         this.feEmploymentDraftMapper = feEmploymentDraftMapper;
96 118
         this.feWorkerRegistrationMapper = feWorkerRegistrationMapper;
@@ -227,6 +249,114 @@ public class SettlementService {
227 249
         return response;
228 250
     }
229 251
 
252
+    /**
253
+     * 企业确认结算并支付(MVP 一次完成)。
254
+     * <ul>
255
+     *   <li>结算单 {@code pending → paid},明细同步 paid</li>
256
+     *   <li>写入 {@code fe_payment_record}(Mock 即时 success)</li>
257
+     *   <li>订单 {@code settlement_pending → settlement_paid}</li>
258
+     *   <li>幂等:已支付则返回现有结算/支付信息</li>
259
+     * </ul>
260
+     */
261
+    @Transactional(rollbackFor = Exception.class)
262
+    public SettlementSummaryResponse confirmAndPay(Long userId, Long orderId, SettlementConfirmPayRequest request) {
263
+        FeEmploymentOrder order = requireOwnedOrder(userId, orderId);
264
+        FeSettlementOrder settlement = findActiveByOrderId(orderId);
265
+        if (settlement == null) {
266
+            throw new BizException(ErrorCode.BAD_REQUEST, "尚未发起结算,请先调用发起结算");
267
+        }
268
+
269
+        // 幂等:已支付
270
+        if (STATUS_PAID.equals(settlement.getSettlementStatus())) {
271
+            ensureOrderAtSettlementPaid(order, LocalDateTime.now());
272
+            return toSummary(order, settlement, false, "结算已支付");
273
+        }
274
+
275
+        String step = order.getCurrentStepCode();
276
+        if (STEP_SETTLEMENT_PAID.equals(step) || isStepAfter(step, STEP_SETTLEMENT_PAID)) {
277
+            ensureOrderAtSettlementPaid(order, LocalDateTime.now());
278
+            return toSummary(order, settlement, false, "订单已进入已支付阶段");
279
+        }
280
+        if (!STEP_SETTLEMENT_PENDING.equals(step)) {
281
+            throw new BizException(ErrorCode.BAD_REQUEST,
282
+                    "仅待结算阶段可确认支付,当前步骤:" + (step == null ? "未知" : step));
283
+        }
284
+        if (!STATUS_PENDING.equals(settlement.getSettlementStatus())
285
+                && !STATUS_CONFIRMED.equals(settlement.getSettlementStatus())
286
+                && !STATUS_PAYING.equals(settlement.getSettlementStatus())) {
287
+            throw new BizException(ErrorCode.BAD_REQUEST,
288
+                    "当前结算单状态不可支付:" + settlement.getSettlementStatus());
289
+        }
290
+
291
+        String channel = resolvePaymentChannel(request != null ? request.getPaymentChannel() : null);
292
+        LocalDateTime now = LocalDateTime.now();
293
+        BigDecimal payAmount = settlement.getTotalOutflow() != null
294
+                ? settlement.getTotalOutflow()
295
+                : (settlement.getNetAmount() != null ? settlement.getNetAmount() : BigDecimal.ZERO);
296
+
297
+        // 1) 结算单确认并置为已支付(MVP 跳过真实渠道回调)
298
+        FeSettlementOrder settlementUpdate = new FeSettlementOrder();
299
+        settlementUpdate.setId(settlement.getId());
300
+        settlementUpdate.setSettlementStatus(STATUS_PAID);
301
+        settlementUpdate.setConfirmedAt(settlement.getConfirmedAt() != null ? settlement.getConfirmedAt() : now);
302
+        settlementUpdate.setConfirmedBy(userId);
303
+        settlementUpdate.setPaidAt(now);
304
+        settlementUpdate.setComplianceTags("[\"合同已签署\",\"结算已确认\",\"支付已完成\"]");
305
+        settlementUpdate.setComplianceRemark("MVP Mock 支付成功,后续可对接真实支付通道");
306
+        settlementUpdate.setUpdateTime(now);
307
+        feSettlementOrderMapper.updateById(settlementUpdate);
308
+        settlement.setSettlementStatus(STATUS_PAID);
309
+        settlement.setConfirmedAt(settlementUpdate.getConfirmedAt());
310
+        settlement.setConfirmedBy(userId);
311
+        settlement.setPaidAt(now);
312
+
313
+        // 2) 明细全部标记已支付
314
+        List<FeSettlementItem> items = feSettlementItemMapper.selectList(
315
+                new LambdaQueryWrapper<FeSettlementItem>()
316
+                        .eq(FeSettlementItem::getSettlementId, settlement.getId()));
317
+        if (items != null) {
318
+            for (FeSettlementItem item : items) {
319
+                FeSettlementItem itemUpdate = new FeSettlementItem();
320
+                itemUpdate.setId(item.getId());
321
+                itemUpdate.setItemStatus(ITEM_STATUS_PAID);
322
+                itemUpdate.setUpdateTime(now);
323
+                feSettlementItemMapper.updateById(itemUpdate);
324
+            }
325
+        }
326
+
327
+        // 3) 支付记录(Mock 即时成功;已有成功记录则复用)
328
+        FePaymentRecord payment = findSuccessPayment(settlement.getId());
329
+        if (payment == null) {
330
+            payment = new FePaymentRecord();
331
+            payment.setPaymentNo(nextPaymentNo());
332
+            payment.setSettlementId(settlement.getId());
333
+            payment.setEnterpriseId(order.getEnterpriseId());
334
+            payment.setPaymentChannel(channel);
335
+            payment.setPaymentAmount(payAmount);
336
+            payment.setPaymentStatus(PAY_STATUS_SUCCESS);
337
+            payment.setExternalTradeNo("MOCK-" + payment.getPaymentNo());
338
+            payment.setPayerUserId(userId);
339
+            payment.setPaidAt(now);
340
+            payment.setCreateTime(now);
341
+            payment.setUpdateTime(now);
342
+            fePaymentRecordMapper.insert(payment);
343
+        }
344
+
345
+        // 4) 订单推进至已支付
346
+        FeEmploymentOrder orderUpdate = new FeEmploymentOrder();
347
+        orderUpdate.setId(order.getId());
348
+        orderUpdate.setCurrentStepCode(STEP_SETTLEMENT_PAID);
349
+        orderUpdate.setOrderStatus(ORDER_STATUS_PAID);
350
+        orderUpdate.setUpdateTime(now);
351
+        feEmploymentOrderMapper.updateById(orderUpdate);
352
+        order.setCurrentStepCode(STEP_SETTLEMENT_PAID);
353
+        order.setOrderStatus(ORDER_STATUS_PAID);
354
+
355
+        log.info("结算确认支付成功 orderId={} settlementId={} paymentNo={} channel={} amount={}",
356
+                orderId, settlement.getId(), payment.getPaymentNo(), channel, payAmount);
357
+        return toSummary(order, settlement, false, "结算已确认并支付成功");
358
+    }
359
+
230 360
     /**
231 361
      * 幂等路径:结算单已存在但订单步骤未跟上时,校正到 settlement_pending。
232 362
      */
@@ -245,6 +375,22 @@ public class SettlementService {
245 375
         order.setOrderStatus(ORDER_STATUS_SETTLING);
246 376
     }
247 377
 
378
+    /** 幂等路径:已支付但订单步骤未跟上时,校正到 settlement_paid。 */
379
+    private void ensureOrderAtSettlementPaid(FeEmploymentOrder order, LocalDateTime now) {
380
+        if (STEP_SETTLEMENT_PAID.equals(order.getCurrentStepCode())
381
+                || isStepAfter(order.getCurrentStepCode(), STEP_SETTLEMENT_PAID)) {
382
+            return;
383
+        }
384
+        FeEmploymentOrder update = new FeEmploymentOrder();
385
+        update.setId(order.getId());
386
+        update.setCurrentStepCode(STEP_SETTLEMENT_PAID);
387
+        update.setOrderStatus(ORDER_STATUS_PAID);
388
+        update.setUpdateTime(now);
389
+        feEmploymentOrderMapper.updateById(update);
390
+        order.setCurrentStepCode(STEP_SETTLEMENT_PAID);
391
+        order.setOrderStatus(ORDER_STATUS_PAID);
392
+    }
393
+
248 394
     /** 本单最新一条非 cancelled 结算单。 */
249 395
     private FeSettlementOrder findActiveByOrderId(Long orderId) {
250 396
         return feSettlementOrderMapper.selectOne(new LambdaQueryWrapper<FeSettlementOrder>()
@@ -392,6 +538,18 @@ public class SettlementService {
392 538
         response.setCreated(created);
393 539
         response.setMessage(message);
394 540
         response.setCreateTime(settlement.getCreateTime());
541
+        response.setConfirmedAt(settlement.getConfirmedAt());
542
+        response.setPaidAt(settlement.getPaidAt());
543
+
544
+        FePaymentRecord payment = findLatestPayment(settlement.getId());
545
+        if (payment != null) {
546
+            response.setPaymentId(payment.getId());
547
+            response.setPaymentNo(payment.getPaymentNo());
548
+            response.setPaymentChannel(payment.getPaymentChannel());
549
+            response.setPaymentStatus(payment.getPaymentStatus());
550
+            response.setPaymentAmount(payment.getPaymentAmount());
551
+            response.setExternalTradeNo(payment.getExternalTradeNo());
552
+        }
395 553
 
396 554
         List<FeSettlementItem> items = feSettlementItemMapper.selectList(
397 555
                 new LambdaQueryWrapper<FeSettlementItem>()
@@ -484,6 +642,40 @@ public class SettlementService {
484 642
         return "STL" + day + rand;
485 643
     }
486 644
 
645
+    /** 支付单号:PAY + yyyyMMdd + 4 位随机数。 */
646
+    private String nextPaymentNo() {
647
+        String day = LocalDateTime.now().format(SETTLEMENT_NO_DAY);
648
+        int rand = ThreadLocalRandom.current().nextInt(1000, 10000);
649
+        return "PAY" + day + rand;
650
+    }
651
+
652
+    /** 解析并校验支付渠道,非法则默认 wechat。 */
653
+    private static String resolvePaymentChannel(String raw) {
654
+        if (!StringUtils.hasText(raw)) {
655
+            return CHANNEL_WECHAT;
656
+        }
657
+        String channel = raw.trim().toLowerCase();
658
+        if (CHANNEL_WECHAT.equals(channel) || CHANNEL_ALIPAY.equals(channel) || CHANNEL_BANK.equals(channel)) {
659
+            return channel;
660
+        }
661
+        throw new BizException(ErrorCode.BAD_REQUEST, "不支持的支付渠道:" + raw + ",可选 wechat/alipay/bank");
662
+    }
663
+
664
+    private FePaymentRecord findSuccessPayment(Long settlementId) {
665
+        return fePaymentRecordMapper.selectOne(new LambdaQueryWrapper<FePaymentRecord>()
666
+                .eq(FePaymentRecord::getSettlementId, settlementId)
667
+                .eq(FePaymentRecord::getPaymentStatus, PAY_STATUS_SUCCESS)
668
+                .orderByDesc(FePaymentRecord::getId)
669
+                .last("LIMIT 1"));
670
+    }
671
+
672
+    private FePaymentRecord findLatestPayment(Long settlementId) {
673
+        return fePaymentRecordMapper.selectOne(new LambdaQueryWrapper<FePaymentRecord>()
674
+                .eq(FePaymentRecord::getSettlementId, settlementId)
675
+                .orderByDesc(FePaymentRecord::getId)
676
+                .last("LIMIT 1"));
677
+    }
678
+
487 679
     /** 判断 current 是否在合规流程中严格位于 target 之后。 */
488 680
     private static boolean isStepAfter(String current, String target) {
489 681
         if (!StringUtils.hasText(current) || !StringUtils.hasText(target)) {

+ 27 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/settlement/dto/SettlementConfirmPayRequest.java

@@ -0,0 +1,27 @@
1
+package com.huimv.employment.service.settlement.dto;
2
+
3
+import com.fasterxml.jackson.annotation.JsonAlias;
4
+import com.fasterxml.jackson.annotation.JsonProperty;
5
+import io.swagger.v3.oas.annotations.media.Schema;
6
+
7
+/**
8
+ * 企业确认结算并支付请求体。
9
+ * <p>MVP 一次完成:确认结算单 + Mock 支付成功 → 订单 {@code settlement_paid}。</p>
10
+ */
11
+@Schema(description = "企业确认结算并支付请求")
12
+public class SettlementConfirmPayRequest {
13
+
14
+    /** wechat / alipay / bank,默认 wechat */
15
+    @JsonProperty("payment_channel")
16
+    @JsonAlias({"paymentChannel", "channel"})
17
+    @Schema(description = "支付渠道:wechat / alipay / bank", example = "wechat")
18
+    private String paymentChannel;
19
+
20
+    public String getPaymentChannel() {
21
+        return paymentChannel;
22
+    }
23
+
24
+    public void setPaymentChannel(String paymentChannel) {
25
+        this.paymentChannel = paymentChannel;
26
+    }
27
+}

+ 88 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/settlement/dto/SettlementSummaryResponse.java

@@ -68,6 +68,30 @@ public class SettlementSummaryResponse {
68 68
     @JsonProperty("create_time")
69 69
     private LocalDateTime createTime;
70 70
 
71
+    @JsonProperty("confirmed_at")
72
+    private LocalDateTime confirmedAt;
73
+
74
+    @JsonProperty("paid_at")
75
+    private LocalDateTime paidAt;
76
+
77
+    @JsonProperty("payment_id")
78
+    private Long paymentId;
79
+
80
+    @JsonProperty("payment_no")
81
+    private String paymentNo;
82
+
83
+    @JsonProperty("payment_channel")
84
+    private String paymentChannel;
85
+
86
+    @JsonProperty("payment_status")
87
+    private String paymentStatus;
88
+
89
+    @JsonProperty("payment_amount")
90
+    private BigDecimal paymentAmount;
91
+
92
+    @JsonProperty("external_trade_no")
93
+    private String externalTradeNo;
94
+
71 95
     private List<SettlementItemResponse> items = new ArrayList<>();
72 96
 
73 97
     private List<SettlementFeeResponse> fees = new ArrayList<>();
@@ -224,6 +248,70 @@ public class SettlementSummaryResponse {
224 248
         this.createTime = createTime;
225 249
     }
226 250
 
251
+    public LocalDateTime getConfirmedAt() {
252
+        return confirmedAt;
253
+    }
254
+
255
+    public void setConfirmedAt(LocalDateTime confirmedAt) {
256
+        this.confirmedAt = confirmedAt;
257
+    }
258
+
259
+    public LocalDateTime getPaidAt() {
260
+        return paidAt;
261
+    }
262
+
263
+    public void setPaidAt(LocalDateTime paidAt) {
264
+        this.paidAt = paidAt;
265
+    }
266
+
267
+    public Long getPaymentId() {
268
+        return paymentId;
269
+    }
270
+
271
+    public void setPaymentId(Long paymentId) {
272
+        this.paymentId = paymentId;
273
+    }
274
+
275
+    public String getPaymentNo() {
276
+        return paymentNo;
277
+    }
278
+
279
+    public void setPaymentNo(String paymentNo) {
280
+        this.paymentNo = paymentNo;
281
+    }
282
+
283
+    public String getPaymentChannel() {
284
+        return paymentChannel;
285
+    }
286
+
287
+    public void setPaymentChannel(String paymentChannel) {
288
+        this.paymentChannel = paymentChannel;
289
+    }
290
+
291
+    public String getPaymentStatus() {
292
+        return paymentStatus;
293
+    }
294
+
295
+    public void setPaymentStatus(String paymentStatus) {
296
+        this.paymentStatus = paymentStatus;
297
+    }
298
+
299
+    public BigDecimal getPaymentAmount() {
300
+        return paymentAmount;
301
+    }
302
+
303
+    public void setPaymentAmount(BigDecimal paymentAmount) {
304
+        this.paymentAmount = paymentAmount;
305
+    }
306
+
307
+    public String getExternalTradeNo() {
308
+        return externalTradeNo;
309
+    }
310
+
311
+    public void setExternalTradeNo(String externalTradeNo) {
312
+        this.externalTradeNo = externalTradeNo;
313
+    }
314
+
227 315
     public List<SettlementItemResponse> getItems() {
228 316
         return items;
229 317
     }