charging-cabinet/docs/tech/小程序用户端.md
12451 619c96c85f docs: 文档分类整理 —— AGENTS.md 只留判据与链接,详细篇按用途归入 docs/ 子目录
## 为什么要做

`AGENTS.md` 曾达 **90,370 B**,而工作区指令预算是 **65,536 B** —— 系统按**保留头部**截断,
于是**尾部约 25 KB 永远不会被加载**:组织架构、数据模型、TCP 协议、数据单位规范、
微信小程序用户端**全部处于"写了但读不到"的状态**。手册实际上部分失效。

## 一、AGENTS.md 瘦身:90,370 → 60,227 B(余量 5,309 B,尾部不再被截断)

三个**纯参考型大段**整段搬迁到 `docs/tech/`(**内容逐字保留,未删改**),
原位置换成「判据 + 链接」:

| 原 AGENTS.md 章节 | 大小 | 现位置 |
|---|---|---|
| `## 部署与维护` | 17,611 B | `docs/tech/部署手册.md` |
| `## 数据模型` | 12,100 B | `docs/tech/数据模型.md` |
| `## 微信小程序用户端` | 10,262 B | `docs/tech/小程序用户端.md` |

替换后的章节**只留改动前必须先知道的硬约束**(例如:产物名与测试环境落地名不一致、
ETXTBSY 的 `.new`→`stop`→`mv`→`start` 流程、`stream{}` 不可重加、发布顺序 device 先 api 后、
建表顺序即依赖顺序且写错报 MySQL 1824、时间列必须 DATETIME、`cabinets.status` 是配置态不表示在线……),
其余指向详细篇。**判据留在常驻加载的 AGENTS.md,逐项参考移出**。

## 二、docs/ 分类(原 21 个文件平铺 → 五个用途目录)

```
docs/
├── README.md          新增:文档地图(分类/权威性/旧名→现名映射/维护约定)
├── protocol/          设备协议(冻结,对硬件交付)★唯一权威
│   ├── 柜控板固件对接文档.md
│   └── 1TN协议标准_*.md / *.pdf
├── tech/              平台技术文档(开发/运维必读)
│   ├── API.md · 数据模型.md · 部署手册.md · 小程序用户端.md
├── contract/          合同·规格·验收标准(商务依据,不描述实现)
├── compound-notes/    单点经验笔记(原有)
└── archive/           ⛔ 历史留痕(只读,不代表现状,不可作依据)
    ├── 开发文档.md(早于现行实现)
    └── 审查报告/深度分析报告-2026-07-06
```

- 19 个文件用 `git mv` 迁移(保留历史),3 个未跟踪文件用普通移动;
- **内容零改动**(唯一例外是引用替换),已核验 `docs/tech/API.md` 的 `git diff` 为空。

## 三、全仓引用同步:40 处 / 10 个文件

路径不只出现在文档里,还散落在**源码注释**(`db/migrate.rs`、`routes/cabinets.rs`、
`workers/report_worker.rs`、`tcp/connection.rs`、`h5-uniapp/utils/format.ts`)、
`test/tcp_e2e_smoke.py`、`tasks/001-*.md`、`README.md`、`AGENTS.md` 中,全部同步。

**并已实测校验命令能失败**:往 `docs/tech/API.md` 植入 `<!-- probe: docs/API.md -->`
→ 命中;删除 → 无输出。
> ⚠️ 过程中踩了一个坑并已修:这些 `.md` 多为 **CRLF**,我第一次用 `\n` 正则删除时
> **没匹配上 `\r\n`**,以为删干净了其实残留了一行 —— 靠 `git diff` 非空才发现。
> 已把「CRLF 必须按 `\r?\n` 处理」写进 `docs/README.md` 的维护约定。

## 四、顺带查实的失效引用(归档保留原文,不改写历史)

- `docs/tcp-protocol.md`:仅出现在 2026-07-06 的两份历史报告里(当时的事实);
- `docs/柜控板-平台TCP接口文档.md`:仅出现在协议文档 **v1.1 变更记录**里
  (记录"已删除该内部重复文档"这件事),**不是活链接**,无需修复。

两者已在 `docs/README.md` 的「旧名 → 现名映射」表中给出对应关系。
2026-09-20 04:42:51 +08:00

166 lines
10 KiB
Markdown
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 的详细参考篇)
> 本文件由 `AGENTS.md` 的「微信小程序用户端」整段搬迁而来(**内容逐字保留,未删改**)。
> 页面清单、构建/上传流程、状态位与 RSSI 的分层展示口径都在这里。
> 门禁命令(`vue-tsc` 类型检查 + `build:mp-weixin`)与「产物不上传服务器」已留在 `AGENTS.md`。
---
## 微信小程序用户端
### 小程序配置
| 参数 | 值 |
|------|-----|
| AppID | `wx226061c550c52e85` |
| AppSecret | `f11dfee9dc73265b75bf54277ebb1047` |
| 账号 | `13951516272@163.com` |
| 消息加解密方式 | 明文模式 |
| 数据格式 | XML |
| 服务器地址(URL) | `https://app.anzhizhichong.com/api/wechat/callback` |
| Token(令牌) | `7GW16XdnFyak0mE32rc8ZPij` |
| EncodingAESKey | `kRijggSqeCFWi3JhfGyK0nzc6VmdygWWkoqyKPl0bYu` |
> ⚠️ 以上密钥仅记录在内网 Git 仓库 `生产环境.txt`,不入代码仓库。
### 构建命令
```bash
cd software/h5-uniapp
npm run build:mp-weixin
# 产物在 dist/build/mp-weixin/,用微信开发者工具导入上传
```
### API 域名配置
小程序请求后端 API 必须配置合法域名(HTTPS):
- 后台「开发管理 → 开发设置 → 服务器域名」
- request 合法域名:`https://app.anzhizhichong.com`
- 开发阶段可在 `manifest.json` 设 `urlCheck: false` 跳过校验
### API base URL
小程序用全 URL(`api/index.ts` 条件编译):
```typescript
// #ifdef MP-WEIXIN
const BASE_URL = 'https://app.anzhizhichong.com/api/pms'
// #endif
// #ifndef MP-WEIXIN
const BASE_URL = '/api/pms'
// #endif
```
### 信号强度显示(RSSI)
设备卡片右上角显示 RSSI 信号强度。
> ✅ **2026-09 已收敛为「两端同源同名、统一 5 档」**(此前两端不一致,见下方"行为变化")。
> **唯一权威实现**:`web/src/utils/deviceStatus.ts` 与 `h5-uniapp/src/utils/deviceStatus.ts`
> 的 `rssiSignal()` / `rssiBarColors()` / `RSSI_TIERS`(**两端逐字一致,改一边必须同步另一边**)。
> 一致性用例:`node test/rssi_signal.test.mjs`(遍历 0–99 逐值断言两端 `level`/`bars`/`color` 相同)。
> ⛔ **页面里不得再自己写一套阈值**(历史上两端各写一套,就是不一致的根因)。
**统一分档表(协议口径:`rssi` 是 0–99 的信号等级,不是 dBm)**
| 阈值 | `level` | 颜色 | 5 格制格数 | PC 图标亮的 `<path>` |
|------|---------|------|-----------|---------------------|
| ≥ 70 | 4 `strong` | `#16a34a` 绿 | 5 | 4 条全亮 |
| ≥ 55 | 3 `good` | `#2563eb` 蓝 | 4 | 3 条 |
| ≥ 40 | 2 `fair` | `#f59e0b` 黄 | 3 | 2 条 |
| ≥ 30 | 1 `weak` | `#f97316` 橙 | 2 | 1 条 |
| < 30 或 null | 0 `none` | `#9ca3af` 灰 | 1(底格) | 0 条(只剩底部圆点 `M2 20h.01`) |
**两端如何消费同一份纯函数:**
- **PC**(`web/src/pages/devices/CabinetGrid.tsx::SignalIconComponent`):`rssiSignal(rssi)` 取 `color` 与 `level`,
4 条 `<path>` 按 `level > i` 决定亮几条;卡片头部数值颜色用同一个 `color`。
- **小程序**(`home/index.vue`、`device-detail/index.vue`):
`v-for="(color, i) in rssiBarColors(cab.rssi)"` 渲染 `bars` 根 `<view class="signal-bar">`,
颜色**内联** `:style="{ background: color }"`;**格数真的随档位变化**。
- 颜色只在 TS 里定义一次(不再有 `.signal.good/.mid/.low/.off` 这类 CSS 颜色规则;
页面 CSS 只负责尺寸/间距)。
**后端(两端共用):** 设备列表接口用 pipeline 批量 `HGET device:{imei} rssi`
(`routes/cabinet_tree.rs::attach_realtime_status`,字段下标见 `DEVICE_LIST_FIELDS`);
小程序侧 `routes/h5.rs::get_cabinets` 同样带 Redis。
**相关文件:**
- 纯函数(权威): `software/web/src/utils/deviceStatus.ts`、`software/h5-uniapp/src/utils/deviceStatus.ts`
- PC 消费点: `software/web/src/pages/devices/CabinetGrid.tsx`(`SignalIconComponent` + 卡片头部)
- 小程序消费点: `software/h5-uniapp/src/pages/home/index.vue`、`pages/device-detail/index.vue`
- 类型: `software/h5-uniapp/src/api/index.ts` — `CabinetItem.rssi`
- 一致性用例: `test/rssi_signal.test.mjs`
> ⚠️ **行为变化(2026-09,小程序端显示会变 —— 这是有意为之)**
> 收敛前:PC 5 档(≥70 绿/≥55 蓝/≥40 黄/≥30 橙/<30 灰,格数随档位增删);
> 小程序只有 **4 档(无 ≥70)**、**30–39 用红色**、且**恒渲染 5 格只改颜色**。
> 收敛后小程序:**新增 ≥70 档(绿/满格)**、**30–39 由红改橙**、**≥55 由绿改蓝**、
> **离线灰由 `#94a3b8` 改为 `#9ca3af`**,且**格数随档位变化**(不再恒 5 格)。
> 影响页面:小程序首页设备卡片、小程序设备详情页头部。
### 近期 UI 改动
1. **设备卡片头部**:**PC 管理端**(`web/src/pages/devices/CabinetGrid.tsx`)去掉"在线/离线"文字 badge,
改为信号图标 + RSSI 数值。
✅ **小程序保留"在线/离线"文字 badge —— 这是有意差异,不是缺陷,不要当 bug 改掉**:
`h5-uniapp/src/pages/home/index.vue:43`(设备卡片,`cab.status===1 ? '在线' : '离线'`)与
`pages/device-detail/index.vue:18`(设备详情;同文件 `:74` 还有**板级**的在线/离线 badge)。
理由(产品决定):PC 用"信号图标 + RSSI 数值"表达;小程序屏幕小、文字 badge 更直观。
> PC 卡片:**图标 + 数值**;小程序:**文字 badge**(有意保留)。
另外 **PC 设备详情页**(`web/src/pages/devices/cabinet.tsx:503`)也仍用 `Tag` 渲染
`CABINET_STATUS_MAP`(`0 离线 / 1 在线 / 2 禁用`)—— 只是**卡片**去掉了,别理解成"全平台都去掉了"。
2. **通道统计**:图标+数字一行排列,图标用 `<svg>` 内联 Lucide(不加容器,flex 居中)
3. **右上角扫码**:scan-line 图标(Lucide),灰色 #6b7280,居中垂直对齐
### 状态位展示约定(**状态位按归属层级展示**)
> 以下三条是**协议 §3.8 / §4.2 / §5.7 的既定结论**(产品已确认),后续不要再反复确认。
1. **柜级状态位 → 显示在"柜子"层面**
- 数据:**⚠️ 这两个名字不是 DB 列**(实测 `cabinets` 表**没有** `status1`/`status2` 列,
它的列是 `id/project_id/abstract_id/imei/iccid/auth_str/name/address/status/created_at/updated_at/
latitude/longitude/work_mode/auth_str_issued/cell`)。它们是:
**Redis 字段** `device:{imei}` 的 `cabinet_status1` / `cabinet_status2`(设备上报原样落,2 位 hex)
→ 接口**原样透出**为响应字段 `status1` / `status2`。
写:`report_worker.rs::write_device_state`;读:`cabinet_tree.rs::attach_realtime_status`(列表)、
`cabinets.rs::fetch_cabinet_realtime`(详情)。
- 位定义(§3.8):`status1` bit0 断电|bit1 烟雾|bit2 水位|bit3 倾倒|**bit4 柜门打开**|
bit5 加热风扇|bit6 接触器断开|bit7 喷淋;`status2` bit0 声光报警|**bit1 柜门打开超时**
(阈值硬件定义,**平台只呈现、不自行判定**);`status2` bit2–7 为保留位,**不展示**。
- 页面:PC `web/src/pages/devices/cabinet.tsx`、小程序 `h5-uniapp/src/pages/device-detail/index.vue`。
- **取不到返回 `null`(未知),绝不落 `"00"`** —— `"00"` 是"全部正常"的有效值。
2. **仓体级状态位 → 显示在"仓体"层面**
- 数据:通道 18 字符 hex 的 `batt_status`(byte0) / `pow_status`(byte1);保留位
(`batt_status` bit2–3、`pow_status` bit3–6)**不展示**。
- 页面:PC 柜子详情里的通道卡片、小程序 `pages/compartment-detail/index.vue`。
- **仓体层面不显示任何"门"状态**:协议**没有**"按仓体(舱门)开门"的指令(§4.2 L403)。
3. **柜门是柜级独立动作,不与业务联动,全平台只有一个开门按钮**
- `open` 是**柜级**指令(**不带 `id`**),语义是**「柜门解锁,下发后由人拉开,门不会自动开」**;
没有"关门"指令(关门也是人为动作)。**全平台只有一扇柜门**,不按仓体区分。
- **柜门状态永远来自 `status_post`(约 60 秒一次),不是实时量** —— 界面上必须标注
"来自最近一次上报(约 60 秒一次),非实时"(§4.2 L428)。
- 开门是**独立流程**,与电池插入、充电开始/结束**没有因果关系也没有先后要求**(§5.7、§7.2):
不得出现在"开始充电"流程里、不得改任何订单状态、不得要求存在订单。
- 入口只有一个:PC 详情页按钮(`<Permission code="charge:open_door">` 包裹)+
小程序设备详情页按钮;**仓体(通道)详情页不放开门按钮**。
- 权限码 `charge:open_door`(后端 `check_permission` 硬校验;`device:door:unlock` 是历史误用的
不存在权限码,**禁止再用**)。
4. **解析函数两端必须同源同名**:`web/src/utils/deviceStatus.ts` 与
`h5-uniapp/src/utils/deviceStatus.ts` 各有一组 `parseCabinetStatus1` / `parseCabinetStatus2` /
`cabinetDoorState` / `cabinetStatusTags`(注释互相指认对方文件,改一边务必同步另一边)。
端到端一致性用例见 `test/device_status_parse.test.mjs`(`node test/device_status_parse.test.mjs`,
逐位遍历 0x00–0xFF 断言两端一致)。
5. **`cabinets.status` 是「启用/禁用」配置态,不表示在线** —— `1` 启用 / `2` 禁用(`0` 是建表默认值
产生的取值,代码不显式写它,语义等同启用);实时在线一律以 Redis `device:{imei}.online` 为准
(唯一权威判据 `routes/cabinet_tree.rs::device_online_flag`,接口字段 `is_online`),
`energy_stats` 的 `online_count`、H5 首页 `online_cabinets`、PC/小程序的「在线/离线」全部消费它
(`CABINET_STATUS_MAP` 只保留启用/禁用两档,**不要再出现"离线"档**)。
### 开发约束(uni-app 特定)
1. **`<text>` 不支持 `display: flex`** — uni-app 的 `<text>` 组件是纯文本组件,不接受 flex 布局。需要 flex 的地方用 `<view>`
2. **SVG 图标居中**:`.stat-chip svg { display: block; flex-shrink: 0 }` 用 CSS 统一控制尺寸,比内联属性更可靠
3. **内联 SVG + v-if**:Vue 3 支持在 `<path>` 上用 `v-if` 条件渲染,适用于动态图标
4. **CSS 覆盖 SVG 尺寸**:用 `.xxx svg { width: Xpx; height: Xpx }` 统一控制图标大小,不在每个 `<svg>` 上写死