feat: 新增一个动物字典管理接口

This commit is contained in:
tangwei 2026-09-02 14:40:15 +08:00
parent 2bbeab1198
commit f19091e0b9
6 changed files with 1047 additions and 11 deletions

View File

@ -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最新一条数据---------------------");
}
}

View File

@ -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;
/**
* <p>
* 动物字典表 前端控制器
* </p>
*/
@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<SdAnimalDictoryB> page = sdAnimalDictoryBService.queryPageList(request);
return ResponseResult.successData(page);
}
@PostMapping("/list")
@Operation(summary = "查询动物字典列表(支持动态过滤和排序,不分页)")
public ResponseResult list(@RequestBody DataSourceRequest request) {
List<SdAnimalDictoryB> 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<SdAnimalDictoryB>()
.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<MultipartFile> 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<MultipartFile> files) {
try {
SdEngInfoBHOperateRequest request = objectMapper.readValue(dataStr, SdEngInfoBHOperateRequest.class);
Map<String, Object> 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<String> 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<String> ids = request == null ? null : request.getIds();
if (ids == null || ids.isEmpty()) {
return ResponseResult.error("ID不能为空");
}
// 删除前先收集所有需要清理的附件ID
List<String> 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<MultipartFile> files) {
if (files == null || files.isEmpty()) {
return;
}
List<String> 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<String> collectAttachmentIds(SdAnimalDictoryB entity) {
List<String> 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<String> oldIds, List<String> newIds) {
for (String oldId : oldIds) {
if (StrUtil.isBlank(oldId)) continue;
if (!newIds.contains(oldId)) {
attachmentUploadService.deleteFile(oldId);
}
}
}
/**
* 批量删除附件
*/
private void deleteAttachments(List<String> attachmentIds) {
for (String id : attachmentIds) {
if (StrUtil.isNotBlank(id)) {
attachmentUploadService.deleteFile(id);
}
}
}
}

View File

@ -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;
/**
* <p>
* 动物字典信息表陆生动物鸟类等用于动物监测数据中动物物种的标识
* </p>
*/
@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;
}

View File

@ -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<SdAnimalDictoryB> {
}

View File

@ -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<SdAnimalDictoryB> {
Page<SdAnimalDictoryB> queryPageList(DataSourceRequest request);
Page<SdAnimalDictoryB> selectPage(String name, String code, Integer category, Integer type, Page<SdAnimalDictoryB> page);
boolean add(SdAnimalDictoryB entity, String source);
boolean update(SdAnimalDictoryB entity, String source);
boolean update(Map<String, Object> patchMap, String source) throws Exception;
boolean delete(List<String> ids, String source);
SdAnimalDictoryB getById(String id);
/**
* 检查名称是否已存在不过滤IS_DELETED因为唯一约束不区分软删除
*/
boolean existsByName(String name);
/**
* 检查名称是否已被其他记录占用排除指定ID
*/
boolean existsByNameExcludeId(String name, String excludeId);
List<SdAnimalDictoryB> findSimilarAnimals(String name, Integer limit);
}

View File

@ -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<SdAnimalDictoryBMapper, SdAnimalDictoryB> 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<String> TRANSIENT_FIELDS = Set.of(
"categoryName", "typeName", "rareName", "specOriginName",
"ptypeName", "situationName", "habitatName", "feedingHabitName",
"activityTimeName", "serialVersionUID"
);
/**
* 代码转名称元数据配置列表
*/
private static final List<CodeToNameMetadataBo> 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<SdAnimalDictoryB> queryPageList(DataSourceRequest request) {
Page<SdAnimalDictoryB> 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<String, Object> 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<String> ids, String source) {
if (ids == null || ids.isEmpty()) return false;
int count = 0;
for (String id : ids) {
SdAnimalDictoryB entity = getById(id);
LambdaUpdateWrapper<SdAnimalDictoryB> 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<SdAnimalDictoryB> 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<SdAnimalDictoryB> 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<SdAnimalDictoryB> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(SdAnimalDictoryB::getId, id)
.eq(SdAnimalDictoryB::getIsDeleted, 0);
return getOne(wrapper);
}
@Override
public Page<SdAnimalDictoryB> selectPage(String name, String code, Integer category, Integer type, Page<SdAnimalDictoryB> page) {
LambdaQueryWrapper<SdAnimalDictoryB> 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<SdAnimalDictoryB> 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<SdAnimalDictoryB> candidates = querySimilarCandidates(rawSearchName, searchName, resultLimit);
if (candidates.isEmpty()) {
return Collections.emptyList();
}
Map<String, Integer> 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<SdAnimalDictoryB> querySimilarCandidates(String rawSearchName, String normalizedSearchName, int resultLimit) {
int candidateLimit = Math.min(Math.max(resultLimit * 30, 120), 500);
LinkedHashMap<String, SdAnimalDictoryB> 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<String, SdAnimalDictoryB> candidateMap, List<SdAnimalDictoryB> 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<SdAnimalDictoryB> queryExactCandidates(String rawSearchName, String normalizedSearchName, int limit) {
if (!StringUtils.hasText(rawSearchName)) {
return Collections.emptyList();
}
LambdaQueryWrapper<SdAnimalDictoryB> 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<SdAnimalDictoryB> queryContainsCandidates(String rawSearchName, String normalizedSearchName, int limit) {
if (!StringUtils.hasText(rawSearchName)) {
return Collections.emptyList();
}
String likeValue = "%" + rawSearchName + "%";
String normalizedLikeValue = "%" + normalizedSearchName + "%";
LambdaQueryWrapper<SdAnimalDictoryB> 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<SdAnimalDictoryB> queryTokenCandidates(String normalizedSearchName, int limit) {
Set<String> 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<SdAnimalDictoryB> 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<SdAnimalDictoryB> buildCandidateSelectWrapper() {
LambdaQueryWrapper<SdAnimalDictoryB> 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<String> leftTokens = splitTokens(left);
Set<String> 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<String> splitTokens(String text) {
String normalized = normalizeText(text);
if (!StringUtils.hasText(normalized)) {
return Collections.emptySet();
}
Set<String> 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;
}
}