Java 两个List(取交集/左连接)

张开发
2026/4/8 17:20:08 15 分钟阅读

分享文章

Java 两个List(取交集/左连接)
示例代码/** * 查询活动可编辑字段列表 * p根据活动ID查询该活动允许编辑的字段列表结果使用缓存以提高性能。/p * * param activityId 活动ID * return 可编辑字段DTO列表 */ Cacheable(value activityEditableFields, key #activityId) public ListActivityEditableFieldDTO queryActivityEditableFields(Integer activityId) { log.debug(【工作流程】查询活动可编辑字段activityId{}, activityId); ListActivityEditableField editableFieldsEntities activityEditableFieldMapper.selectList( new LambdaQueryWrapperActivityEditableField().eq(ActivityEditableField::getActivityId, activityId) ); if (editableFieldsEntities.isEmpty()) { return new ArrayList(); } // 1. 收集所有涉及的表名去重 SetString tableNames editableFieldsEntities.stream() .map(ActivityEditableField::getTableName) .filter(Objects::nonNull) .collect(Collectors.toSet()); // 2. 批量查询所有表的数据字典构建全局映射 MapString, DataDictionary dictMap new HashMap(); for (String tableName : tableNames) { ListDataDictionary dictionaries queryDataDictionarys(tableName); // 该方法已有 Cacheable for (DataDictionary dd : dictionaries) { String key dd.getTableName() : dd.getFieldName(); dictMap.putIfAbsent(key, dd); // 避免重复覆盖 } } // 3. 单次遍历构建结果优化性能 ListActivityEditableFieldDTO result new ArrayList(editableFieldsEntities.size()); for (ActivityEditableField entity : editableFieldsEntities) { // 跳过关键字段为 null 的记录 if (entity.getTableName() null || entity.getFieldName() null) { log.warn(活动可编辑字段存在空表名或空字段名activityId{}, entity{}, activityId, entity); continue; } // 组合键通过 ActivityEditableField 的 tableName fieldName DataDictionary 的 tableName fieldName 取交集 String key entity.getTableName() : entity.getFieldName(); DataDictionary dict dictMap.get(key); // 不在数据字典中也加入结果左连接 // if (dict null) continue; // 不在数据字典中不加入结果取交集 ActivityEditableFieldDTO dto editableFieldConverter.toDTO(entity); dto.setFieldDisplay(dict ! null ? dict.getFieldDisplay() : null); result.add(dto); } return result; } /** * 查询数据字典列表按表名 * p已添加缓存避免重复查询/p */ Cacheable(value dataDictionarys, key #tableName) public ListDataDictionary queryDataDictionarys(String tableName) { return dataDictionaryMapper.selectList( new LambdaQueryWrapperDataDictionary().eq(DataDictionary::getTableName, tableName) ); }您的代码可以运行没有语法错误且左连接的核心语义已实现不在数据字典中的字段仍会返回fieldDisplay设为null。不过有一处细节与严格左连接略有差异当entity.getTableName()或entity.getFieldName()为null时您使用continue跳过了该记录而不是保留并设置fieldDisplay null。建议根据业务需求确认场景当前行为严格左连接行为可编辑字段的 tableName 或 fieldName 为 null脏数据跳过不返回返回fieldDisplay null可编辑字段正常但字典中无匹配返回fieldDisplay null返回fieldDisplay null✅修改建议如果需要保留空键记录java// 不再 continue而是正常加入结果fieldDisplay 为 null if (entity.getTableName() null || entity.getFieldName() null) { log.warn(活动可编辑字段存在空表名或空字段名activityId{}, entity{}, activityId, entity); ActivityEditableFieldDTO dto editableFieldConverter.toDTO(entity); dto.setFieldDisplay(null); result.add(dto); continue; // 或直接走下面的通用逻辑但需要避免 NPE }其他方面评价✅ 批量收集tableNames并按表查询数据字典避免了全表扫描性能良好。✅ 使用dictMap.putIfAbsent避免重复键覆盖安全。✅ 利用Cacheable缓存数据字典和最终结果合理。✅ 代码结构清晰注释完整。结论当前实现对于正常数据完全满足左连接需求且对空键的过滤可能是您期望的防御性处理。如果业务上允许丢弃空键记录则代码“可以”如果要求保留只需调整空键分支即可。spring boot 请实现todo的功能内容 /** * 查询活动可编辑字段列表 * p根据活动ID查询该活动允许编辑的字段列表结果使用缓存以提高性能。/p * * param activityId 活动ID * return 可编辑字段DTO列表 */ Cacheable(value activityEditableFields, key #activityId) public ListActivityEditableFieldDTO queryActivityEditableFields(Integer activityId) { log.debug(【工作流程】查询活动可编辑字段activityId{}, activityId); ListActivityEditableField entities activityEditableFieldMapper.selectList( new LambdaQueryWrapperActivityEditableField().eq(ActivityEditableField::getActivityId, activityId) ); ListDataDictionary dataDictionaries dataDictionaryMapper.selectList(null); // 如何通过ActivityEditableField.tableName DataDictionary.tableName and ActivityEditableField.fieldName DataDictionary.fieldName生成两个List的交集最终生成ListActivityEditableFieldDTO // TODO return entities.stream() .map(entity - { ActivityEditableFieldDTO dto new ActivityEditableFieldDTO(); BeanUtils.copyProperties(entity, dto); return dto; }) .collect(Collectors.toList()); } package com.weiyu.workflow.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; /** * 活动可编辑字段实体类 */ Data NoArgsConstructor EqualsAndHashCode(callSuper false) TableName(EditableFieldsOfActivity) public class ActivityEditableField { /** * 主键 ID */ TableId(value efo_ID, type IdType.AUTO) private Integer id; /** * 活动 ID */ TableField(efo_ActivityID) NotNull Min(1) private Integer activityId; /** * 表名 */ TableField(efo_TableName) NotNull Size(max 100) private String tableName; /** * 字段名 */ TableField(efo_FieldName) NotNull Size(max 100) private String fieldName; /** * 字段别名 */ TableField(efo_FieldAlias) Size(max 100) private String fieldAlias; /** * 不能为空 */ TableField(efo_NotNull) private Boolean notNull; } package com.weiyu.workflow.entity; import com.baomidou.mybatisplus.annotation.TableField; import com.baomidou.mybatisplus.annotation.TableName; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import lombok.NoArgsConstructor; /** * 数据字典实体类 */ Schema(description 数据字典实体类) Data NoArgsConstructor TableName(DataDictionary) public class DataDictionary { /** * 表名 */ TableField(dd_TableName) private String tableName; /** * 字段名称 */ TableField(dd_FieldName) private String fieldName; /** * 字段显示名称 */ TableField(dd_ChineseName) private String fieldDisplay; /** * 是否显示 */ TableField(dd_Visible) private boolean visible true; } package com.weiyu.workflow.dto; import lombok.Data; import lombok.NoArgsConstructor; /** * 活动可编辑字段 DTO */ Data NoArgsConstructor public class ActivityEditableFieldDTO { /** * 字段名 */ private String fieldName; /** * 字段别名 */ private String fieldAlias; /** * 不能为空 */ private Boolean notNull; /** * 字段显示名称 */ private String fieldDisplay; }根据您的需求需要将ActivityEditableField与DataDictionary通过(tableName, fieldName)进行匹配取交集并生成ActivityEditableFieldDTO其中fieldDisplay来自DataDictionary。以下是替换 TODO 部分的实现代码java// 构建 DataDictionary 映射key 为 tableName:fieldName MapString, DataDictionary dictionaryMap dataDictionaries.stream() .collect(Collectors.toMap( dd - dd.getTableName() : dd.getFieldName(), Function.identity(), (existing, replacement) - existing // 如有重复 key保留第一个 )); ListActivityEditableFieldDTO result new ArrayList(); for (ActivityEditableField entity : entities) { String key entity.getTableName() : entity.getFieldName(); DataDictionary dictionary dictionaryMap.get(key); if (dictionary ! null) { ActivityEditableFieldDTO dto new ActivityEditableFieldDTO(); BeanUtils.copyProperties(entity, dto); dto.setFieldDisplay(dictionary.getFieldDisplay()); result.add(dto); } } return result;完整方法体示例javaCacheable(value activityEditableFields, key #activityId) public ListActivityEditableFieldDTO queryActivityEditableFields(Integer activityId) { log.debug(【工作流程】查询活动可编辑字段activityId{}, activityId); ListActivityEditableField entities activityEditableFieldMapper.selectList( new LambdaQueryWrapperActivityEditableField().eq(ActivityEditableField::getActivityId, activityId) ); ListDataDictionary dataDictionaries dataDictionaryMapper.selectList(null); // 构建 DataDictionary 映射 MapString, DataDictionary dictionaryMap dataDictionaries.stream() .collect(Collectors.toMap( dd - dd.getTableName() : dd.getFieldName(), Function.identity(), (existing, replacement) - existing )); ListActivityEditableFieldDTO result new ArrayList(); for (ActivityEditableField entity : entities) { String key entity.getTableName() : entity.getFieldName(); DataDictionary dictionary dictionaryMap.get(key); if (dictionary ! null) { // 只保留交集中的记录 ActivityEditableFieldDTO dto new ActivityEditableFieldDTO(); BeanUtils.copyProperties(entity, dto); dto.setFieldDisplay(dictionary.getFieldDisplay()); result.add(dto); } } return result; }说明使用tableName:fieldName作为组合键确保精确匹配。只返回在DataDictionary中存在的可编辑字段即交集符合“生成两个 List 的交集”的注释要求。fieldDisplay从DataDictionary的dd_ChineseName字段获取并设置到 DTO 中。若需要包含所有ActivityEditableField但仅填充存在的fieldDisplay可移除if (dictionary ! null)判断并在未匹配时设置fieldDisplay为null。当前按交集逻辑实现。请讲解两个List取交集的思路两个 List 取交集的核心思路是基于某种匹配规则找出同时存在于两个集合中的元素。在 Java 中常见做法有两种1. 双重循环暴力匹配原理遍历第一个 List对每个元素再遍历第二个 List按规则比较是否相等若相等则加入结果集。时间复杂度O(n × m)适合数据量很小如几十个的场景。示例javaListA list1 ...; ListB list2 ...; ListA intersection new ArrayList(); for (A a : list1) { for (B b : list2) { if (a.getKey().equals(b.getKey())) { intersection.add(a); break; } } }2. 哈希索引高效匹配原理将其中一个 List 转换为MapKey, Element然后遍历另一个 List用 O(1) 时间判断 Key 是否存在存在则加入结果。时间复杂度O(n m)构建 Map O(m)遍历 n 次查询推荐用于多数生产场景。关键点需要确定一个唯一匹配键例如组合tableName : fieldName且两个对象的键必须能正确比较重写hashCode/equals或使用字符串等不可变键。示例对应您的场景java// 构建字典映射组合键 - DataDictionary 对象 MapString, DataDictionary dictMap dataDictionaries.stream() .collect(Collectors.toMap( dd - dd.getTableName() : dd.getFieldName(), Function.identity() )); // 遍历实体取交集 ListActivityEditableFieldDTO result entities.stream() .filter(entity - dictMap.containsKey(entity.getTableName() : entity.getFieldName())) .map(entity - { /* 转换并填充 fieldDisplay */ }) .collect(Collectors.toList());针对您代码的匹配规则匹配条件ActivityEditableField.tableName DataDictionary.tableName且ActivityEditableField.fieldName DataDictionary.fieldName。组合键使用两个字段拼接成唯一字符串注意分隔符避免冲突例如|或:。结果交集只保留那些在DataDictionary中有完整元数据fieldDisplay的可编辑字段。注意事项键的唯一性确保组合键在DataDictionary中唯一如数据库设计允许多条相同(tableName, fieldName)则需要额外处理例如取第一条或合并。空值处理tableName或fieldName可能为 null需决定是否跳过或转为特殊键如null:null。性能选择数据量几百以内双重循环可接受超过建议用哈希索引。保留原对象信息交集结果通常携带第一个 List 的字段同时从第二个 List 中提取额外属性如fieldDisplay。扩展如果不需要严格交集而是“左连接”有时需要保留第一个 List 的所有元素第二个 List 有匹配就填充没有就留空。此时改用Map.getOrDefault或直接遍历判断即可无需过滤。与直接使用join从数据库中查询返回哪种方式更佳在您的场景中使用数据库 JOIN 查询通常更佳原因如下1. 缓存效率差异显著当前方法带有Cacheable但方法体内每次都会无条件执行dataDictionaryMapper.selectList(null)—— 即使活动字段结果已缓存数据字典全表仍会被重复查询。若数据字典表较大或访问频繁会浪费数据库资源和网络开销。而使用 JOIN缓存未命中时只执行一次 JOIN 查询获取所有必要字段。缓存命中时完全不访问数据库。2. 性能对比关键指标维度内存交集当前数据库 JOIN数据库交互次数2次活动字段 全量数据字典1次数据传输量全量数据字典 活动字段仅关联后的结果缓存命中后仍需查询全量数据字典除非单独缓存不查数据库数据量增长影响数据字典越大内存/网络浪费越严重仅随活动字段数量增长3. 代码简洁性与维护性JOIN 方式推荐javaCacheable(value activityEditableFields, key #activityId) public ListActivityEditableFieldDTO queryActivityEditableFields(Integer activityId) { return activityEditableFieldMapper.selectEditableFieldsWithDict(activityId); }Mapper 中定义javaSelect(SELECT a.efo_FieldName as fieldName, a.efo_FieldAlias as fieldAlias, a.efo_NotNull as notNull, d.dd_ChineseName as fieldDisplay FROM EditableFieldsOfActivity a INNER JOIN DataDictionary d ON a.efo_TableName d.dd_TableName AND a.efo_FieldName d.dd_FieldName WHERE a.efo_ActivityID #{activityId}) ListActivityEditableFieldDTO selectEditableFieldsWithDict(Integer activityId);无需手动处理交集、Map、空值判断。4. 内存交集的适用场景内存交集只在以下情况更有优势两个数据源无法在数据库层 JOIN例如来自不同数据库或外部API。数据量极小且访问频率极低不想增加 SQL 复杂度。需要对结果做复杂的自定义合并逻辑如优先级、去重策略。您的场景不存在这些限制。5. 如果必须保留内存交集为了改善当前实现至少需要对数据字典查询增加缓存javaCacheable(value dataDictionary) public ListDataDictionary getAllDataDictionaries() { return dataDictionaryMapper.selectList(null); }然后在原方法中调用该缓存方法避免重复查库。但这样仍比 JOIN 多一次内存操作和一次数据库交互缓存未命中时。结论采用 JOIN 方式性能更优、代码更短、缓存效果更好。同时JOIN 查询仍可被Cacheable完美支持无需额外处理。采用内存交集的还有更好的实现吗针对当前内存交集实现可以从性能、简洁性、健壮性三个维度进行优化。以下提供几种更好的实现方式1. 优化版单次 Map 查找避免重复计算 Key当前filter map会两次计算组合键、两次查找dictMap。优化为一次查找并过滤nulljavaListActivityEditableFieldDTO result entities.stream() .map(entity - { String key entity.getTableName() : entity.getFieldName(); DataDictionary dict dictMap.get(key); if (dict null) return null; ActivityEditableFieldDTO dto new ActivityEditableFieldDTO(); BeanUtils.copyProperties(entity, dto); dto.setFieldDisplay(dict.getFieldDisplay()); return dto; }) .filter(Objects::nonNull) .collect(Collectors.toList());优点每个 entity 只计算一次组合键、一次 Map 查询。代码更紧凑性能略有提升约减少 30%~50% 的哈希操作。2. 使用传统 for 循环极致性能如果entities数量非常大例如 10 万Stream 的 Lambda 开销可能成为瓶颈。此时可改用 for 循环javaListActivityEditableFieldDTO result new ArrayList(entities.size()); for (ActivityEditableField entity : entities) { String key entity.getTableName() : entity.getFieldName(); DataDictionary dict dictMap.get(key); if (dict null) continue; ActivityEditableFieldDTO dto new ActivityEditableFieldDTO(); BeanUtils.copyProperties(entity, dto); dto.setFieldDisplay(dict.getFieldDisplay()); result.add(dto); }优点无 Stream 框架开销CPU 缓存更友好。可提前预估 ArrayList 容量避免扩容。3. 使用 Map.computeIfPresent函数式风格利用computeIfPresent仅当 Key 存在时才执行转换但需要生成临时对象可读性稍差javaListActivityEditableFieldDTO result entities.stream() .flatMap(entity - { String key entity.getTableName() : entity.getFieldName(); return Optional.ofNullable(dictMap.get(key)) .map(dict - { ActivityEditableFieldDTO dto new ActivityEditableFieldDTO(); BeanUtils.copyProperties(entity, dto); dto.setFieldDisplay(dict.getFieldDisplay()); return dto; }) .stream(); }) .collect(Collectors.toList());不推荐因为比优化版更复杂。4. 提前构建组合键的 Map避免重复拼接字符串如果ActivityEditableField对象会被多次访问可以预先计算组合键存入实体或使用包装类。但一般场景下字符串拼接开销可接受。5. 处理空值健壮性增强为防止tableName或fieldName为null导致拼接异常或匹配失败javaString key (entity.getTableName() null ? : entity.getTableName()) : (entity.getFieldName() null ? : entity.getFieldName());或直接跳过null字段javaif (entity.getTableName() null || entity.getFieldName() null) return null;推荐最终实现平衡性能与可读性javaListActivityEditableFieldDTO result new ArrayList(entities.size()); for (ActivityEditableField entity : entities) { if (entity.getTableName() null || entity.getFieldName() null) continue; String key entity.getTableName() : entity.getFieldName(); DataDictionary dict dictMap.get(key); if (dict null) continue; ActivityEditableFieldDTO dto new ActivityEditableFieldDTO(); BeanUtils.copyProperties(entity, dto); dto.setFieldDisplay(dict.getFieldDisplay()); result.add(dto); }为什么更好性能最优单次循环 单次 Map 查找无 Stream 额外开销。内存友好直接指定ArrayList初始容量减少扩容。健壮性显式处理null字段避免 NPE。可读性逻辑直观易于调试和修改。针对您提供的代码主要问题是只查询了第一个实体对应的表名导致其他表的字段无法匹配数据字典。以下是改进后的完整实现java/** * 查询活动可编辑字段列表 * p根据活动ID查询该活动允许编辑的字段列表结果使用缓存以提高性能。/p * * param activityId 活动ID * return 可编辑字段DTO列表 */ Cacheable(value activityEditableFields, key #activityId) public ListActivityEditableFieldDTO queryActivityEditableFields(Integer activityId) { log.debug(【工作流程】查询活动可编辑字段activityId{}, activityId); ListActivityEditableField editableFieldsEntities activityEditableFieldMapper.selectList( new LambdaQueryWrapperActivityEditableField().eq(ActivityEditableField::getActivityId, activityId) ); if (editableFieldsEntities.isEmpty()) { return new ArrayList(); } // 1. 收集所有涉及的表名去重 SetString tableNames editableFieldsEntities.stream() .map(ActivityEditableField::getTableName) .filter(Objects::nonNull) .collect(Collectors.toSet()); // 2. 批量查询所有表的数据字典构建全局映射 MapString, DataDictionary dictMap new HashMap(); for (String tableName : tableNames) { ListDataDictionary dictionaries queryDataDictionarys(tableName); // 该方法已有 Cacheable for (DataDictionary dd : dictionaries) { String key dd.getTableName() : dd.getFieldName(); dictMap.putIfAbsent(key, dd); // 避免重复覆盖 } } // 3. 单次遍历构建结果优化性能 ListActivityEditableFieldDTO result new ArrayList(editableFieldsEntities.size()); for (ActivityEditableField entity : editableFieldsEntities) { // 跳过关键字段为 null 的记录 if (entity.getTableName() null || entity.getFieldName() null) { log.warn(活动可编辑字段存在空表名或空字段名activityId{}, entity{}, activityId, entity); continue; } String key entity.getTableName() : entity.getFieldName(); DataDictionary dict dictMap.get(key); if (dict null) { // 不在数据字典中不加入结果取交集 continue; } ActivityEditableFieldDTO dto editableFieldConverter.toDTO(entity); dto.setFieldDisplay(dict.getFieldDisplay()); result.add(dto); } return result; } /** * 查询数据字典列表按表名 * 已添加缓存避免重复查询 */ Cacheable(value dataDictionarys, key #tableName) public ListDataDictionary queryDataDictionarys(String tableName) { return dataDictionaryMapper.selectList( new LambdaQueryWrapperDataDictionary().eq(DataDictionary::getTableName, tableName) ); }主要改进点问题原代码改进后多表支持只取第一个tableName其他表字段被错误过滤收集所有tableName分别查询并合并字典映射性能Stream 中两次计算key、两次Map查找单次for循环一次key计算和查找空安全未处理tableName/fieldName为null的情况显式跳过并记录警告日志结果容量未指定初始容量new ArrayList(entities.size())避免扩容补充说明缓存策略外层方法Cacheable缓存最终结果内层queryDataDictionarys按表名缓存数据字典二者配合高效。交集语义只保留同时在EditableFieldsOfActivity和DataDictionary中存在的字段符合原注释要求。扩展性如果未来需要支持“左连接”保留所有可编辑字段无字典时fieldDisplay为 null只需移除if (dict null) continue并设置dto.setFieldDisplay(null)即可。此改进在保证正确性的同时优化了性能与健壮性。如果想要左连接请实现左连接实现保留所有ActivityEditableField记录如果DataDictionary中有匹配则填充fieldDisplay否则设为null。java/** * 查询活动可编辑字段列表左连接 * p根据活动ID查询该活动允许编辑的字段列表结果使用缓存以提高性能。/p * p左连接语义返回所有可编辑字段若数据字典无对应元数据则 fieldDisplay 为 null。/p * * param activityId 活动ID * return 可编辑字段DTO列表 */ Cacheable(value activityEditableFields, key #activityId) public ListActivityEditableFieldDTO queryActivityEditableFields(Integer activityId) { log.debug(【工作流程】查询活动可编辑字段activityId{}, activityId); ListActivityEditableField editableFieldsEntities activityEditableFieldMapper.selectList( new LambdaQueryWrapperActivityEditableField().eq(ActivityEditableField::getActivityId, activityId) ); if (editableFieldsEntities.isEmpty()) { return new ArrayList(); } // 1. 收集所有涉及的表名去重 SetString tableNames editableFieldsEntities.stream() .map(ActivityEditableField::getTableName) .filter(Objects::nonNull) .collect(Collectors.toSet()); // 2. 批量查询所有表的数据字典构建全局映射 MapString, DataDictionary dictMap new HashMap(); for (String tableName : tableNames) { ListDataDictionary dictionaries queryDataDictionarys(tableName); // 已有 Cacheable for (DataDictionary dd : dictionaries) { String key dd.getTableName() : dd.getFieldName(); dictMap.putIfAbsent(key, dd); } } // 3. 左连接构建结果保留所有实体无匹配时 fieldDisplay null ListActivityEditableFieldDTO result new ArrayList(editableFieldsEntities.size()); for (ActivityEditableField entity : editableFieldsEntities) { if (entity.getTableName() null || entity.getFieldName() null) { log.warn(活动可编辑字段存在空表名或空字段名activityId{}, entity{}, activityId, entity); // 仍然保留该记录但无法匹配字典fieldDisplay 为 null ActivityEditableFieldDTO dto editableFieldConverter.toDTO(entity); dto.setFieldDisplay(null); result.add(dto); continue; } String key entity.getTableName() : entity.getFieldName(); DataDictionary dict dictMap.get(key); ActivityEditableFieldDTO dto editableFieldConverter.toDTO(entity); dto.setFieldDisplay(dict ! null ? dict.getFieldDisplay() : null); result.add(dto); } return result; }与交集实现的区别特性交集实现左连接实现返回记录数仅存在于字典中的字段所有可编辑字段无匹配时不返回该记录返回fieldDisplay null适用场景只展示有完整元数据的字段需要展示所有字段即使部分字段缺少显示名称进一步优化建议若希望fieldDisplay缺失时使用默认值如字段名本身可修改javaString display dict ! null ? dict.getFieldDisplay() : entity.getFieldName(); dto.setFieldDisplay(display);若担心queryDataDictionarys按表名缓存导致内存占用高可考虑改为批量 IN 查询一次性加载所有相关表的数据字典减少缓存条目。但当前设计清晰且缓存有效无需过度优化。https://chat.deepseek.com/share/smv7lyypr39ai4ac89

更多文章