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

211 lines
9.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```
或命令行:
```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
```
正常返回:
```json
{
"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`。