diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/fb/controller/FbStationController.java b/backend/src/main/java/com/yfd/platform/qgc_env/fb/controller/FbStationController.java index 35fab7a8..79fcd23a 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/fb/controller/FbStationController.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/fb/controller/FbStationController.java @@ -214,6 +214,12 @@ public class FbStationController { return ResponseResult.successData(fbStationService.getFishRunQgcKendoListCust(dataSourceRequest)); } + @PostMapping("/fishrun/qgc/detail/GetKendoListCust") + @Operation(summary = "全过程鱼类放鱼数据明细(细粒度,按站点+年份+鱼种下钻)") + public ResponseResult getFishRunQgcDetailKendoListCust(@RequestBody DataSourceRequest dataSourceRequest) { + return ResponseResult.successData(fbStationService.getFishRunQgcDetailKendoListCust(dataSourceRequest)); + } + @Log(module = "鱼类增殖站模块", value = "删除全过程鱼类放鱼数据") @PostMapping("/fishrun/qgc/removeFishRunQgcData") @Operation(summary = "删除全过程鱼类放鱼数据(按站点+年份+鱼种)") @@ -222,6 +228,14 @@ public class FbStationController { return ResponseResult.success(); } + @Log(module = "鱼类增殖站模块", value = "删除全过程鱼类放鱼明细数据") + @PostMapping("/fishrun/qgc/removeFishRunQgcDetailData") + @Operation(summary = "删除全过程鱼类放鱼明细数据(按明细ID批量)") + public ResponseResult removeFishRunQgcDetailData(@RequestBody List ids) { + fbStationService.removeFishRunQgcDetailData(ids); + return ResponseResult.success(); + } + @PostMapping("/fishType/GetKendoListCust") @Operation(summary = "增殖站鱼种完成情况统计") public ResponseResult getFishTypeKendoListCust(@RequestBody DataSourceRequest dataSourceRequest) { diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/fb/entity/vo/FbFishRunQgcDetailVo.java b/backend/src/main/java/com/yfd/platform/qgc_env/fb/entity/vo/FbFishRunQgcDetailVo.java new file mode 100644 index 00000000..11959b6f --- /dev/null +++ b/backend/src/main/java/com/yfd/platform/qgc_env/fb/entity/vo/FbFishRunQgcDetailVo.java @@ -0,0 +1,69 @@ +package com.yfd.platform.qgc_env.fb.entity.vo; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.math.BigDecimal; +import java.util.Date; + +/** + * 全过程鱼类放鱼数据明细(细粒度),对应 SD_RPIMNFISH_R 明细行。 + */ +@Data +@Schema(description = "全过程鱼类放鱼数据明细") +public class FbFishRunQgcDetailVo { + + @Schema(description = "明细ID (SD_RPIMNFISH_R.ID)") + private String id; + + @Schema(description = "放流主表ID (SD_ENGFR_R.ID)") + private String rpimnId; + + @Schema(description = "电站站码") + private String stcd; + + @Schema(description = "电站名称") + private String stnm; + + @Schema(description = "放流年份 yyyy") + private String flyr; + + @Schema(description = "鱼种类") + private String ftp; + + @Schema(description = "鱼种类名称") + private String ftpName; + + @Schema(description = "平均长度") + private BigDecimal fsZ; + + @Schema(description = "平均体重") + private BigDecimal fwet; + + @Schema(description = "计划放殖鱼数量") + private Long fcnt; + + @Schema(description = "标记类型") + private String tagte; + + @Schema(description = "标记数量") + private Long tagcnt; + + @Schema(description = "鱼龄") + private String fage; + + @Schema(description = "鱼类档案ID") + private String fid; + + @Schema(description = "保护等级") + private String protectlvl; + + @Schema(description = "亲本来源") + private String fa; + + @Schema(description = "备注") + private String remark; + + @Schema(description = "放流时间") + private Date tm; +} diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/FbStationService.java b/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/FbStationService.java index a4b5bc41..93aaf507 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/FbStationService.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/FbStationService.java @@ -21,6 +21,13 @@ public interface FbStationService { DataSourceResult getFishRunQgcKendoListCust(DataSourceRequest dataSourceRequest); + /** + * 查询全过程鱼类放鱼数据明细(细粒度),用于聚合行下钻查看底层放鱼明细。 + * + * @param dataSourceRequest 支持 stcd/flyr/ftp 等过滤条件与分页排序 + */ + DataSourceResult getFishRunQgcDetailKendoListCust(DataSourceRequest dataSourceRequest); + /** * 批量删除全过程鱼类放鱼数据(按 站点+年份+鱼种 维度逻辑删除 SD_RPIMNFISH_R 明细) * @@ -28,6 +35,13 @@ public interface FbStationService { */ boolean removeFishRunQgcData(List params); + /** + * 按明细ID批量逻辑删除全过程鱼类放鱼数据,并记录操作日志。 + * + * @param ids 明细ID列表(SD_RPIMNFISH_R.ID) + */ + boolean removeFishRunQgcDetailData(List ids); + DataSourceResult getFishTypeKendoListCust(DataSourceRequest dataSourceRequest); DataSourceResult getFishTypeStcdList(DataSourceRequest dataSourceRequest); diff --git a/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/impl/FbStationServiceImpl.java b/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/impl/FbStationServiceImpl.java index e49e5681..1e9a6304 100644 --- a/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/impl/FbStationServiceImpl.java +++ b/backend/src/main/java/com/yfd/platform/qgc_env/fb/service/impl/FbStationServiceImpl.java @@ -195,6 +195,74 @@ public class FbStationServiceImpl implements FbStationService { return result; } + @Override + public DataSourceResult getFishRunQgcDetailKendoListCust(DataSourceRequest dataSourceRequest) { + DataSourceLoadOptionsBase loadOptions = dataSourceRequest == null ? null : dataSourceRequest.toDevRequest(); + String stcd = loadOptions == null ? null : QgcQueryWrapperUtil.getFilterFieldValue(loadOptions, "stcd"); + String flyr = loadOptions == null ? null : QgcQueryWrapperUtil.getFilterFieldValue(loadOptions, "flyr"); + String ftp = loadOptions == null ? null : QgcQueryWrapperUtil.getFilterFieldValue(loadOptions, "ftp"); + + Map paramMap = new HashMap<>(); + StringBuilder sql = new StringBuilder(); + sql.append("SELECT det.ID AS id, ") + .append("run.ID AS rpimnId, ") + .append("run.STCD AS stcd, ") + .append("eng.ENNM AS stnm, ") + .append("TO_CHAR(run.TM, 'YYYY') AS flyr, ") + .append("det.FTP AS ftp, ") + .append(buildFishNameExpr("det.FTP")).append(" AS ftpName, ") + .append("det.FSZ AS fsZ, ") + .append("det.FWET AS fwet, ") + .append("det.FCNT AS fcnt, ") + .append("det.TAGTE AS tagte, ") + .append("det.TAGCNT AS tagcnt, ") + .append("det.FAGE AS fage, ") + .append("det.FID AS fid, ") + .append("det.PROTECTLVL AS protectlvl, ") + .append("det.FA AS fa, ") + .append("det.REMARK AS remark, ") + .append("run.TM AS tm ") + .append("FROM SD_RPIMNFISH_R det ") + .append("INNER JOIN SD_ENGFR_R run ON det.RPIMN_ID = run.ID AND NVL(run.IS_DELETED, 0) = 0 ") + .append("LEFT JOIN SD_ENGINFO_B_H eng ON eng.STCD = run.STCD AND NVL(eng.IS_DELETED, 0) = 0 ") + .append(buildFishJoinSql("det.FTP", "eng.HBRVCD")) + .append("WHERE NVL(det.IS_DELETED, 0) = 0 "); + if (StrUtil.isNotBlank(stcd)) { + paramMap.put("detailStcd", stcd); + sql.append("AND run.STCD = #{map.detailStcd} "); + } + if (StrUtil.isNotBlank(flyr)) { + paramMap.put("detailFlyr", flyr); + sql.append("AND TO_CHAR(run.TM, 'YYYY') = #{map.detailFlyr} "); + } + if (StrUtil.isNotBlank(ftp)) { + paramMap.put("detailFtp", ftp); + sql.append("AND det.FTP = #{map.detailFtp} "); + } + String filterSql = buildFishRunQgcDetailFilterCondition( + dataSourceRequest == null ? null : dataSourceRequest.getFilter(), + paramMap, + new int[]{0} + ); + if (StrUtil.isNotBlank(filterSql)) { + sql.append(" AND ").append(filterSql).append(" "); + } + sql.append(buildFishRunQgcDetailOrderBySql(dataSourceRequest == null ? null : dataSourceRequest.getSort())); + + Page page = loadOptions == null ? null : QgcQueryWrapperUtil.buildPage(loadOptions, loadOptions.getSkip(), loadOptions.getTake()); + List list = microservicDynamicSQLMapper.pageAllListWithResultType( + page, + sql.toString(), + paramMap, + FbFishRunQgcDetailVo.class + ); + DataSourceResult result = new DataSourceResult<>(); + result.setData(list); + result.setTotal(page != null ? page.getTotal() : list.size()); + result.setAggregates(new HashMap<>()); + return result; + } + @Override @Transactional(rollbackFor = Exception.class) public boolean removeFishRunQgcData(List params) { @@ -224,19 +292,67 @@ public class FbStationServiceImpl implements FbStationService { " AND TO_CHAR(run.TM, 'YYYY') = ? )", resolveLogOperator(), param.getFtp(), param.getStcd(), param.getFlyr() ); - recordFishRunDeleteLog(beforeRows, param, deleteCount); + recordFishRunDeleteLog(beforeRows, String.format("删除全过程鱼类放鱼数据(站点:%s,年份:%s,删除条数:%d)", + param.getStcd(), param.getFlyr(), deleteCount), deleteCount); } return true; } + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeFishRunQgcDetailData(List ids) { + if (CollUtil.isEmpty(ids)) { + return true; + } + List distinctIds = ids.stream() + .filter(StrUtil::isNotBlank) + .distinct() + .collect(Collectors.toList()); + if (distinctIds.isEmpty()) { + return true; + } + // 1. 查询删除前的明细行(含站点等信息, 用于记录日志) + List> beforeRows = queryFishRunDetailRowsByIds(distinctIds); + // 2. 分批逻辑删除(ID IN, 防止超过数据库 IN 限制) + int deleteCount = 0; + for (List part : CollUtil.split(distinctIds, 500)) { + deleteCount += batchSoftDeleteFishRunDetail(part); + } + // 3. 记录删除日志(每条明细一条主日志, 子日志挂对应主日志) + recordFishRunDeleteLog(beforeRows, String.format("删除全过程鱼类放鱼数据(明细删除,删除条数:%d)", deleteCount), deleteCount); + return true; + } + private List> queryFishRunDetailRows(FbFishRunQgcDeleteParam param) { - String sql = "SELECT det.ID, det.RPIMN_ID, det.FTP, det.FSZ, det.FWET, det.FCNT, " + + String sql = "SELECT det.ID, det.RPIMN_ID, run.STCD, det.FTP, det.FSZ, det.FWET, det.FCNT, " + "det.TAGTE, det.TAGCNT, det.FAGE, det.FID, det.PROTECTLVL, det.FA, det.REMARK " + "FROM SD_RPIMNFISH_R det " + "INNER JOIN SD_ENGFR_R run ON det.RPIMN_ID = run.ID AND NVL(run.IS_DELETED, 0) = 0 " + "WHERE NVL(det.IS_DELETED, 0) = 0 " + " AND det.FTP = ? AND run.STCD = ? AND TO_CHAR(run.TM, 'YYYY') = ?"; - List> rows = jdbcTemplate.queryForList(sql, param.getFtp(), param.getStcd(), param.getFlyr()); + return queryFishRunDetailRowsInternal(sql, param.getFtp(), param.getStcd(), param.getFlyr()); + } + + private List> queryFishRunDetailRowsByIds(List ids) { + StringBuilder sql = new StringBuilder(); + sql.append("SELECT det.ID, det.RPIMN_ID, run.STCD, det.FTP, det.FSZ, det.FWET, det.FCNT, ") + .append("det.TAGTE, det.TAGCNT, det.FAGE, det.FID, det.PROTECTLVL, det.FA, det.REMARK ") + .append("FROM SD_RPIMNFISH_R det ") + .append("INNER JOIN SD_ENGFR_R run ON det.RPIMN_ID = run.ID AND NVL(run.IS_DELETED, 0) = 0 ") + .append("WHERE NVL(det.IS_DELETED, 0) = 0 ") + .append(" AND det.ID IN ("); + for (int i = 0; i < ids.size(); i++) { + if (i > 0) { + sql.append(", "); + } + sql.append("?"); + } + sql.append(")"); + return queryFishRunDetailRowsInternal(sql.toString(), ids.toArray()); + } + + private List> queryFishRunDetailRowsInternal(String sql, Object... args) { + List> rows = jdbcTemplate.queryForList(sql, args); List> normalized = new ArrayList<>(); for (Map row : rows) { normalized.add(normalizeFishRunLogRow(row)); @@ -244,6 +360,28 @@ public class FbStationServiceImpl implements FbStationService { return normalized; } + private int batchSoftDeleteFishRunDetail(List ids) { + if (CollUtil.isEmpty(ids)) { + return 0; + } + StringBuilder sql = new StringBuilder(); + sql.append("UPDATE SD_RPIMNFISH_R det ") + .append("SET det.IS_DELETED = 1, det.DELETE_USER = ?, det.DELETE_TIME = SYSDATE ") + .append("WHERE NVL(det.IS_DELETED, 0) = 0 ") + .append(" AND det.ID IN ("); + List params = new ArrayList<>(); + params.add(resolveLogOperator()); + for (int i = 0; i < ids.size(); i++) { + if (i > 0) { + sql.append(", "); + } + sql.append("?"); + params.add(ids.get(i)); + } + sql.append(")"); + return jdbcTemplate.update(sql.toString(), params.toArray()); + } + private Map normalizeFishRunLogRow(Map row) { Map normalized = new LinkedHashMap<>(); if (row == null || row.isEmpty()) { @@ -255,19 +393,20 @@ public class FbStationServiceImpl implements FbStationService { return normalized; } - private void recordFishRunDeleteLog(List> rows, FbFishRunQgcDeleteParam param, int deleteCount) { + private void recordFishRunDeleteLog(List> rows, String remark, int deleteCount) { if (CollUtil.isEmpty(rows)) { return; } List details = new ArrayList<>(); for (Map row : rows) { + String recordId = asString(row.get("ID")); + String stcd = asString(row.get("STCD")); MsOperationLog mainLog = new MsOperationLog(); mainLog.setOperator(resolveLogOperator()); - mainLog.setRecordId(String.valueOf(row.get("ID"))); + mainLog.setRecordId(recordId); mainLog.setOperateTime(new Date()); mainLog.setTableName(FISH_RUN_DETAIL_TABLE_NAME); - mainLog.setRemark("删除全过程鱼类放鱼数据(站点:" + param.getStcd() - + ",年份:" + param.getFlyr() + ",删除条数:" + deleteCount + ")"); + mainLog.setRemark(remark); msOperationLogMapper.insert(mainLog); for (Map.Entry entry : row.entrySet()) { if (StrUtil.isBlank(entry.getKey()) || entry.getValue() == null) { @@ -276,7 +415,7 @@ public class FbStationServiceImpl implements FbStationService { MsOperationLogDetail detail = new MsOperationLogDetail(); detail.setMainId(mainLog.getId()); detail.setTableName(FISH_RUN_DETAIL_TABLE_NAME); - detail.setStationCode(param.getStcd()); + detail.setStationCode(stcd); detail.setFieldCode(entry.getKey()); detail.setFieldMeaning(FISH_RUN_FIELD_MEANING_MAP.getOrDefault(entry.getKey(), entry.getKey())); detail.setOldValueCode(formatFishRunLogValue(entry.getValue())); @@ -290,6 +429,10 @@ public class FbStationServiceImpl implements FbStationService { } } + private String asString(Object value) { + return value == null ? null : String.valueOf(value); + } + private String resolveLogOperator() { try { return SecurityUtils.getCurrentUsername(); @@ -1412,6 +1555,184 @@ public class FbStationServiceImpl implements FbStationService { return " ORDER BY " + String.join(", ", orderByParts); } + private String mapFishRunQgcDetailColumn(String field) { + if (StrUtil.isBlank(field)) { + return null; + } + return switch (field) { + case "id" -> "det.ID"; + case "rpimnId" -> "det.RPIMN_ID"; + case "stcd" -> "run.STCD"; + case "stnm" -> "eng.ENNM"; + case "flyr" -> "TO_CHAR(run.TM, 'YYYY')"; + case "ftp" -> "det.FTP"; + case "ftpName" -> buildFishNameExpr("det.FTP"); + case "fsZ" -> "det.FSZ"; + case "fwet" -> "det.FWET"; + case "fcnt" -> "det.FCNT"; + case "tagte" -> "det.TAGTE"; + case "tagcnt" -> "det.TAGCNT"; + case "fage" -> "det.FAGE"; + case "fid" -> "det.FID"; + case "protectlvl" -> "det.PROTECTLVL"; + case "fa" -> "det.FA"; + case "remark" -> "det.REMARK"; + case "tm" -> "run.TM"; + default -> null; + }; + } + + private Object normalizeFishRunQgcDetailValue(String field, Object value) { + if (value == null) { + return null; + } + if ("fcnt".equals(field) || "tagcnt".equals(field)) { + try { + return Long.parseLong(String.valueOf(value).trim()); + } catch (Exception ex) { + return null; + } + } + return value; + } + + private String buildFishRunQgcDetailFilterCondition(DataSourceRequest.FilterDescriptor filter, + Map paramMap, + int[] indexHolder) { + if (filter == null) { + return ""; + } + if (StrUtil.isNotBlank(filter.getField())) { + return buildFishRunQgcDetailLeafCondition(filter, paramMap, indexHolder); + } + if (CollUtil.isEmpty(filter.getFilters())) { + return ""; + } + List conditions = new ArrayList<>(); + for (DataSourceRequest.FilterDescriptor child : filter.getFilters()) { + String childSql = buildFishRunQgcDetailFilterCondition(child, paramMap, indexHolder); + if (StrUtil.isNotBlank(childSql)) { + conditions.add("(" + childSql + ")"); + } + } + if (conditions.isEmpty()) { + return ""; + } + String logic = "or".equalsIgnoreCase(filter.getLogic()) ? " OR " : " AND "; + return String.join(logic, conditions); + } + + private String buildFishRunQgcDetailLeafCondition(DataSourceRequest.FilterDescriptor filter, + Map paramMap, + int[] indexHolder) { + // 这三个字段已在主 SQL 中作为下钻条件处理,避免重复拼接 + if ("stcd".equalsIgnoreCase(filter.getField()) || "flyr".equalsIgnoreCase(filter.getField()) + || "ftp".equalsIgnoreCase(filter.getField())) { + return ""; + } + String column = mapFishRunQgcDetailColumn(filter.getField()); + if (StrUtil.isBlank(column)) { + return ""; + } + String operator = StrUtil.blankToDefault(filter.getOperator(), "eq").toLowerCase(); + if ("isnull".equals(operator)) { + return column + " IS NULL"; + } + if ("isnotnull".equals(operator)) { + return column + " IS NOT NULL"; + } + if ("isempty".equals(operator)) { + return "(" + column + " IS NULL OR " + column + " = '')"; + } + if ("isnotempty".equals(operator)) { + return "(" + column + " IS NOT NULL AND " + column + " <> '')"; + } + if ("in".equals(operator)) { + List values = normalizeQgcBsmfRValues(filter.getValue()); + if (values.isEmpty()) { + return ""; + } + List placeholders = new ArrayList<>(); + for (Object value : values) { + String key = "fishRunQgcDetailP" + indexHolder[0]++; + paramMap.put(key, normalizeFishRunQgcDetailValue(filter.getField(), value)); + placeholders.add("#{map." + key + "}"); + } + return column + " IN (" + String.join(", ", placeholders) + ")"; + } + Object value = filter.getValue(); + if (value == null) { + return ""; + } + String key = "fishRunQgcDetailP" + indexHolder[0]++; + Object normalizedValue = normalizeFishRunQgcDetailValue(filter.getField(), value); + return switch (operator) { + case "eq" -> { + paramMap.put(key, normalizedValue); + yield column + " = #{map." + key + "}"; + } + case "neq" -> { + paramMap.put(key, normalizedValue); + yield column + " <> #{map." + key + "}"; + } + case "gt" -> { + paramMap.put(key, normalizedValue); + yield column + " > #{map." + key + "}"; + } + case "gte" -> { + paramMap.put(key, normalizedValue); + yield column + " >= #{map." + key + "}"; + } + case "lt" -> { + paramMap.put(key, normalizedValue); + yield column + " < #{map." + key + "}"; + } + case "lte" -> { + paramMap.put(key, normalizedValue); + yield column + " <= #{map." + key + "}"; + } + case "contains" -> { + paramMap.put(key, "%" + String.valueOf(normalizedValue).trim() + "%"); + yield column + " LIKE #{map." + key + "}"; + } + case "doesnotcontain" -> { + paramMap.put(key, "%" + String.valueOf(normalizedValue).trim() + "%"); + yield column + " NOT LIKE #{map." + key + "}"; + } + case "startswith" -> { + paramMap.put(key, String.valueOf(normalizedValue).trim() + "%"); + yield column + " LIKE #{map." + key + "}"; + } + case "endswith" -> { + paramMap.put(key, "%" + String.valueOf(normalizedValue).trim()); + yield column + " LIKE #{map." + key + "}"; + } + default -> ""; + }; + } + + private String buildFishRunQgcDetailOrderBySql(List sorts) { + if (CollUtil.isEmpty(sorts)) { + return " ORDER BY run.TM DESC, det.ID DESC"; + } + List orderByParts = new ArrayList<>(); + for (DataSourceRequest.SortDescriptor sort : sorts) { + if (sort == null || StrUtil.isBlank(sort.getField())) { + continue; + } + String column = mapFishRunQgcDetailColumn(sort.getField()); + if (StrUtil.isBlank(column)) { + continue; + } + String direction = "desc".equalsIgnoreCase(sort.getDir()) ? "DESC" : "ASC"; + orderByParts.add(column + " " + direction); + } + if (orderByParts.isEmpty()) { + return " ORDER BY run.TM DESC, det.ID DESC"; + } + return " ORDER BY " + String.join(", ", orderByParts); + } + private Date[] parseFishRunFlyrRange(String flyrFilter) { if (StrUtil.isBlank(flyrFilter)) { return new Date[]{null, null};