Browse Source

增加与ai大模型对话

wwh 3 weeks ago
parent
commit
01c9075deb

+ 4 - 3
doc/灵活用工-mysql.sql

@@ -44,7 +44,7 @@ CREATE TABLE `fe_user` (
44 44
 DROP TABLE IF EXISTS `fe_enterprise`;
45 45
 CREATE TABLE `fe_enterprise` (
46 46
   `id` bigint NOT NULL AUTO_INCREMENT COMMENT '表主键,BIGSERIAL 自增。企业入驻或管理员创建企业时生成。示例:2001。非空;被草稿、订单、会话等多表外键引用。',
47
-  `enterprise_code` VARCHAR(32)     NOT NULL COMMENT '企业业务编码,系统内唯一标识,便于对外 API 与日志引用。企业创建时由编号生成器写入。示例:ent_001。非空;唯一索引 uk_fe_enterprise_code(del_flag=0)。',
47
+  `enterprise_code` VARCHAR(32)     NULL COMMENT '企业业务编码,档案创建后写入,平台内唯一。创建中可为空;唯一索引 uk_fe_enterprise_code(del_flag=0)。',
48 48
   `name` VARCHAR(200)    NOT NULL COMMENT '企业全称,工商注册名称。企业入驻表单或 OCR 识别营业执照时写入。示例:上海某某物流有限公司。非空;用于合同抬头与结算展示。',
49 49
   `credit_code` VARCHAR(32) COMMENT '统一社会信用代码,企业实名认证核心字段。企业提交认证材料时写入,审核通过后不变更。示例:91310000MA1K3XXXXX。可空;认证前可为空;非敏感但需校验格式。',
50 50
   `legal_person` VARCHAR(64) COMMENT '法定代表人姓名。认证材料解析或人工填写时写入。示例:王某某。可空;用于合同与开票信息核对。',
@@ -62,7 +62,7 @@ CREATE TABLE `fe_enterprise` (
62 62
   `del_flag` tinyint        NOT NULL DEFAULT 0 COMMENT '逻辑删除标记。企业注销或管理员删除时置 1。枚举:0=正常;1=已删除。非空,默认 0;软删后释放 enterprise_code 唯一索引。',
63 63
   PRIMARY KEY (`id`),
64 64
   UNIQUE KEY `uk_fe_enterprise_code` (`enterprise_code`, `del_flag`),
65
-  KEY `idx_fe_enterprise_credit` (`credit_code`)
65
+  UNIQUE KEY `uk_fe_enterprise_credit_code` (`credit_code`, `del_flag`)
66 66
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='企业主体表,灵活用工业务归属主体,承载企业认证信息与扩展画像。与 fe_enterprise_user(多管理员)、fe_employment_draft、fe_employment_order 等为一对多关系。注册地址 registered_address 供 AI 草稿默认推断工作地点。';
67 67
 
68 68
 DROP TABLE IF EXISTS `fe_enterprise_user`;
@@ -459,7 +459,8 @@ CREATE TABLE `fe_worker` (
459 459
   `update_time` datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '档案信息或核验状态变更时更新。非空。',
460 460
   `del_flag` tinyint        NOT NULL DEFAULT 0 COMMENT '逻辑删除标记。枚举:0=正常;1=已删除。非空,默认 0。',
461 461
   PRIMARY KEY (`id`),
462
-  KEY `idx_fe_worker_user` (`user_id`),
462
+  UNIQUE KEY `uk_fe_worker_worker_no` (`worker_no`, `del_flag`),
463
+  UNIQUE KEY `uk_fe_worker_user_id` (`user_id`, `del_flag`),
463 464
   KEY `idx_fe_worker_mobile` (`mobile_mask`),
464 465
   CONSTRAINT `fk_fe_worker_user_id` FOREIGN KEY (`user_id`) REFERENCES `fe_user` (`id`)
465 466
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='临时工自然人档案主表,跨登记批次复用,避免重复录入身份证与银行卡。身份证、银行卡、手机号均加密存储(*_enc),列表仅展示脱敏值(*_mask);可关联 fe_user 实现微信绑定。';

+ 5 - 4
doc/灵活用工.sql

@@ -66,7 +66,7 @@ CREATE INDEX idx_fe_user_type ON fe_user (user_type, status);
66 66
 -- 企业主体
67 67
 CREATE TABLE fe_enterprise (
68 68
     id                  BIGSERIAL       PRIMARY KEY,
69
-    enterprise_code     VARCHAR(32)     NOT NULL,                   -- 业务编码 ent_001
69
+    enterprise_code     VARCHAR(32),                                   -- 业务编码,创建后写入
70 70
     name                VARCHAR(200)    NOT NULL,
71 71
     credit_code         VARCHAR(32),                                 -- 统一社会信用代码
72 72
     legal_person        VARCHAR(64),
@@ -82,8 +82,8 @@ CREATE TABLE fe_enterprise (
82 82
     update_time         TIMESTAMPTZ     NOT NULL DEFAULT NOW(),
83 83
     del_flag            SMALLINT        NOT NULL DEFAULT 0
84 84
 );
85
-CREATE UNIQUE INDEX uk_fe_enterprise_code ON fe_enterprise (enterprise_code) WHERE del_flag = 0;
86
-CREATE INDEX idx_fe_enterprise_credit ON fe_enterprise (credit_code);
85
+CREATE UNIQUE INDEX uk_fe_enterprise_code ON fe_enterprise (enterprise_code, del_flag);
86
+CREATE UNIQUE INDEX uk_fe_enterprise_credit_code ON fe_enterprise (credit_code, del_flag);
87 87
 
88 88
 
89 89
 -- 企业-用户绑定(一个企业可有多个管理员)
@@ -478,7 +478,8 @@ CREATE TABLE fe_worker (
478 478
     update_time         TIMESTAMPTZ     NOT NULL DEFAULT NOW(),
479 479
     del_flag            SMALLINT        NOT NULL DEFAULT 0
480 480
 );
481
-CREATE INDEX idx_fe_worker_user ON fe_worker (user_id);
481
+CREATE UNIQUE INDEX uk_fe_worker_worker_no ON fe_worker (worker_no, del_flag);
482
+CREATE UNIQUE INDEX uk_fe_worker_user_id ON fe_worker (user_id, del_flag);
482 483
 CREATE INDEX idx_fe_worker_mobile ON fe_worker (mobile_mask);
483 484
 
484 485
 

+ 8 - 0
huimv-employment/fe-dao/src/main/java/com/huimv/employment/dao/mapper/FeUserMapper.java

@@ -3,6 +3,8 @@ package com.huimv.employment.dao.mapper;
3 3
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
4 4
 import com.huimv.employment.dao.entity.FeUser;
5 5
 import org.apache.ibatis.annotations.Mapper;
6
+import org.apache.ibatis.annotations.Param;
7
+import org.apache.ibatis.annotations.Select;
6 8
 
7 9
 /**
8 10
  * fe_user 表 Mapper。
@@ -10,4 +12,10 @@ import org.apache.ibatis.annotations.Mapper;
10 12
  */
11 13
 @Mapper
12 14
 public interface FeUserMapper extends BaseMapper<FeUser> {
15
+
16
+    /**
17
+     * 按主键加行锁,用于登记等「同一用户仅允许一次」的并发场景。
18
+     */
19
+    @Select("SELECT * FROM fe_user WHERE id = #{userId} AND del_flag = 0 FOR UPDATE")
20
+    FeUser selectByIdForUpdate(@Param("userId") Long userId);
13 21
 }

+ 51 - 5
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/enterprise/EnterpriseService.java

@@ -15,6 +15,9 @@ import com.huimv.employment.dao.mapper.FeUserMapper;
15 15
 import com.huimv.employment.service.config.SecurityProperties;
16 16
 import com.huimv.employment.service.enterprise.dto.EnterpriseRegisterRequest;
17 17
 import com.huimv.employment.service.enterprise.dto.EnterpriseRegisterResponse;
18
+import org.slf4j.Logger;
19
+import org.slf4j.LoggerFactory;
20
+import org.springframework.dao.DuplicateKeyException;
18 21
 import org.springframework.stereotype.Service;
19 22
 import org.springframework.transaction.annotation.Transactional;
20 23
 
@@ -26,6 +29,8 @@ import java.time.LocalDateTime;
26 29
 @Service
27 30
 public class EnterpriseService {
28 31
 
32
+    private static final Logger log = LoggerFactory.getLogger(EnterpriseService.class);
33
+
29 34
     private static final String USER_TYPE_ENTERPRISE = "enterprise";
30 35
     private static final String AUTH_STATUS_PENDING = "pending";
31 36
     private static final String ROLE_OWNER = "owner";
@@ -66,7 +71,7 @@ public class EnterpriseService {
66 71
 
67 72
     @Transactional(rollbackFor = Exception.class)
68 73
     public EnterpriseRegisterResponse register(Long userId, EnterpriseRegisterRequest request) {
69
-        requireEnterpriseUser(userId);
74
+        lockEnterpriseUserForUpdate(userId);
70 75
         if (findBindingByUserId(userId) != null) {
71 76
             throw new BizException(ErrorCode.ENTERPRISE_ALREADY_REGISTERED);
72 77
         }
@@ -90,14 +95,21 @@ public class EnterpriseService {
90 95
         enterprise.setContactMobileEnc(contactMobileEnc);
91 96
         enterprise.setContactMobileMask(contactMobileMask);
92 97
         enterprise.setAuthStatus(AUTH_STATUS_PENDING);
93
-        enterprise.setEnterpriseCode("ENT_TMP_" + System.currentTimeMillis());
94 98
         enterprise.setCreateTime(now);
95 99
         enterprise.setUpdateTime(now);
96 100
         enterprise.setDelFlag(0);
97
-        feEnterpriseMapper.insert(enterprise);
101
+        try {
102
+            feEnterpriseMapper.insert(enterprise);
103
+        } catch (DuplicateKeyException ex) {
104
+            throw handleEnterpriseDuplicate(ex, "insert");
105
+        }
98 106
 
99 107
         enterprise.setEnterpriseCode(generateEnterpriseCode(enterprise.getId()));
100
-        feEnterpriseMapper.updateById(enterprise);
108
+        try {
109
+            feEnterpriseMapper.updateById(enterprise);
110
+        } catch (DuplicateKeyException ex) {
111
+            throw handleEnterpriseDuplicate(ex, "assign enterpriseCode");
112
+        }
101 113
 
102 114
         FeEnterpriseUser binding = new FeEnterpriseUser();
103 115
         binding.setEnterpriseId(enterprise.getId());
@@ -108,11 +120,45 @@ public class EnterpriseService {
108 120
         binding.setCreateTime(now);
109 121
         binding.setUpdateTime(now);
110 122
         binding.setDelFlag(0);
111
-        feEnterpriseUserMapper.insert(binding);
123
+        try {
124
+            feEnterpriseUserMapper.insert(binding);
125
+        } catch (DuplicateKeyException ex) {
126
+            throw handleEnterpriseDuplicate(ex, "bind user");
127
+        }
112 128
 
113 129
         return toRegisterResponse(enterprise);
114 130
     }
115 131
 
132
+    /**
133
+     * 事务内锁定 fe_user 行,串行化同一用户的并发登记请求。
134
+     */
135
+    private FeUser lockEnterpriseUserForUpdate(Long userId) {
136
+        FeUser user = feUserMapper.selectByIdForUpdate(userId);
137
+        if (user == null) {
138
+            throw new BizException(ErrorCode.UNAUTHORIZED);
139
+        }
140
+        if (!USER_TYPE_ENTERPRISE.equals(user.getUserType())) {
141
+            throw new BizException(ErrorCode.ENTERPRISE_ONLY);
142
+        }
143
+        return user;
144
+    }
145
+
146
+    private BizException handleEnterpriseDuplicate(DuplicateKeyException ex, String phase) {
147
+        String message = ex.getMessage();
148
+        if (message != null && (message.contains("uk_fe_enterprise_credit_code") || message.contains("credit_code"))) {
149
+            return new BizException(ErrorCode.BAD_REQUEST, "该统一社会信用代码已被登记");
150
+        }
151
+        if (message != null && (message.contains("uk_fe_enterprise_code") || message.contains("enterprise_code"))) {
152
+            log.error("Duplicate enterprise_code during {}, should not happen", phase, ex);
153
+            return new BizException(ErrorCode.INTERNAL_ERROR);
154
+        }
155
+        if (message != null && message.contains("uk_fe_enterprise_user")) {
156
+            return new BizException(ErrorCode.ENTERPRISE_ALREADY_REGISTERED);
157
+        }
158
+        log.warn("Unexpected duplicate key during enterprise register {}: {}", phase, message);
159
+        return new BizException(ErrorCode.ENTERPRISE_ALREADY_REGISTERED);
160
+    }
161
+
116 162
     private FeEnterpriseUser findBindingByUserId(Long userId) {
117 163
         return feEnterpriseUserMapper.selectOne(new LambdaQueryWrapper<FeEnterpriseUser>()
118 164
                 .eq(FeEnterpriseUser::getUserId, userId)

+ 43 - 3
huimv-employment/fe-service/src/main/java/com/huimv/employment/service/worker/WorkerService.java

@@ -16,6 +16,9 @@ import com.huimv.employment.dao.mapper.FeWorkerMapper;
16 16
 import com.huimv.employment.service.config.SecurityProperties;
17 17
 import com.huimv.employment.service.worker.dto.WorkerRegisterRequest;
18 18
 import com.huimv.employment.service.worker.dto.WorkerRegisterResponse;
19
+import org.slf4j.Logger;
20
+import org.slf4j.LoggerFactory;
21
+import org.springframework.dao.DuplicateKeyException;
19 22
 import org.springframework.stereotype.Service;
20 23
 import org.springframework.transaction.annotation.Transactional;
21 24
 import org.springframework.util.StringUtils;
@@ -28,6 +31,8 @@ import java.time.LocalDateTime;
28 31
 @Service
29 32
 public class WorkerService {
30 33
 
34
+    private static final Logger log = LoggerFactory.getLogger(WorkerService.class);
35
+
31 36
     private static final String USER_TYPE_WORKER = "worker";
32 37
     private static final String VERIFY_STATUS_PENDING = "pending";
33 38
     private static final String CONSENT_VERSION = "v1.0";
@@ -69,7 +74,7 @@ public class WorkerService {
69 74
                                            WorkerRegisterRequest request,
70 75
                                            String clientIp,
71 76
                                            String userAgent) {
72
-        FeUser user = requireWorkerUser(userId);
77
+        FeUser user = lockWorkerUserForUpdate(userId);
73 78
         if (findWorkerByUserId(userId) != null) {
74 79
             throw new BizException(ErrorCode.WORKER_ALREADY_REGISTERED);
75 80
         }
@@ -99,10 +104,18 @@ public class WorkerService {
99 104
         worker.setCreateTime(now);
100 105
         worker.setUpdateTime(now);
101 106
         worker.setDelFlag(0);
102
-        feWorkerMapper.insert(worker);
107
+        try {
108
+            feWorkerMapper.insert(worker);
109
+        } catch (DuplicateKeyException ex) {
110
+            throw handleWorkerDuplicate(ex, "insert");
111
+        }
103 112
 
104 113
         worker.setWorkerNo(generateWorkerNo(worker.getId()));
105
-        feWorkerMapper.updateById(worker);
114
+        try {
115
+            feWorkerMapper.updateById(worker);
116
+        } catch (DuplicateKeyException ex) {
117
+            throw handleWorkerDuplicate(ex, "assign workerNo");
118
+        }
106 119
 
107 120
         saveConsent(worker.getId(), WorkerConsentType.LABOR_CONTRACT, "《电子劳动合同》",
108 121
                 request.getAgreedLaborContract(), now, clientIp, userAgent);
@@ -112,6 +125,33 @@ public class WorkerService {
112 125
         return toRegisterResponse(worker);
113 126
     }
114 127
 
128
+    /**
129
+     * 事务内锁定 fe_user 行,串行化同一用户的并发登记请求。
130
+     */
131
+    private FeUser lockWorkerUserForUpdate(Long userId) {
132
+        FeUser user = feUserMapper.selectByIdForUpdate(userId);
133
+        if (user == null) {
134
+            throw new BizException(ErrorCode.UNAUTHORIZED);
135
+        }
136
+        if (!USER_TYPE_WORKER.equals(user.getUserType())) {
137
+            throw new BizException(ErrorCode.WORKER_ONLY);
138
+        }
139
+        return user;
140
+    }
141
+
142
+    private BizException handleWorkerDuplicate(DuplicateKeyException ex, String phase) {
143
+        String message = ex.getMessage();
144
+        if (message != null && (message.contains("uk_fe_worker_user_id") || message.contains("user_id"))) {
145
+            return new BizException(ErrorCode.WORKER_ALREADY_REGISTERED);
146
+        }
147
+        if (message != null && (message.contains("uk_fe_worker_worker_no") || message.contains("worker_no"))) {
148
+            log.error("Duplicate worker_no during {}, should not happen", phase, ex);
149
+            return new BizException(ErrorCode.INTERNAL_ERROR);
150
+        }
151
+        log.warn("Unexpected duplicate key during worker register {}: {}", phase, message);
152
+        return new BizException(ErrorCode.WORKER_ALREADY_REGISTERED);
153
+    }
154
+
115 155
     private void ensureIdCardNotUsed(String idCardNoEnc, Long currentUserId) {
116 156
         FeWorker existing = feWorkerMapper.selectOne(new LambdaQueryWrapper<FeWorker>()
117 157
                 .eq(FeWorker::getIdCardNoEnc, idCardNoEnc)