Quellcode durchsuchen

优化代码,添加接口

liuchengsen vor 1 Monat
Ursprung
Commit
1238bcd8c2

+ 5 - 0
pom.xml

@@ -141,6 +141,11 @@
                 <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
                 <version>${mybatis-plus.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.baomidou</groupId>
+                <artifactId>mybatis-plus-jsqlparser</artifactId>
+                <version>${mybatis-plus.version}</version>
+            </dependency>
 
             <!-- 阿里云短信 -->
             <dependency>

+ 17 - 0
zhijiayun-gateway/src/main/resources/db/migration-v2.sql

@@ -59,6 +59,8 @@ CREATE TABLE IF NOT EXISTS `t_search_record` (
     `status_text`       VARCHAR(500)  DEFAULT NULL COMMENT '状态文案',
     `platform_results`  LONGTEXT      DEFAULT NULL COMMENT '平台搜索结果(JSON文本)',
     `search_log`        LONGTEXT      DEFAULT NULL COMMENT '搜索日志(JSON文本,可为null)',
+    `platform_count`    INT           NOT NULL DEFAULT 0 COMMENT '涉及平台数量(冗余存储,避免列表查询时解析大字段)',
+    `quote_count`       INT           NOT NULL DEFAULT 0 COMMENT '总报价条数(冗余存储,避免列表查询时解析大字段)',
     `create_time`       DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     PRIMARY KEY (`id`),
     KEY `idx_user_id` (`user_id`),
@@ -168,3 +170,18 @@ SET @sql = IF(@col_exists = 0, 'ALTER TABLE `t_invite_config` ADD COLUMN `instru
 PREPARE stmt FROM @sql;
 EXECUTE stmt;
 DEALLOCATE PREPARE stmt;
+
+-- ============================================
+-- 7. DDL: t_search_record 新增 platform_count / quote_count 列
+-- ============================================
+SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_search_record' AND COLUMN_NAME = 'platform_count');
+SET @sql = IF(@col_exists = 0, 'ALTER TABLE `t_search_record` ADD COLUMN `platform_count` INT NOT NULL DEFAULT 0 COMMENT ''涉及平台数量'' AFTER `search_log`', 'SELECT 1');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+SET @col_exists = (SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 't_search_record' AND COLUMN_NAME = 'quote_count');
+SET @sql = IF(@col_exists = 0, 'ALTER TABLE `t_search_record` ADD COLUMN `quote_count` INT NOT NULL DEFAULT 0 COMMENT ''总报价条数'' AFTER `platform_count`', 'SELECT 1');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;

+ 2 - 0
zhijiayun-gateway/src/main/resources/db/schema.sql

@@ -550,6 +550,8 @@ CREATE TABLE IF NOT EXISTS `t_search_record` (
     `status_text`       VARCHAR(500)  DEFAULT NULL COMMENT '状态文案',
     `platform_results`  LONGTEXT      DEFAULT NULL COMMENT '平台搜索结果(JSON文本)',
     `search_log`        LONGTEXT      DEFAULT NULL COMMENT '搜索日志(JSON文本,可为null)',
+    `platform_count`    INT           NOT NULL DEFAULT 0 COMMENT '涉及平台数量(冗余存储,避免列表查询时解析大字段)',
+    `quote_count`       INT           NOT NULL DEFAULT 0 COMMENT '总报价条数(冗余存储,避免列表查询时解析大字段)',
     `create_time`       DATETIME      DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     PRIMARY KEY (`id`),
     KEY `idx_user_id` (`user_id`),

+ 2 - 0
zhijiayun-gateway/src/test/resources/test-schema.sql

@@ -198,6 +198,8 @@ CREATE TABLE IF NOT EXISTS t_search_record (
     status_text       VARCHAR(500)  DEFAULT NULL COMMENT '状态文案',
     platform_results  LONGTEXT      DEFAULT NULL COMMENT '平台搜索结果(JSON文本)',
     search_log        LONGTEXT      DEFAULT NULL COMMENT '搜索日志(JSON文本)',
+    platform_count    INT           NOT NULL DEFAULT 0 COMMENT '涉及平台数量',
+    quote_count       INT           NOT NULL DEFAULT 0 COMMENT '总报价条数',
     create_time       TIMESTAMP     DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
     PRIMARY KEY (id),
     INDEX idx_user_id (user_id),

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

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

+ 16 - 6
zhijiayun-test/src/main/resources/static/invite-test.html

@@ -286,6 +286,7 @@ let stats = { pass: 0, fail: 0, pending: 0, total: 0 };
  * 示例:":2069620495151230978," → ":\"2069620495151230978\","
  */
 function safeJsonParse(text) {
+    if (!text || text.trim() === '' || text.trim() === 'null') return null;
     const safeText = text.replace(/([:,\[]\s*)(\d{16,})(\s*[,\]\}])/g, '$1"$2"$3');
     return JSON.parse(safeText);
 }
@@ -647,6 +648,11 @@ function renderModule(module) {
     case 'admin-audit': html = renderAdminAuditModule(); break;
     }
     panels.innerHTML = html;
+    // 修复含引号的默认值:从 data-raw 属性恢复正确的 input value
+    panels.querySelectorAll('input[data-raw]').forEach(input => {
+        input.value = input.getAttribute('data-raw');
+        input.removeAttribute('data-raw');
+    });
 }
 
 function toValidatorName(id) {
@@ -1146,7 +1152,10 @@ function testSection(id, title, method, methodClass, path, authTag, authClass, f
     if (fields) {
         fieldsHtml = '<div class="form-grid">';
         fields.forEach(f => {
-            fieldsHtml += `<div class="form-group"><label>${f.label}</label><input type="text" id="${id}-${f.name}" placeholder="${f.placeholder || ''}" value="${f.default || ''}"></div>`;
+            const escapedDefault = (f.default || '').replace(/"/g, '&quot;');
+            const escapedPlaceholder = (f.placeholder || '').replace(/"/g, '&quot;');
+            const hasRaw = f.default && f.default.includes('"');
+            fieldsHtml += `<div class="form-group"><label>${f.label}</label><input type="text" id="${id}-${f.name}" placeholder="${escapedPlaceholder}" value="${escapedDefault}"${hasRaw ? ` data-raw="${escapedDefault}"` : ''}></div>`;
         });
         fieldsHtml += '</div>';
     }
@@ -1488,12 +1497,13 @@ function renderSearchRecordModule() {
     </div>
     ${testSection('sr-save', '保存搜索记录', 'POST', 'post', '/api/search-record/history', '需认证', 'auth-required',
         [{label:'药品名称(drugName)', name:'drugName', placeholder:'阿莫西林', default:'阿莫西林'},
-         {label:'搜索时间(searchedAt)', name:'searchedAt', placeholder:'2026-06-26T08:30:00.000Z', default:'2026-06-26T08:30:00.000Z'},
+         {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结构')}
-    ${testSection('sr-list', '搜索记录列表', 'GET', 'get', '/api/search-record/history?limit={limit}', '需认证', 'auth-required',
-        [{label:'条数(limit)', name:'limit', placeholder:'20', default:'20'}], true, null, '获取当前用户最近的搜索记录,默认20条,最多50条')}
+         {label:'搜索日志(searchLog)', name:'searchLog', placeholder:'null', default:'null', type:'json'}], true, null, '保存一次完整的比价搜索快照,包含平台响应数据。platformResults和searchLog支持任意JSON结构。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等字段')}
     ${testSection('sr-detail', '记录详情', 'GET', 'get', '/api/search-record/history/{id}', '需认证', 'auth-required',
         [{label:'记录ID(id)', name:'id', placeholder:'1', default:'1'}], true, null, '读取单条搜索记录详情(包含完整的platformResults)')}
     ${testSection('sr-delete', '删除记录', 'DELETE', 'delete', '/api/search-record/history/{id}', '需认证', 'auth-required',
@@ -1974,7 +1984,7 @@ function validate_searchRecords(result) { return result.data?.code === 200 && re
 
 // 搜索记录模块验证器
 function validate_srSave(result) { return result.data?.code === 200 && result.data?.data?.id && result.data?.data?.drugName; }
-function validate_srList(result) { return result.data?.code === 200 && Array.isArray(result.data?.data); }
+function validate_srList(result) { return result.data?.code === 200 && result.data?.data?.records !== undefined; }
 function validate_srDetail(result) { return result.data?.code === 200 && result.data?.data?.id; }
 // 平台会话模块验证器
 function validate_psUpload(result) { return result.data?.code === 200 && result.data?.data?.platformCode; }

+ 4 - 0
zhijiayun-user/pom.xml

@@ -36,6 +36,10 @@
             <groupId>com.baomidou</groupId>
             <artifactId>mybatis-plus-spring-boot3-starter</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.baomidou</groupId>
+            <artifactId>mybatis-plus-jsqlparser</artifactId>
+        </dependency>
         <dependency>
             <groupId>org.springframework.boot</groupId>
             <artifactId>spring-boot-starter-data-redis</artifactId>

+ 34 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/config/DevCorsConfig.java

@@ -0,0 +1,34 @@
+package com.xuekairui.user.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Profile;
+import org.springframework.web.cors.CorsConfiguration;
+import org.springframework.web.cors.CorsConfigurationSource;
+import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * 开发环境 CORS 配置
+ * 仅在 dev profile 下生效,为本地跨域开发(如 localhost:8002 访问 localhost:8001)提供兜底。
+ * 生产环境 CORS 由 Nginx 统一处理,此配置不会介入。
+ */
+@Configuration
+@Profile("dev")
+public class DevCorsConfig {
+
+    @Bean
+    public CorsConfigurationSource corsConfigurationSource() {
+        CorsConfiguration config = new CorsConfiguration();
+        config.setAllowedOriginPatterns(List.of("*"));
+        config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
+        config.setAllowedHeaders(List.of("*"));
+        config.setAllowCredentials(true);
+        config.setMaxAge(3600L);
+        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
+        source.registerCorsConfiguration("/**", config);
+        return source;
+    }
+}

+ 10 - 2
zhijiayun-user/src/main/java/com/xuekairui/user/config/SecurityConfig.java

@@ -7,6 +7,7 @@ import com.xuekairui.user.security.JwtAuthenticationFilter;
 import lombok.RequiredArgsConstructor;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
 import org.springframework.http.HttpMethod;
 import org.springframework.http.HttpStatus;
 import org.springframework.http.MediaType;
@@ -29,6 +30,7 @@ import java.nio.charset.StandardCharsets;
 public class SecurityConfig {
 
     private final JwtAuthenticationFilter jwtAuthenticationFilter;
+    private final Environment environment;
 
     /** 公开接口白名单 */
     private static final String[] PUBLIC_URLS = {
@@ -59,10 +61,16 @@ public class SecurityConfig {
 
     @Bean
     public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
+        // dev 环境启用后端 CORS 兜底(DevCorsConfig 提供配置源),生产环境由 Nginx 统一处理
+        boolean isDev = environment.acceptsProfiles(org.springframework.core.env.Profiles.of("dev"));
+        if (isDev) {
+            http.cors(cors -> {});  // 使用默认,自动发现 CorsConfigurationSource Bean
+        } else {
+            http.cors(AbstractHttpConfigurer::disable);
+        }
+
         http
                 .csrf(AbstractHttpConfigurer::disable)
-                // CORS 由 Nginx 统一处理,后端关闭,避免重复/冲突
-                .cors(AbstractHttpConfigurer::disable)
                 .sessionManagement(session ->
                         session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                 .authorizeHttpRequests(auth -> auth

+ 1 - 1
zhijiayun-user/src/main/java/com/xuekairui/user/controller/SearchRecordController.java

@@ -39,7 +39,7 @@ public class SearchRecordController {
 
     /**
      * 分页读取当前用户最近的主搜索历史
-     * GET /api/search-record/history?page=1&size=20
+     * GET /api/search-record/history?pageNo=1&pageSize=20
      */
     @GetMapping("/history")
     public Result<Page<SearchRecordResponse>> listRecent(

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

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

+ 6 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/dto/SearchRecordResponse.java

@@ -28,6 +28,12 @@ public class SearchRecordResponse {
     /** 状态文案 */
     private String statusText;
 
+    /** 涉及平台数量 */
+    private Integer platformCount;
+
+    /** 总报价条数 */
+    private Integer quoteCount;
+
     /** 平台搜索结果(结构化JSON对象) */
     private Object platformResults;
 

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

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

+ 6 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/entity/SearchRecord.java

@@ -40,6 +40,12 @@ public class SearchRecord {
     /** 搜索日志(JSON文本,可为null) */
     private String searchLog;
 
+    /** 涉及平台数量(冗余存储,避免列表查询时解析大字段) */
+    private Integer platformCount;
+
+    /** 总报价条数(冗余存储,避免列表查询时解析大字段) */
+    private Integer quoteCount;
+
     /** 创建时间 */
     @TableField(fill = FieldFill.INSERT)
     private LocalDateTime createTime;

+ 12 - 0
zhijiayun-user/src/main/java/com/xuekairui/user/mapper/SearchRecordMapper.java

@@ -1,12 +1,24 @@
 package com.xuekairui.user.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.xuekairui.user.entity.SearchRecord;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
 
 /**
  * 搜索记录 Mapper
  */
 @Mapper
 public interface SearchRecordMapper extends BaseMapper<SearchRecord> {
+
+    /**
+     * 分页查询用户搜索记录摘要(排除 LONGTEXT 大字段,避免列表查询时加载 platform_results/search_log)
+     */
+    @Select("SELECT id, user_id, drug_name, searched_at, status_text, " +
+            "platform_count, quote_count, create_time " +
+            "FROM t_search_record WHERE user_id = #{userId} " +
+            "ORDER BY searched_at DESC")
+    Page<SearchRecord> selectSummaryPage(Page<SearchRecord> page, @Param("userId") Long userId);
 }

+ 62 - 11
zhijiayun-user/src/main/java/com/xuekairui/user/service/SearchRecordService.java

@@ -31,35 +31,38 @@ public class SearchRecordService {
     private final ObjectMapper objectMapper;
 
     /**
-     * 保存一次主搜索比价记录
+     * 保存一次主搜索比价记录(同时计算并存储 platformCount / quoteCount)
      */
     @Transactional
     public SearchRecordResponse saveRecord(Long userId, SearchRecordSaveRequest request) {
+        Object platformData = request.getPlatformResults();
+        String platformJson = toJsonString(platformData);
         SearchRecord record = SearchRecord.builder()
                 .userId(userId)
                 .drugName(request.getDrugName().trim())
                 .searchedAt(LocalDateTime.now(ZoneId.of("Asia/Shanghai")))
                 .statusText(request.getStatusText())
-                .platformResults(toJsonString(request.getPlatformResults()))
+                .platformResults(platformJson)
                 .searchLog(toJsonString(request.getSearchLog()))
+                .platformCount(countPlatforms(platformData))
+                .quoteCount(countQuotes(platformData))
                 .build();
         searchRecordMapper.insert(record);
-        log.info("保存搜索记录: userId={}, drugName={}, recordId={}", userId, record.getDrugName(), record.getId());
+        log.info("保存搜索记录: userId={}, drugName={}, recordId={}, platforms={}, quotes={}",
+                userId, record.getDrugName(), record.getId(), record.getPlatformCount(), record.getQuoteCount());
         return toResponse(record);
     }
 
     /**
-     * 分页读取当前用户最近的主搜索历史
+     * 分页读取当前用户最近的主搜索历史(不加载 LONGTEXT 大字段,性能优化)
      */
     public Page<SearchRecordResponse> listRecent(Long userId, Integer page, Integer size) {
         int pageNo = page != null && page > 0 ? page : 1;
         int pageSize = Math.min(size != null && size > 0 ? size : 20, 50);
         Page<SearchRecord> pageParam = new Page<>(pageNo, pageSize);
-        LambdaQueryWrapper<SearchRecord> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(SearchRecord::getUserId, userId)
-                .orderByDesc(SearchRecord::getSearchedAt);
-        Page<SearchRecord> resultPage = searchRecordMapper.selectPage(pageParam, wrapper);
-        return (Page<SearchRecordResponse>) resultPage.convert(this::toResponse);
+        // 使用摘要查询,排除 platform_results / search_log 两个 LONGTEXT 字段
+        Page<SearchRecord> resultPage = searchRecordMapper.selectSummaryPage(pageParam, userId);
+        return (Page<SearchRecordResponse>) resultPage.convert(this::toListResponse);
     }
 
     /**
@@ -96,7 +99,7 @@ public class SearchRecordService {
     }
 
     /**
-     * 实体转响应DTO
+     * 实体转响应DTO(详情用:含完整 platformResults / searchLog)
      */
     private SearchRecordResponse toResponse(SearchRecord record) {
         return SearchRecordResponse.builder()
@@ -104,11 +107,56 @@ public class SearchRecordService {
                 .drugName(record.getDrugName())
                 .searchedAt(record.getSearchedAt())
                 .statusText(record.getStatusText())
+                .platformCount(record.getPlatformCount())
+                .quoteCount(record.getQuoteCount())
                 .platformResults(parseJson(record.getPlatformResults()))
                 .searchLog(parseJson(record.getSearchLog()))
                 .build();
     }
 
+    /**
+     * 实体转响应DTO(列表用:不含大字段,平台数/报价数直接读 DB 冗余列)
+     */
+    private SearchRecordResponse toListResponse(SearchRecord record) {
+        return SearchRecordResponse.builder()
+                .id(record.getId())
+                .drugName(record.getDrugName())
+                .searchedAt(record.getSearchedAt())
+                .statusText(record.getStatusText())
+                .platformCount(record.getPlatformCount())
+                .quoteCount(record.getQuoteCount())
+                .build();
+    }
+
+    /** 从 platformResults 中统计平台数量 */
+    private int countPlatforms(Object parsedResults) {
+        if (parsedResults instanceof java.util.List) {
+            return ((java.util.List<?>) parsedResults).size();
+        }
+        return 0;
+    }
+
+    /** 从 platformResults 中统计总报价条数(累加每个平台的 response.products 数组长度) */
+    private int countQuotes(Object parsedResults) {
+        if (!(parsedResults instanceof java.util.List)) {
+            return 0;
+        }
+        int count = 0;
+        for (Object item : (java.util.List<?>) parsedResults) {
+            if (item instanceof java.util.Map) {
+                java.util.Map<?, ?> platform = (java.util.Map<?, ?>) item;
+                Object response = platform.get("response");
+                if (response instanceof java.util.Map) {
+                    Object products = ((java.util.Map<?, ?>) response).get("products");
+                    if (products instanceof java.util.List) {
+                        count += ((java.util.List<?>) products).size();
+                    }
+                }
+            }
+        }
+        return count;
+    }
+
     /**
      * 对象序列化为JSON字符串(存储用)
      */
@@ -117,7 +165,7 @@ public class SearchRecordService {
             return objectMapper.writeValueAsString(obj);
         } catch (JsonProcessingException e) {
             log.warn("JSON序列化失败: {}", e.getMessage());
-            return null;
+            return String.valueOf(obj);
         }
     }
 
@@ -125,6 +173,9 @@ public class SearchRecordService {
      * JSON字符串反序列化为对象(返回用)
      */
     private Object parseJson(String json) {
+        if (json == null) {
+            return null;
+        }
         try {
             return objectMapper.readValue(json, Object.class);
         } catch (JsonProcessingException e) {