# AGENTS.md - 充电柜项目工作手册

## 项目简介

充电柜矩阵系统（Power Matrix System / PMS）
产品名：安知充（anzhizhichong），已有充电站平台在运行，充电柜是新增产品线。
仓数不固定：1柜控板+N仓控板，每仓控板管6仓，最小6仓。

## 设备标识

- **平台抽象ID**：`10-00000000`（绑定柜控板，二维码对应此ID）
- **设备ID**：4G SIM卡15位IMEI（10进制）
- **仓体全局ID**：`平台层ID-仓板ID-仓体ID`
- **扫码链接**：`https://app.anzhizhichong.com/pms/{抽象ID}`

## 技术栈

- 后端：Rust + Axum
- 前端：React + Arco Design Pro（React版）
- 数据库：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 3000，TCP 3001
- 正式环境：API 9000，TCP 9001

**部署路径：**
- 正式环境：/data/pms（api-server、device-server、web）
- 测试环境：/data/pms_dev（api-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 9（2核/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: root，password: Hbhyg731024，database: `pms`
- Redis: `r-bp1lsezvgzmgtwhbtg.redis.rds.aliyuncs.com`，password: Hbhyg731024，db: 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 check，TS→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/pms;
    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;
    }

    location /pms/ {
        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`：IMEI（10进制），`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` | 设备→平台 | 无 | 停电上报 |
| `bat_in` | 设备→平台 | `IMEI-仓控板-通道` | 电池插入（含BMS数据，字段待定） |
| `bat_out` | 设备→平台 | `IMEI-仓控板-通道` | 电池拔出 |
| `on` | **双向** | `IMEI-仓控板-通道` | 开始充电（平台下发/设备上报） |
| `off` | **双向** | `IMEI-仓控板-通道` | 停止供电（平台手动/设备自动） |
| `open` | 平台→设备 | `IMEI` | 开门 |
| `status_get` | 平台→设备 | `IMEI-仓控板` | 查仓板状态 |
| `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解析仓控板数量

### 充电结束原因（平台判断）

| 平台收到 | 判断原因 |
|---------|---------|
| `off` (设备→平台) | `normal` — 正常充满 |
| `off` (平台→设备) | `user_stop` — 用户手动停止 |
| `bat_out` | `normal` — 电池拔出 |
| `bat_in` (通道已有电池) | `forced` — 新电池插入，旧电池强制结束 |
| 超时未响应 | `timeout` — 通信超时 |

## H5用户端

- 路径：`/pms/{抽象ID}`
- 功能：显示柜子每个通道状态，可开始/停止充电
- 无支付/计费功能
