Kaynağa Gözat

增加与ai大模型对话

wwh 2 hafta önce
ebeveyn
işleme
47fc91708f

+ 26 - 3
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/DraftController.java

@@ -6,14 +6,18 @@ import com.huimv.employment.common.web.R;
6 6
 import com.huimv.employment.security.LoginUser;
7 7
 import com.huimv.employment.security.LoginUserHolder;
8 8
 import com.huimv.employment.service.draft.DraftService;
9
+import com.huimv.employment.service.draft.dto.DraftConfirmRequest;
10
+import com.huimv.employment.service.draft.dto.DraftConfirmResponse;
9 11
 import com.huimv.employment.service.draft.dto.DraftDetailResponse;
10 12
 import com.huimv.employment.service.draft.dto.DraftUpdateRequest;
13
+import com.huimv.employment.service.draft.dto.McpDraftCalculateResponse;
11 14
 import io.swagger.v3.oas.annotations.Operation;
12 15
 import io.swagger.v3.oas.annotations.security.SecurityRequirement;
13 16
 import io.swagger.v3.oas.annotations.tags.Tag;
14 17
 import org.springframework.validation.annotation.Validated;
15 18
 import org.springframework.web.bind.annotation.GetMapping;
16 19
 import org.springframework.web.bind.annotation.PathVariable;
20
+import org.springframework.web.bind.annotation.PostMapping;
17 21
 import org.springframework.web.bind.annotation.PutMapping;
18 22
 import org.springframework.web.bind.annotation.RequestBody;
19 23
 import org.springframework.web.bind.annotation.RequestMapping;
@@ -22,13 +26,13 @@ import org.springframework.web.bind.annotation.RestController;
22 26
 /**
23 27
  * 用工草稿接口(小程序 JWT 鉴权)。
24 28
  * <p>
25
- * AI 生成草稿后,企业主通过 {@code GET /drafts/{id}} 查看、{@code PUT /drafts/{id}} 编辑补充字段。
26
- * 草稿创建由 AI 内网接口负责,本模块仅提供读写
29
+ * AI 生成草稿后,企业主通过 {@code GET /drafts/{id}} 查看、{@code PUT /drafts/{id}} 编辑
30
+ * missingFields 为空后可 {@code GET .../cost-comparison} 测算,再 {@code POST .../confirm} 确认此方案
27 31
  * </p>
28 32
  */
29 33
 @RestController
30 34
 @RequestMapping("/api/v1/mp/drafts")
31
-@Tag(name = "用工草稿", description = "草稿查看与编辑,需 JWT 鉴权")
35
+@Tag(name = "用工草稿", description = "草稿查看、编辑、成本测算与方案确认,需 JWT 鉴权")
32 36
 @SecurityRequirement(name = "Authorization")
33 37
 public class DraftController {
34 38
 
@@ -57,6 +61,25 @@ public class DraftController {
57 61
         return R.ok(draftService.update(loginUser.getUserId(), draftId, request));
58 62
     }
59 63
 
64
+    @GetMapping("/{id}/cost-comparison")
65
+    @Operation(summary = "成本测算(方案对比)",
66
+            description = "要求草稿 pending 且 missingFields 为空。返回 SCHEME_A / SCHEME_B 等多套合规方案,供「确认此方案」前展示。")
67
+    public R<McpDraftCalculateResponse> costComparison(@PathVariable("id") Long draftId) {
68
+        LoginUser loginUser = requireEnterprise();
69
+        return R.ok(draftService.calculateCostComparison(loginUser.getUserId(), draftId));
70
+    }
71
+
72
+    @PostMapping("/{id}/confirm")
73
+    @Operation(summary = "确认此方案",
74
+            description = "草稿 pending 且 missingFields 为空后调用。传入 selected_scheme_code(如 SCHEME_A):"
75
+                    + "1)生成正式订单;2)将草稿 status 更新为 confirmed(含 confirmed_at/confirmed_by);"
76
+                    + "若尚未测算会自动先测算。")
77
+    public R<DraftConfirmResponse> confirm(@PathVariable("id") Long draftId,
78
+                                           @Validated @RequestBody DraftConfirmRequest request) {
79
+        LoginUser loginUser = requireEnterprise();
80
+        return R.ok(draftService.confirmScheme(loginUser.getUserId(), draftId, request));
81
+    }
82
+
60 83
     private static LoginUser requireEnterprise() {
61 84
         LoginUser loginUser = LoginUserHolder.require();
62 85
         if (!USER_TYPE_ENTERPRISE.equals(loginUser.getUserType())) {

+ 193 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/draft/DraftService.java

@@ -1,18 +1,26 @@
1 1
 package com.huimv.employment.service.draft;
2 2
 
3 3
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
4 5
 import com.fasterxml.jackson.core.type.TypeReference;
5 6
 import com.fasterxml.jackson.databind.ObjectMapper;
6 7
 import com.huimv.employment.common.exception.BizException;
7 8
 import com.huimv.employment.common.exception.ErrorCode;
9
+import com.huimv.employment.dao.entity.FeDraftCostSnapshot;
8 10
 import com.huimv.employment.dao.entity.FeDraftField;
9 11
 import com.huimv.employment.dao.entity.FeEmploymentDraft;
12
+import com.huimv.employment.dao.entity.FeEmploymentOrder;
10 13
 import com.huimv.employment.dao.entity.FeEnterprise;
14
+import com.huimv.employment.dao.mapper.FeDraftCostSnapshotMapper;
11 15
 import com.huimv.employment.dao.mapper.FeDraftFieldMapper;
12 16
 import com.huimv.employment.dao.mapper.FeEmploymentDraftMapper;
17
+import com.huimv.employment.dao.mapper.FeEmploymentOrderMapper;
18
+import com.huimv.employment.service.draft.dto.DraftConfirmRequest;
19
+import com.huimv.employment.service.draft.dto.DraftConfirmResponse;
13 20
 import com.huimv.employment.service.draft.dto.DraftDetailResponse;
14 21
 import com.huimv.employment.service.draft.dto.DraftFieldResponse;
15 22
 import com.huimv.employment.service.draft.dto.DraftUpdateRequest;
23
+import com.huimv.employment.service.draft.dto.McpDraftCalculateResponse;
16 24
 import com.huimv.employment.service.enterprise.EnterpriseService;
17 25
 import org.springframework.dao.DuplicateKeyException;
18 26
 import org.springframework.stereotype.Service;
@@ -28,6 +36,7 @@ import java.util.HashSet;
28 36
 import java.util.List;
29 37
 import java.util.Map;
30 38
 import java.util.Set;
39
+import java.util.UUID;
31 40
 import java.util.stream.Collectors;
32 41
 
33 42
 /**
@@ -37,6 +46,10 @@ import java.util.stream.Collectors;
37 46
 public class DraftService {
38 47
 
39 48
     private static final String STATUS_PENDING = "pending";
49
+    private static final String STATUS_CONFIRMED = "confirmed";
50
+    private static final String ORDER_STATUS_CONFIRMED = "confirmed";
51
+    private static final String STEP_PLAN_CONFIRMED = "plan_confirmed";
52
+    private static final String CONFIRM_SUCCESS = "success";
40 53
     private static final String SOURCE_USER_INPUT = "user_input";
41 54
     private static final String WORK_LOCATION_SOURCE_USER = "user_input";
42 55
 
@@ -44,6 +57,8 @@ public class DraftService {
44 57
             new HashSet<>(Arrays.asList("daily", "piece", "monthly")));
45 58
     private static final Set<String> ALLOWED_INSURANCE_LEVELS = Collections.unmodifiableSet(
46 59
             new HashSet<>(Arrays.asList("basic", "standard", "premium")));
60
+    private static final Set<String> ALLOWED_SCHEME_CODES = Collections.unmodifiableSet(
61
+            new HashSet<>(Arrays.asList(DraftCostCalculationEngine.SCHEME_A, DraftCostCalculationEngine.SCHEME_B)));
47 62
 
48 63
     private static final Map<String, String> FIELD_LABELS;
49 64
 
@@ -62,16 +77,25 @@ public class DraftService {
62 77
 
63 78
     private final FeEmploymentDraftMapper feEmploymentDraftMapper;
64 79
     private final FeDraftFieldMapper feDraftFieldMapper;
80
+    private final FeDraftCostSnapshotMapper feDraftCostSnapshotMapper;
81
+    private final FeEmploymentOrderMapper feEmploymentOrderMapper;
65 82
     private final EnterpriseService enterpriseService;
83
+    private final DraftCostCalculationEngine draftCostCalculationEngine;
66 84
     private final ObjectMapper objectMapper;
67 85
 
68 86
     public DraftService(FeEmploymentDraftMapper feEmploymentDraftMapper,
69 87
                         FeDraftFieldMapper feDraftFieldMapper,
88
+                        FeDraftCostSnapshotMapper feDraftCostSnapshotMapper,
89
+                        FeEmploymentOrderMapper feEmploymentOrderMapper,
70 90
                         EnterpriseService enterpriseService,
91
+                        DraftCostCalculationEngine draftCostCalculationEngine,
71 92
                         ObjectMapper objectMapper) {
72 93
         this.feEmploymentDraftMapper = feEmploymentDraftMapper;
73 94
         this.feDraftFieldMapper = feDraftFieldMapper;
95
+        this.feDraftCostSnapshotMapper = feDraftCostSnapshotMapper;
96
+        this.feEmploymentOrderMapper = feEmploymentOrderMapper;
74 97
         this.enterpriseService = enterpriseService;
98
+        this.draftCostCalculationEngine = draftCostCalculationEngine;
75 99
         this.objectMapper = objectMapper;
76 100
     }
77 101
 
@@ -98,6 +122,175 @@ public class DraftService {
98 122
         return toDetail(feEmploymentDraftMapper.selectById(draftId), listFields(draftId));
99 123
     }
100 124
 
125
+    /**
126
+     * 草稿就绪(pending 且 missingFields 为空)后测算成本方案,供微信端展示「确认此方案」。
127
+     */
128
+    @Transactional(rollbackFor = Exception.class)
129
+    public McpDraftCalculateResponse calculateCostComparison(Long userId, Long draftId) {
130
+        FeEmploymentDraft draft = requireOwnedReady(userId, draftId);
131
+        List<DraftCostCalculationEngine.CalculatedScheme> schemes = draftCostCalculationEngine.calculate(draft);
132
+        persistCostSnapshots(draft, schemes);
133
+        updateDraftEstimates(draft, schemes, LocalDateTime.now());
134
+        return draftCostCalculationEngine.toApiResponse(schemes);
135
+    }
136
+
137
+    /**
138
+     * 微信端确认此方案:选定 SCHEME_A/SCHEME_B,转正式订单。
139
+     * <p>若尚未测算,将自动先测算再确认。</p>
140
+     */
141
+    @Transactional(rollbackFor = Exception.class)
142
+    public DraftConfirmResponse confirmScheme(Long userId, Long draftId, DraftConfirmRequest request) {
143
+        if (request == null || !StringUtils.hasText(request.getSelectedSchemeCode())) {
144
+            throw new BizException(ErrorCode.BAD_REQUEST, "selected_scheme_code 不能为空");
145
+        }
146
+        FeEmploymentDraft draft = requireOwnedReady(userId, draftId);
147
+        String schemeCode = request.getSelectedSchemeCode().trim().toUpperCase();
148
+        if (!ALLOWED_SCHEME_CODES.contains(schemeCode)) {
149
+            throw new BizException(ErrorCode.BAD_REQUEST, "不支持的方案编码:" + request.getSelectedSchemeCode());
150
+        }
151
+
152
+        FeDraftCostSnapshot snapshot = findLatestSnapshot(draft.getId(), schemeCode);
153
+        if (snapshot == null) {
154
+            List<DraftCostCalculationEngine.CalculatedScheme> schemes = draftCostCalculationEngine.calculate(draft);
155
+            persistCostSnapshots(draft, schemes);
156
+            updateDraftEstimates(draft, schemes, LocalDateTime.now());
157
+            snapshot = findLatestSnapshot(draft.getId(), schemeCode);
158
+        }
159
+        if (snapshot == null) {
160
+            throw new BizException(ErrorCode.BAD_REQUEST, "方案测算失败,请稍后重试");
161
+        }
162
+
163
+        LocalDateTime now = LocalDateTime.now();
164
+        // 1) 转正式订单
165
+        FeEmploymentOrder order = buildOrderFromDraft(draft, snapshot, userId, now);
166
+        order.setOrderNo("tmp_" + UUID.randomUUID().toString().replace("-", "").substring(0, 20));
167
+        try {
168
+            feEmploymentOrderMapper.insert(order);
169
+        } catch (DuplicateKeyException ex) {
170
+            throw new BizException(ErrorCode.BAD_REQUEST, "订单编号冲突,请重试");
171
+        }
172
+        order.setOrderNo(generateOrderNo(order.getId()));
173
+        feEmploymentOrderMapper.updateById(order);
174
+
175
+        // 2) 同步将草稿状态更新为 confirmed(与订单同一事务)
176
+        int updated = feEmploymentDraftMapper.update(null, new LambdaUpdateWrapper<FeEmploymentDraft>()
177
+                .eq(FeEmploymentDraft::getId, draft.getId())
178
+                .eq(FeEmploymentDraft::getStatus, STATUS_PENDING)
179
+                .set(FeEmploymentDraft::getStatus, STATUS_CONFIRMED)
180
+                .set(FeEmploymentDraft::getConfirmedAt, now)
181
+                .set(FeEmploymentDraft::getConfirmedBy, userId)
182
+                .set(FeEmploymentDraft::getSelectedPlanId, snapshot.getId())
183
+                .set(FeEmploymentDraft::getEstimatedTotal, snapshot.getEmployerTotalOutflow())
184
+                .set(FeEmploymentDraft::getEstimatedPerCapita, snapshot.getEmployerAvgCostPerWorker())
185
+                .set(FeEmploymentDraft::getUpdateTime, now));
186
+        if (updated != 1) {
187
+            throw new BizException(ErrorCode.DRAFT_NOT_EDITABLE);
188
+        }
189
+
190
+        DraftConfirmResponse response = new DraftConfirmResponse();
191
+        response.setStatus(CONFIRM_SUCCESS);
192
+        response.setDraftStatus(STATUS_CONFIRMED);
193
+        response.setOrderId(order.getOrderNo());
194
+        response.setDraftId(draft.getId());
195
+        response.setSelectedSchemeCode(schemeCode);
196
+        return response;
197
+    }
198
+
199
+    private FeEmploymentDraft requireOwnedReady(Long userId, Long draftId) {
200
+        FeEmploymentDraft draft = requireOwned(userId, draftId);
201
+        assertEditable(draft);
202
+        List<String> missing = parseStringList(draft.getMissingFields());
203
+        if (!missing.isEmpty()) {
204
+            throw new BizException(ErrorCode.BAD_REQUEST,
205
+                    "草稿信息不完整,请先补全字段:" + String.join(", ", missing));
206
+        }
207
+        return draft;
208
+    }
209
+
210
+    private void persistCostSnapshots(FeEmploymentDraft draft,
211
+                                      List<DraftCostCalculationEngine.CalculatedScheme> schemes) {
212
+        feDraftCostSnapshotMapper.delete(new LambdaQueryWrapper<FeDraftCostSnapshot>()
213
+                .eq(FeDraftCostSnapshot::getDraftId, draft.getId()));
214
+        LocalDateTime now = LocalDateTime.now();
215
+        for (DraftCostCalculationEngine.CalculatedScheme scheme : schemes) {
216
+            FeDraftCostSnapshot snapshot = new FeDraftCostSnapshot();
217
+            snapshot.setDraftId(draft.getId());
218
+            snapshot.setPlanCode(scheme.getCode());
219
+            snapshot.setPlanName(scheme.getTitle());
220
+            snapshot.setEmployerTotalOutflow(scheme.getEmployerTotalOutflow());
221
+            snapshot.setEmployerAvgCostPerWorker(scheme.getEmployerAvgCostPerWorker());
222
+            snapshot.setWorkerTakeHomePerWorker(scheme.getWorkerTakeHomePerWorker());
223
+            snapshot.setIsRecommended(scheme.isRecommended());
224
+            snapshot.setComplianceTags(writeJsonArray(scheme.getTags()));
225
+            snapshot.setCalcVersion(draftCostCalculationEngine.calcVersion());
226
+            snapshot.setCreateTime(now);
227
+            feDraftCostSnapshotMapper.insert(snapshot);
228
+        }
229
+    }
230
+
231
+    private void updateDraftEstimates(FeEmploymentDraft draft,
232
+                                      List<DraftCostCalculationEngine.CalculatedScheme> schemes,
233
+                                      LocalDateTime now) {
234
+        DraftCostCalculationEngine.CalculatedScheme recommended = schemes.stream()
235
+                .filter(DraftCostCalculationEngine.CalculatedScheme::isRecommended)
236
+                .findFirst()
237
+                .orElse(schemes.get(0));
238
+        FeEmploymentDraft update = new FeEmploymentDraft();
239
+        update.setId(draft.getId());
240
+        update.setEstimatedTotal(recommended.getEmployerTotalOutflow());
241
+        update.setEstimatedPerCapita(recommended.getEmployerAvgCostPerWorker());
242
+        update.setUpdateTime(now);
243
+        feEmploymentDraftMapper.updateById(update);
244
+    }
245
+
246
+    private FeDraftCostSnapshot findLatestSnapshot(Long draftId, String schemeCode) {
247
+        return feDraftCostSnapshotMapper.selectOne(new LambdaQueryWrapper<FeDraftCostSnapshot>()
248
+                .eq(FeDraftCostSnapshot::getDraftId, draftId)
249
+                .eq(FeDraftCostSnapshot::getPlanCode, schemeCode)
250
+                .orderByDesc(FeDraftCostSnapshot::getCreateTime)
251
+                .last("LIMIT 1"));
252
+    }
253
+
254
+    private FeEmploymentOrder buildOrderFromDraft(FeEmploymentDraft draft,
255
+                                                  FeDraftCostSnapshot snapshot,
256
+                                                  Long userId,
257
+                                                  LocalDateTime now) {
258
+        FeEmploymentOrder order = new FeEmploymentOrder();
259
+        order.setEnterpriseId(draft.getEnterpriseId());
260
+        order.setUserId(draft.getUserId() != null ? draft.getUserId() : userId);
261
+        order.setDraftId(draft.getId());
262
+        order.setSelectedPlanId(snapshot.getId());
263
+        order.setTitle(StringUtils.hasText(draft.getTitle()) ? draft.getTitle() : "用工订单");
264
+        order.setWorkerCount(draft.getWorkerCount());
265
+        order.setWorkDays(draft.getWorkDays());
266
+        order.setWorkType(draft.getWorkType());
267
+        order.setWorkLocation(draft.getWorkLocation());
268
+        order.setWorkStartDate(draft.getWorkStartDate());
269
+        order.setWorkEndDate(draft.getWorkEndDate());
270
+        order.setOrderStatus(ORDER_STATUS_CONFIRMED);
271
+        order.setCurrentStepCode(STEP_PLAN_CONFIRMED);
272
+        order.setRegisteredCount(0);
273
+        order.setPendingCount(draft.getWorkerCount());
274
+        order.setAbnormalCount(0);
275
+        order.setTotalOutflow(snapshot.getEmployerTotalOutflow());
276
+        order.setCreateTime(now);
277
+        order.setUpdateTime(now);
278
+        order.setDelFlag(0);
279
+        return order;
280
+    }
281
+
282
+    private static String generateOrderNo(Long orderId) {
283
+        return String.format("ord_%d", orderId);
284
+    }
285
+
286
+    private String writeJsonArray(List<String> values) {
287
+        try {
288
+            return objectMapper.writeValueAsString(values);
289
+        } catch (Exception ex) {
290
+            return "[]";
291
+        }
292
+    }
293
+
101 294
     private FeEmploymentDraft requireOwned(Long userId, Long draftId) {
102 295
         FeEnterprise enterprise = enterpriseService.findEnterpriseByUserId(userId);
103 296
         if (enterprise == null) {