2026-06-18-M8-P1-日志基础设施实现计划.md 28 KB

M8 全量操作日志 · P1 基础设施 实现计划

执行状态(2026-06-18 体检确认):本计划已执行完成——operation_log 表 + aivfo-oplog 微服务(端口 10060)+ 消费入库 + 保留期清理全部落地;端到端发测试消息→入库验证通过(V-122 自测达成,真机操作触发待复验)。详见 进度/交接卡.md进度/进度状态.yaml。后续 P2(Java 接入)/P3(C# 端)/Pjava 亦已完成。

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 建好"日志入库"地基——log 库新增 operation_log 表 + 新建 aivfo-oplog 微服务消费 Kafka topic tl-oplog 入库 + 保留期清理;做完后发一条测试 JSON 消息能落库即验证通过。

Architecture: 新建单模块 Spring Boot 微服务 aivfo-oplog(结构照搬 aivfo-service),用现有 aivfo-kafka-spring-bootKafkaFactory/ReceiveMessage/ConsumerManualCommit)消费 tl-oplog,JSON 反序列化为 OperationLog 实体,经 mybatis-plus 写 logoperation_log 表。保留期用 @Scheduled 定时删过期。本计划是 P1;P2(Java 接入)/P3(C# 端) 另出。

Tech Stack: Java 11 / Spring Boot / aivfo-framework(parent aivfo-business-parent:1.2.0-SNAPSHOT) / aivfo-kafka-spring-boot / mybatis-plus / Nacos / MySQL log 库 / Jackson(JSON)。

前置环境(已就绪):JDK11+Maven@C:\TLData\tools、Nexus 凭证已配、mvn install aivfo-framework 先行;中间件 7 容器在跑(含 Kafka localhost:9092loglocalhost:3306 root/root);详见 项目文档/开发环境/环境与账号清单.md、记忆 java-microservice-runtime


File Structure(P1 新增/修改)

时差项目源代码/
├─ sql/
│  └─ migrations/2026-06-18-operation-log.sql        [新增] operation_log 建表
└─ aivfo-oplog/                                       [新增微服务,结构仿 aivfo-service]
   ├─ pom.xml                                          父=aivfo-business-parent;依赖 kafka/mybatis-plus/web/log/nacos starter
   └─ src/main/
      ├─ java/com/aivfo/oplog/
      │  ├─ OplogApplication.java                      启动类
      │  ├─ entity/OperationLog.java                   mybatis-plus 实体(对应 operation_log)
      │  ├─ entity/OperationLogMessage.java            Kafka JSON 消息 DTO(C#/Java 共用 schema)
      │  ├─ mapper/OperationLogMapper.java             BaseMapper
      │  ├─ service/OperationLogService.java           +impl/OperationLogServiceImpl.java  IService
      │  ├─ kafka/OplogReceiveMessage.java             ReceiveMessage 实现:JSON→实体→入库
      │  ├─ kafka/OplogConsumerRunner.java             启动时拉起 tl-oplog 消费者
      │  └─ schedule/RetentionCleanTask.java           保留期清理(@Scheduled)
      └─ resources/
         ├─ application.properties                     spring.application.name=aivfo-oplog + active profile
         └─ application-local.properties               nacos/datasource(log库)/kafka 地址(localhost)

说明:operation_log 与现有 system_log 同在 log 库,但 system_log 由 logback LoggerDBAppender 写、operation_log 由本微服务写,两者经 trace_id 关联(见 14 §6)。


Task 1: operation_log 建表 SQL

Files:

  • Create: 时差项目源代码/sql/migrations/2026-06-18-operation-log.sql

  • [ ] Step 1: 写建表脚本(字段对齐 14 §5;引擎/字符集同 system_log:InnoDB/utf8mb4)

    -- 全量操作日志表(操作审计)。库:log。与 system_log 并列,共享 trace_id 可 join。见 需求文档/14 §5。
    -- 幂等:DROP IF EXISTS + CREATE。
    DROP TABLE IF EXISTS `operation_log`;
    CREATE TABLE `operation_log` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增id',
    `trace_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '全链路串联ID',
    `parent_id` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '父子调用层级ID',
    `op_time` datetime(3) NULL DEFAULT NULL COMMENT '操作时间(毫秒)',
    `project` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '端/服务:operate/front/tl-control...',
    `module` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '功能模块(可读):对焦/串口/患者...',
    `operation` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '操作(可读):一键标定/打开端口...',
    `operator` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '谁:登录用户/工程师/系统/设备SN',
    `input` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '输入(JSON;大对象只存文件名/关键字段)',
    `output` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '输出(JSON;同上)',
    `result` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '结果:成功/失败',
    `error` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL COMMENT '报错:消息+堆栈摘要',
    `elapsed_ms` bigint NULL DEFAULT NULL COMMENT '耗时(ms)',
    `level` varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '级别(操作级默认)',
    `house_sn` int NULL DEFAULT NULL COMMENT '仓室号(便于按舱过滤)',
    `well_sn` int NULL DEFAULT NULL COMMENT 'well号',
    `tl_sn` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '设备SN',
    `host` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '来源主机/IP',
    `create_time` datetime(3) NULL DEFAULT NULL COMMENT '入库时间',
    PRIMARY KEY (`id`) USING BTREE,
    INDEX `idx_trace`(`trace_id`) USING BTREE COMMENT '按trace_id拉链路/join system_log',
    INDEX `idx_query`(`project`, `module`, `op_time`) USING BTREE COMMENT '按项目>模块>时间查询',
    INDEX `idx_device`(`tl_sn`, `house_sn`, `op_time`) USING BTREE COMMENT '按设备/舱查询',
    INDEX `idx_optime`(`op_time`) USING BTREE COMMENT '保留期清理用'
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '全量操作日志(操作审计);与system_log经trace_id关联' ROW_FORMAT = Dynamic;
    
  • [ ] Step 2: 应用到 log 库并校验

Run:

export PATH="$PATH:/c/Program Files/Docker/Docker/resources/bin"
docker exec -i tl-mysql mysql -uroot -proot log < "/c/TLData/trae_projects/Project_TL/时差项目源代码/sql/migrations/2026-06-18-operation-log.sql"
docker exec tl-mysql mysql -uroot -proot -N -e "SELECT COUNT(*) FROM information_schema.columns WHERE table_schema='log' AND table_name='operation_log';"

Expected: 列数 = 19(输出 19)。

  • [ ] Step 3: Commit

    cd 时差项目源代码 && git add sql/migrations/2026-06-18-operation-log.sql
    git commit -m "M8-01: operation_log 建表脚本(log库,操作审计表)"
    

Task 2: aivfo-oplog 微服务骨架

Files:

  • Create: 时差项目源代码/aivfo-oplog/pom.xml
  • Create: 时差项目源代码/aivfo-oplog/src/main/java/com/aivfo/oplog/OplogApplication.java
  • Create: 时差项目源代码/aivfo-oplog/src/main/resources/application.properties
  • Create: 时差项目源代码/aivfo-oplog/src/main/resources/application-local.properties

  • [ ] Step 1: 写 pom.xml(parent 同 aivfo-service;依赖在 aivfo-service 基础上加 kafka + nacos starter)

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <artifactId>aivfo-business-parent</artifactId>
        <groupId>com.aivfo</groupId>
        <version>1.2.0-SNAPSHOT</version>
    </parent>
    <artifactId>aivfo-oplog</artifactId>
    <version>1.0.0-SNAPSHOT</version>
    <packaging>jar</packaging>
    <description>全量操作日志微服务:消费 tl-oplog 入 operation_log</description>
    
    <dependencies>
        <dependency><groupId>com.aivfo</groupId><artifactId>aivfo-web-servlet-spring-boot-starter</artifactId></dependency>
        <dependency><groupId>com.aivfo</groupId><artifactId>aivfo-mybatis-plus-spring-boot-starter</artifactId></dependency>
        <dependency><groupId>com.aivfo</groupId><artifactId>aivfo-log-spring-boot-starter</artifactId></dependency>
        <dependency><groupId>com.aivfo</groupId><artifactId>aivfo-kafka-spring-boot-starter</artifactId></dependency>
        <dependency><groupId>com.aivfo</groupId><artifactId>aivfo-nacos-spring-boot-starter</artifactId></dependency>
    </dependencies>
    
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration><mainClass>com.aivfo.oplog.OplogApplication</mainClass></configuration>
                <executions><execution><goals><goal>repackage</goal></goals></execution></executions>
            </plugin>
        </plugins>
    </build>
    </project>
    

⚠ 执行时核对:starter 的确切 artifactId/版本以 aivfo-data-transmission 的 pom 为准(它同时用了 kafka+mybatis-plus+nacos)。若 aivfo-kafka-spring-boot-starter/aivfo-nacos-spring-boot-starter 名称不符,照 data-transmission pom 改正。

  • [ ] Step 2: 写启动类 OplogApplication.java

    package com.aivfo.oplog;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.scheduling.annotation.EnableScheduling;
    
    @SpringBootApplication(scanBasePackages = "com.aivfo")
    @EnableScheduling
    @Slf4j
    public class OplogApplication {
    public static void main(String[] args) {
        SpringApplication.run(OplogApplication.class, args);
        log.info("全量操作日志服务 aivfo-oplog 启动成功!");
    }
    }
    
  • [ ] Step 3: 写 application.properties + application-local.properties

application.properties:

spring.application.name=aivfo-oplog
spring.profiles.active=local
server.port=10060

application-local.properties(datasource 指 log 库;nacos/kafka 用 localhost;参照 data-transmission application-local 的 key 名):

server.ip=localhost
# 数据源:log 库
aivfo.mybatis-plus.datasource.jdbcUrl=jdbc:mysql://${server.ip}:3306/log?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&rewriteBatchedStatements=true
aivfo.mybatis-plus.datasource.username=root
aivfo.mybatis-plus.datasource.password=root
# nacos 注册
spring.cloud.nacos.discovery.server-addr=${server.ip}:8848
# kafka
aivfo.kafka.properties.ips=${server.ip}:9092
aivfo.kafka.properties.consumer.autoOffsetReset=earliest
aivfo.kafka.properties.consumer.group=aivfo-oplog
# 操作日志 topic + 保留天数
aivfo.oplog.topic=tl-oplog
aivfo.oplog.topic.partition.number=3
aivfo.oplog.retention.days=30

aivfo.kafka.properties.* 的确切 key 以 data-transmission application-local.properties 为准(execute 时 grep aivfo.kafka 对照)。

  • Step 4: 编译骨架(先 install framework,再编译本服务)

Run:

cd 时差项目源代码/aivfo-oplog
mvn -q -DskipTests compile 2>&1 | tail -5; echo "EXIT=${PIPESTATUS[0]}"

Expected: EXIT=0(此时无 Kafka/实体代码,仅骨架,能编译)。

  • [ ] Step 5: Commit

    cd 时差项目源代码 && git add aivfo-oplog
    git commit -m "M8-01: aivfo-oplog 微服务骨架(pom/启动类/配置)"
    

Task 3: OperationLog 实体 + Mapper + Service

Files:

  • Create: aivfo-oplog/src/main/java/com/aivfo/oplog/entity/OperationLog.java
  • Create: aivfo-oplog/src/main/java/com/aivfo/oplog/mapper/OperationLogMapper.java
  • Create: aivfo-oplog/src/main/java/com/aivfo/oplog/service/OperationLogService.java
  • Create: aivfo-oplog/src/main/java/com/aivfo/oplog/service/impl/OperationLogServiceImpl.java

  • [ ] Step 1: 写实体 OperationLog.java(mybatis-plus;字段对应 operation_log 列)

    package com.aivfo.oplog.entity;
    
    import com.baomidou.mybatisplus.annotation.IdType;
    import com.baomidou.mybatisplus.annotation.TableId;
    import com.baomidou.mybatisplus.annotation.TableName;
    import lombok.Data;
    import java.time.LocalDateTime;
    
    @Data
    @TableName("operation_log")
    public class OperationLog {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String traceId;
    private String parentId;
    private LocalDateTime opTime;
    private String project;
    private String module;
    private String operation;
    private String operator;
    private String input;
    private String output;
    private String result;
    private String error;
    private Long elapsedMs;
    private String level;
    private Integer houseSn;
    private Integer wellSn;
    private String tlSn;
    private String host;
    private LocalDateTime createTime;
    }
    

注:mybatis-plus 默认驼峰转下划线(traceId→trace_id),与表列一致;若项目关了该开关需加 @TableField。执行时对照 data-transmission 的某 DAO 是否用 @TableField

  • Step 2: 写 Mapper + Service + Impl

OperationLogMapper.java:

package com.aivfo.oplog.mapper;
import com.aivfo.oplog.entity.OperationLog;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OperationLogMapper extends BaseMapper<OperationLog> {}

OperationLogService.java:

package com.aivfo.oplog.service;
import com.aivfo.oplog.entity.OperationLog;
import com.baomidou.mybatisplus.extension.service.IService;

public interface OperationLogService extends IService<OperationLog> {}

OperationLogServiceImpl.java:

package com.aivfo.oplog.service.impl;
import com.aivfo.oplog.entity.OperationLog;
import com.aivfo.oplog.mapper.OperationLogMapper;
import com.aivfo.oplog.service.OperationLogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;

@Service
public class OperationLogServiceImpl extends ServiceImpl<OperationLogMapper, OperationLog> implements OperationLogService {}
  • [ ] Step 3: 在启动类加 @MapperScan(OplogApplication.java 类注解上方加)

    @org.mybatis.spring.annotation.MapperScan("com.aivfo.oplog.mapper")
    

(加在 @SpringBootApplication 同级注解处)

  • Step 4: 编译

Run: cd 时差项目源代码/aivfo-oplog && mvn -q -DskipTests compile 2>&1 | tail -5; echo EXIT=${PIPESTATUS[0]} Expected: EXIT=0

  • [ ] Step 5: Commit

    cd 时差项目源代码 && git add aivfo-oplog && git commit -m "M8-01: OperationLog 实体/mapper/service(mybatis-plus)"
    

Task 4: Kafka 消息 DTO + 消费者入库

Files:

  • Create: aivfo-oplog/src/main/java/com/aivfo/oplog/entity/OperationLogMessage.java
  • Create: aivfo-oplog/src/main/java/com/aivfo/oplog/kafka/OplogReceiveMessage.java
  • Create: aivfo-oplog/src/main/java/com/aivfo/oplog/kafka/OplogConsumerRunner.java

  • [ ] Step 1: 写 Kafka JSON 消息 DTO OperationLogMessage.java(C#/Java 共用 schema,字段名与 14 §5 一致,JSON 驼峰)

    package com.aivfo.oplog.entity;
    
    import lombok.Data;
    
    /** Kafka tl-oplog 消息体(JSON)。C# 与 Java 生产端按此字段名序列化。见 需求文档/14 §5。 */
    @Data
    public class OperationLogMessage {
    private String traceId;
    private String parentId;
    private Long time;        // epoch millis
    private String project;
    private String module;
    private String operation;
    private String operator;
    private String input;
    private String output;
    private String result;
    private String error;
    private Long elapsedMs;
    private String level;
    private Integer houseSn;
    private Integer wellSn;
    private String tlSn;
    private String host;
    }
    
  • [ ] Step 2: 写消费回调 OplogReceiveMessage.java(实现 ReceiveMessage<String, byte[]>;JSON→实体→入库;全程 try 兜底,解析失败丢弃不阻塞)

    package com.aivfo.oplog.kafka;
    
    import com.aivfo.oplog.entity.OperationLog;
    import com.aivfo.oplog.entity.OperationLogMessage;
    import com.aivfo.oplog.service.OperationLogService;
    import com.aivfo.kafka.callback.ReceiveMessage;
    import com.fasterxml.jackson.databind.ObjectMapper;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.kafka.clients.consumer.ConsumerRecord;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.nio.charset.StandardCharsets;
    import java.time.Instant;
    import java.time.LocalDateTime;
    import java.time.ZoneId;
    
    @Slf4j
    @Component("oplogReceive")
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class OplogReceiveMessage implements ReceiveMessage<String, byte[]> {
    final OperationLogService operationLogService;
    static final ObjectMapper MAPPER = new ObjectMapper();
    
    @Override
    public boolean receive(ConsumerRecord<String, byte[]> record) {
        try {
            OperationLogMessage msg = MAPPER.readValue(new String(record.value(), StandardCharsets.UTF_8), OperationLogMessage.class);
            OperationLog entity = new OperationLog();
            entity.setTraceId(msg.getTraceId());
            entity.setParentId(msg.getParentId());
            entity.setOpTime(msg.getTime() == null ? null
                    : LocalDateTime.ofInstant(Instant.ofEpochMilli(msg.getTime()), ZoneId.systemDefault()));
            entity.setProject(msg.getProject());
            entity.setModule(msg.getModule());
            entity.setOperation(msg.getOperation());
            entity.setOperator(msg.getOperator());
            entity.setInput(msg.getInput());
            entity.setOutput(msg.getOutput());
            entity.setResult(msg.getResult());
            entity.setError(msg.getError());
            entity.setElapsedMs(msg.getElapsedMs());
            entity.setLevel(msg.getLevel());
            entity.setHouseSn(msg.getHouseSn());
            entity.setWellSn(msg.getWellSn());
            entity.setTlSn(msg.getTlSn());
            entity.setHost(msg.getHost());
            entity.setCreateTime(LocalDateTime.now());
            operationLogService.save(entity);
            return true;
        } catch (Exception e) {
            // 解析/入库失败:记错、丢弃该条,不阻塞后续(操作日志不应反向影响系统)
            log.error("操作日志入库失败,topic={},offset={}", record.topic(), record.offset(), e);
            return true; // 返回 true 提交位移,避免坏消息无限重投
        }
    }
    }
    
  • [ ] Step 3: 写消费者启动 OplogConsumerRunner.java(应用启动后拉起 tl-oplog 消费者,仿 KafkaManage.startPictureReceiveTopic 用 KafkaFactory/TopicManage

    package com.aivfo.oplog.kafka;
    
    import com.aivfo.kafka.KafkaFactory;
    import com.aivfo.kafka.callback.ReceiveMessage;
    import com.aivfo.kafka.consumer.ConsumerManualCommit;
    import com.aivfo.kafka.topic.TopicManage;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.stereotype.Component;
    
    import java.time.Duration;
    import java.util.Arrays;
    import java.util.Map;
    
    @Slf4j
    @Component
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class OplogConsumerRunner implements ApplicationRunner {
    final KafkaFactory kafkaFactory;
    final TopicManage topicManage;
    final Map<String, ReceiveMessage> receiveMessageInfo; // spring 收集所有 ReceiveMessage bean,key=bean名
    // ReceiveErrorInfo:照 data-transmission 注入其 receiveErrorInfo;若该 bean 在 starter 提供则 @Autowired,否则按 data-transmission 的实现复制一个最简版到本包
    
    @Value("${aivfo.oplog.topic:tl-oplog}")
    String topic;
    @Value("${aivfo.oplog.topic.partition.number:3}")
    int partitions;
    
    @Override
    public void run(org.springframework.boot.ApplicationArguments args) {
        topicManage.createTopic(topic, partitions);
        for (int i = 0; i < partitions; i++) {
            ConsumerManualCommit consumer = kafkaFactory.buildConsumerManualCommit(
                    receiveMessageInfo.get("oplogReceive"), topic, null, Arrays.asList(i), Duration.ofSeconds(5));
            consumer.start();
        }
        log.info("aivfo-oplog 开始监听 topic[{}],分区数={}", topic, partitions);
    }
    }
    

buildConsumerManualCommit 第三参在 data-transmission 传的是 receiveErrorInfo(ReceiveErrorInfo)。执行时:先看 com.aivfo.kafka.KafkaFactory#buildConsumerManualCommit 签名 + data-transmission 的 ReceiveErrorInfo 来源。若 starter 不强制,可传 null;否则照 data-transmission 复制一个最简 ReceiveErrorInfo 实现到 com.aivfo.oplog.kafka

  • Step 4: 编译

Run: cd 时差项目源代码/aivfo-oplog && mvn -q -DskipTests compile 2>&1 | tail -8; echo EXIT=${PIPESTATUS[0]} Expected: EXIT=0(若报 ReceiveErrorInfo/buildConsumerManualCommit 签名不符,按上方 ⚠ 对照 data-transmission 修正)

  • [ ] Step 5: Commit

    cd 时差项目源代码 && git add aivfo-oplog && git commit -m "M8-01: tl-oplog 消费者(JSON→operation_log 入库,失败兜底)"
    

Task 5: 保留期清理定时任务

Files:

  • Create: aivfo-oplog/src/main/java/com/aivfo/oplog/schedule/RetentionCleanTask.java

  • [ ] Step 1: 写清理任务(每天凌晨删 op_time 早于保留天数的;用 mybatis-plus LambdaQuery delete)

    package com.aivfo.oplog.schedule;
    
    import com.aivfo.oplog.entity.OperationLog;
    import com.aivfo.oplog.service.OperationLogService;
    import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.scheduling.annotation.Scheduled;
    import org.springframework.stereotype.Component;
    
    import java.time.LocalDateTime;
    
    @Slf4j
    @Component
    @RequiredArgsConstructor(onConstructor = @__(@Autowired))
    public class RetentionCleanTask {
    final OperationLogService operationLogService;
    
    @Value("${aivfo.oplog.retention.days:30}")
    int retentionDays;
    
    /** 每天 03:17 清理过期操作日志(避开整点) */
    @Scheduled(cron = "0 17 3 * * ?")
    public void clean() {
        LocalDateTime cutoff = LocalDateTime.now().minusDays(retentionDays);
        LambdaQueryWrapper<OperationLog> w = new LambdaQueryWrapper<>();
        w.lt(OperationLog::getOpTime, cutoff);
        long before = operationLogService.count();
        boolean ok = operationLogService.remove(w);
        log.info("操作日志保留期清理:保留{}天,cutoff={},删除前总数={},结果={}", retentionDays, cutoff, before, ok);
    }
    }
    
  • [ ] Step 2: 编译 + Commit

    cd 时差项目源代码/aivfo-oplog && mvn -q -DskipTests compile 2>&1 | tail -3; echo EXIT=${PIPESTATUS[0]}
    cd 时差项目源代码 && git add aivfo-oplog && git commit -m "M8-01: operation_log 保留期清理定时任务"
    

Task 6: 端到端验证(发测试消息 → 入库)

Files: 无(验证步骤)

  • [ ] Step 1: install + 打包 + 启动 aivfo-oplog

    cd 时差项目源代码/aivfo-oplog && mvn -q -DskipTests install 2>&1 | tail -3
    java -jar target/aivfo-oplog-1.0.0-SNAPSHOT.jar > /c/TLData/_setup/oplog.log 2>&1 &
    sleep 40
    grep -aE "Started OplogApplication|开始监听 topic|register finished" /c/TLData/_setup/oplog.log | tail -5
    

Expected: 见 Started OplogApplication开始监听 topic[tl-oplog]、Nacos register finished

  • [ ] Step 2: 用 Kafka 控制台生产者发一条测试 JSON 消息

    export PATH="$PATH:/c/Program Files/Docker/Docker/resources/bin"
    MSG='{"traceId":"test-trace-001","time":1718700000000,"project":"test","module":"串口","operation":"打开端口","operator":"工程师","input":"{\"port\":\"COM3\"}","result":"失败","error":"超时无响应","elapsedMs":1200,"level":"INFO","houseSn":3}'
    echo "$MSG" | docker exec -i tl-kafka /opt/kafka/bin/kafka-console-producer.sh --bootstrap-server localhost:9092 --topic tl-oplog
    sleep 5
    

⚠ kafka 镜像内脚本路径以 apache/kafka:3.7.0 为准(常见 /opt/kafka/bin/);若不符,docker exec tl-kafka sh -c 'ls /opt/kafka/bin | grep console-producer' 找正确路径。

  • [ ] Step 3: 查 operation_log 验证入库

    docker exec tl-mysql mysql -uroot -proot -N -e "SELECT trace_id,project,module,operation,operator,result,error,house_sn FROM log.operation_log WHERE trace_id='test-trace-001';"
    

Expected: 查到一行:test-trace-001 test 串口 打开端口 工程师 失败 超时无响应 3

  • [ ] Step 4: 停掉测试服务

    # 找到 oplog 进程并停止(或用 TaskStop 若由后台任务起)
    netstat -ano | grep ":10060.*LISTENING"   # 拿 PID
    # taskkill //PID <pid> //F   (Windows) 或停止后台任务
    
  • [ ] Step 5: 回写进度 + Commit

按回写纪律更新 进度状态.yaml/交接卡.md/进度数据.js:M8-01(P1 基础设施)完成、operation_log 入库链路验证通过(V-122 部分达成)。

cd 时差项目源代码 && git add 项目文档/进度
git commit -m "M8-01 完成: P1 日志基础设施入库链路验证通过(发测试消息→operation_log)"

Self-Review

Spec coverage(对照 14 号 §6 基础设施部分):

  • ✅ operation_log 表(Task 1,字段对齐 §5)
  • ✅ Kafka topic tl-oplog(Task 4 Step 3 createTopic)
  • ✅ aivfo-oplog 微服务消费入库(Task 2-4)
  • ✅ 大对象只存文件名(schema 层面 input/output 为 text,由生产端 P2/P3 负责只放文件名——本计划接收端不限制)
  • ✅ 保留期可配 + 清理(Task 5)
  • ✅ 分级管理索引(Task 1 idx_query 项目>模块>时间)
  • ⏭ 两级日志的"调试级本地文件"= C# 端 P3 范畴,不在 P1
  • ⏭ traceId/parentId 生成与透传 = P2(Java)/P3(C#) 生产端,P1 只接收存储

Placeholder scan: 无 TODO/TBD;3 处 ⚠ 是"执行时照 data-transmission 对照确认"的明确校验指令(starter artifactId / buildConsumerManualCommit 签名+ReceiveErrorInfo / kafka 脚本路径),非占位——给了确切对照对象和动作。

Type consistency: OperationLog(实体,下划线列驼峰属性) / OperationLogMessage(JSON DTO,time 为 epoch millis) / operation_log(表) 三者字段一一对应;消费端 OplogReceiveMessage 把 Message.time(Long)→entity.opTime(LocalDateTime) 转换明确;oplogReceive bean 名在 Task 4 Step 2(@Component) 与 Step 3(receiveMessageInfo.get) 一致。

风险/依赖: 本计划假设 aivfo-kafka-spring-bootKafkaFactory.buildConsumerManualCommit/TopicManage.createTopic/ReceiveMessage API 与 data-transmission 用法一致(已读其源码确认)。3 处 ⚠ 是执行时唯一需对照现有代码的点。


后续(P2/P3,本计划不含)

  • P2 · Java 接入:扩展 @OperateLog 切面采集 → 发 Kafka tl-oplog(JSON,字段同 OperationLogMessage);网关生成/透传 traceId;各微服务关键方法铺注解。
  • P3 · C# 端Aivfo.OperationLog 组件(异步队列→Kafka + traceId 生成透传 + 配置开关 + 调试级本地文件);operate/front 全埋。