AdminKnowledgeService.java 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. package com.pharmacopoeia.service;
  2. import com.fasterxml.jackson.core.JsonProcessingException;
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import com.pharmacopoeia.entity.Drug;
  5. import com.pharmacopoeia.entity.KnowledgePoint;
  6. import com.pharmacopoeia.entity.Question;
  7. import com.pharmacopoeia.entity.QuickAsk;
  8. import com.pharmacopoeia.entity.QuickAskAuditLog;
  9. import com.pharmacopoeia.repository.DrugRepository;
  10. import com.pharmacopoeia.repository.KnowledgePointRepository;
  11. import com.pharmacopoeia.repository.QuestionRepository;
  12. import com.pharmacopoeia.repository.QuickAskAuditLogRepository;
  13. import com.pharmacopoeia.repository.QuickAskRepository;
  14. import com.pharmacopoeia.util.IpUtils;
  15. import org.springframework.data.domain.PageRequest;
  16. import org.springframework.jdbc.core.JdbcTemplate;
  17. import org.springframework.security.core.context.SecurityContextHolder;
  18. import org.springframework.stereotype.Service;
  19. import org.springframework.transaction.annotation.Transactional;
  20. import org.springframework.web.context.request.RequestContextHolder;
  21. import org.springframework.web.context.request.ServletRequestAttributes;
  22. import java.util.*;
  23. import java.time.Instant;
  24. import java.time.LocalDate;
  25. import java.time.ZoneOffset;
  26. import java.util.function.Consumer;
  27. @Service
  28. public class AdminKnowledgeService {
  29. private final DrugRepository drugRepository;
  30. private final KnowledgePointRepository knowledgePointRepository;
  31. private final QuestionRepository questionRepository;
  32. private final JdbcTemplate jdbc;
  33. private final ObjectMapper mapper = new ObjectMapper();
  34. private final QuickAskRepository quickAskRepository;
  35. private final QuickAskAuditLogRepository auditLogRepository;
  36. public AdminKnowledgeService(DrugRepository drugRepository,
  37. KnowledgePointRepository knowledgePointRepository,
  38. QuestionRepository questionRepository,
  39. QuickAskRepository quickAskRepository,
  40. QuickAskAuditLogRepository auditLogRepository,
  41. JdbcTemplate jdbc) {
  42. this.drugRepository = drugRepository;
  43. this.knowledgePointRepository = knowledgePointRepository;
  44. this.questionRepository = questionRepository;
  45. this.quickAskRepository = quickAskRepository;
  46. this.auditLogRepository = auditLogRepository;
  47. this.jdbc = jdbc;
  48. }
  49. // ==================== Drug CRUD ====================
  50. public Map<String, Object> listDrugs(String keyword, String category, int page, int pageSize) {
  51. var pageable = PageRequest.of(page - 1, pageSize);
  52. var result = (keyword != null && !keyword.isBlank())
  53. ? drugRepository.searchByKeyword(keyword, pageable)
  54. : (category != null && !category.isBlank())
  55. ? drugRepository.findByCategoryAndIsActiveTrue(category, pageable)
  56. : drugRepository.findByIsActiveTrue(pageable);
  57. var items = result.getContent().stream()
  58. .map(this::drugToMap)
  59. .collect(java.util.stream.Collectors.toList());
  60. Map<String, Object> response = new LinkedHashMap<>();
  61. response.put("items", items);
  62. response.put("total", result.getTotalElements());
  63. response.put("page", page);
  64. response.put("page_size", pageSize);
  65. return response;
  66. }
  67. public Optional<Map<String, Object>> getDrug(String drugId) {
  68. return drugRepository.findByDrugId(drugId).map(this::drugToMap);
  69. }
  70. @Transactional
  71. public Map<String, Object> createDrug(Map<String, Object> req) {
  72. Drug drug = Drug.builder()
  73. .drugId((String) req.get("drugId"))
  74. .name((String) req.get("name"))
  75. .nameEn((String) req.get("nameEn"))
  76. .pinyin((String) req.get("pinyin"))
  77. .category((String) req.get("category"))
  78. .subcategory((String) req.get("subcategory"))
  79. .approvalNumber((String) req.get("approvalNumber"))
  80. .sourceVersion((String) req.get("sourceVersion"))
  81. .sourceVolume((String) req.get("sourceVolume"))
  82. .sourcePage((String) req.get("sourcePage"))
  83. .isActive(true)
  84. .build();
  85. drug.setSections(castToMap(req.get("sections")));
  86. drugRepository.save(drug);
  87. return toMap("drug_id", drug.getDrugId(), "已创建");
  88. }
  89. @Transactional
  90. public Map<String, Object> updateDrug(String drugId, Map<String, Object> req) {
  91. return drugRepository.findByDrugId(drugId).map(d -> {
  92. applyStrIfPresent(req, "name", d::setName);
  93. applyStrIfPresent(req, "nameEn", d::setNameEn);
  94. applyStrIfPresent(req, "pinyin", d::setPinyin);
  95. applyStrIfPresent(req, "category", d::setCategory);
  96. applyStrIfPresent(req, "subcategory", d::setSubcategory);
  97. applyStrIfPresent(req, "approvalNumber", d::setApprovalNumber);
  98. applyStrIfPresent(req, "sourceVersion", d::setSourceVersion);
  99. applyStrIfPresent(req, "sourceVolume", d::setSourceVolume);
  100. applyStrIfPresent(req, "sourcePage", d::setSourcePage);
  101. applyIfPresent(req, "sections", v -> d.setSections(castToMap(v)));
  102. applyIfPresent(req, "isActive", v -> d.setIsActive((Boolean) v));
  103. drugRepository.save(d);
  104. return toMap("drug_id", drugId, "已更新");
  105. }).orElseGet(() -> toMap("drug_id", drugId, "药品不存在"));
  106. }
  107. @Transactional
  108. public Map<String, Object> deleteDrug(String drugId) {
  109. return drugRepository.findByDrugId(drugId).map(d -> {
  110. d.setIsActive(false);
  111. drugRepository.save(d);
  112. return toMap("drug_id", drugId, "已标记删除");
  113. }).orElseGet(() -> toMap("drug_id", drugId, "药品不存在"));
  114. }
  115. // ==================== KnowledgePoint CRUD ====================
  116. public Map<String, Object> listKnowledgePoints(String subject, String chapterId,
  117. Integer difficulty, String frequency,
  118. int page, int pageSize) {
  119. StringBuilder sql = new StringBuilder("SELECT * FROM knowledge_points WHERE 1=1");
  120. List<Object> params = new ArrayList<>();
  121. appendFilters(sql, params,
  122. new SqlFilter("subject", subject),
  123. new SqlFilter("chapter_id", chapterId),
  124. new SqlFilter("difficulty", difficulty),
  125. new SqlFilter("frequency", frequency));
  126. sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
  127. params.add(pageSize);
  128. params.add((page - 1) * pageSize);
  129. List<Map<String, Object>> items = jdbc.queryForList(sql.toString(), params.toArray());
  130. Map<String, Object> response = new LinkedHashMap<>();
  131. response.put("items", items);
  132. response.put("page", page);
  133. response.put("page_size", pageSize);
  134. return response;
  135. }
  136. public Optional<KnowledgePoint> getKnowledgePoint(String pointId) {
  137. return knowledgePointRepository.findByPointId(pointId);
  138. }
  139. @Transactional
  140. public Map<String, Object> createKnowledgePoint(Map<String, Object> req) {
  141. KnowledgePoint kp = KnowledgePoint.builder()
  142. .pointId((String) req.get("pointId"))
  143. .subject((String) req.get("subject"))
  144. .chapterId((String) req.get("chapterId"))
  145. .chapterName((String) req.get("chapterName"))
  146. .title((String) req.get("title"))
  147. .content((String) req.get("content"))
  148. .difficulty(req.get("difficulty") != null ? (Integer) req.get("difficulty") : 1)
  149. .frequency((String) req.get("frequency"))
  150. .source((String) req.get("source"))
  151. .build();
  152. writeJsonIfPresent(req, "relatedDrugs", kp::setRelatedDrugs);
  153. knowledgePointRepository.save(kp);
  154. return toMap("point_id", kp.getPointId(), "已创建");
  155. }
  156. @Transactional
  157. public Map<String, Object> updateKnowledgePoint(String pointId, Map<String, Object> req) {
  158. return knowledgePointRepository.findByPointId(pointId).map(kp -> {
  159. applyStrIfPresent(req, "chapterName", kp::setChapterName);
  160. applyStrIfPresent(req, "title", kp::setTitle);
  161. applyStrIfPresent(req, "content", kp::setContent);
  162. applyIfPresent(req, "difficulty", v -> kp.setDifficulty((Integer) v));
  163. applyStrIfPresent(req, "frequency", kp::setFrequency);
  164. applyStrIfPresent(req, "source", kp::setSource);
  165. writeJsonIfPresent(req, "relatedDrugs", kp::setRelatedDrugs);
  166. knowledgePointRepository.save(kp);
  167. return toMap("point_id", pointId, "已更新");
  168. }).orElseGet(() -> toMap("point_id", pointId, "知识点不存在"));
  169. }
  170. @Transactional
  171. public Map<String, Object> deleteKnowledgePoint(String pointId) {
  172. knowledgePointRepository.findByPointId(pointId).ifPresent(knowledgePointRepository::delete);
  173. return toMap("point_id", pointId, "已删除");
  174. }
  175. // ==================== Question CRUD ====================
  176. public Map<String, Object> listQuestions(String subject, String chapterId,
  177. String questionType, Integer difficulty,
  178. Boolean audited, int page, int pageSize) {
  179. StringBuilder sql = new StringBuilder("SELECT * FROM questions WHERE 1=1");
  180. List<Object> params = new ArrayList<>();
  181. appendFilters(sql, params,
  182. new SqlFilter("subject", subject),
  183. new SqlFilter("chapter_id", chapterId),
  184. new SqlFilter("question_type", questionType),
  185. new SqlFilter("difficulty", difficulty),
  186. new SqlFilter("audited", audited));
  187. sql.append(" ORDER BY created_at DESC LIMIT ? OFFSET ?");
  188. params.add(pageSize);
  189. params.add((page - 1) * pageSize);
  190. List<Map<String, Object>> items = jdbc.queryForList(sql.toString(), params.toArray());
  191. Map<String, Object> response = new LinkedHashMap<>();
  192. response.put("items", items);
  193. response.put("page", page);
  194. response.put("page_size", pageSize);
  195. return response;
  196. }
  197. public Optional<Question> getQuestion(String questionId) {
  198. return questionRepository.findByQuestionId(questionId);
  199. }
  200. @Transactional
  201. public Map<String, Object> createQuestion(Map<String, Object> req) {
  202. Question q = Question.builder()
  203. .questionId((String) req.get("questionId"))
  204. .questionType((String) req.get("questionType"))
  205. .subject((String) req.get("subject"))
  206. .chapterId((String) req.get("chapterId"))
  207. .difficulty(req.get("difficulty") != null ? (Integer) req.get("difficulty") : 1)
  208. .content((String) req.get("content"))
  209. .answer((String) req.get("answer"))
  210. .explanation((String) req.get("explanation"))
  211. .frequency((String) req.get("frequency"))
  212. .source((String) req.get("source"))
  213. .audited(false)
  214. .correctCount(0)
  215. .attemptCount(0)
  216. .build();
  217. writeJsonIfPresent(req, "options", q::setOptions);
  218. writeJsonIfPresent(req, "knowledgePointIds", q::setKnowledgePointIds);
  219. questionRepository.save(q);
  220. return toMap("question_id", q.getQuestionId(), "已创建");
  221. }
  222. @Transactional
  223. public Map<String, Object> updateQuestion(String questionId, Map<String, Object> req) {
  224. return questionRepository.findByQuestionId(questionId).map(q -> {
  225. applyStrIfPresent(req, "questionType", q::setQuestionType);
  226. applyStrIfPresent(req, "subject", q::setSubject);
  227. applyStrIfPresent(req, "chapterId", q::setChapterId);
  228. applyIfPresent(req, "difficulty", v -> q.setDifficulty((Integer) v));
  229. applyStrIfPresent(req, "content", q::setContent);
  230. applyStrIfPresent(req, "answer", q::setAnswer);
  231. applyStrIfPresent(req, "explanation", q::setExplanation);
  232. applyStrIfPresent(req, "frequency", q::setFrequency);
  233. applyStrIfPresent(req, "source", q::setSource);
  234. applyIfPresent(req, "audited", v -> q.setAudited((Boolean) v));
  235. writeJsonIfPresent(req, "options", q::setOptions);
  236. writeJsonIfPresent(req, "knowledgePointIds", q::setKnowledgePointIds);
  237. questionRepository.save(q);
  238. return toMap("question_id", questionId, "已更新");
  239. }).orElseGet(() -> toMap("question_id", questionId, "题目不存在"));
  240. }
  241. @Transactional
  242. public Map<String, Object> deleteQuestion(String questionId) {
  243. questionRepository.findByQuestionId(questionId).ifPresent(questionRepository::delete);
  244. return toMap("question_id", questionId, "已删除");
  245. }
  246. @Transactional
  247. public Map<String, Object> batchAudit(List<String> questionIds, boolean audited) {
  248. for (String qid : questionIds) {
  249. questionRepository.findByQuestionId(qid).ifPresent(q -> {
  250. q.setAudited(audited);
  251. questionRepository.save(q);
  252. });
  253. }
  254. Map<String, Object> r = new LinkedHashMap<>();
  255. r.put("message", "批量审核完成");
  256. r.put("question_ids", questionIds);
  257. r.put("audited", audited);
  258. return r;
  259. }
  260. // ==================== 快捷提问标签管理 ====================
  261. /** 获取所有激活的标签,按 sort_order 排序(公开接口,无需鉴权),最多返回6个 */
  262. public List<QuickAsk> listQuickAsks() {
  263. return quickAskRepository.findByIsActiveTrueOrderBySortOrderAscIdAsc(
  264. org.springframework.data.domain.PageRequest.of(0, MAX_ACTIVE_QUICK_ASKS));
  265. }
  266. /** 管理端:分页获取所有标签(含禁用) */
  267. public Map<String, Object> listAllQuickAsks(int page, int pageSize) {
  268. var pageable = PageRequest.of(page - 1, pageSize);
  269. var result = quickAskRepository.findAllByOrderBySortOrderAscIdAsc(pageable);
  270. return Map.of(
  271. "items", result.getContent(),
  272. "page", page,
  273. "page_size", pageSize,
  274. "total", result.getTotalElements(),
  275. "total_pages", result.getTotalPages()
  276. );
  277. }
  278. private static final int MAX_ACTIVE_QUICK_ASKS = 6;
  279. /** 新增标签 */
  280. @Transactional
  281. public Map<String, Object> createQuickAsk(String label, String question, int sortOrder) {
  282. String trimmedLabel = label.trim();
  283. if (quickAskRepository.countByLabel(trimmedLabel) > 0) {
  284. throw new IllegalArgumentException("推荐位文字不能重复");
  285. }
  286. if (quickAskRepository.count() >= MAX_ACTIVE_QUICK_ASKS) {
  287. throw new IllegalArgumentException("最多只能添加" + MAX_ACTIVE_QUICK_ASKS + "个推荐位");
  288. }
  289. String operator = getCurrentOperator();
  290. QuickAsk qa = QuickAsk.builder()
  291. .label(trimmedLabel)
  292. .question(question != null ? question.trim() : trimmedLabel)
  293. .sortOrder(sortOrder)
  294. .isActive(true)
  295. .createdBy(operator)
  296. .build();
  297. qa = quickAskRepository.save(qa);
  298. writeAuditLog(qa.getId(), "CREATE", operator, qa.getLabel(), qa.getQuestion(), qa.getSortOrder(), qa.getIsActive());
  299. return Map.of("ok", true, "id", qa.getId(), "message", "已创建");
  300. }
  301. /** 更新标签 */
  302. @Transactional
  303. public Map<String, Object> updateQuickAsk(int id, String label, String question, Integer sortOrder, Boolean isActive) {
  304. var opt = quickAskRepository.findById(id);
  305. if (opt.isEmpty()) {
  306. throw new IllegalArgumentException("未找到");
  307. }
  308. QuickAsk qa = opt.get();
  309. if (label != null && !label.isBlank()) {
  310. String trimmedLabel = label.trim();
  311. if (quickAskRepository.countByLabelAndIdNot(trimmedLabel, id) > 0) {
  312. throw new IllegalArgumentException("推荐位文字不能重复");
  313. }
  314. qa.setLabel(trimmedLabel);
  315. }
  316. Optional.ofNullable(question).ifPresent(v -> qa.setQuestion(v.trim()));
  317. Optional.ofNullable(sortOrder).ifPresent(qa::setSortOrder);
  318. Optional.ofNullable(isActive).ifPresent(qa::setIsActive);
  319. String operator = getCurrentOperator();
  320. qa.setUpdatedBy(operator);
  321. quickAskRepository.save(qa);
  322. writeAuditLog(id, "UPDATE", operator, qa.getLabel(), qa.getQuestion(), qa.getSortOrder(), qa.getIsActive());
  323. long activeCount = quickAskRepository.countByIsActiveTrue();
  324. String message = "已更新";
  325. if (Boolean.TRUE.equals(qa.getIsActive()) && activeCount > MAX_ACTIVE_QUICK_ASKS) {
  326. message = "已更新(当前上架推荐位" + activeCount + "个,超过" + MAX_ACTIVE_QUICK_ASKS + "个上限)";
  327. }
  328. return Map.of("ok", true, "id", id, "message", message);
  329. }
  330. /** 删除标签 */
  331. @Transactional
  332. public Map<String, Object> deleteQuickAsk(int id) {
  333. var opt = quickAskRepository.findById(id);
  334. if (opt.isEmpty()) {
  335. throw new IllegalArgumentException("未找到");
  336. }
  337. QuickAsk qa = opt.get();
  338. String operator = getCurrentOperator();
  339. writeAuditLog(id, "DELETE", operator, qa.getLabel(), qa.getQuestion(), qa.getSortOrder(), qa.getIsActive());
  340. quickAskRepository.delete(qa);
  341. return Map.of("ok", true, "message", "已删除");
  342. }
  343. // ==================== 排序管理 ====================
  344. public Map<String, Object> reorderQuickAsks(List<Map<String, Object>> items) {
  345. String operator = getCurrentOperator();
  346. int updated = 0;
  347. for (Map<String, Object> item : items) {
  348. int id = ((Number) item.get("id")).intValue();
  349. int sortOrder = ((Number) item.get("sortOrder")).intValue();
  350. updated += quickAskRepository.updateSortOrder(id, sortOrder, operator);
  351. }
  352. return Map.of("ok", updated > 0, "message", "已更新 " + updated + " 条排序");
  353. }
  354. private void writeAuditLog(int quickAskId, String action, String operator,
  355. String label, String question, Integer sortOrder, Boolean isActive) {
  356. auditLogRepository.save(QuickAskAuditLog.builder()
  357. .quickAskId(quickAskId).action(action)
  358. .label(label).question(question)
  359. .sortOrder(sortOrder != null ? sortOrder : 0)
  360. .isActive(isActive != null ? isActive : true)
  361. .operator(operator)
  362. .ip(getClientIp())
  363. .build());
  364. }
  365. /** 从当前请求中获取客户端 IP */
  366. private static String getClientIp() {
  367. var attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
  368. if (attrs != null) {
  369. return IpUtils.getClientIp(attrs.getRequest());
  370. }
  371. return "unknown";
  372. }
  373. public Map<String, Object> listQuickAskAuditLog(int page, int pageSize, String action,
  374. String startDate, String endDate) {
  375. var pageable = PageRequest.of(page - 1, pageSize);
  376. Instant startTime = startDate != null && !startDate.isBlank()
  377. ? LocalDate.parse(startDate).atStartOfDay(ZoneOffset.UTC).toInstant()
  378. : null;
  379. Instant endTime = endDate != null && !endDate.isBlank()
  380. ? LocalDate.parse(endDate).plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant()
  381. : null;
  382. var result = auditLogRepository.findByFilters(
  383. action != null && !action.isBlank() ? action : null,
  384. startTime, endTime, pageable);
  385. return Map.of(
  386. "items", result.getContent(),
  387. "page", page,
  388. "page_size", pageSize,
  389. "total", result.getTotalElements(),
  390. "total_pages", result.getTotalPages()
  391. );
  392. }
  393. // ==================== Helpers ====================
  394. /** 从 SecurityContext 获取当前操作员标识 */
  395. private String getCurrentOperator() {
  396. var auth = SecurityContextHolder.getContext().getAuthentication();
  397. if (auth != null && auth.isAuthenticated() && !"anonymousUser".equals(auth.getPrincipal())) {
  398. return auth.getName();
  399. }
  400. return "anonymous";
  401. }
  402. @SuppressWarnings("unchecked")
  403. private Map<String, Object> castToMap(Object obj) {
  404. if (obj instanceof Map) {
  405. return (Map<String, Object>) obj;
  406. }
  407. return new HashMap<>();
  408. }
  409. // ---- 通用 if-reduction helpers ----
  410. /** map 中存在 key 且非 null 时执行 setter */
  411. private void applyIfPresent(Map<String, Object> req, String key, Consumer<Object> setter) {
  412. Optional.ofNullable(req.get(key)).ifPresent(setter);
  413. }
  414. /** map 中存在 key 且为 非空字符串 时执行 setter */
  415. private void applyStrIfPresent(Map<String, Object> req, String key, Consumer<String> setter) {
  416. Optional.ofNullable(req.get(key))
  417. .map(Object::toString)
  418. .filter(s -> !s.isBlank())
  419. .ifPresent(setter);
  420. }
  421. /** 将 map 中的对象序列化为 JSON,非 null 时执行 setter */
  422. private void writeJsonIfPresent(Map<String, Object> req, String key, Consumer<String> setter) {
  423. Optional.ofNullable(req.get(key)).ifPresent(val -> {
  424. try { setter.accept(mapper.writeValueAsString(val)); } catch (JsonProcessingException ignored) {}
  425. });
  426. }
  427. /** SQL 动态筛选字段 */
  428. private record SqlFilter(String column, Object value) {}
  429. private void appendFilters(StringBuilder sql, List<Object> params, SqlFilter... filters) {
  430. for (var f : filters) {
  431. if (f.value == null || f.value instanceof String s && s.isBlank()) {
  432. continue;
  433. }
  434. sql.append(" AND ").append(f.column).append(" = ?");
  435. params.add(f.value);
  436. }
  437. }
  438. private Map<String, Object> drugToMap(Drug d) {
  439. Map<String, Object> m = new LinkedHashMap<>();
  440. m.put("drug_id", d.getDrugId());
  441. m.put("name", d.getName());
  442. m.put("name_en", d.getNameEn());
  443. m.put("pinyin", d.getPinyin());
  444. m.put("category", d.getCategory());
  445. m.put("subcategory", d.getSubcategory());
  446. m.put("approval_number", d.getApprovalNumber());
  447. m.put("sections", d.getSections());
  448. m.put("source_version", d.getSourceVersion());
  449. m.put("source_volume", d.getSourceVolume());
  450. m.put("source_page", d.getSourcePage());
  451. m.put("is_active", d.getIsActive());
  452. m.put("created_at", d.getCreatedAt());
  453. m.put("updated_at", d.getUpdatedAt());
  454. return m;
  455. }
  456. private Map<String, Object> toMap(String k1, Object v1, Object v2) {
  457. Map<String, Object> m = new LinkedHashMap<>();
  458. m.put(k1, v1);
  459. m.put("message", v2);
  460. return m;
  461. }
  462. }