diff --git a/backend/src/main/java/com/yfd/platform/config/JobRunner.java b/backend/src/main/java/com/yfd/platform/config/JobRunner.java
index 00a1f203..f6d180e5 100644
--- a/backend/src/main/java/com/yfd/platform/config/JobRunner.java
+++ b/backend/src/main/java/com/yfd/platform/config/JobRunner.java
@@ -75,16 +75,16 @@ public class JobRunner implements ApplicationRunner {
ao.setStartTime(DateUtil.beginOfYear(DateUtil.offset(ao.getEndTime(), DateField.YEAR,-1)));
}
envWqDataService.wqLastData(ao);
-// log.info("--------------------wq最新一条数据---------------------");
-// eqJobService.eqAnchorPointLastDate(ao);
-// log.info("--------------------eq最新一条数据---------------------");
-// eqJobService.eqAnchorPointLastDate(ao);
-// log.info("--------------------eq最新一条数据---------------------");
-// dwJobService.wtLastData(ao);
-// log.info("--------------------wt最新一条数据---------------------");
-// dwJobService.wtvtPointData(ao);
-// log.info("--------------------wt最新一条数据---------------------");
-// fhJobService.zqLastData(ao);
-// log.info("--------------------fh最新一条数据---------------------");
+ log.info("--------------------wq最新一条数据---------------------");
+ eqJobService.eqAnchorPointLastDate(ao);
+ log.info("--------------------eq最新一条数据---------------------");
+ eqJobService.eqAnchorPointLastDate(ao);
+ log.info("--------------------eq最新一条数据---------------------");
+ dwJobService.wtLastData(ao);
+ log.info("--------------------wt最新一条数据---------------------");
+ dwJobService.wtvtPointData(ao);
+ log.info("--------------------wt最新一条数据---------------------");
+ fhJobService.zqLastData(ao);
+ log.info("--------------------fh最新一条数据---------------------");
}
}
diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdAnimalDictoryBController.java b/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdAnimalDictoryBController.java
new file mode 100644
index 00000000..13a00cbe
--- /dev/null
+++ b/backend/src/main/java/com/yfd/platform/qgc_base/controller/SdAnimalDictoryBController.java
@@ -0,0 +1,249 @@
+package com.yfd.platform.qgc_base.controller;
+
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yfd.platform.annotation.Log;
+import com.yfd.platform.common.DataSourceRequest;
+import com.yfd.platform.config.ResponseResult;
+import com.yfd.platform.qgc_base.domain.SdAnimalDictoryB;
+import com.yfd.platform.qgc_base.domain.SdEngInfoBHOperateRequest;
+import com.yfd.platform.qgc_base.service.ISdAnimalDictoryBService;
+import com.yfd.platform.qgc_data.service.AttachmentUploadService;
+import com.yfd.platform.utils.DataSourceRequestUtil;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ *
+ * 动物字典表 前端控制器
+ *
+ */
+@Slf4j
+@RestController
+@RequestMapping("/env/animalDictory")
+@Tag(name = "动物字典管理")
+public class SdAnimalDictoryBController {
+
+ @Resource
+ private ISdAnimalDictoryBService sdAnimalDictoryBService;
+
+ @Resource
+ private ObjectMapper objectMapper;
+
+ @Resource
+ private AttachmentUploadService attachmentUploadService;
+
+ @PostMapping("/queryPageList")
+ @Operation(summary = "分页查询动物字典列表(支持动态过滤和排序)")
+ public ResponseResult queryPageList(@RequestBody DataSourceRequest request) {
+ Page page = sdAnimalDictoryBService.queryPageList(request);
+ return ResponseResult.successData(page);
+ }
+
+ @PostMapping("/list")
+ @Operation(summary = "查询动物字典列表(支持动态过滤和排序,不分页)")
+ public ResponseResult list(@RequestBody DataSourceRequest request) {
+ List list = DataSourceRequestUtil.executeList(request, SdAnimalDictoryB.class, sdAnimalDictoryBService);
+ return ResponseResult.successData(list);
+ }
+
+ @GetMapping("/listByName")
+ @Operation(summary = "根据名称查询所有动物字典")
+ public ResponseResult listByName(@RequestParam(required = false) String name) {
+ return ResponseResult.successData(sdAnimalDictoryBService.list(
+ new LambdaQueryWrapper()
+ .eq(StrUtil.isNotBlank(name), SdAnimalDictoryB::getName, name)
+ .select(SdAnimalDictoryB::getId, SdAnimalDictoryB::getName, SdAnimalDictoryB::getAlias)
+ ));
+ }
+
+ @GetMapping("/getById")
+ @Operation(summary = "根据ID查询动物字典")
+ public ResponseResult getById(@RequestParam String id) {
+ return ResponseResult.successData(sdAnimalDictoryBService.getById(id));
+ }
+
+ @Log(module = "动物字典管理", value = "新增动物字典")
+ @PostMapping("/add")
+ @Operation(summary = "新增动物字典")
+ public ResponseResult add(@RequestParam("data") String dataStr,
+ @RequestParam(value = "files", required = false) List files) {
+ try {
+ SdEngInfoBHOperateRequest request = objectMapper.readValue(dataStr, SdEngInfoBHOperateRequest.class);
+ SdAnimalDictoryB entity = request == null || request.getEngInfo() == null ? null
+ : objectMapper.convertValue(request.getEngInfo(), SdAnimalDictoryB.class);
+ if (entity == null) {
+ return ResponseResult.error("数据不能为空");
+ }
+ entity.setId(IdUtil.fastUUID());
+ entity.setCode(entity.getId());
+
+ // 名称重名校验
+ if (sdAnimalDictoryBService.existsByName(entity.getName())) {
+ return ResponseResult.error("该动物名称已存在");
+ }
+
+ // 上传文件并设置附件ID到实体
+ applyUploadedFiles(entity, files);
+
+ boolean result = sdAnimalDictoryBService.add(entity, request.getSource());
+ return result ? ResponseResult.success("新增成功") : ResponseResult.error("新增失败");
+ } catch (Exception e) {
+ log.error("新增动物字典失败", e);
+ return ResponseResult.error("新增失败: " + e.getMessage());
+ }
+ }
+
+ @Log(module = "动物字典管理", value = "修改动物字典")
+ @PostMapping("/update")
+ @Operation(summary = "修改动物字典")
+ public ResponseResult update(@RequestParam("data") String dataStr,
+ @RequestParam(value = "files", required = false) List files) {
+ try {
+ SdEngInfoBHOperateRequest request = objectMapper.readValue(dataStr, SdEngInfoBHOperateRequest.class);
+ Map engInfo = request == null ? null : request.getEngInfo();
+ if (engInfo == null || engInfo.get("id") == null) {
+ return ResponseResult.error("数据或ID不能为空");
+ }
+
+ // 名称重名校验(仅当本次修改了名称时校验)
+ Object newName = engInfo.get("name");
+ if (newName != null && StrUtil.isNotBlank(newName.toString())) {
+ String name = newName.toString();
+ if (sdAnimalDictoryBService.existsByNameExcludeId(name, (String) engInfo.get("id"))) {
+ return ResponseResult.error("该动物名称已存在");
+ }
+ }
+
+ // 获取修改前的实体,用于后续清理旧文件
+ SdAnimalDictoryB before = sdAnimalDictoryBService.getById((String) engInfo.get("id"));
+ List oldAttachmentIds = before != null ? collectAttachmentIds(before) : Collections.emptyList();
+
+ // 上传新文件并设置附件ID
+ SdAnimalDictoryB tempEntity = objectMapper.convertValue(engInfo, SdAnimalDictoryB.class);
+ applyUploadedFiles(tempEntity, files);
+ // 将文件处理后 inffile 合并回 patch map
+ engInfo.put("inffile", tempEntity.getInffile());
+
+ boolean result = sdAnimalDictoryBService.update(engInfo, request.getSource());
+ if (result) {
+ // 清理被替换的旧文件
+ SdAnimalDictoryB after = sdAnimalDictoryBService.getById((String) engInfo.get("id"));
+ deleteReplacedFiles(oldAttachmentIds, collectAttachmentIds(after));
+ }
+ return result ? ResponseResult.success("修改成功") : ResponseResult.error("修改失败");
+ } catch (Exception e) {
+ log.error("修改动物字典失败", e);
+ return ResponseResult.error("修改失败: " + e.getMessage());
+ }
+ }
+
+ @Log(module = "动物字典管理", value = "删除动物字典")
+ @PostMapping("/delete")
+ @Operation(summary = "删除动物字典")
+ public ResponseResult delete(@RequestBody SdEngInfoBHOperateRequest request) {
+ List ids = request == null ? null : request.getIds();
+ if (ids == null || ids.isEmpty()) {
+ return ResponseResult.error("ID不能为空");
+ }
+
+ // 删除前先收集所有需要清理的附件ID
+ List allAttachmentIds = new ArrayList<>();
+ for (String id : ids) {
+ SdAnimalDictoryB entity = sdAnimalDictoryBService.getById(id);
+ if (entity != null) {
+ allAttachmentIds.addAll(collectAttachmentIds(entity));
+ }
+ }
+
+ boolean result = sdAnimalDictoryBService.delete(ids, request.getSource());
+ if (result) {
+ // 删除关联的附件
+ deleteAttachments(allAttachmentIds);
+ }
+ return result ? ResponseResult.success("删除成功") : ResponseResult.error("删除失败");
+ }
+
+ @GetMapping("/similar")
+ @Operation(summary = "查询相似动物列表")
+ public ResponseResult findSimilarAnimals(
+ @RequestParam String name,
+ @RequestParam(required = false, defaultValue = "10") Integer limit) {
+ return ResponseResult.successData(sdAnimalDictoryBService.findSimilarAnimals(name, limit));
+ }
+
+ // ==================== 文件处理辅助方法 ====================
+
+ /**
+ * 上传文件并追加附件ID到inffile字段(与已有ID以逗号拼接)
+ */
+ private void applyUploadedFiles(SdAnimalDictoryB entity, List files) {
+ if (files == null || files.isEmpty()) {
+ return;
+ }
+ List attachmentIds = attachmentUploadService.uploadMultipartFiles(files);
+ if (attachmentIds.isEmpty()) {
+ return;
+ }
+ String newIds = attachmentIds.stream()
+ .filter(StrUtil::isNotBlank)
+ .collect(Collectors.joining(","));
+ if (StrUtil.isBlank(newIds)) {
+ return;
+ }
+ String existing = StrUtil.isNotBlank(entity.getInffile()) ? entity.getInffile() : "";
+ String merged = StrUtil.isBlank(existing) ? newIds : existing + "," + newIds;
+ entity.setInffile(merged);
+ }
+
+ /**
+ * 收集实体中inffile字段的附件ID
+ */
+ private List collectAttachmentIds(SdAnimalDictoryB entity) {
+ List ids = new ArrayList<>();
+ if (StrUtil.isNotBlank(entity.getInffile())) {
+ Arrays.stream(entity.getInffile().split(","))
+ .map(String::trim)
+ .filter(StrUtil::isNotBlank)
+ .forEach(ids::add);
+ }
+ return ids;
+ }
+
+ /**
+ * 删除被替换的旧文件(old中有但new中没有的)
+ */
+ private void deleteReplacedFiles(List oldIds, List newIds) {
+ for (String oldId : oldIds) {
+ if (StrUtil.isBlank(oldId)) continue;
+ if (!newIds.contains(oldId)) {
+ attachmentUploadService.deleteFile(oldId);
+ }
+ }
+ }
+
+ /**
+ * 批量删除附件
+ */
+ private void deleteAttachments(List attachmentIds) {
+ for (String id : attachmentIds) {
+ if (StrUtil.isNotBlank(id)) {
+ attachmentUploadService.deleteFile(id);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdAnimalDictoryB.java b/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdAnimalDictoryB.java
new file mode 100644
index 00000000..01fab077
--- /dev/null
+++ b/backend/src/main/java/com/yfd/platform/qgc_base/domain/SdAnimalDictoryB.java
@@ -0,0 +1,217 @@
+package com.yfd.platform.qgc_base.domain;
+
+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 com.yfd.platform.annotation.FieldChinese;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ *
+ * 动物字典信息表(陆生动物、鸟类等,用于动物监测数据中动物物种的标识)
+ *
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@TableName("SD_ANIMALDICTORY_B")
+public class SdAnimalDictoryB implements Serializable {
+
+ private static final long serialVersionUID = 1L;
+
+ /** 主键 */
+ @TableId(type = IdType.INPUT)
+ @FieldChinese("主键")
+ private String id;
+
+ /** 编码 */
+ @FieldChinese("编码")
+ private String code;
+
+ /** 名称 */
+ @FieldChinese("名称")
+ private String name;
+
+ /** 英文名称 */
+ @FieldChinese("英文名称")
+ private String nameEn;
+
+ /** 俗名 */
+ @FieldChinese("俗名")
+ private String alias;
+
+ /** 动物类群:1=哺乳动物 2=鸟类 3=两栖动物 4=爬行动物 5=节肢动物 6=其他 */
+ @FieldChinese("动物类群")
+ private Integer category;
+
+ @TableField(exist = false)
+ private String categoryName;
+
+ /** 分类(生境类型):1=陆生 2=水生 3=两栖 */
+ @FieldChinese("生境类型")
+ private Integer type;
+
+ @TableField(exist = false)
+ private String typeName;
+
+ /** 是否珍稀动物:0=否 1=是 */
+ @FieldChinese("是否珍稀动物")
+ private Integer rare;
+
+ @TableField(exist = false)
+ private String rareName;
+
+ /** 物种来源:1=本土物种 2=外来物种 */
+ @FieldChinese("物种来源")
+ private Integer specOrigin;
+
+ @TableField(exist = false)
+ private String specOriginName;
+
+ /** 保护级别 */
+ @FieldChinese("保护级别")
+ private Integer ptype;
+
+ @TableField(exist = false)
+ private String ptypeName;
+
+ /** 保护/灭绝等级:1=极危 2=濒危 3=易危 4=近危 5=低危 */
+ @FieldChinese("保护等级")
+ private Integer situation;
+
+ @TableField(exist = false)
+ private String situationName;
+
+ /** 主要生活环境 */
+ @FieldChinese("主要生活环境")
+ private Integer habitat;
+
+ @TableField(exist = false)
+ private String habitatName;
+
+ /** 门 */
+ @FieldChinese("门")
+ private String filum;
+
+ /** 纲 */
+ @FieldChinese("纲")
+ private String classis;
+
+ /** 目 */
+ @FieldChinese("目")
+ private String orders;
+
+ /** 科 */
+ @FieldChinese("科")
+ private String family;
+
+ /** 属 */
+ @FieldChinese("属")
+ private String genus;
+
+ /** 种 */
+ @FieldChinese("种")
+ private String species;
+
+ /** 体型大小 */
+ @FieldChinese("体型大小")
+ private String fsz;
+
+ /** 食性:1=肉食 2=草食 3=杂食 */
+ @FieldChinese("食性")
+ private String feedingHabit;
+
+ @TableField(exist = false)
+ private String feedingHabitName;
+
+ /** 活动时律:1=昼行 2=夜行 3=晨昏 */
+ @FieldChinese("活动时律")
+ private String activityTime;
+
+ @TableField(exist = false)
+ private String activityTimeName;
+
+ /** 分布区域 */
+ @FieldChinese("分布区域")
+ private String distribution;
+
+ /** 形态特征/识别特征 */
+ @FieldChinese("形态特征")
+ private String symbol;
+
+ /** 栖息习性 */
+ @FieldChinese("栖息习性")
+ private String habitation;
+
+ /** LOGO */
+ @FieldChinese("LOGO")
+ private String logo;
+
+ /** 介绍 */
+ @FieldChinese("介绍")
+ private String introduce;
+
+ /** 介绍弹窗图片 */
+ @FieldChinese("介绍弹窗图片")
+ private String inffile;
+
+ /** 附件 */
+ @FieldChinese("附件")
+ private String fid;
+
+ /** 描述 */
+ @FieldChinese("描述")
+ private String description;
+
+ /** 是否启用:0=禁用 1=启用 */
+ @FieldChinese("是否启用")
+ private Integer enable;
+
+ /** 系统内置项:0=否 1=是 */
+ @FieldChinese("系统内置项")
+ private Integer internal;
+
+ /** 排序 */
+ @FieldChinese("排序")
+ private Integer orderIndex;
+
+ /** 创建人 */
+ @FieldChinese("创建人")
+ private String recordUser;
+
+ /** 创建时间 */
+ @FieldChinese("创建时间")
+ private Date recordTime;
+
+ /** 更新人 */
+ @FieldChinese("更新人")
+ private String modifyUser;
+
+ /** 更新时间 */
+ @FieldChinese("更新时间")
+ private Date modifyTime;
+
+ /** 是否已删除 */
+ @FieldChinese("是否已删除")
+ private Integer isDeleted;
+
+ /** 删除人 */
+ @FieldChinese("删除人")
+ private String deleteUser;
+
+ /** 删除时间 */
+ @FieldChinese("删除时间")
+ private Date deleteTime;
+
+ /** 数据来源 */
+ @FieldChinese("数据来源")
+ private String vlsr;
+
+ /** 数据来源时间 */
+ @FieldChinese("数据来源时间")
+ private Date vlsrTm;
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/mapper/SdAnimalDictoryBMapper.java b/backend/src/main/java/com/yfd/platform/qgc_base/mapper/SdAnimalDictoryBMapper.java
new file mode 100644
index 00000000..dbc3a573
--- /dev/null
+++ b/backend/src/main/java/com/yfd/platform/qgc_base/mapper/SdAnimalDictoryBMapper.java
@@ -0,0 +1,9 @@
+package com.yfd.platform.qgc_base.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.yfd.platform.qgc_base.domain.SdAnimalDictoryB;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface SdAnimalDictoryBMapper extends BaseMapper {
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdAnimalDictoryBService.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdAnimalDictoryBService.java
new file mode 100644
index 00000000..5dd11231
--- /dev/null
+++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/ISdAnimalDictoryBService.java
@@ -0,0 +1,38 @@
+package com.yfd.platform.qgc_base.service;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.yfd.platform.common.DataSourceRequest;
+import com.yfd.platform.qgc_base.domain.SdAnimalDictoryB;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ISdAnimalDictoryBService extends IService {
+
+ Page queryPageList(DataSourceRequest request);
+
+ Page selectPage(String name, String code, Integer category, Integer type, Page page);
+
+ boolean add(SdAnimalDictoryB entity, String source);
+
+ boolean update(SdAnimalDictoryB entity, String source);
+
+ boolean update(Map patchMap, String source) throws Exception;
+
+ boolean delete(List ids, String source);
+
+ SdAnimalDictoryB getById(String id);
+
+ /**
+ * 检查名称是否已存在(不过滤IS_DELETED,因为唯一约束不区分软删除)
+ */
+ boolean existsByName(String name);
+
+ /**
+ * 检查名称是否已被其他记录占用(排除指定ID)
+ */
+ boolean existsByNameExcludeId(String name, String excludeId);
+
+ List findSimilarAnimals(String name, Integer limit);
+}
\ No newline at end of file
diff --git a/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdAnimalDictoryBServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdAnimalDictoryBServiceImpl.java
new file mode 100644
index 00000000..dade104b
--- /dev/null
+++ b/backend/src/main/java/com/yfd/platform/qgc_base/service/impl/SdAnimalDictoryBServiceImpl.java
@@ -0,0 +1,523 @@
+package com.yfd.platform.qgc_base.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.yfd.platform.common.DataSourceRequest;
+import com.yfd.platform.qgc_base.domain.SdAnimalDictoryB;
+import com.yfd.platform.qgc_base.mapper.SdAnimalDictoryBMapper;
+import com.yfd.platform.qgc_base.service.ISdAnimalDictoryBService;
+import com.yfd.platform.qgc_base.service.IMsOperationLogService;
+import com.yfd.platform.utils.CodeToNameMetadataBo;
+import com.yfd.platform.utils.DataSourceRequestUtil;
+import com.yfd.platform.utils.DictCodeToNameConverter;
+import com.yfd.platform.utils.SecurityUtils;
+import jakarta.annotation.Resource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.util.*;
+import java.util.Locale;
+import java.util.stream.Collectors;
+
+/**
+ * 动物字典 Service 实现类
+ */
+@Service
+public class SdAnimalDictoryBServiceImpl extends ServiceImpl implements ISdAnimalDictoryBService {
+
+ private static final String TABLE_NAME = "SD_ANIMALDICTORY_B";
+
+ @Resource
+ private IMsOperationLogService msOperationLogService;
+
+ @Resource
+ private DictCodeToNameConverter dictCodeToNameConverter;
+
+ @Resource
+ private PatchUpdateHelper patchUpdateHelper;
+
+ /** SdAnimalDictoryB 中 @TableField(exist = false) 的字段名 + serialVersionUID */
+ private static final Set TRANSIENT_FIELDS = Set.of(
+ "categoryName", "typeName", "rareName", "specOriginName",
+ "ptypeName", "situationName", "habitatName", "feedingHabitName",
+ "activityTimeName", "serialVersionUID"
+ );
+
+ /**
+ * 代码转名称元数据配置列表
+ */
+ private static final List CODE_TO_NAME_META_LIST = new ArrayList<>();
+
+ static {
+ CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("category").modifyProperty("categoryName").dictType("STATIC").dictSource("ANIMAL_CATEGORY").build());
+ CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("type").modifyProperty("typeName").dictType("STATIC").dictSource("HABITAT_TYPE").build());
+ CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("rare").modifyProperty("rareName").dictType("STATIC").dictSource("TY_SF").build());
+ CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("specOrigin").modifyProperty("specOriginName").dictType("STATIC").dictSource("SPEC_ORIGIN").build());
+ CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("ptype").modifyProperty("ptypeName").dictType("STATIC").dictSource("PTYPE").build());
+ CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("situation").modifyProperty("situationName").dictType("STATIC").dictSource("SITUATION").build());
+ CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("habitat").modifyProperty("habitatName").dictType("STATIC").dictSource("HABITAT").build());
+ CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("feedingHabit").modifyProperty("feedingHabitName").dictType("STATIC").dictSource("FEEDING_HABIT").build());
+ CODE_TO_NAME_META_LIST.add(CodeToNameMetadataBo.builder().codeProperty("activityTime").modifyProperty("activityTimeName").dictType("STATIC").dictSource("ACTIVITY_TIME").build());
+ }
+
+ @Override
+ public Page queryPageList(DataSourceRequest request) {
+ Page page = DataSourceRequestUtil.executeQuery(request, SdAnimalDictoryB.class, this);
+ dictCodeToNameConverter.convertCodeToName(page, CODE_TO_NAME_META_LIST);
+ return page;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public boolean add(SdAnimalDictoryB entity, String source) {
+ boolean result = this.save(entity);
+ if (result) {
+ dictCodeToNameConverter.convertCodeToName(entity, CODE_TO_NAME_META_LIST);
+ msOperationLogService.recordAddDetailLog(entity.getId(), TABLE_NAME, entity, source, CODE_TO_NAME_META_LIST);
+ }
+ return result;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public boolean update(SdAnimalDictoryB entity, String source) {
+ SdAnimalDictoryB before = this.getById(entity.getId());
+ boolean result = this.updateById(entity);
+ if (result && before != null) {
+ dictCodeToNameConverter.convertCodeToName(before, CODE_TO_NAME_META_LIST);
+ dictCodeToNameConverter.convertCodeToName(entity, CODE_TO_NAME_META_LIST);
+ msOperationLogService.recordModifyDetailLog(entity.getId(), TABLE_NAME, before, entity, source, CODE_TO_NAME_META_LIST);
+ }
+ return result;
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public boolean update(Map patchMap, String source) throws Exception {
+ return patchUpdateHelper.execute(
+ this, patchMap, source, TABLE_NAME, SdAnimalDictoryB.class,
+ TRANSIENT_FIELDS, CODE_TO_NAME_META_LIST, "id");
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public boolean delete(List ids, String source) {
+ if (ids == null || ids.isEmpty()) return false;
+ int count = 0;
+ for (String id : ids) {
+ SdAnimalDictoryB entity = getById(id);
+ LambdaUpdateWrapper wrapper = new LambdaUpdateWrapper<>();
+ wrapper.eq(SdAnimalDictoryB::getId, id);
+ wrapper.set(SdAnimalDictoryB::getIsDeleted, 1);
+ wrapper.set(SdAnimalDictoryB::getDeleteUser, SecurityUtils.getCurrentUsername());
+ wrapper.set(SdAnimalDictoryB::getDeleteTime, new Date());
+ if (this.update(wrapper)) {
+ dictCodeToNameConverter.convertCodeToName(entity, CODE_TO_NAME_META_LIST);
+ msOperationLogService.recordDeleteDetailLog(entity.getName(), TABLE_NAME, entity, source, CODE_TO_NAME_META_LIST);
+ count++;
+ }
+ }
+ return count > 0;
+ }
+
+ @Override
+ public boolean existsByName(String name) {
+ if (!StringUtils.hasText(name)) {
+ return false;
+ }
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(SdAnimalDictoryB::getName, name);
+ return count(wrapper) > 0;
+ }
+
+ @Override
+ public boolean existsByNameExcludeId(String name, String excludeId) {
+ if (!StringUtils.hasText(name)) {
+ return false;
+ }
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(SdAnimalDictoryB::getName, name);
+ if (StringUtils.hasText(excludeId)) {
+ wrapper.ne(SdAnimalDictoryB::getId, excludeId);
+ }
+ return count(wrapper) > 0;
+ }
+
+ @Override
+ public SdAnimalDictoryB getById(String id) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(SdAnimalDictoryB::getId, id)
+ .eq(SdAnimalDictoryB::getIsDeleted, 0);
+ return getOne(wrapper);
+ }
+
+ @Override
+ public Page selectPage(String name, String code, Integer category, Integer type, Page page) {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.eq(SdAnimalDictoryB::getIsDeleted, 0);
+
+ if (StringUtils.hasText(name)) {
+ wrapper.like(SdAnimalDictoryB::getName, name);
+ }
+ if (StringUtils.hasText(code)) {
+ wrapper.eq(SdAnimalDictoryB::getCode, code);
+ }
+ if (category != null) {
+ wrapper.eq(SdAnimalDictoryB::getCategory, category);
+ }
+ if (type != null) {
+ wrapper.eq(SdAnimalDictoryB::getType, type);
+ }
+
+ wrapper.orderByDesc(SdAnimalDictoryB::getOrderIndex);
+ return page(page, wrapper);
+ }
+
+ @Override
+ public List findSimilarAnimals(String name, Integer limit) {
+ if (!StringUtils.hasText(name)) {
+ return Collections.emptyList();
+ }
+
+ int resultLimit = (limit != null && limit > 0) ? limit : 10;
+ String rawSearchName = name.trim();
+ String searchName = normalizeText(rawSearchName);
+
+ List candidates = querySimilarCandidates(rawSearchName, searchName, resultLimit);
+ if (candidates.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ Map searchScoreMap = new HashMap<>();
+ for (SdAnimalDictoryB animal : candidates) {
+ if (animal == null || !StringUtils.hasText(animal.getId())) {
+ continue;
+ }
+ int score = calculateSearchScore(searchName, animal);
+ if (score > 0) {
+ searchScoreMap.put(animal.getId(), score);
+ }
+ }
+
+ if (searchScoreMap.isEmpty()) {
+ return Collections.emptyList();
+ }
+
+ SdAnimalDictoryB reference = candidates.stream()
+ .filter(a -> a != null && searchScoreMap.containsKey(a.getId()))
+ .max((a1, a2) -> {
+ int scoreCompare = Integer.compare(searchScoreMap.get(a1.getId()), searchScoreMap.get(a2.getId()));
+ if (scoreCompare != 0) return scoreCompare;
+ int orderCompare = Integer.compare(nullSafe(a1.getOrderIndex()), nullSafe(a2.getOrderIndex()));
+ if (orderCompare != 0) return orderCompare;
+ return safeText(a2.getName()).compareTo(safeText(a1.getName()));
+ })
+ .orElse(null);
+
+ if (reference == null) {
+ return Collections.emptyList();
+ }
+
+ return candidates.stream()
+ .filter(a -> a != null && StringUtils.hasText(a.getId()))
+ .map(a -> new AbstractMap.SimpleEntry<>(a, buildFinalScore(reference, a, searchScoreMap.getOrDefault(a.getId(), 0))))
+ .filter(e -> e.getValue() > 0)
+ .sorted((e1, e2) -> {
+ int scoreCompare = Integer.compare(e2.getValue(), e1.getValue());
+ if (scoreCompare != 0) return scoreCompare;
+ int orderCompare = Integer.compare(nullSafe(e2.getKey().getOrderIndex()), nullSafe(e1.getKey().getOrderIndex()));
+ if (orderCompare != 0) return orderCompare;
+ return safeText(e1.getKey().getName()).compareTo(safeText(e2.getKey().getName()));
+ })
+ .limit(resultLimit)
+ .map(Map.Entry::getKey)
+ .collect(Collectors.toList());
+ }
+
+ private List querySimilarCandidates(String rawSearchName, String normalizedSearchName, int resultLimit) {
+ int candidateLimit = Math.min(Math.max(resultLimit * 30, 120), 500);
+ LinkedHashMap candidateMap = new LinkedHashMap<>();
+
+ mergeCandidates(candidateMap, queryExactCandidates(rawSearchName, normalizedSearchName, candidateLimit));
+ if (candidateMap.size() < candidateLimit) {
+ mergeCandidates(candidateMap, queryContainsCandidates(rawSearchName, normalizedSearchName, candidateLimit));
+ }
+ if (candidateMap.size() < candidateLimit) {
+ mergeCandidates(candidateMap, queryTokenCandidates(normalizedSearchName, candidateLimit));
+ }
+
+ return new ArrayList<>(candidateMap.values());
+ }
+
+ private void mergeCandidates(Map candidateMap, List candidates) {
+ if (candidates == null || candidates.isEmpty()) return;
+ for (SdAnimalDictoryB animal : candidates) {
+ if (animal == null || !StringUtils.hasText(animal.getId())) continue;
+ candidateMap.putIfAbsent(animal.getId(), animal);
+ }
+ }
+
+ private List queryExactCandidates(String rawSearchName, String normalizedSearchName, int limit) {
+ if (!StringUtils.hasText(rawSearchName)) {
+ return Collections.emptyList();
+ }
+ LambdaQueryWrapper wrapper = buildCandidateSelectWrapper();
+ wrapper.and(w -> w
+ .eq(SdAnimalDictoryB::getName, rawSearchName)
+ .or().eq(SdAnimalDictoryB::getAlias, rawSearchName)
+ .or().eq(SdAnimalDictoryB::getNameEn, rawSearchName)
+ .or().eq(SdAnimalDictoryB::getSpecies, rawSearchName)
+ .or().eq(SdAnimalDictoryB::getGenus, rawSearchName)
+ .or().eq(SdAnimalDictoryB::getFamily, rawSearchName)
+ .or().eq(SdAnimalDictoryB::getOrders, rawSearchName)
+ .or().apply("LOWER(NAME) = {0}", normalizedSearchName)
+ .or().apply("LOWER(ALIAS) = {0}", normalizedSearchName)
+ .or().apply("LOWER(NAME_EN) = {0}", normalizedSearchName)
+ .or().apply("LOWER(SPECIES) = {0}", normalizedSearchName)
+ .or().apply("LOWER(GENUS) = {0}", normalizedSearchName)
+ .or().apply("LOWER(FAMILY) = {0}", normalizedSearchName)
+ .or().apply("LOWER(ORDERS) = {0}", normalizedSearchName));
+ wrapper.orderByDesc(SdAnimalDictoryB::getOrderIndex);
+ return page(new Page<>(1, limit), wrapper).getRecords();
+ }
+
+ private List queryContainsCandidates(String rawSearchName, String normalizedSearchName, int limit) {
+ if (!StringUtils.hasText(rawSearchName)) {
+ return Collections.emptyList();
+ }
+ String likeValue = "%" + rawSearchName + "%";
+ String normalizedLikeValue = "%" + normalizedSearchName + "%";
+ LambdaQueryWrapper wrapper = buildCandidateSelectWrapper();
+ wrapper.and(w -> w
+ .like(SdAnimalDictoryB::getName, rawSearchName)
+ .or().like(SdAnimalDictoryB::getAlias, rawSearchName)
+ .or().like(SdAnimalDictoryB::getNameEn, rawSearchName)
+ .or().like(SdAnimalDictoryB::getSpecies, rawSearchName)
+ .or().like(SdAnimalDictoryB::getGenus, rawSearchName)
+ .or().like(SdAnimalDictoryB::getFamily, rawSearchName)
+ .or().like(SdAnimalDictoryB::getOrders, rawSearchName)
+ .or().like(SdAnimalDictoryB::getIntroduce, rawSearchName)
+ .or().like(SdAnimalDictoryB::getAlias, rawSearchName)
+ .or().apply("LOWER(NAME) LIKE {0}", normalizedLikeValue)
+ .or().apply("LOWER(ALIAS) LIKE {0}", normalizedLikeValue)
+ .or().apply("LOWER(NAME_EN) LIKE {0}", normalizedLikeValue)
+ .or().apply("LOWER(SPECIES) LIKE {0}", normalizedLikeValue)
+ .or().apply("LOWER(GENUS) LIKE {0}", normalizedLikeValue)
+ .or().apply("LOWER(FAMILY) LIKE {0}", normalizedLikeValue)
+ .or().apply("LOWER(ORDERS) LIKE {0}", normalizedLikeValue)
+ .or().apply("LOWER(INTRODUCE) LIKE {0}", normalizedLikeValue));
+ wrapper.orderByDesc(SdAnimalDictoryB::getOrderIndex);
+ return page(new Page<>(1, limit), wrapper).getRecords();
+ }
+
+ private List queryTokenCandidates(String normalizedSearchName, int limit) {
+ Set tokens = splitTokens(normalizedSearchName).stream()
+ .filter(StringUtils::hasText)
+ .filter(token -> token.length() >= 2)
+ .limit(5)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ if (tokens.isEmpty()) {
+ return Collections.emptyList();
+ }
+ LambdaQueryWrapper wrapper = buildCandidateSelectWrapper();
+ wrapper.and(w -> {
+ boolean first = true;
+ for (String token : tokens) {
+ String likeValue = "%" + token + "%";
+ if (!first) {
+ w.or();
+ }
+ w.apply("LOWER(NAME) LIKE {0}", likeValue)
+ .or().apply("LOWER(ALIAS) LIKE {0}", likeValue)
+ .or().apply("LOWER(NAME_EN) LIKE {0}", likeValue)
+ .or().apply("LOWER(SPECIES) LIKE {0}", likeValue)
+ .or().apply("LOWER(GENUS) LIKE {0}", likeValue)
+ .or().apply("LOWER(FAMILY) LIKE {0}", likeValue)
+ .or().apply("LOWER(ORDERS) LIKE {0}", likeValue)
+ .or().apply("LOWER(INTRODUCE) LIKE {0}", likeValue);
+ first = false;
+ }
+ });
+ wrapper.orderByDesc(SdAnimalDictoryB::getOrderIndex);
+ return page(new Page<>(1, limit), wrapper).getRecords();
+ }
+
+ private LambdaQueryWrapper buildCandidateSelectWrapper() {
+ LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();
+ wrapper.select(
+ SdAnimalDictoryB::getId,
+ SdAnimalDictoryB::getCode,
+ SdAnimalDictoryB::getName,
+ SdAnimalDictoryB::getNameEn,
+ SdAnimalDictoryB::getAlias,
+ SdAnimalDictoryB::getIntroduce,
+ SdAnimalDictoryB::getOrders,
+ SdAnimalDictoryB::getFamily,
+ SdAnimalDictoryB::getGenus,
+ SdAnimalDictoryB::getSpecies,
+ SdAnimalDictoryB::getCategory,
+ SdAnimalDictoryB::getType,
+ SdAnimalDictoryB::getHabitat,
+ SdAnimalDictoryB::getFeedingHabit,
+ SdAnimalDictoryB::getActivityTime,
+ SdAnimalDictoryB::getPtype,
+ SdAnimalDictoryB::getSituation,
+ SdAnimalDictoryB::getRare,
+ SdAnimalDictoryB::getOrderIndex,
+ SdAnimalDictoryB::getIsDeleted
+ );
+ wrapper.eq(SdAnimalDictoryB::getIsDeleted, 0);
+ return wrapper;
+ }
+
+ private int calculateSimilarity(SdAnimalDictoryB ref, SdAnimalDictoryB target) {
+ if (ref == null || target == null) {
+ return 0;
+ }
+ if (StringUtils.hasText(ref.getId()) && ref.getId().equals(target.getId())) {
+ return 5000;
+ }
+ int score = 0;
+ score += calculateFieldSimilarity(ref.getName(), target.getName(), 45);
+ score += calculateFieldSimilarity(ref.getNameEn(), target.getNameEn(), 22);
+ score += calculateFieldSimilarity(ref.getAlias(), target.getAlias(), 26);
+ score += calculateFieldSimilarity(ref.getSpecies(), target.getSpecies(), 30);
+ score += calculateFieldSimilarity(ref.getGenus(), target.getGenus(), 24);
+ score += calculateFieldSimilarity(ref.getFamily(), target.getFamily(), 18);
+ score += calculateFieldSimilarity(ref.getOrders(), target.getOrders(), 14);
+ score += calculateFieldSimilarity(ref.getIntroduce(), target.getIntroduce(), 8);
+
+ if (bothEqual(ref.getCategory(), target.getCategory())) score += 5;
+ if (bothEqual(ref.getType(), target.getType())) score += 5;
+ if (bothEqual(ref.getHabitat(), target.getHabitat())) score += 5;
+ if (sameText(ref.getFeedingHabit(), target.getFeedingHabit())) score += 3;
+ if (sameText(ref.getActivityTime(), target.getActivityTime())) score += 3;
+ if (bothEqual(ref.getPtype(), target.getPtype())) score += 2;
+ if (bothEqual(ref.getSituation(), target.getSituation())) score += 2;
+ if (bothEqual(ref.getRare(), target.getRare())) score += 2;
+
+ return score;
+ }
+
+ private int calculateSearchScore(String searchName, SdAnimalDictoryB animal) {
+ int score = 0;
+ score += calculateFieldSearchScore(searchName, animal.getName(), 220, 140, 90, 40);
+ score += calculateFieldSearchScore(searchName, animal.getAlias(), 180, 120, 80, 30);
+ score += calculateFieldSearchScore(searchName, animal.getNameEn(), 160, 100, 70, 25);
+ score += calculateFieldSearchScore(searchName, animal.getSpecies(), 140, 90, 60, 25);
+ score += calculateFieldSearchScore(searchName, animal.getGenus(), 120, 80, 55, 20);
+ score += calculateFieldSearchScore(searchName, animal.getFamily(), 100, 70, 45, 18);
+ score += calculateFieldSearchScore(searchName, animal.getOrders(), 90, 60, 40, 15);
+ score += calculateFieldSearchScore(searchName, animal.getIntroduce(), 45, 30, 0, 12);
+ return score;
+ }
+
+ private int buildFinalScore(SdAnimalDictoryB reference, SdAnimalDictoryB current, int searchScore) {
+ int totalScore = searchScore;
+ if (reference != null) {
+ totalScore += calculateSimilarity(reference, current);
+ }
+ return totalScore;
+ }
+
+ private int calculateFieldSearchScore(String searchText, String candidate, int exactScore, int containsScore,
+ int reverseContainsScore, int overlapMaxScore) {
+ String normalizedSearch = normalizeText(searchText);
+ String normalizedCandidate = normalizeText(candidate);
+ if (!StringUtils.hasText(normalizedSearch) || !StringUtils.hasText(normalizedCandidate)) {
+ return 0;
+ }
+ if (normalizedCandidate.equals(normalizedSearch)) {
+ return exactScore;
+ }
+ if (normalizedCandidate.contains(normalizedSearch)) {
+ return containsScore;
+ }
+ if (reverseContainsScore > 0 && normalizedSearch.contains(normalizedCandidate) && normalizedCandidate.length() >= 2) {
+ return reverseContainsScore;
+ }
+ return calculateTextOverlapScore(normalizedSearch, normalizedCandidate, overlapMaxScore);
+ }
+
+ private int calculateFieldSimilarity(String left, String right, int exactScore) {
+ String normalizedLeft = normalizeText(left);
+ String normalizedRight = normalizeText(right);
+ if (!StringUtils.hasText(normalizedLeft) || !StringUtils.hasText(normalizedRight)) {
+ return 0;
+ }
+ if (normalizedLeft.equals(normalizedRight)) {
+ return exactScore;
+ }
+ if (normalizedLeft.contains(normalizedRight) || normalizedRight.contains(normalizedLeft)) {
+ return Math.max(1, exactScore / 2);
+ }
+ return calculateTextOverlapScore(normalizedLeft, normalizedRight, Math.max(1, exactScore / 3));
+ }
+
+ private int calculateTextOverlapScore(String left, String right, int maxScore) {
+ if (!StringUtils.hasText(left) || !StringUtils.hasText(right) || maxScore <= 0) {
+ return 0;
+ }
+ Set leftTokens = splitTokens(left);
+ Set rightTokens = splitTokens(right);
+ if (leftTokens.isEmpty() || rightTokens.isEmpty()) {
+ return 0;
+ }
+ long commonCount = leftTokens.stream().filter(rightTokens::contains).count();
+ if (commonCount <= 0) {
+ return 0;
+ }
+ int base = Math.min(leftTokens.size(), rightTokens.size());
+ if (base <= 0) {
+ return 0;
+ }
+ return (int) Math.min(maxScore, Math.round((double) commonCount * maxScore / base));
+ }
+
+ private Set splitTokens(String text) {
+ String normalized = normalizeText(text);
+ if (!StringUtils.hasText(normalized)) {
+ return Collections.emptySet();
+ }
+ Set tokens = new LinkedHashSet<>();
+ String[] parts = normalized.split("[\\s,,;;、/\\\\()()]+");
+ for (String part : parts) {
+ if (StringUtils.hasText(part)) {
+ tokens.add(part);
+ }
+ }
+ if (tokens.isEmpty()) {
+ for (int i = 0; i < normalized.length(); i++) {
+ char ch = normalized.charAt(i);
+ if (!Character.isWhitespace(ch)) {
+ tokens.add(String.valueOf(ch));
+ }
+ }
+ }
+ return tokens;
+ }
+
+ private boolean bothEqual(Integer a, Integer b) {
+ return a != null && a.equals(b) && !ObjectUtil.isEmpty(a);
+ }
+
+ private boolean sameText(String a, String b) {
+ return StringUtils.hasText(a) && StringUtils.hasText(b) && a.equals(b);
+ }
+
+ private int nullSafe(Integer v) {
+ return v == null ? 0 : v;
+ }
+
+ private String normalizeText(String text) {
+ return StringUtils.hasText(text) ? text.trim().toLowerCase(Locale.ROOT) : "";
+ }
+
+ private String safeText(String text) {
+ return text == null ? "" : text;
+ }
+}
\ No newline at end of file