Sfoglia il codice sorgente

增加与ai大模型对话

wwh 4 giorni fa
parent
commit
6e44f845b3
14 ha cambiato i file con 899 aggiunte e 10 eliminazioni
  1. 77 0
      huimv-employment/fe-api/src/main/java/com/huimv/employment/ApiSignUtil.java
  2. 1 1
      huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/AiProxyController.java
  3. 2 1
      huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/ContractController.java
  4. 54 0
      huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/NotificationController.java
  5. 3 0
      huimv-employment/fe-api/src/main/resources/application.yml
  6. 179 0
      huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/entity/FeNotification.java
  7. 9 0
      huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/mapper/FeNotificationMapper.java
  8. 1 1
      huimv-employment/fe-integration/src/main/java/com/huimv/employment/integration/kb/KbApiProperties.java
  9. 11 4
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/contract/ContractService.java
  10. 226 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/notification/NotificationService.java
  11. 177 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/notification/dto/NotificationItemResponse.java
  12. 89 0
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/notification/dto/NotificationPageResponse.java
  13. 55 1
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/order/EmploymentOrderService.java
  14. 15 2
      huimv-employment/fe-service/src/main/java/com/huimv/employment/service/settlement/SettlementService.java

+ 77 - 0
huimv-employment/fe-api/src/main/java/com/huimv/employment/ApiSignUtil.java

@@ -0,0 +1,77 @@
1
+package com.huimv.employment;
2
+
3
+import java.security.MessageDigest;
4
+import java.util.*;
5
+
6
+public class ApiSignUtil {
7
+
8
+    /**
9
+     * 构建请求签名
10
+     *
11
+     * @param params    请求参数 Map(query 参数,key=参数名,value=参数值)
12
+     * @param timestamp 时间戳(毫秒)
13
+     * @param secret    平台分配的 appPublicKey
14
+     * @return 32位小写MD5签名字符串
15
+     */
16
+    public static String buildSign(Map<String, String> params, long timestamp, String secret) {
17
+        // 1. 参数字典序排列(TreeMap 自动排序)
18
+        TreeMap<String, String> sorted = new TreeMap<>(params);
19
+
20
+        // 2. 拼接签名原串:k1=v1&k2=v2...&timestamp=xxx&key=secret
21
+        StringBuilder sb = new StringBuilder();
22
+        for (Map.Entry<String, String> entry : sorted.entrySet()) {
23
+            if (entry.getValue() != null && !entry.getValue().isEmpty()) {
24
+                sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
25
+            }
26
+        }
27
+        sb.append("timestamp=").append(timestamp);
28
+        sb.append("&key=").append(secret);
29
+
30
+        // 3. MD5 → 32位小写十六进制
31
+        return md5(sb.toString());
32
+    }
33
+
34
+    private static String md5(String input) {
35
+        try {
36
+            MessageDigest md = MessageDigest.getInstance("MD5");
37
+            md.reset();
38
+            md.update(input.getBytes("UTF-8"));
39
+            byte[] digest = md.digest();
40
+            StringBuilder sb = new StringBuilder(digest.length * 2);
41
+            for (byte b : digest) {
42
+                sb.append(String.format("%02x", b & 0xff));
43
+            }
44
+            return sb.toString();
45
+        } catch (Exception e) {
46
+            throw new RuntimeException("MD5 error", e);
47
+        }
48
+    }
49
+
50
+    // ────────── 使用示例 ──────────
51
+
52
+    public static void main(String[] args) {
53
+        String appKey = "AK_eaf449b6fb9f21";
54
+        String secret = "70c1b93ae1df463809ed3b41156e4791";
55
+
56
+        // 示例1:任务大厅列表
57
+        Map<String, String> hallParams = new LinkedHashMap<>();
58
+        hallParams.put("current", "1");
59
+        hallParams.put("size", "108");
60
+//        hallParams.put("occupationCode", "OCC001");
61
+
62
+        long ts = System.currentTimeMillis();
63
+        String sign = buildSign(hallParams, ts, secret);
64
+
65
+        System.out.println("X-App-Key   : " + appKey);
66
+        System.out.println("X-Timestamp : " + ts);
67
+        System.out.println("X-Sign      : " + sign);
68
+
69
+        // 示例2:任务详情(无Query参数)
70
+        Map<String, String> emptyParams = new LinkedHashMap<>();
71
+        long ts2 = System.currentTimeMillis();
72
+        String sign2 = buildSign(emptyParams, ts2, secret);
73
+
74
+        System.out.println("\n[任务详情] X-Timestamp: " + ts2);
75
+        System.out.println("[任务详情] X-Sign     : " + sign2);
76
+    }
77
+}

+ 1 - 1
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/AiProxyController.java

@@ -25,7 +25,7 @@ import javax.servlet.http.HttpServletResponse;
25
 import java.io.IOException;
25
 import java.io.IOException;
26
 
26
 
27
 /**
27
 /**
28
- * 智能体网关代理(小程序 JWT 鉴权;上游暂不需要 api-key)。
28
+ * 智能体网关代理(小程序 JWT 鉴权;上游 Authorization 使用 fe.kb.api-key)。
29
  */
29
  */
30
 @RestController
30
 @RestController
31
 @RequestMapping("/api/v1/mp/ai")
31
 @RequestMapping("/api/v1/mp/ai")

+ 2 - 1
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/ContractController.java

@@ -63,7 +63,8 @@ public class ContractController {
63
     @PostMapping("/{id}/confirm-sign")
63
     @PostMapping("/{id}/confirm-sign")
64
     @Operation(summary = "确认签署完成",
64
     @Operation(summary = "确认签署完成",
65
             description = "临时工确认签署(Mock 闭环)。sign_status→signed,登记 contract_signing→completed。"
65
             description = "临时工确认签署(Mock 闭环)。sign_status→signed,登记 contract_signing→completed。"
66
-                    + "订单不自动开工,须企业调用 POST /orders/{orderId}/start-work。已签署幂等返回。",
66
+                    + "期望用工人数全部签署后订单自动 contract_signing→work_started;"
67
+                    + "未招满须企业 POST /orders/{orderId}/start-work 缺编确认。已签署幂等返回。",
67
             parameters = {
68
             parameters = {
68
                     @Parameter(name = "id", in = ParameterIn.PATH, required = true, description = "fe_contract.id")
69
                     @Parameter(name = "id", in = ParameterIn.PATH, required = true, description = "fe_contract.id")
69
             })
70
             })

+ 54 - 0
huimv-employment/fe-api/src/main/java/com/huimv/employment/controller/mp/NotificationController.java

@@ -0,0 +1,54 @@
1
+package com.huimv.employment.controller.mp;
2
+
3
+import com.huimv.employment.common.web.R;
4
+import com.huimv.employment.security.LoginUserHolder;
5
+import com.huimv.employment.service.notification.NotificationService;
6
+import com.huimv.employment.service.notification.dto.NotificationPageResponse;
7
+import io.swagger.v3.oas.annotations.Operation;
8
+import io.swagger.v3.oas.annotations.Parameter;
9
+import io.swagger.v3.oas.annotations.enums.ParameterIn;
10
+import io.swagger.v3.oas.annotations.security.SecurityRequirement;
11
+import io.swagger.v3.oas.annotations.tags.Tag;
12
+import org.springframework.web.bind.annotation.GetMapping;
13
+import org.springframework.web.bind.annotation.RequestMapping;
14
+import org.springframework.web.bind.annotation.RequestParam;
15
+import org.springframework.web.bind.annotation.RestController;
16
+
17
+/**
18
+ * 消息与待办中心:当前登录用户查询本人通知。
19
+ */
20
+@RestController
21
+@RequestMapping("/api/v1/mp/notifications")
22
+@Tag(name = "消息与待办", description = "查询当前用户消息与待办")
23
+@SecurityRequirement(name = "Authorization")
24
+public class NotificationController {
25
+
26
+    private final NotificationService notificationService;
27
+
28
+    public NotificationController(NotificationService notificationService) {
29
+        this.notificationService = notificationService;
30
+    }
31
+
32
+    @GetMapping
33
+    @Operation(summary = "我的消息与待办",
34
+            description = "按当前 JWT 用户 user_id 分页查询。可选 notify_type / category / read_flag / handled_flag 过滤。"
35
+                    + "返回 unread_count、pending_count 便于角标展示。")
36
+    public R<NotificationPageResponse> listMine(
37
+            @Parameter(name = "notify_type", in = ParameterIn.QUERY,
38
+                    description = "pending / error / info / success")
39
+            @RequestParam(value = "notify_type", required = false) String notifyType,
40
+            @Parameter(name = "category", in = ParameterIn.QUERY,
41
+                    description = "settlement / registration / verify / workflow")
42
+            @RequestParam(value = "category", required = false) String category,
43
+            @Parameter(name = "read_flag", in = ParameterIn.QUERY, description = "是否已读")
44
+            @RequestParam(value = "read_flag", required = false) Boolean readFlag,
45
+            @Parameter(name = "handled_flag", in = ParameterIn.QUERY, description = "待办是否已处理")
46
+            @RequestParam(value = "handled_flag", required = false) Boolean handledFlag,
47
+            @Parameter(name = "page", in = ParameterIn.QUERY, description = "页码,从 1 开始")
48
+            @RequestParam(value = "page", required = false) Integer page,
49
+            @Parameter(name = "size", in = ParameterIn.QUERY, description = "每页条数,默认 20,最大 100")
50
+            @RequestParam(value = "size", required = false) Integer size) {
51
+        Long userId = LoginUserHolder.require().getUserId();
52
+        return R.ok(notificationService.listByUser(userId, notifyType, category, readFlag, handledFlag, page, size));
53
+    }
54
+}

+ 3 - 0
huimv-employment/fe-api/src/main/resources/application.yml

@@ -70,6 +70,9 @@ fe:
70
     scheme: http
70
     scheme: http
71
     host: harrison1.iask.in
71
     host: harrison1.iask.in
72
     port: 9188
72
     port: 9188
73
+    # 上游 /api/console/chat 鉴权:Authorization: Bearer {api-key}
74
+    api-key: eyJzdWIiOiAiYWRtaW4iLCAiZXhwIjogNDkzODQ5NTQxMywgImlhdCI6IDE3ODQ4OTU0MTMsICJqdGkiOiAiYjdiMjQ4M2ZhYmQwOWM0NWEwNzQ1NThhNTJiZjBjYjkifQ==.bbab9a6fa67f5f8ec3e14e6e017b17f0ad193d8eca42b6591175861918598884
75
+    auth-header-name: Authorization
73
     default-model: default
76
     default-model: default
74
     default-agent-id: default
77
     default-agent-id: default
75
     default-knowledge-base: default
78
     default-knowledge-base: default

+ 179 - 0
huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/entity/FeNotification.java

@@ -0,0 +1,179 @@
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.time.LocalDateTime;
8
+
9
+/**
10
+ * 消息与待办中心 {@code fe_notification}。
11
+ * <p>推送结算待确认、实名失败、登记进度等;通过 {@code read_flag}/{@code handled_flag} 区分已读与已办。</p>
12
+ */
13
+@TableName("fe_notification")
14
+public class FeNotification {
15
+
16
+    @TableId(type = IdType.AUTO)
17
+    private Long id;
18
+
19
+    private Long enterpriseId;
20
+
21
+    private Long userId;
22
+
23
+    /** pending / error / info / success */
24
+    private String notifyType;
25
+
26
+    /** settlement / registration / verify / workflow */
27
+    private String category;
28
+
29
+    private String title;
30
+
31
+    private String content;
32
+
33
+    /** settlement / registration / order 等 */
34
+    private String bizType;
35
+
36
+    private Long bizId;
37
+
38
+    private String bizNo;
39
+
40
+    private String actionUrl;
41
+
42
+    private Boolean readFlag;
43
+
44
+    private Boolean handledFlag;
45
+
46
+    private LocalDateTime createTime;
47
+
48
+    private LocalDateTime readTime;
49
+
50
+    private LocalDateTime handledTime;
51
+
52
+    public Long getId() {
53
+        return id;
54
+    }
55
+
56
+    public void setId(Long id) {
57
+        this.id = id;
58
+    }
59
+
60
+    public Long getEnterpriseId() {
61
+        return enterpriseId;
62
+    }
63
+
64
+    public void setEnterpriseId(Long enterpriseId) {
65
+        this.enterpriseId = enterpriseId;
66
+    }
67
+
68
+    public Long getUserId() {
69
+        return userId;
70
+    }
71
+
72
+    public void setUserId(Long userId) {
73
+        this.userId = userId;
74
+    }
75
+
76
+    public String getNotifyType() {
77
+        return notifyType;
78
+    }
79
+
80
+    public void setNotifyType(String notifyType) {
81
+        this.notifyType = notifyType;
82
+    }
83
+
84
+    public String getCategory() {
85
+        return category;
86
+    }
87
+
88
+    public void setCategory(String category) {
89
+        this.category = category;
90
+    }
91
+
92
+    public String getTitle() {
93
+        return title;
94
+    }
95
+
96
+    public void setTitle(String title) {
97
+        this.title = title;
98
+    }
99
+
100
+    public String getContent() {
101
+        return content;
102
+    }
103
+
104
+    public void setContent(String content) {
105
+        this.content = content;
106
+    }
107
+
108
+    public String getBizType() {
109
+        return bizType;
110
+    }
111
+
112
+    public void setBizType(String bizType) {
113
+        this.bizType = bizType;
114
+    }
115
+
116
+    public Long getBizId() {
117
+        return bizId;
118
+    }
119
+
120
+    public void setBizId(Long bizId) {
121
+        this.bizId = bizId;
122
+    }
123
+
124
+    public String getBizNo() {
125
+        return bizNo;
126
+    }
127
+
128
+    public void setBizNo(String bizNo) {
129
+        this.bizNo = bizNo;
130
+    }
131
+
132
+    public String getActionUrl() {
133
+        return actionUrl;
134
+    }
135
+
136
+    public void setActionUrl(String actionUrl) {
137
+        this.actionUrl = actionUrl;
138
+    }
139
+
140
+    public Boolean getReadFlag() {
141
+        return readFlag;
142
+    }
143
+
144
+    public void setReadFlag(Boolean readFlag) {
145
+        this.readFlag = readFlag;
146
+    }
147
+
148
+    public Boolean getHandledFlag() {
149
+        return handledFlag;
150
+    }
151
+
152
+    public void setHandledFlag(Boolean handledFlag) {
153
+        this.handledFlag = handledFlag;
154
+    }
155
+
156
+    public LocalDateTime getCreateTime() {
157
+        return createTime;
158
+    }
159
+
160
+    public void setCreateTime(LocalDateTime createTime) {
161
+        this.createTime = createTime;
162
+    }
163
+
164
+    public LocalDateTime getReadTime() {
165
+        return readTime;
166
+    }
167
+
168
+    public void setReadTime(LocalDateTime readTime) {
169
+        this.readTime = readTime;
170
+    }
171
+
172
+    public LocalDateTime getHandledTime() {
173
+        return handledTime;
174
+    }
175
+
176
+    public void setHandledTime(LocalDateTime handledTime) {
177
+        this.handledTime = handledTime;
178
+    }
179
+}

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

+ 1 - 1
huimv-employment/fe-integration/src/main/java/com/huimv/employment/integration/kb/KbApiProperties.java

@@ -18,7 +18,7 @@ public class KbApiProperties {
18
 
18
 
19
     private int port = 9188;
19
     private int port = 9188;
20
 
20
 
21
-    /** 访问令牌(暂不需要;外网远程访问时再配置 fe.kb.api-key) */
21
+    /** 访问令牌;配置后上游请求带 Authorization: Bearer {apiKey} */
22
     private String apiKey = "";
22
     private String apiKey = "";
23
 
23
 
24
     private String authHeaderName = "Authorization";
24
     private String authHeaderName = "Authorization";

+ 11 - 4
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/contract/ContractService.java

@@ -17,9 +17,11 @@ import com.huimv.employment.dao.mapper.FeWorkerRegistrationMapper;
17
 import com.huimv.employment.service.contract.dto.ContractItemResponse;
17
 import com.huimv.employment.service.contract.dto.ContractItemResponse;
18
 import com.huimv.employment.service.contract.dto.ContractSummaryResponse;
18
 import com.huimv.employment.service.contract.dto.ContractSummaryResponse;
19
 import com.huimv.employment.service.enterprise.EnterpriseService;
19
 import com.huimv.employment.service.enterprise.EnterpriseService;
20
+import com.huimv.employment.service.order.EmploymentOrderService;
20
 import com.huimv.employment.service.worker.WorkerService;
21
 import com.huimv.employment.service.worker.WorkerService;
21
 import org.slf4j.Logger;
22
 import org.slf4j.Logger;
22
 import org.slf4j.LoggerFactory;
23
 import org.slf4j.LoggerFactory;
24
+import org.springframework.context.annotation.Lazy;
23
 import org.springframework.dao.DuplicateKeyException;
25
 import org.springframework.dao.DuplicateKeyException;
24
 import org.springframework.stereotype.Service;
26
 import org.springframework.stereotype.Service;
25
 import org.springframework.transaction.annotation.Transactional;
27
 import org.springframework.transaction.annotation.Transactional;
@@ -62,6 +64,7 @@ public class ContractService {
62
     private final EnterpriseService enterpriseService;
64
     private final EnterpriseService enterpriseService;
63
     private final WorkerService workerService;
65
     private final WorkerService workerService;
64
     private final ESignClient eSignClient;
66
     private final ESignClient eSignClient;
67
+    private final EmploymentOrderService employmentOrderService;
65
 
68
 
66
     public ContractService(FeContractMapper feContractMapper,
69
     public ContractService(FeContractMapper feContractMapper,
67
                            FeEmploymentOrderMapper feEmploymentOrderMapper,
70
                            FeEmploymentOrderMapper feEmploymentOrderMapper,
@@ -70,7 +73,8 @@ public class ContractService {
70
                            FeEnterpriseMapper feEnterpriseMapper,
73
                            FeEnterpriseMapper feEnterpriseMapper,
71
                            EnterpriseService enterpriseService,
74
                            EnterpriseService enterpriseService,
72
                            WorkerService workerService,
75
                            WorkerService workerService,
73
-                           ESignClient eSignClient) {
76
+                           ESignClient eSignClient,
77
+                           @Lazy EmploymentOrderService employmentOrderService) {
74
         this.feContractMapper = feContractMapper;
78
         this.feContractMapper = feContractMapper;
75
         this.feEmploymentOrderMapper = feEmploymentOrderMapper;
79
         this.feEmploymentOrderMapper = feEmploymentOrderMapper;
76
         this.feWorkerRegistrationMapper = feWorkerRegistrationMapper;
80
         this.feWorkerRegistrationMapper = feWorkerRegistrationMapper;
@@ -79,6 +83,7 @@ public class ContractService {
79
         this.enterpriseService = enterpriseService;
83
         this.enterpriseService = enterpriseService;
80
         this.workerService = workerService;
84
         this.workerService = workerService;
81
         this.eSignClient = eSignClient;
85
         this.eSignClient = eSignClient;
86
+        this.employmentOrderService = employmentOrderService;
82
     }
87
     }
83
 
88
 
84
     /**
89
     /**
@@ -238,7 +243,8 @@ public class ContractService {
238
     /**
243
     /**
239
      * 临时工确认签署完成(Mock 电子签闭环;真实电子签可改为回调驱动)。
244
      * 临时工确认签署完成(Mock 电子签闭环;真实电子签可改为回调驱动)。
240
      * <p>将合同置为 {@code signed},登记 {@code contract_signing → completed}。
245
      * <p>将合同置为 {@code signed},登记 {@code contract_signing → completed}。
241
-     * 订单不自动开工,须企业调用确认开工接口。</p>
246
+     * 当订单期望人数全部签署完成后,自动推进订单到 {@code work_started};
247
+     * 未招满(缺编)不自动开工,仍须企业确认。</p>
242
      */
248
      */
243
     @Transactional(rollbackFor = Exception.class)
249
     @Transactional(rollbackFor = Exception.class)
244
     public ContractItemResponse confirmSign(Long userId, Long contractId) {
250
     public ContractItemResponse confirmSign(Long userId, Long contractId) {
@@ -246,6 +252,7 @@ public class ContractService {
246
         FeContract contract = requireOwnedContract(worker.getId(), contractId);
252
         FeContract contract = requireOwnedContract(worker.getId(), contractId);
247
 
253
 
248
         if (SIGN_SIGNED.equals(contract.getSignStatus())) {
254
         if (SIGN_SIGNED.equals(contract.getSignStatus())) {
255
+            employmentOrderService.tryAutoStartWorkWhenFullySigned(contract.getOrderId());
249
             return getMyContract(userId, contractId);
256
             return getMyContract(userId, contractId);
250
         }
257
         }
251
         if (SIGN_VOIDED.equals(contract.getSignStatus())) {
258
         if (SIGN_VOIDED.equals(contract.getSignStatus())) {
@@ -263,8 +270,9 @@ public class ContractService {
263
         update.setUpdateTime(now);
270
         update.setUpdateTime(now);
264
         feContractMapper.updateById(update);
271
         feContractMapper.updateById(update);
265
 
272
 
266
-        // 个人登记推进为 completed;订单不自动开工/结算,须企业另行确认
267
         markRegistrationCompleted(contract.getRegistrationId(), now);
273
         markRegistrationCompleted(contract.getRegistrationId(), now);
274
+        // 期望人数齐签 → 自动进入开始用工
275
+        employmentOrderService.tryAutoStartWorkWhenFullySigned(contract.getOrderId());
268
         log.info("合同签署完成 contractId={} workerId={} registrationId={} orderId={}",
276
         log.info("合同签署完成 contractId={} workerId={} registrationId={} orderId={}",
269
                 contract.getId(), worker.getId(), contract.getRegistrationId(), contract.getOrderId());
277
                 contract.getId(), worker.getId(), contract.getRegistrationId(), contract.getOrderId());
270
         return getMyContract(userId, contractId);
278
         return getMyContract(userId, contractId);
@@ -272,7 +280,6 @@ public class ContractService {
272
 
280
 
273
     /**
281
     /**
274
      * 签署完成后将登记从 {@code contract_signing} 置为 {@code completed}。
282
      * 签署完成后将登记从 {@code contract_signing} 置为 {@code completed}。
275
-     * <p>订单步骤不变:开工见 {@code EmploymentOrderService#startWork},结算见 {@code SettlementService#startSettlement}。</p>
276
      */
283
      */
277
     private void markRegistrationCompleted(Long registrationId, LocalDateTime now) {
284
     private void markRegistrationCompleted(Long registrationId, LocalDateTime now) {
278
         if (registrationId == null) {
285
         if (registrationId == null) {

+ 226 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/notification/NotificationService.java

@@ -0,0 +1,226 @@
1
+package com.huimv.employment.service.notification;
2
+
3
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
4
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
5
+import com.huimv.employment.dao.entity.FeEmploymentOrder;
6
+import com.huimv.employment.dao.entity.FeNotification;
7
+import com.huimv.employment.dao.entity.FeSettlementOrder;
8
+import com.huimv.employment.dao.mapper.FeNotificationMapper;
9
+import com.huimv.employment.service.notification.dto.NotificationItemResponse;
10
+import com.huimv.employment.service.notification.dto.NotificationPageResponse;
11
+import org.slf4j.Logger;
12
+import org.slf4j.LoggerFactory;
13
+import org.springframework.stereotype.Service;
14
+import org.springframework.transaction.annotation.Transactional;
15
+import org.springframework.util.StringUtils;
16
+
17
+import java.math.BigDecimal;
18
+import java.time.LocalDateTime;
19
+import java.util.Collections;
20
+import java.util.List;
21
+import java.util.stream.Collectors;
22
+
23
+/**
24
+ * 消息与待办中心:写入与查询 {@code fe_notification}。
25
+ */
26
+@Service
27
+public class NotificationService {
28
+
29
+    private static final Logger log = LoggerFactory.getLogger(NotificationService.class);
30
+
31
+    private static final String TYPE_PENDING = "pending";
32
+    private static final String CATEGORY_SETTLEMENT = "settlement";
33
+    private static final String BIZ_SETTLEMENT = "settlement";
34
+
35
+    private static final int MIN_PAGE = 1;
36
+    private static final int MIN_SIZE = 1;
37
+    private static final int MAX_SIZE = 100;
38
+    private static final int DEFAULT_SIZE = 20;
39
+
40
+    private final FeNotificationMapper feNotificationMapper;
41
+
42
+    public NotificationService(FeNotificationMapper feNotificationMapper) {
43
+        this.feNotificationMapper = feNotificationMapper;
44
+    }
45
+
46
+    /**
47
+     * 分页查询当前用户消息与待办(按 create_time 倒序)。
48
+     */
49
+    public NotificationPageResponse listByUser(Long userId,
50
+                                               String notifyType,
51
+                                               String category,
52
+                                               Boolean readFlag,
53
+                                               Boolean handledFlag,
54
+                                               Integer page,
55
+                                               Integer size) {
56
+        if (userId == null) {
57
+            NotificationPageResponse empty = new NotificationPageResponse();
58
+            empty.setItems(Collections.emptyList());
59
+            empty.setTotal(0);
60
+            empty.setPage(MIN_PAGE);
61
+            empty.setSize(DEFAULT_SIZE);
62
+            empty.setHasMore(false);
63
+            empty.setUnreadCount(0);
64
+            empty.setPendingCount(0);
65
+            return empty;
66
+        }
67
+
68
+        int resolvedPage = page == null || page < MIN_PAGE ? MIN_PAGE : page;
69
+        int resolvedSize = size == null || size < MIN_SIZE ? DEFAULT_SIZE : Math.min(size, MAX_SIZE);
70
+
71
+        LambdaQueryWrapper<FeNotification> wrapper = new LambdaQueryWrapper<FeNotification>()
72
+                .eq(FeNotification::getUserId, userId);
73
+        if (StringUtils.hasText(notifyType)) {
74
+            wrapper.eq(FeNotification::getNotifyType, notifyType.trim());
75
+        }
76
+        if (StringUtils.hasText(category)) {
77
+            wrapper.eq(FeNotification::getCategory, category.trim());
78
+        }
79
+        if (readFlag != null) {
80
+            wrapper.eq(FeNotification::getReadFlag, readFlag);
81
+        }
82
+        if (handledFlag != null) {
83
+            wrapper.eq(FeNotification::getHandledFlag, handledFlag);
84
+        }
85
+        wrapper.orderByDesc(FeNotification::getCreateTime).orderByDesc(FeNotification::getId);
86
+
87
+        Page<FeNotification> result = feNotificationMapper.selectPage(
88
+                new Page<>(resolvedPage, resolvedSize), wrapper);
89
+
90
+        NotificationPageResponse response = new NotificationPageResponse();
91
+        response.setItems(result.getRecords().stream().map(this::toItem).collect(Collectors.toList()));
92
+        response.setTotal(result.getTotal());
93
+        response.setPage(resolvedPage);
94
+        response.setSize(resolvedSize);
95
+        response.setHasMore((long) resolvedPage * resolvedSize < result.getTotal());
96
+        response.setUnreadCount(countUnread(userId));
97
+        response.setPendingCount(countPendingUnhandled(userId));
98
+        return response;
99
+    }
100
+
101
+    private long countUnread(Long userId) {
102
+        Long count = feNotificationMapper.selectCount(new LambdaQueryWrapper<FeNotification>()
103
+                .eq(FeNotification::getUserId, userId)
104
+                .eq(FeNotification::getReadFlag, Boolean.FALSE));
105
+        return count == null ? 0L : count;
106
+    }
107
+
108
+    private long countPendingUnhandled(Long userId) {
109
+        Long count = feNotificationMapper.selectCount(new LambdaQueryWrapper<FeNotification>()
110
+                .eq(FeNotification::getUserId, userId)
111
+                .eq(FeNotification::getNotifyType, TYPE_PENDING)
112
+                .eq(FeNotification::getHandledFlag, Boolean.FALSE));
113
+        return count == null ? 0L : count;
114
+    }
115
+
116
+    private NotificationItemResponse toItem(FeNotification n) {
117
+        NotificationItemResponse item = new NotificationItemResponse();
118
+        item.setId(n.getId());
119
+        item.setEnterpriseId(n.getEnterpriseId());
120
+        item.setNotifyType(n.getNotifyType());
121
+        item.setCategory(n.getCategory());
122
+        item.setTitle(n.getTitle());
123
+        item.setContent(n.getContent());
124
+        item.setBizType(n.getBizType());
125
+        item.setBizId(n.getBizId());
126
+        item.setBizNo(n.getBizNo());
127
+        item.setActionUrl(n.getActionUrl());
128
+        item.setReadFlag(n.getReadFlag());
129
+        item.setHandledFlag(n.getHandledFlag());
130
+        item.setCreateTime(n.getCreateTime());
131
+        item.setReadTime(n.getReadTime());
132
+        item.setHandledTime(n.getHandledTime());
133
+        return item;
134
+    }
135
+
136
+    /**
137
+     * 发起结算后推送「结算待确认」待办(幂等:同一结算单未办结待办已存在则不重复写)。
138
+     *
139
+     * @param recipientUserId 接收人;优先订单下单人,缺省为发起结算的企业用户
140
+     */
141
+    @Transactional(rollbackFor = Exception.class)
142
+    public void pushSettlementPendingConfirm(FeEmploymentOrder order,
143
+                                             FeSettlementOrder settlement,
144
+                                             Long recipientUserId) {
145
+        if (order == null || settlement == null || settlement.getId() == null) {
146
+            return;
147
+        }
148
+        if (findUnhandledSettlementTodo(settlement.getId()) != null) {
149
+            log.info("结算待确认待办已存在,跳过重复推送 settlementId={}", settlement.getId());
150
+            return;
151
+        }
152
+
153
+        Long userId = recipientUserId;
154
+        if (userId == null && order.getUserId() != null) {
155
+            userId = order.getUserId();
156
+        }
157
+
158
+        String orderTitle = StringUtils.hasText(order.getTitle()) ? order.getTitle().trim() : "用工订单";
159
+        String settlementNo = settlement.getSettlementNo() != null ? settlement.getSettlementNo() : "";
160
+        int workerCount = settlement.getWorkerCount() != null ? settlement.getWorkerCount() : 0;
161
+        BigDecimal total = settlement.getTotalOutflow() != null ? settlement.getTotalOutflow() : BigDecimal.ZERO;
162
+
163
+        FeNotification n = new FeNotification();
164
+        n.setEnterpriseId(order.getEnterpriseId());
165
+        n.setUserId(userId);
166
+        n.setNotifyType(TYPE_PENDING);
167
+        n.setCategory(CATEGORY_SETTLEMENT);
168
+        n.setTitle("结算待确认");
169
+        n.setContent(String.format("%s(%s)已生成结算单,共 %d 人,应付 ¥%s,请确认并支付。",
170
+                orderTitle,
171
+                StringUtils.hasText(order.getOrderNo()) ? order.getOrderNo() : ("#" + order.getId()),
172
+                workerCount,
173
+                total.toPlainString()));
174
+        n.setBizType(BIZ_SETTLEMENT);
175
+        n.setBizId(settlement.getId());
176
+        n.setBizNo(settlementNo);
177
+        n.setActionUrl("/packageA/home/index?open=settlement&order_id=" + order.getId()
178
+                + "&settlement_id=" + settlement.getId());
179
+        n.setReadFlag(Boolean.FALSE);
180
+        n.setHandledFlag(Boolean.FALSE);
181
+        n.setCreateTime(LocalDateTime.now());
182
+        feNotificationMapper.insert(n);
183
+        log.info("已推送结算待确认待办 notificationId={} settlementId={} userId={} enterpriseId={}",
184
+                n.getId(), settlement.getId(), userId, order.getEnterpriseId());
185
+    }
186
+
187
+    /**
188
+     * 结算确认支付后,将对应「结算待确认」待办标记为已处理。
189
+     */
190
+    @Transactional(rollbackFor = Exception.class)
191
+    public void markSettlementTodoHandled(Long settlementId) {
192
+        if (settlementId == null) {
193
+            return;
194
+        }
195
+        List<FeNotification> list = feNotificationMapper.selectList(
196
+                new LambdaQueryWrapper<FeNotification>()
197
+                        .eq(FeNotification::getBizType, BIZ_SETTLEMENT)
198
+                        .eq(FeNotification::getBizId, settlementId)
199
+                        .eq(FeNotification::getHandledFlag, Boolean.FALSE));
200
+        if (list == null || list.isEmpty()) {
201
+            return;
202
+        }
203
+        LocalDateTime now = LocalDateTime.now();
204
+        for (FeNotification n : list) {
205
+            FeNotification update = new FeNotification();
206
+            update.setId(n.getId());
207
+            update.setHandledFlag(Boolean.TRUE);
208
+            update.setHandledTime(now);
209
+            if (!Boolean.TRUE.equals(n.getReadFlag())) {
210
+                update.setReadFlag(Boolean.TRUE);
211
+                update.setReadTime(now);
212
+            }
213
+            feNotificationMapper.updateById(update);
214
+        }
215
+        log.info("结算待办已标记处理 settlementId={} count={}", settlementId, list.size());
216
+    }
217
+
218
+    private FeNotification findUnhandledSettlementTodo(Long settlementId) {
219
+        return feNotificationMapper.selectOne(new LambdaQueryWrapper<FeNotification>()
220
+                .eq(FeNotification::getBizType, BIZ_SETTLEMENT)
221
+                .eq(FeNotification::getBizId, settlementId)
222
+                .eq(FeNotification::getHandledFlag, Boolean.FALSE)
223
+                .orderByDesc(FeNotification::getId)
224
+                .last("LIMIT 1"));
225
+    }
226
+}

+ 177 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/notification/dto/NotificationItemResponse.java

@@ -0,0 +1,177 @@
1
+package com.huimv.employment.service.notification.dto;
2
+
3
+import com.fasterxml.jackson.annotation.JsonProperty;
4
+import io.swagger.v3.oas.annotations.media.Schema;
5
+
6
+import java.time.LocalDateTime;
7
+
8
+@Schema(description = "消息与待办列表项")
9
+public class NotificationItemResponse {
10
+
11
+    @JsonProperty("id")
12
+    private Long id;
13
+
14
+    @JsonProperty("enterprise_id")
15
+    private Long enterpriseId;
16
+
17
+    @JsonProperty("notify_type")
18
+    @Schema(description = "pending / error / info / success", example = "pending")
19
+    private String notifyType;
20
+
21
+    @JsonProperty("category")
22
+    @Schema(description = "settlement / registration / verify / workflow", example = "settlement")
23
+    private String category;
24
+
25
+    @JsonProperty("title")
26
+    private String title;
27
+
28
+    @JsonProperty("content")
29
+    private String content;
30
+
31
+    @JsonProperty("biz_type")
32
+    private String bizType;
33
+
34
+    @JsonProperty("biz_id")
35
+    private Long bizId;
36
+
37
+    @JsonProperty("biz_no")
38
+    private String bizNo;
39
+
40
+    @JsonProperty("action_url")
41
+    private String actionUrl;
42
+
43
+    @JsonProperty("read_flag")
44
+    private Boolean readFlag;
45
+
46
+    @JsonProperty("handled_flag")
47
+    private Boolean handledFlag;
48
+
49
+    @JsonProperty("create_time")
50
+    private LocalDateTime createTime;
51
+
52
+    @JsonProperty("read_time")
53
+    private LocalDateTime readTime;
54
+
55
+    @JsonProperty("handled_time")
56
+    private LocalDateTime handledTime;
57
+
58
+    public Long getId() {
59
+        return id;
60
+    }
61
+
62
+    public void setId(Long id) {
63
+        this.id = id;
64
+    }
65
+
66
+    public Long getEnterpriseId() {
67
+        return enterpriseId;
68
+    }
69
+
70
+    public void setEnterpriseId(Long enterpriseId) {
71
+        this.enterpriseId = enterpriseId;
72
+    }
73
+
74
+    public String getNotifyType() {
75
+        return notifyType;
76
+    }
77
+
78
+    public void setNotifyType(String notifyType) {
79
+        this.notifyType = notifyType;
80
+    }
81
+
82
+    public String getCategory() {
83
+        return category;
84
+    }
85
+
86
+    public void setCategory(String category) {
87
+        this.category = category;
88
+    }
89
+
90
+    public String getTitle() {
91
+        return title;
92
+    }
93
+
94
+    public void setTitle(String title) {
95
+        this.title = title;
96
+    }
97
+
98
+    public String getContent() {
99
+        return content;
100
+    }
101
+
102
+    public void setContent(String content) {
103
+        this.content = content;
104
+    }
105
+
106
+    public String getBizType() {
107
+        return bizType;
108
+    }
109
+
110
+    public void setBizType(String bizType) {
111
+        this.bizType = bizType;
112
+    }
113
+
114
+    public Long getBizId() {
115
+        return bizId;
116
+    }
117
+
118
+    public void setBizId(Long bizId) {
119
+        this.bizId = bizId;
120
+    }
121
+
122
+    public String getBizNo() {
123
+        return bizNo;
124
+    }
125
+
126
+    public void setBizNo(String bizNo) {
127
+        this.bizNo = bizNo;
128
+    }
129
+
130
+    public String getActionUrl() {
131
+        return actionUrl;
132
+    }
133
+
134
+    public void setActionUrl(String actionUrl) {
135
+        this.actionUrl = actionUrl;
136
+    }
137
+
138
+    public Boolean getReadFlag() {
139
+        return readFlag;
140
+    }
141
+
142
+    public void setReadFlag(Boolean readFlag) {
143
+        this.readFlag = readFlag;
144
+    }
145
+
146
+    public Boolean getHandledFlag() {
147
+        return handledFlag;
148
+    }
149
+
150
+    public void setHandledFlag(Boolean handledFlag) {
151
+        this.handledFlag = handledFlag;
152
+    }
153
+
154
+    public LocalDateTime getCreateTime() {
155
+        return createTime;
156
+    }
157
+
158
+    public void setCreateTime(LocalDateTime createTime) {
159
+        this.createTime = createTime;
160
+    }
161
+
162
+    public LocalDateTime getReadTime() {
163
+        return readTime;
164
+    }
165
+
166
+    public void setReadTime(LocalDateTime readTime) {
167
+        this.readTime = readTime;
168
+    }
169
+
170
+    public LocalDateTime getHandledTime() {
171
+        return handledTime;
172
+    }
173
+
174
+    public void setHandledTime(LocalDateTime handledTime) {
175
+        this.handledTime = handledTime;
176
+    }
177
+}

+ 89 - 0
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/notification/dto/NotificationPageResponse.java

@@ -0,0 +1,89 @@
1
+package com.huimv.employment.service.notification.dto;
2
+
3
+import com.fasterxml.jackson.annotation.JsonProperty;
4
+import io.swagger.v3.oas.annotations.media.Schema;
5
+
6
+import java.util.List;
7
+
8
+@Schema(description = "消息与待办分页结果")
9
+public class NotificationPageResponse {
10
+
11
+    @JsonProperty("items")
12
+    private List<NotificationItemResponse> items;
13
+
14
+    @JsonProperty("total")
15
+    private long total;
16
+
17
+    @JsonProperty("page")
18
+    private int page;
19
+
20
+    @JsonProperty("size")
21
+    private int size;
22
+
23
+    @JsonProperty("has_more")
24
+    private boolean hasMore;
25
+
26
+    @JsonProperty("unread_count")
27
+    @Schema(description = "当前用户未读总数(不受本页筛选影响)")
28
+    private long unreadCount;
29
+
30
+    @JsonProperty("pending_count")
31
+    @Schema(description = "当前用户未办结待办数(notify_type=pending 且 handled_flag=false)")
32
+    private long pendingCount;
33
+
34
+    public List<NotificationItemResponse> getItems() {
35
+        return items;
36
+    }
37
+
38
+    public void setItems(List<NotificationItemResponse> items) {
39
+        this.items = items;
40
+    }
41
+
42
+    public long getTotal() {
43
+        return total;
44
+    }
45
+
46
+    public void setTotal(long total) {
47
+        this.total = total;
48
+    }
49
+
50
+    public int getPage() {
51
+        return page;
52
+    }
53
+
54
+    public void setPage(int page) {
55
+        this.page = page;
56
+    }
57
+
58
+    public int getSize() {
59
+        return size;
60
+    }
61
+
62
+    public void setSize(int size) {
63
+        this.size = size;
64
+    }
65
+
66
+    public boolean isHasMore() {
67
+        return hasMore;
68
+    }
69
+
70
+    public void setHasMore(boolean hasMore) {
71
+        this.hasMore = hasMore;
72
+    }
73
+
74
+    public long getUnreadCount() {
75
+        return unreadCount;
76
+    }
77
+
78
+    public void setUnreadCount(long unreadCount) {
79
+        this.unreadCount = unreadCount;
80
+    }
81
+
82
+    public long getPendingCount() {
83
+        return pendingCount;
84
+    }
85
+
86
+    public void setPendingCount(long pendingCount) {
87
+        this.pendingCount = pendingCount;
88
+    }
89
+}

+ 55 - 1
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/order/EmploymentOrderService.java

@@ -17,6 +17,8 @@ import com.huimv.employment.service.enterprise.EnterpriseService;
17
 import com.huimv.employment.service.order.dto.EmploymentOrderItemResponse;
17
 import com.huimv.employment.service.order.dto.EmploymentOrderItemResponse;
18
 import com.huimv.employment.service.order.dto.OrderStartWorkRequest;
18
 import com.huimv.employment.service.order.dto.OrderStartWorkRequest;
19
 import com.huimv.employment.service.order.dto.OrderStartWorkResponse;
19
 import com.huimv.employment.service.order.dto.OrderStartWorkResponse;
20
+import org.slf4j.Logger;
21
+import org.slf4j.LoggerFactory;
20
 import org.springframework.stereotype.Service;
22
 import org.springframework.stereotype.Service;
21
 import org.springframework.transaction.annotation.Transactional;
23
 import org.springframework.transaction.annotation.Transactional;
22
 import org.springframework.util.StringUtils;
24
 import org.springframework.util.StringUtils;
@@ -30,12 +32,15 @@ import java.util.List;
30
  * 企业端用工订单:会话下订单列表、开工就绪查询与确认开工。
32
  * 企业端用工订单:会话下订单列表、开工就绪查询与确认开工。
31
  * <p>
33
  * <p>
32
  * 确认开工规则:已过审工人须全部签完合同;已签人数 &lt; 计划人数时须显式缺编确认;
34
  * 确认开工规则:已过审工人须全部签完合同;已签人数 &lt; 计划人数时须显式缺编确认;
33
- * 成功后订单 {@code contract_signing → work_started}。齐签不自动开工。
35
+ * 成功后订单 {@code contract_signing → work_started}。
36
+ * 期望人数全部签署完成后会自动进入开始用工;缺编须企业手动确认。
34
  * </p>
37
  * </p>
35
  */
38
  */
36
 @Service
39
 @Service
37
 public class EmploymentOrderService {
40
 public class EmploymentOrderService {
38
 
41
 
42
+    private static final Logger log = LoggerFactory.getLogger(EmploymentOrderService.class);
43
+
39
     /** 订单步骤:合同签署中(可确认开工的前置步骤) */
44
     /** 订单步骤:合同签署中(可确认开工的前置步骤) */
40
     private static final String STEP_CONTRACT_SIGNING = "contract_signing";
45
     private static final String STEP_CONTRACT_SIGNING = "contract_signing";
41
     /** 订单步骤:开始用工 */
46
     /** 订单步骤:开始用工 */
@@ -175,6 +180,55 @@ public class EmploymentOrderService {
175
         return result;
180
         return result;
176
     }
181
     }
177
 
182
 
183
+    /**
184
+     * 合同签署后尝试自动进入「开始用工」。
185
+     * <p>条件:订单处于 {@code contract_signing};已过审工人全部已签;
186
+     * 已签人数 ≥ 计划人数(招满)。缺编不自动推进,仍须企业 {@link #startWork}。</p>
187
+     *
188
+     * @return true 表示本次已自动推进到 {@code work_started}
189
+     */
190
+    @Transactional(rollbackFor = Exception.class)
191
+    public boolean tryAutoStartWorkWhenFullySigned(Long orderId) {
192
+        if (orderId == null) {
193
+            return false;
194
+        }
195
+        FeEmploymentOrder order = feEmploymentOrderMapper.selectById(orderId);
196
+        if (order == null) {
197
+            return false;
198
+        }
199
+        if (STEP_WORK_STARTED.equals(order.getCurrentStepCode())
200
+                || isStepAfter(order.getCurrentStepCode(), STEP_WORK_STARTED)) {
201
+            return false;
202
+        }
203
+        if (!STEP_CONTRACT_SIGNING.equals(order.getCurrentStepCode())) {
204
+            return false;
205
+        }
206
+
207
+        OrderStartWorkResponse status = buildStartWorkStatus(order, true);
208
+        if (status.getApprovedCount() == null || status.getApprovedCount() <= 0) {
209
+            return false;
210
+        }
211
+        if (!Boolean.TRUE.equals(status.getAllApprovedSigned())) {
212
+            return false;
213
+        }
214
+        // 未招满:不自动开工,留给企业缺编确认
215
+        if (Boolean.TRUE.equals(status.getUnderstaffed())) {
216
+            log.info("已齐签但未达期望人数,不自动开工 orderId={} signed={}/{}",
217
+                    orderId, status.getSignedCount(), status.getPlannedCount());
218
+            return false;
219
+        }
220
+
221
+        LocalDateTime now = LocalDateTime.now();
222
+        FeEmploymentOrder update = new FeEmploymentOrder();
223
+        update.setId(order.getId());
224
+        update.setCurrentStepCode(STEP_WORK_STARTED);
225
+        update.setUpdateTime(now);
226
+        feEmploymentOrderMapper.updateById(update);
227
+        log.info("期望用工人员均已签署,自动进入开始用工 orderId={} signed={}/{}",
228
+                orderId, status.getSignedCount(), status.getPlannedCount());
229
+        return true;
230
+    }
231
+
178
     /** 校验企业身份且订单归属当前企业。 */
232
     /** 校验企业身份且订单归属当前企业。 */
179
     private FeEmploymentOrder requireOwnedOrder(Long userId, Long orderId) {
233
     private FeEmploymentOrder requireOwnedOrder(Long userId, Long orderId) {
180
         enterpriseService.requireEnterpriseUser(userId);
234
         enterpriseService.requireEnterpriseUser(userId);

+ 15 - 2
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/settlement/SettlementService.java

@@ -23,6 +23,7 @@ import com.huimv.employment.dao.mapper.FeSettlementItemMapper;
23
 import com.huimv.employment.dao.mapper.FeSettlementOrderMapper;
23
 import com.huimv.employment.dao.mapper.FeSettlementOrderMapper;
24
 import com.huimv.employment.dao.mapper.FeWorkerRegistrationMapper;
24
 import com.huimv.employment.dao.mapper.FeWorkerRegistrationMapper;
25
 import com.huimv.employment.service.enterprise.EnterpriseService;
25
 import com.huimv.employment.service.enterprise.EnterpriseService;
26
+import com.huimv.employment.service.notification.NotificationService;
26
 import com.huimv.employment.service.order.dto.OrderCompleteRequest;
27
 import com.huimv.employment.service.order.dto.OrderCompleteRequest;
27
 import com.huimv.employment.service.order.dto.OrderCompleteResponse;
28
 import com.huimv.employment.service.order.dto.OrderCompleteResponse;
28
 import com.huimv.employment.service.settlement.dto.OrderStartSettlementRequest;
29
 import com.huimv.employment.service.settlement.dto.OrderStartSettlementRequest;
@@ -104,6 +105,7 @@ public class SettlementService {
104
     private final FeDraftCostSnapshotMapper feDraftCostSnapshotMapper;
105
     private final FeDraftCostSnapshotMapper feDraftCostSnapshotMapper;
105
     private final FeServicePlanMapper feServicePlanMapper;
106
     private final FeServicePlanMapper feServicePlanMapper;
106
     private final EnterpriseService enterpriseService;
107
     private final EnterpriseService enterpriseService;
108
+    private final NotificationService notificationService;
107
 
109
 
108
     public SettlementService(FeSettlementOrderMapper feSettlementOrderMapper,
110
     public SettlementService(FeSettlementOrderMapper feSettlementOrderMapper,
109
                              FeSettlementItemMapper feSettlementItemMapper,
111
                              FeSettlementItemMapper feSettlementItemMapper,
@@ -114,7 +116,8 @@ public class SettlementService {
114
                              FeWorkerRegistrationMapper feWorkerRegistrationMapper,
116
                              FeWorkerRegistrationMapper feWorkerRegistrationMapper,
115
                              FeDraftCostSnapshotMapper feDraftCostSnapshotMapper,
117
                              FeDraftCostSnapshotMapper feDraftCostSnapshotMapper,
116
                              FeServicePlanMapper feServicePlanMapper,
118
                              FeServicePlanMapper feServicePlanMapper,
117
-                             EnterpriseService enterpriseService) {
119
+                             EnterpriseService enterpriseService,
120
+                             NotificationService notificationService) {
118
         this.feSettlementOrderMapper = feSettlementOrderMapper;
121
         this.feSettlementOrderMapper = feSettlementOrderMapper;
119
         this.feSettlementItemMapper = feSettlementItemMapper;
122
         this.feSettlementItemMapper = feSettlementItemMapper;
120
         this.feSettlementFeeMapper = feSettlementFeeMapper;
123
         this.feSettlementFeeMapper = feSettlementFeeMapper;
@@ -125,6 +128,7 @@ public class SettlementService {
125
         this.feDraftCostSnapshotMapper = feDraftCostSnapshotMapper;
128
         this.feDraftCostSnapshotMapper = feDraftCostSnapshotMapper;
126
         this.feServicePlanMapper = feServicePlanMapper;
129
         this.feServicePlanMapper = feServicePlanMapper;
127
         this.enterpriseService = enterpriseService;
130
         this.enterpriseService = enterpriseService;
131
+        this.notificationService = notificationService;
128
     }
132
     }
129
 
133
 
130
     /**
134
     /**
@@ -151,10 +155,12 @@ public class SettlementService {
151
     public SettlementSummaryResponse startSettlement(Long userId, Long orderId, OrderStartSettlementRequest request) {
155
     public SettlementSummaryResponse startSettlement(Long userId, Long orderId, OrderStartSettlementRequest request) {
152
         FeEmploymentOrder order = requireOwnedOrder(userId, orderId);
156
         FeEmploymentOrder order = requireOwnedOrder(userId, orderId);
153
 
157
 
154
-        // 幂等:重复发起不新建结算单
158
+        // 幂等:重复发起不新建结算单;仍确保有「结算待确认」待办
155
         FeSettlementOrder existing = findActiveByOrderId(orderId);
159
         FeSettlementOrder existing = findActiveByOrderId(orderId);
156
         if (existing != null) {
160
         if (existing != null) {
157
             ensureOrderAtSettlementPending(order, LocalDateTime.now());
161
             ensureOrderAtSettlementPending(order, LocalDateTime.now());
162
+            Long recipient = order.getUserId() != null ? order.getUserId() : userId;
163
+            notificationService.pushSettlementPendingConfirm(order, existing, recipient);
158
             return toSummary(order, existing, false, "结算单已存在");
164
             return toSummary(order, existing, false, "结算单已存在");
159
         }
165
         }
160
 
166
 
@@ -246,6 +252,10 @@ public class SettlementService {
246
         order.setCurrentStepCode(STEP_SETTLEMENT_PENDING);
252
         order.setCurrentStepCode(STEP_SETTLEMENT_PENDING);
247
         order.setOrderStatus(ORDER_STATUS_SETTLING);
253
         order.setOrderStatus(ORDER_STATUS_SETTLING);
248
 
254
 
255
+        // 5) 消息与待办中心:结算待确认
256
+        Long recipient = order.getUserId() != null ? order.getUserId() : userId;
257
+        notificationService.pushSettlementPendingConfirm(order, settlement, recipient);
258
+
249
         log.info("发起结算成功 orderId={} settlementId={} settlementNo={} workers={}",
259
         log.info("发起结算成功 orderId={} settlementId={} settlementNo={} workers={}",
250
                 orderId, settlement.getId(), settlement.getSettlementNo(), workers.size());
260
                 orderId, settlement.getId(), settlement.getSettlementNo(), workers.size());
251
 
261
 
@@ -358,6 +368,9 @@ public class SettlementService {
358
         order.setCurrentStepCode(STEP_SETTLEMENT_PAID);
368
         order.setCurrentStepCode(STEP_SETTLEMENT_PAID);
359
         order.setOrderStatus(ORDER_STATUS_PAID);
369
         order.setOrderStatus(ORDER_STATUS_PAID);
360
 
370
 
371
+        // 5) 关闭「结算待确认」待办
372
+        notificationService.markSettlementTodoHandled(settlement.getId());
373
+
361
         log.info("结算确认支付成功 orderId={} settlementId={} paymentNo={} channel={} amount={}",
374
         log.info("结算确认支付成功 orderId={} settlementId={} paymentNo={} channel={} amount={}",
362
                 orderId, settlement.getId(), payment.getPaymentNo(), channel, payAmount);
375
                 orderId, settlement.getId(), payment.getPaymentNo(), channel, payAmount);
363
         return toSummary(order, settlement, false, "结算已确认并支付成功");
376
         return toSummary(order, settlement, false, "结算已确认并支付成功");