feat: 监测规则操作历史接口 + 证据关联规则数量 + 法条详情补全

- 新增 MonitoringRuleController#getHistories 操作历史查询接口
- 规则详情补全法条 lawName/clauseCode/clauseContent 字段,避免前端二次查询
- 证据记录列表新增 relatedRuleCount,批量查询避免 N+1
- MonitoringRuleDetailVO 时间字段 Date → LocalDateTime
- 新增 V10.0.1__CW_clue_transfer_init_data.sql 线索转办初始化数据
- CLAUDE.md 补充 Karpathy 编码准则

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
chenjy 2026-06-13 22:10:09 +08:00
parent fce2858794
commit bd9429d3de
8 changed files with 233 additions and 4 deletions

View File

@ -143,3 +143,35 @@ gz-oarms-web/docs/prd-llm/
- F. Harness Engineering全流程、模块开发进度总表、核心实体跨域依赖、待开发任务建议顺序 - F. Harness Engineering全流程、模块开发进度总表、核心实体跨域依赖、待开发任务建议顺序
前后端联调指南见 `docs/前后端联调指南.md`,包含 10 个模块的接口清单、DTO 字段、数据库表映射及高频错误模式。 前后端联调指南见 `docs/前后端联调指南.md`,包含 10 个模块的接口清单、DTO 字段、数据库表映射及高频错误模式。
## Coding Guidelines (Karpathy)
编码行为准则,偏向谨慎而非速度。简单任务可灵活判断。
### 1. 先思考再编码
- 不要假设。不确定时先问。
- 存在多种理解时,列出选项而非静默选择。
- 有更简单的方案时说出来。必要时反驳。
- 不清楚就停,指出困惑点。
### 2. 简洁优先
- 只实现被要求的功能,不加额外特性。
- 单次使用的代码不做抽象。
- 没有要求的"灵活性"或"可配置性"不加。
- 不可能发生的场景不加错误处理。
- 200 行能缩减到 50 行就重写。
### 3. 精准修改
- 只改必须改的。不"改善"相邻代码、注释或格式。
- 能用的代码不重构。匹配已有风格。
- 发现无关死代码时提及但不删除。
- 自己的改动产生的废弃 import/变量/函数要清理。
### 4. 目标驱动
- 将任务转化为可验证的目标。
- 多步骤任务先列出简短计划和验证点。
- 成功标准要明确,避免模糊目标(如"让它能用")。

View File

@ -0,0 +1,119 @@
-- ============================================================================
-- OARMS - CW-4 线索转办管理 初始数据
-- Database: DM8 (达梦)
-- Schema: OARMS
-- Version: V10.0.1
-- Date: 2026-05-27
-- Description: 基于 CW-MC-001 线索的转办记录 + 操作日志
-- Note:
-- 1. CW-MC-001 当前线索状态为 1(待转办),创建一条已转办记录
-- 2. 同时将线索状态更新为 2(已转办)
-- 3. 转办目标使用行政区划中的区域编码
-- ============================================================================
-- 1. CW-MC-001(白云万达虚假宣传) → 转办到白云区市场监督管理局
INSERT INTO OARMS.CW_CLUE_TRANSFER_RECORD (
ID, CLUE_ID, TRANSFER_TARGET_DISTRICT, TRANSFER_TARGET_DEPARTMENT,
TRANSFER_TARGET_PERSON, TRANSFER_DESCRIPTION, TRANSFER_PERSON,
TRANSFERRED_AT, TRANSFER_STATUS, EXTERNAL_CLUE_ID,
CREATE_BY, CREATE_TIME, CREATE_NAME, UPDATE_BY, UPDATE_TIME, UPDATE_NAME
) VALUES (
'CW-CTR-001', 'CW-MC-001', '440111', '白云区市场监督管理局',
'张志明', '该线索涉及白云万达广场LED大屏虚假宣传"包治百病",建议转白云区市监局处理',
'系统管理员',
TO_TIMESTAMP('2026-05-25 10:00:00', 'YYYY-MM-DD HH24:MI:SS'), 'transferred', 'EXT-CLUE-20260525-001',
'system', TO_TIMESTAMP('2026-05-25 10:00:00', 'YYYY-MM-DD HH24:MI:SS'), '系统',
'system', TO_TIMESTAMP('2026-05-25 10:00:00', 'YYYY-MM-DD HH24:MI:SS'), '系统'
);
-- 2. CW-MC-001 → 转办记录2天河区已办结状态
INSERT INTO OARMS.CW_CLUE_TRANSFER_RECORD (
ID, CLUE_ID, TRANSFER_TARGET_DISTRICT, TRANSFER_TARGET_DEPARTMENT,
TRANSFER_TARGET_PERSON, TRANSFER_DESCRIPTION, TRANSFER_PERSON,
TRANSFERRED_AT, TRANSFER_STATUS, EXTERNAL_CLUE_ID,
DISPOSAL_RESULT, DISPOSAL_COMPLETED_AT,
CREATE_BY, CREATE_TIME, CREATE_NAME, UPDATE_BY, UPDATE_TIME, UPDATE_NAME
) VALUES (
'CW-CTR-002', 'CW-MC-001', '440106', '天河区市场监督管理局',
'李伟强', '同时转天河区市监局协助调查广告主信息',
'系统管理员',
TO_TIMESTAMP('2026-05-26 09:30:00', 'YYYY-MM-DD HH24:MI:SS'), 'completed', 'EXT-CLUE-20260526-002',
'已联系广告主责令拆除违规广告内容罚款5000元',
TO_TIMESTAMP('2026-05-26 16:00:00', 'YYYY-MM-DD HH24:MI:SS'),
'system', TO_TIMESTAMP('2026-05-26 09:30:00', 'YYYY-MM-DD HH24:MI:SS'), '系统',
'system', TO_TIMESTAMP('2026-05-26 16:00:00', 'YYYY-MM-DD HH24:MI:SS'), '系统'
);
-- ===================== 转办操作日志 =====================
-- CW-CTR-001: 提交 + 推送成功
INSERT INTO OARMS.CW_TRANSFER_OPERATION_LOG (
ID, CLUE_TRANSFER_RECORD_ID, OPERATION_TYPE, OPERATION_DETAIL,
FROM_STATUS, TO_STATUS, OPERATOR, OPERATED_AT
) VALUES (
'CW-TOL-001', 'CW-CTR-001', 'submit',
'提交线索转办,目标:白云区市场监督管理局',
NULL, 'transferred', '系统管理员',
TO_TIMESTAMP('2026-05-25 10:00:00', 'YYYY-MM-DD HH24:MI:SS')
);
INSERT INTO OARMS.CW_TRANSFER_OPERATION_LOG (
ID, CLUE_TRANSFER_RECORD_ID, OPERATION_TYPE, OPERATION_DETAIL,
FROM_STATUS, TO_STATUS, OPERATOR, OPERATED_AT
) VALUES (
'CW-TOL-002', 'CW-CTR-001', 'push_success',
'线索推送成功外部线索ID: EXT-CLUE-20260525-001',
'transferred', 'transferred', '系统管理员',
TO_TIMESTAMP('2026-05-25 10:00:05', 'YYYY-MM-DD HH24:MI:SS')
);
-- CW-CTR-002: 提交 + 推送成功 + 状态同步 + 结果反馈
INSERT INTO OARMS.CW_TRANSFER_OPERATION_LOG (
ID, CLUE_TRANSFER_RECORD_ID, OPERATION_TYPE, OPERATION_DETAIL,
FROM_STATUS, TO_STATUS, OPERATOR, OPERATED_AT
) VALUES (
'CW-TOL-003', 'CW-CTR-002', 'submit',
'提交线索转办,目标:天河区市场监督管理局',
NULL, 'transferred', '系统管理员',
TO_TIMESTAMP('2026-05-26 09:30:00', 'YYYY-MM-DD HH24:MI:SS')
);
INSERT INTO OARMS.CW_TRANSFER_OPERATION_LOG (
ID, CLUE_TRANSFER_RECORD_ID, OPERATION_TYPE, OPERATION_DETAIL,
FROM_STATUS, TO_STATUS, OPERATOR, OPERATED_AT
) VALUES (
'CW-TOL-004', 'CW-CTR-002', 'push_success',
'线索推送成功外部线索ID: EXT-CLUE-20260526-002',
'transferred', 'transferred', '系统管理员',
TO_TIMESTAMP('2026-05-26 09:30:05', 'YYYY-MM-DD HH24:MI:SS')
);
INSERT INTO OARMS.CW_TRANSFER_OPERATION_LOG (
ID, CLUE_TRANSFER_RECORD_ID, OPERATION_TYPE, OPERATION_DETAIL,
FROM_STATUS, TO_STATUS, OPERATOR, OPERATED_AT
) VALUES (
'CW-TOL-005', 'CW-CTR-002', 'status_update',
'状态从 transferred 变更为 processing',
'transferred', 'processing', '外部系统回调',
TO_TIMESTAMP('2026-05-26 14:00:00', 'YYYY-MM-DD HH24:MI:SS')
);
INSERT INTO OARMS.CW_TRANSFER_OPERATION_LOG (
ID, CLUE_TRANSFER_RECORD_ID, OPERATION_TYPE, OPERATION_DETAIL,
FROM_STATUS, TO_STATUS, OPERATOR, OPERATED_AT
) VALUES (
'CW-TOL-006', 'CW-CTR-002', 'status_update',
'状态从 processing 变更为 completed',
'processing', 'completed', '外部系统回调',
TO_TIMESTAMP('2026-05-26 16:00:00', 'YYYY-MM-DD HH24:MI:SS')
);
-- ===================== 更新线索状态 =====================
-- CW-MC-001 从待转办(1)更新为已转办(2)
UPDATE OARMS.CW_MONITORING_CLUE
SET CLUE_STATUS = 2,
TRANSFERRED_AT = TO_TIMESTAMP('2026-05-25 10:00:00', 'YYYY-MM-DD HH24:MI:SS'),
UPDATE_BY = 'system',
UPDATE_TIME = TO_TIMESTAMP('2026-05-26 16:00:00', 'YYYY-MM-DD HH24:MI:SS'),
UPDATE_NAME = '系统'
WHERE ID = 'CW-MC-001';

View File

@ -112,4 +112,9 @@ public class EvidenceRecordDetailVO {
* 状态变更历史列表 * 状态变更历史列表
*/ */
private List<Map<String, Object>> statusHistory; private List<Map<String, Object>> statusHistory;
/**
* 关联规则数量
*/
private Integer relatedRuleCount;
} }

View File

@ -20,6 +20,8 @@ import com.chinaweal.youfool.prj.modules.screen.entity.ScreenEntity;
import com.chinaweal.youfool.prj.modules.system.entity.vo.LoginUserVO; import com.chinaweal.youfool.prj.modules.system.entity.vo.LoginUserVO;
import com.chinaweal.youfool.prj.modules.system.util.DataScopeHelper; import com.chinaweal.youfool.prj.modules.system.util.DataScopeHelper;
import com.chinaweal.youfool.prj.modules.screen.mapper.ScreenMapper; import com.chinaweal.youfool.prj.modules.screen.mapper.ScreenMapper;
import com.chinaweal.youfool.prj.modules.evidence.relation.entity.EvidenceRuleRelationEntity;
import com.chinaweal.youfool.prj.modules.evidence.relation.mapper.EvidenceRuleRelationMapper;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
@ -30,6 +32,7 @@ import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors; import java.util.stream.Collectors;
/** /**
@ -45,6 +48,7 @@ public class EvidenceRecordServiceImpl extends ServiceImpl<EvidenceRecordMapper,
implements IEvidenceRecordService { implements IEvidenceRecordService {
private final EvidenceStatusHistoryMapper evidenceStatusHistoryMapper; private final EvidenceStatusHistoryMapper evidenceStatusHistoryMapper;
private final EvidenceRuleRelationMapper evidenceRuleRelationMapper;
private final ScreenMapper screenMapper; private final ScreenMapper screenMapper;
@Override @Override
@ -89,9 +93,24 @@ public class EvidenceRecordServiceImpl extends ServiceImpl<EvidenceRecordMapper,
// 转换为VO // 转换为VO
Page<EvidenceRecordDetailVO> voPage = new Page<>(entityPage.getCurrent(), Page<EvidenceRecordDetailVO> voPage = new Page<>(entityPage.getCurrent(),
entityPage.getSize(), entityPage.getTotal()); entityPage.getSize(), entityPage.getTotal());
// 批量查询关联规则数量
Set<String> evidenceIds = entityPage.getRecords().stream()
.map(EvidenceRecordEntity::getId).collect(Collectors.toSet());
Map<String, Long> ruleCountMap = Map.of();
if (!evidenceIds.isEmpty()) {
LambdaQueryWrapper<EvidenceRuleRelationEntity> ruleWrapper = new LambdaQueryWrapper<>();
ruleWrapper.in(EvidenceRuleRelationEntity::getEvidenceId, evidenceIds);
List<EvidenceRuleRelationEntity> allRelations = evidenceRuleRelationMapper.selectList(ruleWrapper);
ruleCountMap = allRelations.stream()
.collect(Collectors.groupingBy(EvidenceRuleRelationEntity::getEvidenceId, Collectors.counting()));
}
Map<String, Long> finalRuleCountMap = ruleCountMap;
List<EvidenceRecordDetailVO> voList = entityPage.getRecords().stream().map(entity -> { List<EvidenceRecordDetailVO> voList = entityPage.getRecords().stream().map(entity -> {
EvidenceRecordDetailVO vo = new EvidenceRecordDetailVO(); EvidenceRecordDetailVO vo = new EvidenceRecordDetailVO();
BeanUtils.copyProperties(entity, vo); BeanUtils.copyProperties(entity, vo);
vo.setRelatedRuleCount(finalRuleCountMap.getOrDefault(entity.getId(), 0L).intValue());
return vo; return vo;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
voPage.setRecords(voList); voPage.setRecords(voList);

View File

@ -192,4 +192,17 @@ public class MonitoringRuleController {
log.info("[OK] 导出监测规则数据"); log.info("[OK] 导出监测规则数据");
return monitoringRuleService.exportData(query); return monitoringRuleService.exportData(query);
} }
/**
* 查询规则操作历史
*
* @param id 规则ID
* @return 操作历史列表
*/
@GetMapping("histories")
@Operation(summary = "查询规则操作历史")
public RestResult<List<Map<String, Object>>> getHistories(String id) {
log.info("[OK] 查询规则操作历史: id={}", id);
return monitoringRuleService.getHistories(id);
}
} }

View File

@ -5,7 +5,7 @@ import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data; import lombok.Data;
import lombok.experimental.Accessors; import lombok.experimental.Accessors;
import java.util.Date; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -52,10 +52,10 @@ public class MonitoringRuleDetailVO {
private Integer enableStatus; private Integer enableStatus;
@JsonFormat(pattern = DateUtil.DATETIME_DEFAULT_FORMAT, timezone = "GMT+8") @JsonFormat(pattern = DateUtil.DATETIME_DEFAULT_FORMAT, timezone = "GMT+8")
private Date createTime; private LocalDateTime createTime;
@JsonFormat(pattern = DateUtil.DATETIME_DEFAULT_FORMAT, timezone = "GMT+8") @JsonFormat(pattern = DateUtil.DATETIME_DEFAULT_FORMAT, timezone = "GMT+8")
private Date updateTime; private LocalDateTime updateTime;
private String createName; private String createName;

View File

@ -89,4 +89,12 @@ public interface IMonitoringRuleService {
* @return 导出数据 * @return 导出数据
*/ */
RestResult<List<Map<String, Object>>> exportData(MonitoringRuleQuery query); RestResult<List<Map<String, Object>>> exportData(MonitoringRuleQuery query);
/**
* 查询规则操作历史
*
* @param id 规则ID
* @return 操作历史列表
*/
RestResult<List<Map<String, Object>>> getHistories(String id);
} }

View File

@ -18,6 +18,8 @@ import com.chinaweal.youfool.prj.modules.rule.mapper.MonitoringRuleMapper;
import com.chinaweal.youfool.prj.modules.rule.mapper.RuleLawClauseRelMapper; import com.chinaweal.youfool.prj.modules.rule.mapper.RuleLawClauseRelMapper;
import com.chinaweal.youfool.prj.modules.rule.mapper.RuleOperationHistoryMapper; import com.chinaweal.youfool.prj.modules.rule.mapper.RuleOperationHistoryMapper;
import com.chinaweal.youfool.prj.modules.rule.service.IMonitoringRuleService; import com.chinaweal.youfool.prj.modules.rule.service.IMonitoringRuleService;
import com.chinaweal.youfool.prj.modules.law.entity.LawClauseEntity;
import com.chinaweal.youfool.prj.modules.law.mapper.LawClauseMapper;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanUtils;
@ -46,6 +48,7 @@ public class MonitoringRuleServiceImpl extends ServiceImpl<MonitoringRuleMapper,
private final RuleLawClauseRelMapper ruleLawClauseRelMapper; private final RuleLawClauseRelMapper ruleLawClauseRelMapper;
private final RuleOperationHistoryMapper ruleOperationHistoryMapper; private final RuleOperationHistoryMapper ruleOperationHistoryMapper;
private final LawClauseMapper lawClauseMapper;
private final UserBaseService userBaseService; private final UserBaseService userBaseService;
@Override @Override
@ -98,9 +101,20 @@ public class MonitoringRuleServiceImpl extends ServiceImpl<MonitoringRuleMapper,
clauseWrapper.eq(RuleLawClauseRelEntity::getRuleId, id); clauseWrapper.eq(RuleLawClauseRelEntity::getRuleId, id);
List<RuleLawClauseRelEntity> clauseRels = ruleLawClauseRelMapper.selectList(clauseWrapper); List<RuleLawClauseRelEntity> clauseRels = ruleLawClauseRelMapper.selectList(clauseWrapper);
List<Map<String, Object>> lawClauses = clauseRels.stream().map(rel -> { List<Map<String, Object>> lawClauses = clauseRels.stream().map(rel -> {
Map<String, Object> map = new HashMap<>(); Map<String, Object> map = new LinkedHashMap<>();
map.put("id", rel.getId()); map.put("id", rel.getId());
map.put("clauseId", rel.getClauseId()); map.put("clauseId", rel.getClauseId());
LawClauseEntity clause = lawClauseMapper.selectById(rel.getClauseId());
if (clause == null) {
LambdaQueryWrapper<LawClauseEntity> codeWrapper = new LambdaQueryWrapper<>();
codeWrapper.eq(LawClauseEntity::getClauseCode, rel.getClauseId());
clause = lawClauseMapper.selectOne(codeWrapper);
}
if (clause != null) {
map.put("lawName", clause.getLawName());
map.put("clauseCode", clause.getClauseCode());
map.put("clauseContent", clause.getClauseContent());
}
return map; return map;
}).collect(Collectors.toList()); }).collect(Collectors.toList());
vo.setLawClauses(lawClauses); vo.setLawClauses(lawClauses);
@ -301,6 +315,25 @@ public class MonitoringRuleServiceImpl extends ServiceImpl<MonitoringRuleMapper,
return RestResult.ok(exportList); return RestResult.ok(exportList);
} }
@Override
public RestResult<List<Map<String, Object>>> getHistories(String id) {
LambdaQueryWrapper<RuleOperationHistoryEntity> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(RuleOperationHistoryEntity::getRuleId, id);
wrapper.orderByDesc(RuleOperationHistoryEntity::getOperatedAt);
List<RuleOperationHistoryEntity> histories = ruleOperationHistoryMapper.selectList(wrapper);
List<Map<String, Object>> list = histories.stream().map(h -> {
Map<String, Object> map = new LinkedHashMap<>();
map.put("id", h.getId());
map.put("operator", h.getOperator());
map.put("operationType", h.getOperationType());
map.put("beforeChange", h.getBeforeChange());
map.put("afterChange", h.getAfterChange());
map.put("operatedAt", h.getOperatedAt());
return map;
}).collect(Collectors.toList());
return RestResult.ok(list);
}
// ========================================================================= // =========================================================================
// 私有方法 // 私有方法
// ========================================================================= // =========================================================================