liuchengsen 1 сар өмнө
parent
commit
5a11388cf3

+ 48 - 0
zhijiayun-common/src/main/java/com/xuekairui/common/FlexibleLocalDateTimeDeserializer.java

@@ -0,0 +1,48 @@
+package com.xuekairui.common;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.deser.std.StdDeserializer;
+
+import java.io.IOException;
+import java.time.LocalDateTime;
+
+/**
+ * 灵活的 LocalDateTime 反序列化器
+ * 同时兼容两种前端常见格式:
+ * <ul>
+ *   <li>yyyy-MM-dd HH:mm:ss(北京时间,如 2026-06-26 08:30:00)</li>
+ *   <li>ISO-8601 UTC(如 2026-07-02T03:24:23.988Z)</li>
+ * </ul>
+ * 优先尝试 yyyy-MM-dd HH:mm:ss,失败后回退 ISO-8601 UTC
+ */
+public class FlexibleLocalDateTimeDeserializer extends StdDeserializer<LocalDateTime> {
+
+    public FlexibleLocalDateTimeDeserializer() {
+        super(LocalDateTime.class);
+    }
+
+    @Override
+    public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+        String text = p.getText();
+        if (text == null || text.isBlank()) {
+            return null;
+        }
+        String trimmed = text.trim();
+
+        // 优先尝试 yyyy-MM-dd HH:mm:ss
+        LocalDateTime result = ParseUtil.parseDateTime(trimmed, null);
+        if (result != null) {
+            return result;
+        }
+
+        // 回退 ISO-8601 UTC(如 2026-07-02T03:24:23.988Z)
+        result = ParseUtil.parseIsoUtc(trimmed, null);
+        if (result != null) {
+            return result;
+        }
+
+        // 两种格式都失败,抛出异常
+        throw new IOException("无法解析日期时间: '" + trimmed + "',支持格式:yyyy-MM-dd HH:mm:ss 或 ISO-8601 UTC");
+    }
+}

+ 6 - 0
zhijiayun-gateway/pom.xml

@@ -64,6 +64,12 @@
             <artifactId>spring-boot-starter-data-redis</artifactId>
         </dependency>
 
+        <!-- Actuator(健康检查端点 /actuator/health,供部署脚本和 Docker 探活) -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-actuator</artifactId>
+        </dependency>
+
         <!-- Test -->
         <dependency>
             <groupId>org.springframework.boot</groupId>

+ 3 - 1
zhijiayun-gateway/src/main/resources/application-prod.yml

@@ -23,7 +23,7 @@ spring:
       host: ${REDIS_HOST:localhost}
       port: ${REDIS_PORT:6379}
       password: ${REDIS_PASSWORD:123456}
-      database: ${REDIS_DATABASE:1}
+      database: ${REDIS_DATABASE:0}
       timeout: 5000ms
       lettuce:
         pool:
@@ -54,6 +54,8 @@ wechat:
 
 # 阿里云短信:生产部署时通过环境变量覆盖
 sms:
+  test-mode: true
+  test-code: "123456"
   access-key-id: ${ALIBABA_CLOUD_ACCESS_KEY_ID:LTAI5tDwjfteBvivYN41r8sJ}
   access-key-secret: ${ALIBABA_CLOUD_ACCESS_KEY_SECRET:yowuOGi2nYYnrqGpO3qcz94C4brcPp}
   sign-name: ${SMS_SIGN_NAME:开邻}

+ 4 - 3
zhijiayun-invite/src/main/java/com/xuekairui/invite/dto/QuotaGrantRequest.java

@@ -1,6 +1,7 @@
 package com.xuekairui.invite.dto;
 
-import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.xuekairui.common.FlexibleLocalDateTimeDeserializer;
 import jakarta.validation.constraints.Min;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
@@ -22,8 +23,8 @@ public class QuotaGrantRequest {
     @Min(1)
     private Integer quotaCount;
 
-    /** 过期时间(可选,不填则永不过期,格式:yyyy-MM-dd HH:mm:ss) */
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    /** 过期时间(可选,不填则永不过期,兼容 yyyy-MM-dd HH:mm:ss 和 ISO-8601 UTC) */
+    @JsonDeserialize(using = FlexibleLocalDateTimeDeserializer.class)
     private LocalDateTime expireTime;
 
     /** 备注 */

+ 15 - 4
zhijiayun-test/src/main/resources/static/invite-test.html

@@ -1128,6 +1128,17 @@ const FIELD_DESC = {
         ['createTime', 'DateTime', '创建时间']
     ],
 
+    'searchRecordResponse': [
+        ['id', 'Long', '记录ID(雪花算法)'],
+        ['drugName', 'String', '药品名称'],
+        ['searchedAt', 'DateTime', '搜索时间(yyyy-MM-dd HH:mm:ss)'],
+        ['statusText', 'String', '状态文案(如:已返回 2 个平台的可比价结果)'],
+        ['platformCount', 'Integer', '涉及平台数量(后端自动从platformResults计算)'],
+        ['quoteCount', 'Integer', '总报价条数(后端自动从platformResults计算)'],
+        ['platformResults', 'Object/Array', '平台搜索结果(结构化JSON对象,仅详情接口返回,列表接口为null)'],
+        ['searchLog', 'Object/null', '搜索日志(仅详情接口返回,列表接口为null)']
+    ],
+
 
 };
 
@@ -1499,13 +1510,13 @@ function renderSearchRecordModule() {
         [{label:'药品名称(drugName)', name:'drugName', placeholder:'阿莫西林', default:'阿莫西林'},
          {label:'搜索时间(searchedAt)', name:'searchedAt', placeholder:'yyyy-MM-dd HH:mm:ss', default:'2026-06-26 08:30:00'},
          {label:'状态文案(statusText)', name:'statusText', placeholder:'已返回 2 个平台的可比价结果。', default:'已返回 2 个平台的可比价结果。'},
-         {label:'平台结果(platformResults)', name:'platformResults', placeholder:'[{"platformId":"yiyaocheng","platformName":"壹药城"}]', default:'[{"platformId":"yiyaocheng","platformName":"壹药城","response":{"status":"ok","products":[]}}]', type:'json'},
-         {label:'搜索日志(searchLog)', name:'searchLog', placeholder:'null', default:'null', type:'json'}], true, null, '保存一次完整的比价搜索快照,包含平台响应数据。platformResults和searchLog支持任意JSON结构。searchedAt格式:yyyy-MM-dd HH:mm:ss')}
+         {label:'平台结果(platformResults)', name:'platformResults', placeholder:'[{"platformId":"yiyaocheng","platformName":"壹药城"}]', default:'[{"platformId":"yiyaocheng","platformName":"壹药城","response":{"status":"ok","products":[{"drugName":"阿莫西林","price":12.5}]}}]', type:'json'},
+         {label:'搜索日志(searchLog)', name:'searchLog', placeholder:'null', default:'null', type:'json'}], true, 'searchRecordResponse', '保存一次完整的比价搜索快照。platformResults为JSON数组,每个元素含platformId/platformName/response;后端会自动计算platformCount(平台数)和quoteCount(报价条数)。searchedAt格式:yyyy-MM-dd HH:mm:ss')}
     ${testSection('sr-list', '搜索记录列表', 'GET', 'get', '/api/search-record/history?pageNo={pageNo}&pageSize={pageSize}', '需认证', 'auth-required',
         [{label:'页码(pageNo)', name:'pageNo', placeholder:'1', default:'1'},
-         {label:'每页条数(pageSize)', name:'pageSize', placeholder:'20', default:'20'}], true, null, '分页查询当前用户搜索记录,返回分页对象含total/records/pages等字段')}
+         {label:'每页条数(pageSize)', name:'pageSize', placeholder:'20', default:'20'}], true, 'searchRecordResponse', '分页查询当前用户搜索记录(轻量摘要,不含platformResults/searchLog大字段),返回分页对象含total/records/pages等字段')}
     ${testSection('sr-detail', '记录详情', 'GET', 'get', '/api/search-record/history/{id}', '需认证', 'auth-required',
-        [{label:'记录ID(id)', name:'id', placeholder:'1', default:'1'}], true, null, '读取单条搜索记录详情(包含完整的platformResults)')}
+        [{label:'记录ID(id)', name:'id', placeholder:'1', default:'1'}], true, 'searchRecordResponse', '读取单条搜索记录详情(含完整platformResults和searchLog)')}
     ${testSection('sr-delete', '删除记录', 'DELETE', 'delete', '/api/search-record/history/{id}', '需认证', 'auth-required',
         [{label:'记录ID(id)', name:'id', placeholder:'1', default:'1'}], false, null, '删除单条搜索记录(幂等,不存在也返回成功)')}
     ${testSection('sr-clear', '清空记录', 'DELETE', 'delete', '/api/search-record/history', '需认证', 'auth-required', null, false, null, '清空当前用户全部搜索记录')}`;

+ 6 - 5
zhijiayun-user/src/main/java/com/xuekairui/user/dto/GrantMembershipRequest.java

@@ -1,6 +1,7 @@
 package com.xuekairui.user.dto;
 
-import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.xuekairui.common.FlexibleLocalDateTimeDeserializer;
 import jakarta.validation.constraints.NotBlank;
 import jakarta.validation.constraints.NotNull;
 import lombok.Data;
@@ -26,15 +27,15 @@ public class GrantMembershipRequest {
     private String level;
 
     /**
-     * 生效开始时间(可选,默认当前时间,格式:yyyy-MM-dd HH:mm:ss
+     * 生效开始时间(可选,默认当前时间,兼容 yyyy-MM-dd HH:mm:ss 和 ISO-8601 UTC
      */
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonDeserialize(using = FlexibleLocalDateTimeDeserializer.class)
     private LocalDateTime effectiveFrom;
 
     /**
-     * 生效结束时间(可选,NULL表示永久,格式:yyyy-MM-dd HH:mm:ss
+     * 生效结束时间(可选,NULL表示永久,兼容 yyyy-MM-dd HH:mm:ss 和 ISO-8601 UTC
      */
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JsonDeserialize(using = FlexibleLocalDateTimeDeserializer.class)
     private LocalDateTime effectiveTo;
 
     /**

+ 4 - 3
zhijiayun-user/src/main/java/com/xuekairui/user/dto/SearchRecordSaveRequest.java

@@ -1,6 +1,7 @@
 package com.xuekairui.user.dto;
 
-import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
+import com.xuekairui.common.FlexibleLocalDateTimeDeserializer;
 import jakarta.validation.constraints.NotBlank;
 import lombok.Data;
 
@@ -19,8 +20,8 @@ public class SearchRecordSaveRequest {
     @NotBlank(message = "药品名称不能为空")
     private String drugName;
 
-    /** 搜索时间(格式:yyyy-MM-dd HH:mm:ss) */
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    /** 搜索时间(兼容 yyyy-MM-dd HH:mm:ss 和 ISO-8601 UTC 两种格式) */
+    @JsonDeserialize(using = FlexibleLocalDateTimeDeserializer.class)
     private LocalDateTime searchedAt;
 
     /** 状态文案 */