|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+package com.huimv.employment.service.enterprise;
|
|
|
2
|
+
|
|
|
3
|
+import com.fasterxml.jackson.core.type.TypeReference;
|
|
|
4
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
5
|
+import com.huimv.employment.common.exception.BizException;
|
|
|
6
|
+import com.huimv.employment.common.exception.ErrorCode;
|
|
|
7
|
+import com.huimv.employment.dao.entity.FeConversation;
|
|
|
8
|
+import com.huimv.employment.dao.entity.FeEnterprise;
|
|
|
9
|
+import com.huimv.employment.dao.mapper.FeEnterpriseMapper;
|
|
|
10
|
+import com.huimv.employment.service.conversation.ConversationService;
|
|
|
11
|
+import com.huimv.employment.service.enterprise.dto.McpEnterpriseProfileResponse;
|
|
|
12
|
+import com.huimv.employment.service.enterprise.dto.McpEnterpriseProfileUpdateRequest;
|
|
|
13
|
+import com.huimv.employment.service.enterprise.dto.McpEnterpriseProfileUpdateResponse;
|
|
|
14
|
+import org.slf4j.Logger;
|
|
|
15
|
+import org.slf4j.LoggerFactory;
|
|
|
16
|
+import org.springframework.stereotype.Service;
|
|
|
17
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
18
|
+import org.springframework.util.StringUtils;
|
|
|
19
|
+
|
|
|
20
|
+import java.time.LocalDateTime;
|
|
|
21
|
+import java.util.Arrays;
|
|
|
22
|
+import java.util.Collections;
|
|
|
23
|
+import java.util.HashSet;
|
|
|
24
|
+import java.util.LinkedHashMap;
|
|
|
25
|
+import java.util.Map;
|
|
|
26
|
+import java.util.Set;
|
|
|
27
|
+
|
|
|
28
|
+/**
|
|
|
29
|
+ * MCP 企业画像接口(交互规则 v2 §6.2)。
|
|
|
30
|
+ * <p>通过 {@code X-Session-Id} 关联会话与企业,Agent 无需传递 enterprise_id。</p>
|
|
|
31
|
+ */
|
|
|
32
|
+@Service
|
|
|
33
|
+public class McpEnterpriseProfileService {
|
|
|
34
|
+
|
|
|
35
|
+ private static final Logger log = LoggerFactory.getLogger(McpEnterpriseProfileService.class);
|
|
|
36
|
+
|
|
|
37
|
+ private static final String STATUS_SUCCESS = "success";
|
|
|
38
|
+ private static final String PREF_PREFIX = "preferences.";
|
|
|
39
|
+
|
|
|
40
|
+ /** 可写入 profile_json.preferences 的偏好键 */
|
|
|
41
|
+ private static final Set<String> ALLOWED_PREFERENCE_KEYS = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
|
|
42
|
+ "settlement_cycle",
|
|
|
43
|
+ "default_region",
|
|
|
44
|
+ "default_settlement_mode",
|
|
|
45
|
+ "default_work_type",
|
|
|
46
|
+ "default_insurance_level"
|
|
|
47
|
+ )));
|
|
|
48
|
+
|
|
|
49
|
+ /** settlement_cycle 合法值 */
|
|
|
50
|
+ private static final Set<String> ALLOWED_SETTLEMENT_CYCLES = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(
|
|
|
51
|
+ "daily", "weekly", "biweekly", "monthly"
|
|
|
52
|
+ )));
|
|
|
53
|
+
|
|
|
54
|
+ private final FeEnterpriseMapper feEnterpriseMapper;
|
|
|
55
|
+ private final ConversationService conversationService;
|
|
|
56
|
+ private final EnterpriseService enterpriseService;
|
|
|
57
|
+ private final ObjectMapper objectMapper;
|
|
|
58
|
+
|
|
|
59
|
+ public McpEnterpriseProfileService(FeEnterpriseMapper feEnterpriseMapper,
|
|
|
60
|
+ ConversationService conversationService,
|
|
|
61
|
+ EnterpriseService enterpriseService,
|
|
|
62
|
+ ObjectMapper objectMapper) {
|
|
|
63
|
+ this.feEnterpriseMapper = feEnterpriseMapper;
|
|
|
64
|
+ this.conversationService = conversationService;
|
|
|
65
|
+ this.enterpriseService = enterpriseService;
|
|
|
66
|
+ this.objectMapper = objectMapper;
|
|
|
67
|
+ }
|
|
|
68
|
+
|
|
|
69
|
+ public McpEnterpriseProfileResponse getProfile(String sessionId) {
|
|
|
70
|
+ FeEnterprise enterprise = requireEnterpriseBySession(sessionId);
|
|
|
71
|
+ return toResponse(enterprise);
|
|
|
72
|
+ }
|
|
|
73
|
+
|
|
|
74
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
75
|
+ public McpEnterpriseProfileUpdateResponse updateProfile(String sessionId, McpEnterpriseProfileUpdateRequest request) {
|
|
|
76
|
+ FeEnterprise enterprise = requireEnterpriseBySession(sessionId);
|
|
|
77
|
+ Map<String, Object> profileRoot = parseProfileJson(enterprise.getProfileJson());
|
|
|
78
|
+ Map<String, Object> preferences = extractPreferences(profileRoot);
|
|
|
79
|
+
|
|
|
80
|
+ int updatedCount = 0;
|
|
|
81
|
+ for (Map.Entry<String, Object> entry : request.getUpdates().entrySet()) {
|
|
|
82
|
+ if (entry.getKey() == null) {
|
|
|
83
|
+ continue;
|
|
|
84
|
+ }
|
|
|
85
|
+ String key = entry.getKey().trim();
|
|
|
86
|
+ if (!StringUtils.hasText(key)) {
|
|
|
87
|
+ continue;
|
|
|
88
|
+ }
|
|
|
89
|
+ Object value = entry.getValue();
|
|
|
90
|
+ if (applyUpdate(enterprise, profileRoot, preferences, key, value)) {
|
|
|
91
|
+ updatedCount++;
|
|
|
92
|
+ }
|
|
|
93
|
+ }
|
|
|
94
|
+
|
|
|
95
|
+ if (updatedCount == 0) {
|
|
|
96
|
+ throw new BizException(ErrorCode.BAD_REQUEST, "未识别任何可更新字段");
|
|
|
97
|
+ }
|
|
|
98
|
+
|
|
|
99
|
+ profileRoot.put("preferences", preferences);
|
|
|
100
|
+ enterprise.setProfileJson(writeProfileJson(profileRoot));
|
|
|
101
|
+ enterprise.setUpdateTime(LocalDateTime.now());
|
|
|
102
|
+ feEnterpriseMapper.updateById(enterprise);
|
|
|
103
|
+
|
|
|
104
|
+ McpEnterpriseProfileUpdateResponse response = new McpEnterpriseProfileUpdateResponse();
|
|
|
105
|
+ response.setUpdatedFields(updatedCount);
|
|
|
106
|
+ response.setStatus(STATUS_SUCCESS);
|
|
|
107
|
+ return response;
|
|
|
108
|
+ }
|
|
|
109
|
+
|
|
|
110
|
+ private FeEnterprise requireEnterpriseBySession(String sessionId) {
|
|
|
111
|
+ FeConversation conversation = conversationService.requireBySessionId(sessionId);
|
|
|
112
|
+ return enterpriseService.requireById(conversation.getEnterpriseId());
|
|
|
113
|
+ }
|
|
|
114
|
+
|
|
|
115
|
+ private McpEnterpriseProfileResponse toResponse(FeEnterprise enterprise) {
|
|
|
116
|
+ Map<String, Object> profileRoot = parseProfileJson(enterprise.getProfileJson());
|
|
|
117
|
+ Map<String, Object> preferences = extractPreferences(profileRoot);
|
|
|
118
|
+
|
|
|
119
|
+ McpEnterpriseProfileResponse response = new McpEnterpriseProfileResponse();
|
|
|
120
|
+ response.setCompanyName(enterprise.getName());
|
|
|
121
|
+ response.setCompanyAddress(enterprise.getRegisteredAddress());
|
|
|
122
|
+ response.setIndustry(enterprise.getIndustry());
|
|
|
123
|
+ response.setTaxQualification(readString(profileRoot.get("tax_qualification")));
|
|
|
124
|
+ response.setPreferences(preferences);
|
|
|
125
|
+ return response;
|
|
|
126
|
+ }
|
|
|
127
|
+
|
|
|
128
|
+ private boolean applyUpdate(FeEnterprise enterprise,
|
|
|
129
|
+ Map<String, Object> profileRoot,
|
|
|
130
|
+ Map<String, Object> preferences,
|
|
|
131
|
+ String key,
|
|
|
132
|
+ Object value) {
|
|
|
133
|
+ switch (key) {
|
|
|
134
|
+ case "company_name":
|
|
|
135
|
+ enterprise.setName(requireNonBlankText(value, key));
|
|
|
136
|
+ return true;
|
|
|
137
|
+ case "company_address":
|
|
|
138
|
+ enterprise.setRegisteredAddress(requireNonBlankText(value, key));
|
|
|
139
|
+ return true;
|
|
|
140
|
+ case "industry":
|
|
|
141
|
+ enterprise.setIndustry(toNullableText(value));
|
|
|
142
|
+ return true;
|
|
|
143
|
+ case "scale":
|
|
|
144
|
+ enterprise.setScale(toNullableText(value));
|
|
|
145
|
+ return true;
|
|
|
146
|
+ case "tax_qualification":
|
|
|
147
|
+ profileRoot.put("tax_qualification", toNullableText(value));
|
|
|
148
|
+ return true;
|
|
|
149
|
+ default:
|
|
|
150
|
+ break;
|
|
|
151
|
+ }
|
|
|
152
|
+
|
|
|
153
|
+ if (key.startsWith(PREF_PREFIX)) {
|
|
|
154
|
+ String prefKey = key.substring(PREF_PREFIX.length());
|
|
|
155
|
+ if (!StringUtils.hasText(prefKey)) {
|
|
|
156
|
+ throw new BizException(ErrorCode.BAD_REQUEST, "偏好键不能为空:" + key);
|
|
|
157
|
+ }
|
|
|
158
|
+ if (!ALLOWED_PREFERENCE_KEYS.contains(prefKey)) {
|
|
|
159
|
+ throw new BizException(ErrorCode.BAD_REQUEST, "不支持的偏好字段:" + prefKey);
|
|
|
160
|
+ }
|
|
|
161
|
+ String prefValue = toNullableText(value);
|
|
|
162
|
+ validatePreferenceValue(prefKey, prefValue);
|
|
|
163
|
+ if (prefValue == null) {
|
|
|
164
|
+ preferences.remove(prefKey);
|
|
|
165
|
+ } else {
|
|
|
166
|
+ preferences.put(prefKey, prefValue);
|
|
|
167
|
+ }
|
|
|
168
|
+ return true;
|
|
|
169
|
+ }
|
|
|
170
|
+
|
|
|
171
|
+ log.debug("忽略未知的企业画像更新字段:{}", key);
|
|
|
172
|
+ return false;
|
|
|
173
|
+ }
|
|
|
174
|
+
|
|
|
175
|
+ private void validatePreferenceValue(String prefKey, String value) {
|
|
|
176
|
+ if (value == null) {
|
|
|
177
|
+ return;
|
|
|
178
|
+ }
|
|
|
179
|
+ if ("settlement_cycle".equals(prefKey) && !ALLOWED_SETTLEMENT_CYCLES.contains(value)) {
|
|
|
180
|
+ throw new BizException(ErrorCode.BAD_REQUEST,
|
|
|
181
|
+ "settlement_cycle 须为 daily/weekly/biweekly/monthly 之一");
|
|
|
182
|
+ }
|
|
|
183
|
+ if ("default_settlement_mode".equals(prefKey)) {
|
|
|
184
|
+ Set<String> modes = new HashSet<>(Arrays.asList("daily", "piece", "monthly"));
|
|
|
185
|
+ if (!modes.contains(value)) {
|
|
|
186
|
+ throw new BizException(ErrorCode.BAD_REQUEST,
|
|
|
187
|
+ "default_settlement_mode 须为 daily/piece/monthly 之一");
|
|
|
188
|
+ }
|
|
|
189
|
+ }
|
|
|
190
|
+ }
|
|
|
191
|
+
|
|
|
192
|
+ private Map<String, Object> parseProfileJson(String profileJson) {
|
|
|
193
|
+ if (!StringUtils.hasText(profileJson)) {
|
|
|
194
|
+ return new LinkedHashMap<>();
|
|
|
195
|
+ }
|
|
|
196
|
+ try {
|
|
|
197
|
+ Map<String, Object> parsed = objectMapper.readValue(profileJson.trim(),
|
|
|
198
|
+ new TypeReference<Map<String, Object>>() {
|
|
|
199
|
+ });
|
|
|
200
|
+ return parsed != null ? new LinkedHashMap<>(parsed) : new LinkedHashMap<>();
|
|
|
201
|
+ } catch (Exception ex) {
|
|
|
202
|
+ log.warn("解析 profile_json 失败,将使用空对象:{}", ex.getMessage());
|
|
|
203
|
+ return new LinkedHashMap<>();
|
|
|
204
|
+ }
|
|
|
205
|
+ }
|
|
|
206
|
+
|
|
|
207
|
+ @SuppressWarnings("unchecked")
|
|
|
208
|
+ private Map<String, Object> extractPreferences(Map<String, Object> profileRoot) {
|
|
|
209
|
+ Object raw = profileRoot.get("preferences");
|
|
|
210
|
+ if (raw instanceof Map) {
|
|
|
211
|
+ return new LinkedHashMap<>((Map<String, Object>) raw);
|
|
|
212
|
+ }
|
|
|
213
|
+ return new LinkedHashMap<>();
|
|
|
214
|
+ }
|
|
|
215
|
+
|
|
|
216
|
+ private String writeProfileJson(Map<String, Object> profileRoot) {
|
|
|
217
|
+ try {
|
|
|
218
|
+ return objectMapper.writeValueAsString(profileRoot);
|
|
|
219
|
+ } catch (Exception ex) {
|
|
|
220
|
+ throw new BizException(ErrorCode.INTERNAL_ERROR);
|
|
|
221
|
+ }
|
|
|
222
|
+ }
|
|
|
223
|
+
|
|
|
224
|
+ private static String requireNonBlankText(Object value, String key) {
|
|
|
225
|
+ String text = toNullableText(value);
|
|
|
226
|
+ if (text == null) {
|
|
|
227
|
+ throw new BizException(ErrorCode.BAD_REQUEST, "字段 " + key + " 不能为空");
|
|
|
228
|
+ }
|
|
|
229
|
+ return text;
|
|
|
230
|
+ }
|
|
|
231
|
+
|
|
|
232
|
+ private static String toNullableText(Object value) {
|
|
|
233
|
+ if (value == null) {
|
|
|
234
|
+ return null;
|
|
|
235
|
+ }
|
|
|
236
|
+ String text = String.valueOf(value).trim();
|
|
|
237
|
+ return text.isEmpty() ? null : text;
|
|
|
238
|
+ }
|
|
|
239
|
+
|
|
|
240
|
+ private static String readString(Object value) {
|
|
|
241
|
+ return value != null ? String.valueOf(value) : null;
|
|
|
242
|
+ }
|
|
|
243
|
+}
|