Browse Source

增加成本测算、提交草稿

wwh 3 weeks ago
parent
commit
0e67378587
13 changed files with 1039 additions and 0 deletions
  1. 27 0
      huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mcp/McpDraftController.java
  2. 157 0
      huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/entity/FeDraftCostSnapshot.java
  3. 230 0
      huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/entity/FeEmploymentOrder.java
  4. 9 0
      huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/mapper/FeDraftCostSnapshotMapper.java
  5. 9 0
      huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/mapper/FeEmploymentOrderMapper.java
  6. 218 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/DraftCostCalculationEngine.java
  7. 163 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/McpDraftService.java
  8. 21 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftCalculateResponse.java
  9. 46 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftSchemeCostResponse.java
  10. 65 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftSchemeResponse.java
  11. 40 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftSchemeWorkerIncomeResponse.java
  12. 23 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftSubmitRequest.java
  13. 31 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftSubmitResponse.java

+ 27 - 0
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mcp/McpDraftController.java

@@ -2,9 +2,12 @@ package com.huimv.employment.controller.mcp;
2 2
 
3 3
 import com.huimv.employment.common.web.R;
4 4
 import com.huimv.employment.service.draft.McpDraftService;
5
+import com.huimv.employment.service.draft.dto.McpDraftCalculateResponse;
5 6
 import com.huimv.employment.service.draft.dto.McpDraftGenerateRequest;
6 7
 import com.huimv.employment.service.draft.dto.McpDraftGenerateResponse;
7 8
 import com.huimv.employment.service.draft.dto.McpDraftInfoResponse;
9
+import com.huimv.employment.service.draft.dto.McpDraftSubmitRequest;
10
+import com.huimv.employment.service.draft.dto.McpDraftSubmitResponse;
8 11
 import com.huimv.employment.service.draft.dto.McpDraftUpdateRequest;
9 12
 import com.huimv.employment.service.draft.dto.McpDraftUpdateResponse;
10 13
 import io.swagger.v3.oas.annotations.Operation;
@@ -81,4 +84,28 @@ public class McpDraftController {
81 84
     public R<McpDraftInfoResponse> info(@RequestHeader(HEADER_SESSION_ID) String sessionId) {
82 85
         return R.ok(mcpDraftService.getInfo(sessionId));
83 86
     }
87
+
88
+    @PostMapping("/calculate")
89
+    @Operation(summary = "成本测算",
90
+            description = "草稿 status=ready 后调用,返回 SCHEME_A/SCHEME_B 多套合规方案。",
91
+            parameters = {
92
+                    @Parameter(name = HEADER_SESSION_ID, in = ParameterIn.HEADER, required = true),
93
+                    @Parameter(name = "X-Fe-Ai-Key", in = ParameterIn.HEADER)
94
+            })
95
+    public R<McpDraftCalculateResponse> calculate(@RequestHeader(HEADER_SESSION_ID) String sessionId) {
96
+        return R.ok(mcpDraftService.calculate(sessionId));
97
+    }
98
+
99
+    @PostMapping("/submit")
100
+    @Operation(summary = "确认并提交草稿",
101
+            description = "用户选定方案后转化为正式订单,需先调用 calculate。",
102
+            parameters = {
103
+                    @Parameter(name = HEADER_SESSION_ID, in = ParameterIn.HEADER, required = true),
104
+                    @Parameter(name = "X-Fe-Ai-Key", in = ParameterIn.HEADER)
105
+            })
106
+    public R<McpDraftSubmitResponse> submit(
107
+            @RequestHeader(HEADER_SESSION_ID) String sessionId,
108
+            @Validated @RequestBody McpDraftSubmitRequest request) {
109
+        return R.ok(mcpDraftService.submit(sessionId, request));
110
+    }
84 111
 }

+ 157 - 0
huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/entity/FeDraftCostSnapshot.java

@@ -0,0 +1,157 @@
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_draft_cost_snapshot} 实体。
12
+ */
13
+@TableName("fe_draft_cost_snapshot")
14
+public class FeDraftCostSnapshot {
15
+
16
+    @TableId(type = IdType.AUTO)
17
+    private Long id;
18
+
19
+    private Long draftId;
20
+
21
+    private Long planId;
22
+
23
+    private String planCode;
24
+
25
+    private String planName;
26
+
27
+    private BigDecimal employerTotalOutflow;
28
+
29
+    private BigDecimal employerAvgCostPerWorker;
30
+
31
+    private BigDecimal workerTakeHomePerWorker;
32
+
33
+    private Boolean isRecommended;
34
+
35
+    private String statusTag;
36
+
37
+    /** JSON 数组字符串 */
38
+    private String complianceTags;
39
+
40
+    private String notice;
41
+
42
+    private String calcVersion;
43
+
44
+    private LocalDateTime createTime;
45
+
46
+    public Long getId() {
47
+        return id;
48
+    }
49
+
50
+    public void setId(Long id) {
51
+        this.id = id;
52
+    }
53
+
54
+    public Long getDraftId() {
55
+        return draftId;
56
+    }
57
+
58
+    public void setDraftId(Long draftId) {
59
+        this.draftId = draftId;
60
+    }
61
+
62
+    public Long getPlanId() {
63
+        return planId;
64
+    }
65
+
66
+    public void setPlanId(Long planId) {
67
+        this.planId = planId;
68
+    }
69
+
70
+    public String getPlanCode() {
71
+        return planCode;
72
+    }
73
+
74
+    public void setPlanCode(String planCode) {
75
+        this.planCode = planCode;
76
+    }
77
+
78
+    public String getPlanName() {
79
+        return planName;
80
+    }
81
+
82
+    public void setPlanName(String planName) {
83
+        this.planName = planName;
84
+    }
85
+
86
+    public BigDecimal getEmployerTotalOutflow() {
87
+        return employerTotalOutflow;
88
+    }
89
+
90
+    public void setEmployerTotalOutflow(BigDecimal employerTotalOutflow) {
91
+        this.employerTotalOutflow = employerTotalOutflow;
92
+    }
93
+
94
+    public BigDecimal getEmployerAvgCostPerWorker() {
95
+        return employerAvgCostPerWorker;
96
+    }
97
+
98
+    public void setEmployerAvgCostPerWorker(BigDecimal employerAvgCostPerWorker) {
99
+        this.employerAvgCostPerWorker = employerAvgCostPerWorker;
100
+    }
101
+
102
+    public BigDecimal getWorkerTakeHomePerWorker() {
103
+        return workerTakeHomePerWorker;
104
+    }
105
+
106
+    public void setWorkerTakeHomePerWorker(BigDecimal workerTakeHomePerWorker) {
107
+        this.workerTakeHomePerWorker = workerTakeHomePerWorker;
108
+    }
109
+
110
+    public Boolean getIsRecommended() {
111
+        return isRecommended;
112
+    }
113
+
114
+    public void setIsRecommended(Boolean recommended) {
115
+        isRecommended = recommended;
116
+    }
117
+
118
+    public String getStatusTag() {
119
+        return statusTag;
120
+    }
121
+
122
+    public void setStatusTag(String statusTag) {
123
+        this.statusTag = statusTag;
124
+    }
125
+
126
+    public String getComplianceTags() {
127
+        return complianceTags;
128
+    }
129
+
130
+    public void setComplianceTags(String complianceTags) {
131
+        this.complianceTags = complianceTags;
132
+    }
133
+
134
+    public String getNotice() {
135
+        return notice;
136
+    }
137
+
138
+    public void setNotice(String notice) {
139
+        this.notice = notice;
140
+    }
141
+
142
+    public String getCalcVersion() {
143
+        return calcVersion;
144
+    }
145
+
146
+    public void setCalcVersion(String calcVersion) {
147
+        this.calcVersion = calcVersion;
148
+    }
149
+
150
+    public LocalDateTime getCreateTime() {
151
+        return createTime;
152
+    }
153
+
154
+    public void setCreateTime(LocalDateTime createTime) {
155
+        this.createTime = createTime;
156
+    }
157
+}

+ 230 - 0
huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/entity/FeEmploymentOrder.java

@@ -0,0 +1,230 @@
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.TableLogic;
6
+import com.baomidou.mybatisplus.annotation.TableName;
7
+
8
+import java.math.BigDecimal;
9
+import java.time.LocalDate;
10
+import java.time.LocalDateTime;
11
+
12
+/**
13
+ * 用工订单主表 {@code fe_employment_order} 实体。
14
+ */
15
+@TableName("fe_employment_order")
16
+public class FeEmploymentOrder {
17
+
18
+    @TableId(type = IdType.AUTO)
19
+    private Long id;
20
+
21
+    private String orderNo;
22
+
23
+    private Long enterpriseId;
24
+
25
+    private Long draftId;
26
+
27
+    private Long selectedPlanId;
28
+
29
+    private String title;
30
+
31
+    private Integer workerCount;
32
+
33
+    private Integer workDays;
34
+
35
+    private String workType;
36
+
37
+    private String workLocation;
38
+
39
+    private LocalDate workStartDate;
40
+
41
+    private LocalDate workEndDate;
42
+
43
+    /** confirmed / in_progress / settling / completed 等 */
44
+    private String orderStatus;
45
+
46
+    private String currentStepCode;
47
+
48
+    private Integer registeredCount;
49
+
50
+    private Integer pendingCount;
51
+
52
+    private Integer abnormalCount;
53
+
54
+    private BigDecimal totalOutflow;
55
+
56
+    private LocalDateTime createTime;
57
+
58
+    private LocalDateTime updateTime;
59
+
60
+    @TableLogic
61
+    private Integer delFlag;
62
+
63
+    public Long getId() {
64
+        return id;
65
+    }
66
+
67
+    public void setId(Long id) {
68
+        this.id = id;
69
+    }
70
+
71
+    public String getOrderNo() {
72
+        return orderNo;
73
+    }
74
+
75
+    public void setOrderNo(String orderNo) {
76
+        this.orderNo = orderNo;
77
+    }
78
+
79
+    public Long getEnterpriseId() {
80
+        return enterpriseId;
81
+    }
82
+
83
+    public void setEnterpriseId(Long enterpriseId) {
84
+        this.enterpriseId = enterpriseId;
85
+    }
86
+
87
+    public Long getDraftId() {
88
+        return draftId;
89
+    }
90
+
91
+    public void setDraftId(Long draftId) {
92
+        this.draftId = draftId;
93
+    }
94
+
95
+    public Long getSelectedPlanId() {
96
+        return selectedPlanId;
97
+    }
98
+
99
+    public void setSelectedPlanId(Long selectedPlanId) {
100
+        this.selectedPlanId = selectedPlanId;
101
+    }
102
+
103
+    public String getTitle() {
104
+        return title;
105
+    }
106
+
107
+    public void setTitle(String title) {
108
+        this.title = title;
109
+    }
110
+
111
+    public Integer getWorkerCount() {
112
+        return workerCount;
113
+    }
114
+
115
+    public void setWorkerCount(Integer workerCount) {
116
+        this.workerCount = workerCount;
117
+    }
118
+
119
+    public Integer getWorkDays() {
120
+        return workDays;
121
+    }
122
+
123
+    public void setWorkDays(Integer workDays) {
124
+        this.workDays = workDays;
125
+    }
126
+
127
+    public String getWorkType() {
128
+        return workType;
129
+    }
130
+
131
+    public void setWorkType(String workType) {
132
+        this.workType = workType;
133
+    }
134
+
135
+    public String getWorkLocation() {
136
+        return workLocation;
137
+    }
138
+
139
+    public void setWorkLocation(String workLocation) {
140
+        this.workLocation = workLocation;
141
+    }
142
+
143
+    public LocalDate getWorkStartDate() {
144
+        return workStartDate;
145
+    }
146
+
147
+    public void setWorkStartDate(LocalDate workStartDate) {
148
+        this.workStartDate = workStartDate;
149
+    }
150
+
151
+    public LocalDate getWorkEndDate() {
152
+        return workEndDate;
153
+    }
154
+
155
+    public void setWorkEndDate(LocalDate workEndDate) {
156
+        this.workEndDate = workEndDate;
157
+    }
158
+
159
+    public String getOrderStatus() {
160
+        return orderStatus;
161
+    }
162
+
163
+    public void setOrderStatus(String orderStatus) {
164
+        this.orderStatus = orderStatus;
165
+    }
166
+
167
+    public String getCurrentStepCode() {
168
+        return currentStepCode;
169
+    }
170
+
171
+    public void setCurrentStepCode(String currentStepCode) {
172
+        this.currentStepCode = currentStepCode;
173
+    }
174
+
175
+    public Integer getRegisteredCount() {
176
+        return registeredCount;
177
+    }
178
+
179
+    public void setRegisteredCount(Integer registeredCount) {
180
+        this.registeredCount = registeredCount;
181
+    }
182
+
183
+    public Integer getPendingCount() {
184
+        return pendingCount;
185
+    }
186
+
187
+    public void setPendingCount(Integer pendingCount) {
188
+        this.pendingCount = pendingCount;
189
+    }
190
+
191
+    public Integer getAbnormalCount() {
192
+        return abnormalCount;
193
+    }
194
+
195
+    public void setAbnormalCount(Integer abnormalCount) {
196
+        this.abnormalCount = abnormalCount;
197
+    }
198
+
199
+    public BigDecimal getTotalOutflow() {
200
+        return totalOutflow;
201
+    }
202
+
203
+    public void setTotalOutflow(BigDecimal totalOutflow) {
204
+        this.totalOutflow = totalOutflow;
205
+    }
206
+
207
+    public LocalDateTime getCreateTime() {
208
+        return createTime;
209
+    }
210
+
211
+    public void setCreateTime(LocalDateTime createTime) {
212
+        this.createTime = createTime;
213
+    }
214
+
215
+    public LocalDateTime getUpdateTime() {
216
+        return updateTime;
217
+    }
218
+
219
+    public void setUpdateTime(LocalDateTime updateTime) {
220
+        this.updateTime = updateTime;
221
+    }
222
+
223
+    public Integer getDelFlag() {
224
+        return delFlag;
225
+    }
226
+
227
+    public void setDelFlag(Integer delFlag) {
228
+        this.delFlag = delFlag;
229
+    }
230
+}

+ 9 - 0
huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/mapper/FeDraftCostSnapshotMapper.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.FeDraftCostSnapshot;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface FeDraftCostSnapshotMapper extends BaseMapper<FeDraftCostSnapshot> {
9
+}

+ 9 - 0
huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/mapper/FeEmploymentOrderMapper.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.FeEmploymentOrder;
5
+import org.apache.ibatis.annotations.Mapper;
6
+
7
+@Mapper
8
+public interface FeEmploymentOrderMapper extends BaseMapper<FeEmploymentOrder> {
9
+}

+ 218 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/DraftCostCalculationEngine.java

@@ -0,0 +1,218 @@
1
+package com.huimv.employment.service.draft;
2
+
3
+import com.huimv.employment.dao.entity.FeEmploymentDraft;
4
+import com.huimv.employment.service.draft.dto.McpDraftCalculateResponse;
5
+import com.huimv.employment.service.draft.dto.McpDraftSchemeCostResponse;
6
+import com.huimv.employment.service.draft.dto.McpDraftSchemeResponse;
7
+import com.huimv.employment.service.draft.dto.McpDraftSchemeWorkerIncomeResponse;
8
+import org.springframework.stereotype.Component;
9
+
10
+import java.math.BigDecimal;
11
+import java.math.RoundingMode;
12
+import java.util.Arrays;
13
+import java.util.List;
14
+
15
+/**
16
+ * 草稿成本测算引擎(v2 §6.3.4 黑盒)。
17
+ * <p>一期内置 SCHEME_A(灵活用工)与 SCHEME_B(劳务报酬)两套规则,后续可接入 fe_service_plan。</p>
18
+ */
19
+@Component
20
+public class DraftCostCalculationEngine {
21
+
22
+    public static final String SCHEME_A = "SCHEME_A";
23
+    public static final String SCHEME_B = "SCHEME_B";
24
+    private static final String CALC_VERSION = "v1";
25
+
26
+    /** 灵活用工平台服务费率 2% */
27
+    private static final BigDecimal SCHEME_A_SERVICE_FEE_RATE = new BigDecimal("0.02");
28
+    /** 劳务报酬预扣税率(示例:3000 元对应 440 元) */
29
+    private static final BigDecimal SCHEME_B_TAX_RATE = new BigDecimal("440").divide(new BigDecimal("3000"), 8, RoundingMode.HALF_UP);
30
+
31
+    public List<CalculatedScheme> calculate(FeEmploymentDraft draft) {
32
+        BigDecimal laborTotal = draft.getEstimatedTotal();
33
+        int workerCount = draft.getWorkerCount() != null && draft.getWorkerCount() > 0
34
+                ? draft.getWorkerCount() : 1;
35
+        return Arrays.asList(
36
+                buildSchemeA(laborTotal, workerCount),
37
+                buildSchemeB(laborTotal, workerCount));
38
+    }
39
+
40
+    public McpDraftCalculateResponse toApiResponse(List<CalculatedScheme> schemes) {
41
+        McpDraftCalculateResponse response = new McpDraftCalculateResponse();
42
+        for (CalculatedScheme scheme : schemes) {
43
+            response.getSchemes().add(toSchemeResponse(scheme));
44
+        }
45
+        return response;
46
+    }
47
+
48
+    public String calcVersion() {
49
+        return CALC_VERSION;
50
+    }
51
+
52
+    private CalculatedScheme buildSchemeA(BigDecimal laborTotal, int workerCount) {
53
+        BigDecimal companyPay = scaleMoney(laborTotal);
54
+        BigDecimal serviceFee = scaleMoney(companyPay.multiply(SCHEME_A_SERVICE_FEE_RATE));
55
+        BigDecimal totalCost = companyPay.add(serviceFee);
56
+        BigDecimal gross = companyPay;
57
+        BigDecimal tax = BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
58
+        BigDecimal net = gross;
59
+        return new CalculatedScheme(
60
+                SCHEME_A,
61
+                "灵活用工模式(推荐)",
62
+                Arrays.asList("合规", "低税负"),
63
+                true,
64
+                companyPay, serviceFee, totalCost,
65
+                gross, tax, net,
66
+                totalCost, net, workerCount);
67
+    }
68
+
69
+    private CalculatedScheme buildSchemeB(BigDecimal laborTotal, int workerCount) {
70
+        BigDecimal companyPay = scaleMoney(laborTotal);
71
+        BigDecimal totalCost = companyPay;
72
+        BigDecimal gross = companyPay;
73
+        BigDecimal tax = scaleMoney(gross.multiply(SCHEME_B_TAX_RATE));
74
+        BigDecimal net = gross.subtract(tax);
75
+        return new CalculatedScheme(
76
+                SCHEME_B,
77
+                "劳务报酬模式",
78
+                Arrays.asList("传统"),
79
+                false,
80
+                companyPay, null, totalCost,
81
+                gross, tax, net,
82
+                totalCost, net, workerCount);
83
+    }
84
+
85
+    private McpDraftSchemeResponse toSchemeResponse(CalculatedScheme scheme) {
86
+        McpDraftSchemeResponse response = new McpDraftSchemeResponse();
87
+        response.setCode(scheme.getCode());
88
+        response.setTitle(scheme.getTitle());
89
+        response.setTags(scheme.getTags());
90
+
91
+        McpDraftSchemeCostResponse cost = new McpDraftSchemeCostResponse();
92
+        cost.setCompanyPay(formatMoney(scheme.getCompanyPay()));
93
+        if (scheme.getServiceFee() != null) {
94
+            cost.setServiceFee(formatMoney(scheme.getServiceFee()));
95
+        }
96
+        cost.setTotalCost(formatMoney(scheme.getTotalCost()));
97
+        response.setCost(cost);
98
+
99
+        McpDraftSchemeWorkerIncomeResponse income = new McpDraftSchemeWorkerIncomeResponse();
100
+        income.setGross(formatMoney(scheme.getGross()));
101
+        income.setTax(formatMoney(scheme.getTax()));
102
+        income.setNet(formatMoney(scheme.getNet()));
103
+        response.setWorkerIncome(income);
104
+        return response;
105
+    }
106
+
107
+    private static BigDecimal scaleMoney(BigDecimal value) {
108
+        return value.setScale(2, RoundingMode.HALF_UP);
109
+    }
110
+
111
+    private static String formatMoney(BigDecimal value) {
112
+        return scaleMoney(value).toPlainString();
113
+    }
114
+
115
+    /**
116
+     * 测算结果内部模型,供快照落库与 API 转换共用。
117
+     */
118
+    public static final class CalculatedScheme {
119
+
120
+        private final String code;
121
+        private final String title;
122
+        private final List<String> tags;
123
+        private final boolean recommended;
124
+        private final BigDecimal companyPay;
125
+        private final BigDecimal serviceFee;
126
+        private final BigDecimal totalCost;
127
+        private final BigDecimal gross;
128
+        private final BigDecimal tax;
129
+        private final BigDecimal net;
130
+        private final BigDecimal employerTotalOutflow;
131
+        private final BigDecimal workerTakeHomePerWorker;
132
+        private final int workerCount;
133
+
134
+        public CalculatedScheme(String code,
135
+                                String title,
136
+                                List<String> tags,
137
+                                boolean recommended,
138
+                                BigDecimal companyPay,
139
+                                BigDecimal serviceFee,
140
+                                BigDecimal totalCost,
141
+                                BigDecimal gross,
142
+                                BigDecimal tax,
143
+                                BigDecimal net,
144
+                                BigDecimal employerTotalOutflow,
145
+                                BigDecimal workerTakeHomePerWorker,
146
+                                int workerCount) {
147
+            this.code = code;
148
+            this.title = title;
149
+            this.tags = tags;
150
+            this.recommended = recommended;
151
+            this.companyPay = companyPay;
152
+            this.serviceFee = serviceFee;
153
+            this.totalCost = totalCost;
154
+            this.gross = gross;
155
+            this.tax = tax;
156
+            this.net = net;
157
+            this.employerTotalOutflow = employerTotalOutflow;
158
+            this.workerTakeHomePerWorker = workerCount > 0
159
+                    ? workerTakeHomePerWorker.divide(BigDecimal.valueOf(workerCount), 2, RoundingMode.HALF_UP)
160
+                    : workerTakeHomePerWorker;
161
+            this.workerCount = workerCount;
162
+        }
163
+
164
+        public String getCode() {
165
+            return code;
166
+        }
167
+
168
+        public String getTitle() {
169
+            return title;
170
+        }
171
+
172
+        public List<String> getTags() {
173
+            return tags;
174
+        }
175
+
176
+        public boolean isRecommended() {
177
+            return recommended;
178
+        }
179
+
180
+        public BigDecimal getCompanyPay() {
181
+            return companyPay;
182
+        }
183
+
184
+        public BigDecimal getServiceFee() {
185
+            return serviceFee;
186
+        }
187
+
188
+        public BigDecimal getTotalCost() {
189
+            return totalCost;
190
+        }
191
+
192
+        public BigDecimal getGross() {
193
+            return gross;
194
+        }
195
+
196
+        public BigDecimal getTax() {
197
+            return tax;
198
+        }
199
+
200
+        public BigDecimal getNet() {
201
+            return net;
202
+        }
203
+
204
+        public BigDecimal getEmployerTotalOutflow() {
205
+            return employerTotalOutflow;
206
+        }
207
+
208
+        public BigDecimal getEmployerAvgCostPerWorker() {
209
+            return workerCount > 0
210
+                    ? employerTotalOutflow.divide(BigDecimal.valueOf(workerCount), 2, RoundingMode.HALF_UP)
211
+                    : employerTotalOutflow;
212
+        }
213
+
214
+        public BigDecimal getWorkerTakeHomePerWorker() {
215
+            return workerTakeHomePerWorker;
216
+        }
217
+    }
218
+}

+ 163 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/McpDraftService.java

@@ -5,15 +5,22 @@ import com.fasterxml.jackson.databind.ObjectMapper;
5 5
 import com.huimv.employment.common.exception.BizException;
6 6
 import com.huimv.employment.common.exception.ErrorCode;
7 7
 import com.huimv.employment.dao.entity.FeConversation;
8
+import com.huimv.employment.dao.entity.FeDraftCostSnapshot;
8 9
 import com.huimv.employment.dao.entity.FeDraftField;
9 10
 import com.huimv.employment.dao.entity.FeEmploymentDraft;
11
+import com.huimv.employment.dao.entity.FeEmploymentOrder;
10 12
 import com.huimv.employment.dao.entity.FeEnterprise;
13
+import com.huimv.employment.dao.mapper.FeDraftCostSnapshotMapper;
11 14
 import com.huimv.employment.dao.mapper.FeDraftFieldMapper;
12 15
 import com.huimv.employment.dao.mapper.FeEmploymentDraftMapper;
16
+import com.huimv.employment.dao.mapper.FeEmploymentOrderMapper;
13 17
 import com.huimv.employment.service.conversation.ConversationService;
18
+import com.huimv.employment.service.draft.dto.McpDraftCalculateResponse;
14 19
 import com.huimv.employment.service.draft.dto.McpDraftGenerateRequest;
15 20
 import com.huimv.employment.service.draft.dto.McpDraftGenerateResponse;
16 21
 import com.huimv.employment.service.draft.dto.McpDraftInfoResponse;
22
+import com.huimv.employment.service.draft.dto.McpDraftSubmitRequest;
23
+import com.huimv.employment.service.draft.dto.McpDraftSubmitResponse;
17 24
 import com.huimv.employment.service.draft.dto.McpDraftUpdateRequest;
18 25
 import com.huimv.employment.service.draft.dto.McpDraftUpdateResponse;
19 26
 import com.huimv.employment.service.enterprise.EnterpriseService;
@@ -47,6 +54,10 @@ public class McpDraftService {
47 54
     private static final Logger log = LoggerFactory.getLogger(McpDraftService.class);
48 55
 
49 56
     private static final String DB_STATUS_PENDING = "pending";
57
+    private static final String DB_STATUS_CONFIRMED = "confirmed";
58
+    private static final String ORDER_STATUS_CONFIRMED = "confirmed";
59
+    private static final String STEP_PLAN_CONFIRMED = "plan_confirmed";
60
+    private static final String SUBMIT_SUCCESS = "success";
50 61
     private static final String MCP_STATUS_INCOMPLETE = "incomplete";
51 62
     private static final String MCP_STATUS_READY = "ready";
52 63
     private static final String MCP_STATUS_SUBMITTED = "submitted";
@@ -63,21 +74,33 @@ public class McpDraftService {
63 74
     private static final Set<String> ALLOWED_SETTLEMENT_MODES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
64 75
             "daily", "piece", "monthly")));
65 76
 
77
+    private static final Set<String> ALLOWED_SCHEME_CODES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
78
+            DraftCostCalculationEngine.SCHEME_A, DraftCostCalculationEngine.SCHEME_B)));
79
+
66 80
     private final FeEmploymentDraftMapper feEmploymentDraftMapper;
67 81
     private final FeDraftFieldMapper feDraftFieldMapper;
82
+    private final FeDraftCostSnapshotMapper feDraftCostSnapshotMapper;
83
+    private final FeEmploymentOrderMapper feEmploymentOrderMapper;
68 84
     private final ConversationService conversationService;
69 85
     private final EnterpriseService enterpriseService;
86
+    private final DraftCostCalculationEngine draftCostCalculationEngine;
70 87
     private final ObjectMapper objectMapper;
71 88
 
72 89
     public McpDraftService(FeEmploymentDraftMapper feEmploymentDraftMapper,
73 90
                            FeDraftFieldMapper feDraftFieldMapper,
91
+                           FeDraftCostSnapshotMapper feDraftCostSnapshotMapper,
92
+                           FeEmploymentOrderMapper feEmploymentOrderMapper,
74 93
                            ConversationService conversationService,
75 94
                            EnterpriseService enterpriseService,
95
+                           DraftCostCalculationEngine draftCostCalculationEngine,
76 96
                            ObjectMapper objectMapper) {
77 97
         this.feEmploymentDraftMapper = feEmploymentDraftMapper;
78 98
         this.feDraftFieldMapper = feDraftFieldMapper;
99
+        this.feDraftCostSnapshotMapper = feDraftCostSnapshotMapper;
100
+        this.feEmploymentOrderMapper = feEmploymentOrderMapper;
79 101
         this.conversationService = conversationService;
80 102
         this.enterpriseService = enterpriseService;
103
+        this.draftCostCalculationEngine = draftCostCalculationEngine;
81 104
         this.objectMapper = objectMapper;
82 105
     }
83 106
 
@@ -138,6 +161,146 @@ public class McpDraftService {
138 161
         return toInfoResponse(draft);
139 162
     }
140 163
 
164
+    @Transactional(rollbackFor = Exception.class)
165
+    public McpDraftCalculateResponse calculate(String sessionId) {
166
+        FeConversation conversation = conversationService.requireBySessionId(sessionId);
167
+        FeEmploymentDraft draft = requireReadyDraft(conversation.getId());
168
+        List<DraftCostCalculationEngine.CalculatedScheme> schemes = draftCostCalculationEngine.calculate(draft);
169
+        persistCostSnapshots(draft, schemes);
170
+        updateDraftEstimates(draft, schemes, LocalDateTime.now());
171
+        return draftCostCalculationEngine.toApiResponse(schemes);
172
+    }
173
+
174
+    @Transactional(rollbackFor = Exception.class)
175
+    public McpDraftSubmitResponse submit(String sessionId, McpDraftSubmitRequest request) {
176
+        FeConversation conversation = conversationService.requireBySessionId(sessionId);
177
+        FeEmploymentDraft draft = requireReadyDraft(conversation.getId());
178
+        String schemeCode = request.getSelectedSchemeCode().trim().toUpperCase();
179
+        if (!ALLOWED_SCHEME_CODES.contains(schemeCode)) {
180
+            throw new BizException(ErrorCode.BAD_REQUEST, "不支持的方案编码:" + request.getSelectedSchemeCode());
181
+        }
182
+        FeDraftCostSnapshot snapshot = findLatestSnapshot(draft.getId(), schemeCode);
183
+        if (snapshot == null) {
184
+            throw new BizException(ErrorCode.BAD_REQUEST, "请先调用 calculate 获取方案后再提交");
185
+        }
186
+
187
+        LocalDateTime now = LocalDateTime.now();
188
+        FeEmploymentOrder order = buildOrderFromDraft(draft, snapshot, now);
189
+        order.setOrderNo("tmp_" + UUID.randomUUID().toString().replace("-", "").substring(0, 20));
190
+        try {
191
+            feEmploymentOrderMapper.insert(order);
192
+        } catch (DuplicateKeyException ex) {
193
+            throw new BizException(ErrorCode.BAD_REQUEST, "订单编号冲突,请重试");
194
+        }
195
+        order.setOrderNo(generateOrderNo(order.getId()));
196
+        feEmploymentOrderMapper.updateById(order);
197
+
198
+        FeEmploymentDraft draftUpdate = new FeEmploymentDraft();
199
+        draftUpdate.setId(draft.getId());
200
+        draftUpdate.setStatus(DB_STATUS_CONFIRMED);
201
+        draftUpdate.setConfirmedAt(now);
202
+        draftUpdate.setEstimatedTotal(snapshot.getEmployerTotalOutflow());
203
+        draftUpdate.setEstimatedPerCapita(snapshot.getEmployerAvgCostPerWorker());
204
+        draftUpdate.setUpdateTime(now);
205
+        feEmploymentDraftMapper.updateById(draftUpdate);
206
+
207
+        McpDraftSubmitResponse response = new McpDraftSubmitResponse();
208
+        response.setStatus(SUBMIT_SUCCESS);
209
+        response.setOrderId(order.getOrderNo());
210
+        return response;
211
+    }
212
+
213
+    private FeEmploymentDraft requireReadyDraft(Long conversationId) {
214
+        FeEmploymentDraft draft = requirePendingDraft(conversationId);
215
+        List<String> missing = parseMissingFields(draft.getMissingFields());
216
+        if (!missing.isEmpty()) {
217
+            throw new BizException(ErrorCode.BAD_REQUEST,
218
+                    "草稿信息不完整,请先补全字段:" + String.join(", ", missing));
219
+        }
220
+        return draft;
221
+    }
222
+
223
+    private void persistCostSnapshots(FeEmploymentDraft draft,
224
+                                      List<DraftCostCalculationEngine.CalculatedScheme> schemes) {
225
+        feDraftCostSnapshotMapper.delete(new LambdaQueryWrapper<FeDraftCostSnapshot>()
226
+                .eq(FeDraftCostSnapshot::getDraftId, draft.getId()));
227
+        LocalDateTime now = LocalDateTime.now();
228
+        for (DraftCostCalculationEngine.CalculatedScheme scheme : schemes) {
229
+            FeDraftCostSnapshot snapshot = new FeDraftCostSnapshot();
230
+            snapshot.setDraftId(draft.getId());
231
+            snapshot.setPlanCode(scheme.getCode());
232
+            snapshot.setPlanName(scheme.getTitle());
233
+            snapshot.setEmployerTotalOutflow(scheme.getEmployerTotalOutflow());
234
+            snapshot.setEmployerAvgCostPerWorker(scheme.getEmployerAvgCostPerWorker());
235
+            snapshot.setWorkerTakeHomePerWorker(scheme.getWorkerTakeHomePerWorker());
236
+            snapshot.setIsRecommended(scheme.isRecommended());
237
+            snapshot.setComplianceTags(writeJsonArray(scheme.getTags()));
238
+            snapshot.setCalcVersion(draftCostCalculationEngine.calcVersion());
239
+            snapshot.setCreateTime(now);
240
+            feDraftCostSnapshotMapper.insert(snapshot);
241
+        }
242
+    }
243
+
244
+    private void updateDraftEstimates(FeEmploymentDraft draft,
245
+                                      List<DraftCostCalculationEngine.CalculatedScheme> schemes,
246
+                                      LocalDateTime now) {
247
+        DraftCostCalculationEngine.CalculatedScheme recommended = schemes.stream()
248
+                .filter(DraftCostCalculationEngine.CalculatedScheme::isRecommended)
249
+                .findFirst()
250
+                .orElse(schemes.get(0));
251
+        FeEmploymentDraft update = new FeEmploymentDraft();
252
+        update.setId(draft.getId());
253
+        update.setEstimatedTotal(recommended.getEmployerTotalOutflow());
254
+        update.setEstimatedPerCapita(recommended.getEmployerAvgCostPerWorker());
255
+        update.setUpdateTime(now);
256
+        feEmploymentDraftMapper.updateById(update);
257
+    }
258
+
259
+    private FeDraftCostSnapshot findLatestSnapshot(Long draftId, String schemeCode) {
260
+        return feDraftCostSnapshotMapper.selectOne(new LambdaQueryWrapper<FeDraftCostSnapshot>()
261
+                .eq(FeDraftCostSnapshot::getDraftId, draftId)
262
+                .eq(FeDraftCostSnapshot::getPlanCode, schemeCode)
263
+                .orderByDesc(FeDraftCostSnapshot::getCreateTime)
264
+                .last("LIMIT 1"));
265
+    }
266
+
267
+    private FeEmploymentOrder buildOrderFromDraft(FeEmploymentDraft draft,
268
+                                                  FeDraftCostSnapshot snapshot,
269
+                                                  LocalDateTime now) {
270
+        FeEmploymentOrder order = new FeEmploymentOrder();
271
+        order.setEnterpriseId(draft.getEnterpriseId());
272
+        order.setDraftId(draft.getId());
273
+        order.setTitle(StringUtils.hasText(draft.getTitle()) ? draft.getTitle() : "用工订单");
274
+        order.setWorkerCount(draft.getWorkerCount());
275
+        order.setWorkDays(draft.getWorkDays());
276
+        order.setWorkType(draft.getWorkType());
277
+        order.setWorkLocation(draft.getWorkLocation());
278
+        order.setWorkStartDate(draft.getWorkStartDate());
279
+        order.setWorkEndDate(draft.getWorkEndDate());
280
+        order.setOrderStatus(ORDER_STATUS_CONFIRMED);
281
+        order.setCurrentStepCode(STEP_PLAN_CONFIRMED);
282
+        order.setRegisteredCount(0);
283
+        order.setPendingCount(draft.getWorkerCount());
284
+        order.setAbnormalCount(0);
285
+        order.setTotalOutflow(snapshot.getEmployerTotalOutflow());
286
+        order.setCreateTime(now);
287
+        order.setUpdateTime(now);
288
+        order.setDelFlag(0);
289
+        return order;
290
+    }
291
+
292
+    private static String generateOrderNo(Long orderId) {
293
+        return String.format("ord_%d", orderId);
294
+    }
295
+
296
+    private String writeJsonArray(List<String> values) {
297
+        try {
298
+            return objectMapper.writeValueAsString(values);
299
+        } catch (Exception ex) {
300
+            return "[]";
301
+        }
302
+    }
303
+
141 304
     private void validateGenerateRequest(McpDraftGenerateRequest request) {
142 305
         if (request == null || !StringUtils.hasText(request.getSourceType())) {
143 306
             throw new BizException(ErrorCode.BAD_REQUEST, "source_type 不能为空");

+ 21 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftCalculateResponse.java

@@ -0,0 +1,21 @@
1
+package com.huimv.employment.service.draft.dto;
2
+
3
+import io.swagger.v3.oas.annotations.media.Schema;
4
+
5
+import java.util.ArrayList;
6
+import java.util.List;
7
+
8
+@Schema(description = "MCP 成本测算响应(v2 §6.3.4)")
9
+public class McpDraftCalculateResponse {
10
+
11
+    @Schema(description = "合规方案列表")
12
+    private List<McpDraftSchemeResponse> schemes = new ArrayList<>();
13
+
14
+    public List<McpDraftSchemeResponse> getSchemes() {
15
+        return schemes;
16
+    }
17
+
18
+    public void setSchemes(List<McpDraftSchemeResponse> schemes) {
19
+        this.schemes = schemes;
20
+    }
21
+}

+ 46 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftSchemeCostResponse.java

@@ -0,0 +1,46 @@
1
+package com.huimv.employment.service.draft.dto;
2
+
3
+import com.fasterxml.jackson.annotation.JsonInclude;
4
+import com.fasterxml.jackson.annotation.JsonProperty;
5
+import io.swagger.v3.oas.annotations.media.Schema;
6
+
7
+@JsonInclude(JsonInclude.Include.NON_NULL)
8
+@Schema(description = "方案企业成本")
9
+public class McpDraftSchemeCostResponse {
10
+
11
+    @JsonProperty("company_pay")
12
+    @Schema(description = "企业应付劳务费", example = "3000.00")
13
+    private String companyPay;
14
+
15
+    @JsonProperty("service_fee")
16
+    @Schema(description = "平台服务费", example = "60.00")
17
+    private String serviceFee;
18
+
19
+    @JsonProperty("total_cost")
20
+    @Schema(description = "企业总支出", example = "3060.00")
21
+    private String totalCost;
22
+
23
+    public String getCompanyPay() {
24
+        return companyPay;
25
+    }
26
+
27
+    public void setCompanyPay(String companyPay) {
28
+        this.companyPay = companyPay;
29
+    }
30
+
31
+    public String getServiceFee() {
32
+        return serviceFee;
33
+    }
34
+
35
+    public void setServiceFee(String serviceFee) {
36
+        this.serviceFee = serviceFee;
37
+    }
38
+
39
+    public String getTotalCost() {
40
+        return totalCost;
41
+    }
42
+
43
+    public void setTotalCost(String totalCost) {
44
+        this.totalCost = totalCost;
45
+    }
46
+}

+ 65 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftSchemeResponse.java

@@ -0,0 +1,65 @@
1
+package com.huimv.employment.service.draft.dto;
2
+
3
+import io.swagger.v3.oas.annotations.media.Schema;
4
+
5
+import java.util.ArrayList;
6
+import java.util.List;
7
+
8
+@Schema(description = "成本测算方案")
9
+public class McpDraftSchemeResponse {
10
+
11
+    @Schema(description = "方案编码", example = "SCHEME_A")
12
+    private String code;
13
+
14
+    @Schema(description = "方案标题", example = "灵活用工模式(推荐)")
15
+    private String title;
16
+
17
+    @Schema(description = "标签", example = "[\"合规\", \"低税负\"]")
18
+    private List<String> tags = new ArrayList<>();
19
+
20
+    @Schema(description = "企业成本")
21
+    private McpDraftSchemeCostResponse cost;
22
+
23
+    @Schema(description = "员工收入")
24
+    private McpDraftSchemeWorkerIncomeResponse workerIncome;
25
+
26
+    public String getCode() {
27
+        return code;
28
+    }
29
+
30
+    public void setCode(String code) {
31
+        this.code = code;
32
+    }
33
+
34
+    public String getTitle() {
35
+        return title;
36
+    }
37
+
38
+    public void setTitle(String title) {
39
+        this.title = title;
40
+    }
41
+
42
+    public List<String> getTags() {
43
+        return tags;
44
+    }
45
+
46
+    public void setTags(List<String> tags) {
47
+        this.tags = tags;
48
+    }
49
+
50
+    public McpDraftSchemeCostResponse getCost() {
51
+        return cost;
52
+    }
53
+
54
+    public void setCost(McpDraftSchemeCostResponse cost) {
55
+        this.cost = cost;
56
+    }
57
+
58
+    public McpDraftSchemeWorkerIncomeResponse getWorkerIncome() {
59
+        return workerIncome;
60
+    }
61
+
62
+    public void setWorkerIncome(McpDraftSchemeWorkerIncomeResponse workerIncome) {
63
+        this.workerIncome = workerIncome;
64
+    }
65
+}

+ 40 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftSchemeWorkerIncomeResponse.java

@@ -0,0 +1,40 @@
1
+package com.huimv.employment.service.draft.dto;
2
+
3
+import io.swagger.v3.oas.annotations.media.Schema;
4
+
5
+@Schema(description = "方案员工收入")
6
+public class McpDraftSchemeWorkerIncomeResponse {
7
+
8
+    @Schema(description = "税前收入", example = "3000.00")
9
+    private String gross;
10
+
11
+    @Schema(description = "代扣税费", example = "0.00")
12
+    private String tax;
13
+
14
+    @Schema(description = "到手收入", example = "3000.00")
15
+    private String net;
16
+
17
+    public String getGross() {
18
+        return gross;
19
+    }
20
+
21
+    public void setGross(String gross) {
22
+        this.gross = gross;
23
+    }
24
+
25
+    public String getTax() {
26
+        return tax;
27
+    }
28
+
29
+    public void setTax(String tax) {
30
+        this.tax = tax;
31
+    }
32
+
33
+    public String getNet() {
34
+        return net;
35
+    }
36
+
37
+    public void setNet(String net) {
38
+        this.net = net;
39
+    }
40
+}

+ 23 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftSubmitRequest.java

@@ -0,0 +1,23 @@
1
+package com.huimv.employment.service.draft.dto;
2
+
3
+import com.fasterxml.jackson.annotation.JsonProperty;
4
+import io.swagger.v3.oas.annotations.media.Schema;
5
+
6
+import javax.validation.constraints.NotBlank;
7
+
8
+@Schema(description = "MCP 确认提交草稿请求(v2 §6.3.5)")
9
+public class McpDraftSubmitRequest {
10
+
11
+    @NotBlank(message = "selected_scheme_code 不能为空")
12
+    @JsonProperty("selected_scheme_code")
13
+    @Schema(description = "用户选定的方案编码", example = "SCHEME_A", requiredMode = Schema.RequiredMode.REQUIRED)
14
+    private String selectedSchemeCode;
15
+
16
+    public String getSelectedSchemeCode() {
17
+        return selectedSchemeCode;
18
+    }
19
+
20
+    public void setSelectedSchemeCode(String selectedSchemeCode) {
21
+        this.selectedSchemeCode = selectedSchemeCode;
22
+    }
23
+}

+ 31 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/dto/McpDraftSubmitResponse.java

@@ -0,0 +1,31 @@
1
+package com.huimv.employment.service.draft.dto;
2
+
3
+import com.fasterxml.jackson.annotation.JsonProperty;
4
+import io.swagger.v3.oas.annotations.media.Schema;
5
+
6
+@Schema(description = "MCP 确认提交草稿响应(v2 §6.3.5)")
7
+public class McpDraftSubmitResponse {
8
+
9
+    @Schema(description = "提交结果", example = "success")
10
+    private String status;
11
+
12
+    @JsonProperty("order_id")
13
+    @Schema(description = "生成的正式订单 ID", example = "ord_98765")
14
+    private String orderId;
15
+
16
+    public String getStatus() {
17
+        return status;
18
+    }
19
+
20
+    public void setStatus(String status) {
21
+        this.status = status;
22
+    }
23
+
24
+    public String getOrderId() {
25
+        return orderId;
26
+    }
27
+
28
+    public void setOrderId(String orderId) {
29
+        this.orderId = orderId;
30
+    }
31
+}