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