Files
hepu/aomin-gateway/README.md
T
aomin a7605ef8b8 chore(repo): 导入 M-Z 业务底座 V1.9 基线并建立仓库规范
按周工交付的 SEC-Z-01 清理后源码建立 M-Z 主干,并按 Git 仓库惯例重排目录。

- aomin-gateway/  后端工程 87 文件,原样保留,路径与构建命令未变
- demo/           演示前端与设备模拟器 9 文件(原 02_前端与设备端),非正式三端
- docs/handover/  交付与安全清理材料 6 份,内容未改
- docs/contract/  J-00 规格、OpenAPI、内部五接口、状态机、Mock 样例
- docs/reference/ 指纹与校验清单,含 84 个源码文件的 sha256 清单

目录重排只动位置不动内容:源码树 84 文件哈希与交付一致,逐文件自校验通过;
12 条 Flyway 迁移与交付包内镜像逐字节一致。

未纳入:迁移镜像目录(与工程内一套重复,改留指纹)、target/、证据 raw、
应急恢复脚本、评审用虚构台账数据。

安全:SEC-Z-01 15 处明文已清零,导入前重新扫描无新增命中,本仓库无历史污染。
行尾按交付字节原样保存(.gitattributes 设 * -text),不做归一化以免整树摘要失效。
2026-11-13 17:32:37 +08:00

9.6 KiB
Raw Blame History

aomin-gateway

鹤仆·守望 业务层接入网关(M 域)。承载 J-00 V1.8 里 /internal/iot/v1/** 五个接口。

技术栈:JDK 21 / Spring Boot 3.3.5 / MyBatis-Plus 3.5.7 / Flyway / MySQL 8 / Redis。


一、启动前提

MySQL 和 Redis 必须先起来(本机是绿色版,不注册服务):

G:\aomin-dev\start-mysql.bat
G:\aomin-dev\start-redis.bat

两个窗口都显示 OK 之后再启本服务。

二、启动

双击:

G:\aomin-dev\app\aomin-gateway\start-gateway.bat

或命令行:

set JAVA_HOME=G:\aomin-dev\jdk-21.0.2
cd /d G:\aomin-dev\app\aomin-gateway
mvn -s G:\aomin-dev\maven\conf\settings.xml spring-boot:run

看到 Tomcat started on port 8080 即成功。

三、验证

浏览器或 curl:

http://127.0.0.1:8080/health

正常返回:

{
  "code": 0,
  "msg": "ok",
  "data": {
    "app": "aomin-gateway",
    "db": "connected",
    "tables": 83,
    "schema": "aomin_dev",
    "redis": "connected",
    "flyway": "1.0.0",
    "status": "UP"
  }
}

tables 必须是 83,flyway 必须是 1.0.0。 少一个就说明基线没认上。

四、数据库

项 值
地址 127.0.0.1:3306
库 aomin_dev
用户 / 密码 root / 评审机本地演示库口令(见 05_启动构建与测试说明 §6;生产用环境变量覆盖)
表数 87 张业务表(83 基线 + sys_user_region_scope + iot_raw_ref_gap + alarm_dead_letter),含 flyway_schema_history 共 87

Flyway 基线约定

src/main/resources/db/migration/V1.0.0__baseline_schema.sql 是 83 张表的完整 DDL,一经提交不得修改。

  • 本机库是手动建的,首次启动时 Flyway 用 baseline-on-migrate 把它认定为 V1.0.0,不重复建表;
  • 空库部署(测试环境、生产)时,Flyway 会完整执行 V1.0.0 建全部表;
  • 今后任何改表,新增 V1.0.1__xxx.sql、V1.0.2__xxx.sql,不要动 V1.0.0。

改表前先在 flyway_schema_history 里确认当前版本,避免版本号撞车。

五、代码结构

cn.aomin.gateway
├── GatewayApplication          @EnableScheduling(命令扫描 + 租约回收要靠它)
├── common
│   ├── ApiResponse             {code, msg, data}
│   ├── BizException            400 / 401 / 409
│   └── GlobalExceptionHandler  统一兜底
├── config
│   ├── GatewayProperties       gateway.* 配置绑定
│   ├── WebConfig               拦截器挂载 /internal/**
│   └── RedisConfig             String 序列化
├── interceptor
│   └── AuthInterceptor         M-01:Token → 时间戳 → nonce → 签名(顺序即成本顺序)
├── security
│   ├── CachedBodyFilter        缓存 body(验签要读,流只能读一次)
│   ├── SignatureVerifier       HMAC-SHA256(token, method\npath\nts\nnonce\nsha256(body))
│   ├── NonceStore              Redis SETNX,key 带 token 摘要前缀
│   ├── RateLimiter             通道 + 设备双维度;设备维度走 pipeline
│   ├── IdempotencyStore        四态:NEW / REPLAY / IN_PROGRESS / CONFLICT
│   └── IdempotentExecutor      把幂等包在业务处理外面
├── controller
│   ├── HealthController        /health 四环节自检
│   ├── IngestController        I-01 / I-02 / I-12
│   └── CommandController       I-18 / I-05
├── service
│   ├── IngestService           M-02 单事务 + 逐条 SAVEPOINT
│   ├── StatusService           M-03 覆盖写
│   ├── RawFrameService         M-04 两张原文表分流
│   └── CommandService          M-05 长轮询 + 租约 + 状态流转
├── dao                         全部 JdbcTemplate,没有 MyBatis Mapper
│   ├── DeviceDao               一次 IN 批量查
│   ├── IotEventDao             18 列 INSERT
│   ├── TechCacheDao            GREATEST 防回退的 upsert
│   ├── RawFrameDao             两张原文表
│   ├── UnregisteredDao         两个场景分开(带/不带 last_payload)
│   └── CommandDao              FOR UPDATE SKIP LOCKED
├── domain
│   ├── Channel                 四通道枚举(含 vendor 映射、落哪张原文表)
│   ├── DeviceRow / TimeResult  record
│   └── dto                     请求/响应 DTO,字段名一律按 J-00 V1.8 驼峰
└── support
    ├── EventTimeResolver       event_time 派生(纯函数,四入两出)
    └── EventValidator          字段级校验,reason 只用 J-00 那四个词

没有 MyBatis Mapper:逐条 SAVEPOINT、批量 IN、upsert 都要精确控制 SQL,JdbcTemplate 更直接。pom 里的 mybatis-plus 只是留口子,不扫描任何东西。

六、五个接口:全部已实现

单元 接口 状态
M-01-U1 /internal/** 公共拦截层 ✅ Token / 时间戳 / nonce / 签名(gateway.auth.enabled 控制)
M-02-U1 POST /internal/iot/v1/events/batch ✅ 单事务 + 逐条 SAVEPOINT,四计数
M-03-U1 POST /internal/iot/v1/device-status/batch ✅ device_tech_cache 覆盖写
M-04-U1 POST /internal/iot/v1/raw-frames ✅ 按通道分落两张原文表
M-05-U1 GET /internal/iot/v1/commands/pending ✅ DeferredResult + 500ms 扫描
M-05-U2 POST /internal/iot/v1/commands/result ✅ 五个 result 值分别处置

冒烟测试(2026-11-05 已跑通)

场景 期望 实测
I-01 正常一条 accepted=1 ✅
I-01 原样重发 duplicated=1 ✅(撞 uk_event_id)
I-01 未建档设备 unregistered=1,进 unregistered_device ✅
I-01 缺 tsSource rejected=1 SCHEMA_INVALID ✅
I-01 前缀与通道不匹配 rejected=1 SENSOR_ID_INVALID ✅
I-01 墓碑帧 event_time=1900-01-01、is_backfill=1、落 p1900 ✅
I-02 online=true 写 ONLINE ✅
I-02 online=false 且时间更早 不改 online_status、last_seen_at 不回退 ✅
I-02 battery 百分比 保持 NULL,只写 battery_mv ✅
I-12 G 线 payload 存 Base64 文本、payload_size=解码字节数 ✅ FROM_BASE64() 可还原
I-12 同帧重发 duplicated=1 ✅(撞 uk_md5)
I-12 sensorId 为空 device_sn='UNKNOWN'、parse_result='FIELD_MISSING' ✅
I-18 拉取 待下发 → 已下发,打 60s 租约 ✅
I-18 长轮询唤醒 有新命令 500ms 内返回 ✅
I-18 空等超时 到点返回空列表,不报错 ✅
I-05 ACKED code=0 设备有回应,租约清空(原写作"已生效",语义纠正:回执≠执行成功,见下) ✅
租约回收 已下发 → 待下发,reclaim_count+1,retry_count 不变 ✅
幂等同 key 同 body 返回首次结果 ✅
幂等同 key 异 body 409 ✅

上表是 2026-11-05 的实测记录,如实保留;命令状态值已在 V1.2(11-10)改为英文七态并落码。 目标态 PENDING/CLAIMED/SENT/ACKED/FAILED/TIMEOUT/CANCELLED(J-00 V1.8 §3.4), 迁移脚本 V1.0.23__command_seven_states.sql 把存量中文六值一次刷掉; CommandDao 七态全量实现,领取置 CLAIMED(不再写「已下发」)并记租约代次。 ACKED 只代表设备有回应,不等于执行成功 —— 是否生效看 resultCode(0 为成功)。 详见 04_OpenAPI与内部IoT契约/状态机.md §二。

命令链与告警重试的可重跑验证见 06_统一闭环题自测证据/cmd_chain_test.py(23 条)与 alarm_retry_test.py(5 条)。

库里留了两台测试设备(A1B2C3D4 = OWN_GW 门磁、MIR:861234567890123 = MIR_CELL 水浸),其余测试数据已清空,可以直接拿这两台 curl 自测。

七、两个必须遵守的硬约束

  1. M-02 同步落库,不要异步入队。 返回值要带 accepted / rejected / duplicated / unregistered 四个计数,其中 unregistered 必须现查 device 表才知道,入队就丢了。
  2. 查 device 必须 IN (…) 批量查。 200 条一批、每秒多批,退化成 200 次单查会把连接池打满(现在只有 10 个连接)。

八、配置开关

配置项 默认 说明
gateway.auth.enabled false 打开后启用 Token/签名/时间戳/nonce 校验。联调初期先别开,否则 J 模块一个请求都进不来
spring.flyway.enabled true 关掉就不会自动建表/升级,慎用
logging.level.cn.aomin.gateway DEBUG 上线改 INFO

日志文件:G:\aomin-dev\logs\app.log(按 50MB 滚动,留 15 个)。

九、两个已踩过的坑(别再踩)

1. 端口被改成 52228

日志里出现 Tomcat initialized with port 52228,然后报 Port 52228 was already in use。

原因:系统环境变量里有个 SERVER__PORT=52228(WorkBuddy 之类的工具注入的),Spring Boot 的能量绑定把它当成 server.port 读进去,优先级高于 application.yml。

处理:start-gateway.bat 里已经 set "SERVER__PORT=" 并用 --server.port=8080 显式指定(命令行参数优先级最高)。如果手工用 java -jar 起,一定带上 --server.port=8080。

2. JDBC URL 里写 characterEncoding=utf8mb4

会直接抛 Unsupported character encoding 'utf8mb4',连接池建不起来、Flyway 也跟着挂。

characterEncoding 要写 Java 字符集名 UTF-8,驱动会自动协商到 MySQL 的 utf8mb4。要指定排序规则用 connectionCollation=utf8mb4_0900_ai_ci。