charging-cabinet/AGENTS.md

704 lines
18 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.

# AGENTS.md - 充电柜项目工作手册
## 项目简介
充电柜矩阵系统Power Matrix System / PMS
产品名安知充anzhizhichong已有充电站平台在运行充电柜是新增产品线。
仓数不固定1柜控板+N仓控板每仓控板管6仓最小6仓。
## 设备标识
- **平台抽象ID**`10-00000000`绑定柜控板二维码对应此ID
- **设备ID**4G SIM卡15位IMEI10进制
- **仓体全局ID**`平台层ID-仓板ID-仓体ID`
- **扫码链接**`https://app.anzhizhichong.com/pms/{抽象ID}`
## 技术栈
- 后端Rust + Axum
- 前端React + Arco Design ProReact版
- 数据库MySQL阿里云RDS开发/生产共用
- 缓存Redis阿里云Redis开发/生产共用
- 设备通讯TCP长连接柜控板4G模块Air780E Cat.1
- 权限RBAC精确到按钮级别权限码如 device:door:unlock
## 项目结构
```
D:\charging-cabinet\
├── tasks/ # 任务文件opencode执行
├── docs/ # 开发文档、协议文档
├── deploy/ # 部署配置文件
│ ├── nginx/ # Nginx配置
│ ├── *.service # systemd服务单元
│ └── deploy-*.sh # 部署脚本
├── software/
│ ├── api-server/ # Rust API服务
│ ├── device-server/ # Rust TCP设备服务
│ └── web/ # React前端
├── hardware/ # 硬件资料(原理图/PCB等
└── test/ # 测试用例/报告
```
## 部署架构
### 环境规划
| 环境 | 域名 | API服务 | TCP服务 | Redis DB | 部署路径 | URL路径 |
|------|------|---------|---------|----------|----------|---------|
| 正式环境 | app.anzhizhichong.com | :9000 | :9001 | db5 | /data/pms | /pms/ |
| 测试环境 | test.anzhizhichong.com | :3000 | :3001 | db6 | /data/pms_dev | /pms/ |
**服务端口分配:**
- 测试环境API 3000TCP 3001
- 正式环境API 9000TCP 9001
**部署路径:**
- 产品介绍页:/data/landing
- 正式环境:/data/pmsapi-server、device-server、web
- 测试环境:/data/pms_devapi-server、device-server、web
**Nginx配置**
- 正式: /etc/nginx/conf.d/app.anzhizhichong.com.conf
- 测试: /etc/nginx/conf.d/test.anzhizhichong.com.conf
### 服务器信息
- **外网IP**: 118.31.119.206
- **内网IP**: 172.25.42.239
- **系统**: CentOS Stream 92核/3.5G/49G
- **ZeroTier**: 网络ID `63e843dd42f54b9c`,服务器节点 `12a91dd47d`IP `10.8.0.252`
- **本地电脑ZeroTier IP**: `10.8.0.248`
- **已装软件**: Nginx 1.20.1、ZeroTier 1.16.2、socat开机自启
- **服务**: mysql-proxy(3306→RDS)、redis-proxy(6379→Redis)、Gitea(3080)、acme.sh
- **不装**: Docker/Node/Rust本地编译传二进制
- **SSH**: plink连接首次需 `echo y` 接受host key
### 正式环境数据库
- MySQL: `rm-bp1bb7yiv5n8z4uvp.mysql.rds.aliyuncs.com`user: rootpassword: Hbhyg731024database: `pms`
- Redis: `r-bp1lsezvgzmgtwhbtg.redis.rds.aliyuncs.com`password: Hbhyg731024db: 5
### 本地开发环境
**数据库连接通过ZeroTier**
```
MySQL: 10.8.0.252:3306
user: root
password: Hbhyg731024@
database: pms_dev (新建不动AZZCWeChat*)
Redis: 10.8.0.252:6379
password: Hbhyg731024@
db: 6 (测试环境用db6不动db1-5)
```
**代理说明:**
- 服务器nginx stream转发MySQL(3306)和Redis(6379)到阿里云RDS
- systemd自启`Restart=always` 自动恢复
- 本地直连 `10.8.0.252`无需SSH隧道
### 证书
- 阿里云免费SSL证书acme.sh自动续期
- 存放路径: `/etc/nginx/ssl/{域名}.pem` + `/etc/nginx/ssl/{域名}.key`
- 已配置: app.anzhizhichong.com、test.anzhizhichong.com
### 内部服务
- **Gitea**: `https://app.anzhizhichong.com/doc/` (端口3080)
- 组织: anzhizhichong
- 仓库: charging-cabinet
- 用户: admin / skyp76
## 开发约束
### 禁止操作
- **禁止在服务器上编译Rust** — 2核/3.5G会OOM本地编译传二进制
- **禁止使用socat fork模式** — 进程泄漏会耗尽内存用nginx stream代理
### 编码规范
- 任务需求写到 `tasks/` 目录的md文件
- 通过Agent子代理包裹opencode执行异步通知
- 任务要拆的尽量细
- 质量约束模板固定追加到每个任务文件
- opencode超时必须报告用户不能fallback手动执行
- 审查用不同模型(不用开发同款)
### 编码任务下发
- 任务需求写到 `tasks/` 目录的md文件
- 通过Agent子代理包裹opencode执行异步通知
- 质量约束模板固定追加到每个任务文件
### 质量约束模板
所有编码任务必须追加以下约束:
1. 完成代码后执行对应栈静态校验Rust→cargo checkTS→tsc --noEmit零报错零警告
2. 分层拆分单函数≤80行命名语义化完整注释
3. 所有外部IO/网络请求异常捕获禁止裸panic
4. 分支逻辑全覆盖,不遗漏兜底分支
5. 常量抽离不使用废弃API
6. Rust内存安全合理管理所有权禁用unsafe无合理理由
7. React函数式组件+Hooks不写Class组件
### 验收流程
1. 静态校验命令通过
2. 跑起来看效果
3. 抽查关键逻辑(异常处理、权限校验、协议解析)
## 部署与维护
### SSH连接
```bash
# 外网IP推荐
echo y | plink -ssh root@118.31.119.206 -pw "Hbhyg731024@"
# ZeroTier内网IP
echo y | plink -ssh root@10.8.0.252 -pw "Hbhyg731024@"
```
### 数据库连接
```bash
# MySQL通过ZeroTier代理
mysql -h 10.8.0.252 -u root -p
# 密码Hbhyg731024@
# 测试环境库pms_dev
# 正式环境库pms
# Redis通过ZeroTier代理
redis-cli -h 10.8.0.252 -a "Hbhyg731024@"
# 测试环境db6
# 正式环境db5
```
### Nginx配置
**正式环境** (`/etc/nginx/conf.d/app.anzhizhichong.com.conf`):
```nginx
server {
listen 80;
server_name app.anzhizhichong.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name app.anzhizhichong.com;
ssl_certificate /etc/nginx/ssl/app.anzhizhichong.com.pem;
ssl_certificate_key /etc/nginx/ssl/app.anzhizhichong.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
# 产品介绍页
root /data/landing;
index index.html;
location /doc/ {
proxy_pass http://127.0.0.1:3080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 500m;
}
location /pms/api/ {
proxy_pass http://127.0.0.1:9000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
# PMS管理前端
location /pms/ {
alias /data/pms/web/;
try_files $uri $uri/ /pms/index.html;
}
}
```
**测试环境** (`/etc/nginx/conf.d/test.anzhizhichong.com.conf`):
```nginx
server {
listen 80;
server_name test.anzhizhichong.com;
return 301 https://$server_name$request_uri;
}
server {
listen 443 ssl http2;
server_name test.anzhizhichong.com;
ssl_certificate /etc/nginx/ssl/test.anzhizhichong.com.pem;
ssl_certificate_key /etc/nginx/ssl/test.anzhizhichong.com.key;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /data/pms_dev;
index index.html;
location /pms/api/ {
proxy_pass http://127.0.0.1:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
location /pms/ {
try_files $uri $uri/ /pms/index.html;
}
}
```
> 测试环境SSL证书与正式环境共用test是app证书的SAN域名部署时复制pem+key即可。
### 部署步骤
**1. 本地编译**
```bash
# 后端Rust
cd software/api-server
cargo build --release
cd software/device-server
cargo build --release
# 前端React
cd software/web
npm run build
```
**2. 传输到服务器**
```bash
# 传输后端二进制(测试环境)
scp software/api-server/target/release/api-server root@118.31.119.206:/data/pms_dev/api-server/
scp software/device-server/target/release/device-server root@118.31.119.206:/data/pms_dev/device-server/
# 传输后端二进制(正式环境)
scp software/api-server/target/release/api-server root@118.31.119.206:/data/pms/api-server/
scp software/device-server/target/release/device-server root@118.31.119.206:/data/pms/device-server/
# 传输前端静态文件
scp -r software/web/dist/* root@118.31.119.206:/data/pms_dev/web/ # 测试环境
scp -r software/web/dist/* root@118.31.119.206:/data/pms/web/ # 正式环境
```
**3. 服务器重启服务**
```bash
# SSH到服务器
echo y | plink -ssh root@118.31.119.206 -pw "Hbhyg731024@"
# 重启测试环境
systemctl restart pms-api-test
systemctl restart pms-device-test
# 重启正式环境
systemctl restart pms-api-prod
systemctl restart pms-device-prod
# 重启Nginx
systemctl restart nginx
```
### 服务管理
```bash
# 查看服务状态
systemctl status pms-api-test # 测试环境API
systemctl status pms-api-prod # 正式环境API
systemctl status nginx
# 查看日志
journalctl -u pms-api-test -f
journalctl -u pms-api-prod -f
tail -f /var/log/nginx/error.log
```
## 硬件架构
- 柜控板(ID128)×1 + 仓控板(ID1-N)×N每仓控板管6个仓体
- 仓控板与电源1TN RS485协议翌工
- 仓控板与BMS星恒Modbus / 天能485 / 无协议(平台可配置)
- 仓控板与柜控板自定义485协议14+功能码)
- 柜控板与后台4G TCP长连接Air780E模块
## 开发范围(当前阶段)
### 做
- 后台管理系统Web端
- 用户端H5页面`/pms/{id}`,微信扫码可访问)
- 设备TCP通讯服务协议文档已就绪
### 不做
- 微信小程序名额暂不可用先用H5
- 支付/计费(本系统不涉及)
## 组织架构
```
组织(企业) → 项目(放置地点) → 设备(充电柜)
```
设备卖给哪个企业就划拨给该组织,设备放置的具体地点称为项目,设备必须划拨到项目下。
## 数据模型
### 核心关系
```
organization(组织/企业)
├── project(项目/放置地点)
│ └── cabinet(充电柜)
│ ├── cabin_board(仓控板×N)
│ │ └── compartment(仓体×6)
│ ├── charge_record(充电记录)
│ └── energy_stat(能耗统计)
├── user(用户)
└── device_log(设备日志)
operation_log(操作日志)
```
### 数据库表
| 表名 | 说明 |
|------|------|
| organizations | 组织表 |
| projects | 项目表 |
| cabinets | 充电柜表IMEI、abstract_id、auth_str |
| cabin_boards | 仓控板表 |
| compartments | 仓体表 |
| users | 用户表(手机号登录) |
| charge_records | 充电记录表 |
| device_logs | 设备日志表 |
| energy_stats | 能耗统计表 |
| operation_logs | 操作日志表 |
## TCP通讯协议
- 长连接无需PING包
- 空闲1分钟上报充电15秒上报
- 超时5000ms
- JSON去空格回车省流量LF处理粘包
- `dev_id`IMEI10进制`sub_device_id`485 ID
- `msg_id`自增会话ID
- 全局通道号:`dev_id-sub_device_id-通道号`(如`123456789012345-01-6`
### 签名机制
- 设备首次连接调 `auth_str` 接口获取8位安全码仅一次
- `sign = SHA256(dev_id + auth_str + timestamp).substring(56, 63)`
- 时间戳秒级平台校验±60秒
### 指令清单
| 动作 | 方向 | id格式 | 说明 |
|------|------|--------|------|
| `login` | 设备→平台 | 无 | 登录含sign+timestamp |
| `auth_str` | 设备→平台 | 无 | 获取安全码(仅首次) |
| `status_post` | 设备→平台 | 无 | 定时上报rssi+status字典 |
| `pow_fail` | 设备→平台 | 无 | 12V电源掉电上报 |
| `bat_in` | 设备→平台 | `IMEI-仓控板-通道` | 电池插入含BMS数据字段待定 |
| `bat_out` | 设备→平台 | `IMEI-仓控板-通道` | 电池拔出 |
| `on` | **双向** | `IMEI-仓控板-通道` | 开始充电(平台下发/设备上报) |
| `off` | **双向** | `IMEI-仓控板-通道` | 停止供电(平台手动/设备自动) |
| `open` | 平台→设备 | `IMEI` | 开门 |
| `pow_on` | 平台→设备 | `IMEI` | 接触器合闸恢复380V供电 |
| `pow_off` | 平台→设备 | `IMEI` | 接触器分闸切断380V供电 |
| `reset` | 平台→设备 | `IMEI``IMEI-仓控板` | 重启整柜或仓控板 |
### status_post格式
```json
{
"act": "status_post",
"msg_id": 42,
"dev_id": "123456789012345",
"rssi": 85,
"status": {
"123456789012345-01": "000100020003000400050006",
"123456789012345-02": "000100020003000400050006"
}
}
```
- key: `IMEI-仓控板ID`
- value: 6个通道状态拼接每通道2字节=4个HEX字符
- 平台从key解析仓控板数量
### pow_fail格式
```json
{
"act": "pow_fail",
"msg_id": 15,
"dev_id": "123456789012345",
"info": "0"
}
```
**字段说明:**
- `act` — 指令类型:`pow_fail`
- `msg_id` — 自增会话ID
- `dev_id` — 设备IMEI
- `info` — 停电类型:`0`=直流停电(12V掉电)`1`=交流停电(380V掉电)
**pow_fail**:柜控板检测到电源掉电时上报。直流停电(12V)时设备切换到后备电池供电,仍可远程通讯;交流停电(380V)时充电电源不可用。
### pow_on/pow_off格式
```json
{
"act": "pow_on",
"msg_id": 16,
"dev_id": "123456789012345"
}
```
**字段说明:**
- `act` — 指令类型:`pow_on`(接触器合闸)、`pow_off`(接触器分闸)
- `msg_id` — 自增会话ID
- `dev_id` — 设备IMEI
**pow_on**平台下发合闸指令柜控板吸合接触器恢复380V充电电源供电。
**pow_off**平台下发分闸指令柜控板断开接触器切断380V充电电源供电。
### 指令详情
#### login设备→平台
设备登录,验证签名。
```json
// 请求
{"act":"login","msg_id":1,"dev_id":"123456789012345","sign":"abc12345","timestamp":1719800000,"h_ver":"1.0.0","s_ver":"1.0.0"}
// 响应
{"suc":1,"msg_id":1,"cell":6}
```
**字段说明:**
- `sign` — 签名SHA256(dev_id+auth_str+timestamp)后8位
- `timestamp` — 秒级时间戳平台校验±60秒
- `h_ver` — 硬件版本
- `s_ver` — 软件版本
- `cell` — 仓控板数量(响应)
---
#### auth_str设备→平台
获取8位安全码仅首次连接调用。
```json
// 请求
{"act":"auth_str","msg_id":2,"dev_id":"123456789012345","ccid":"89860000000000000000"}
// 响应
{"suc":1,"msg_id":2,"data":{"auth_str":"abcd1234"}}
```
**字段说明:**
- `ccid` — SIM卡ICCID号
- `auth_str` — 8位随机安全码响应仅返回一次
---
#### status_post设备→平台
定时上报空闲60秒/充电15秒。
```json
// 请求
{"act":"status_post","msg_id":10,"dev_id":"123456789012345","rssi":85,"status":{"123456789012345-01":"000100020003000400050006"}}
// 响应
{"suc":1,"msg_id":10}
```
**status字段说明**
- key: `IMEI-仓控板ID`
- value: 6个通道状态拼接每通道2字节=4个HEX字符
---
#### pow_fail设备→平台
电源掉电上报。
```json
// 请求
{"act":"pow_fail","msg_id":15,"dev_id":"123456789012345","info":"0"}
// 响应
{"suc":1,"msg_id":15}
```
**字段说明:**
- `info` — 停电类型:`0`=直流停电(12V掉电)`1`=交流停电(380V掉电)
---
#### pow_on平台→设备
接触器合闸恢复380V充电电源供电。
```json
// 请求
{"act":"pow_on","msg_id":16,"dev_id":"123456789012345"}
// 响应
{"suc":1,"msg_id":16}
```
---
#### pow_off平台→设备
接触器分闸切断380V充电电源供电。
```json
// 请求
{"act":"pow_off","msg_id":16,"dev_id":"123456789012345"}
// 响应
{"suc":1,"msg_id":16}
```
---
#### bat_in设备→平台
电池插入事件,立即上报。
```json
// 请求
{"act":"bat_in","msg_id":20,"id":"123456789012345-01-6","bms":{"soc":45,"voltage":52.3}}
// 响应
{"suc":1,"msg_id":20}
```
**平台处理:**
1. 记录电池信息
2. 验证电池合规性
3. 自动下发 `on` 指令
---
#### bat_out设备→平台
电池拔出事件,立即上报。
```json
// 请求
{"act":"bat_out","msg_id":21,"id":"123456789012345-01-6"}
// 响应
{"suc":1,"msg_id":21}
```
---
#### on双向
**平台→设备:** 开始充电
```json
// 请求
{"act":"on","msg_id":30,"id":"123456789012345-01-6"}
// 响应
{"suc":1,"msg_id":30}
```
**设备→平台:** 充电开始确认(自动充电模式)
```json
// 上报
{"act":"on","msg_id":30,"id":"123456789012345-01-6"}
```
---
#### off双向
**平台→设备:** 手动停止充电
```json
// 请求
{"act":"off","msg_id":31,"id":"123456789012345-01-6"}
// 响应
{"suc":1,"msg_id":31}
```
**设备→平台:** 充满自动停止
```json
// 上报
{"act":"off","msg_id":31,"id":"123456789012345-01-6"}
```
---
#### open平台→设备
开门只需IMEI。
```json
// 请求
{"act":"open","msg_id":40,"id":"123456789012345"}
// 响应
{"suc":1,"msg_id":40}
```
**错误码63 = 门锁故障。**
---
#### reset平台→设备
重启整柜或仓控板。
```json
// 重启整柜
{"act":"reset","msg_id":60,"id":"123456789012345"}
// 重启仓控板
{"act":"reset","msg_id":61,"id":"123456789012345-01"}
// 响应
{"suc":1,"msg_id":60}
```
### 充电结束原因(平台判断)
| 平台收到 | 判断原因 |
|---------|---------|
| `off` (设备→平台) | `normal` — 正常充满 |
| `off` (平台→设备) | `user_stop` — 用户手动停止 |
| `bat_out` | `normal` — 电池拔出 |
| `bat_in` (通道已有电池) | `forced` — 新电池插入,旧电池强制结束 |
| 超时未响应 | `timeout` — 通信超时 |
## H5用户端
- 路径:`/pms/{抽象ID}`
- 功能:显示柜子每个通道状态,可开始/停止充电
- 无支付/计费功能