# 数脉医联 · 技术接手文档

> 项目：`tcm-health-platform`（中医智能健康管理平台 + 自建图床）
> 版本：2.0.0
> 最后更新：2026-04-22（新增 7.6 备份与配置安全推送三层防护；实时语音识别已接入）
> 目标读者：接手本项目的后续开发 / 运维工程师

---

## 目录

1. [项目概览](#一项目概览)
2. [技术栈](#二技术栈)
3. [目录结构](#三目录结构)
4. [数据库设计](#四数据库设计)
5. [环境变量](#五环境变量)
6. [本地启动](#六本地启动)
7. [服务器部署](#七服务器部署)
8. [HTTP API 文档](#八http-api-文档)
9. [前端页面与模块](#九前端页面与模块)
10. [运维脚本与工具](#十运维脚本与工具)
11. [常见问题 / 踩坑](#十一常见问题--踩坑)
12. [外部依赖密钥](#十二外部依赖密钥)

---

## 一、项目概览

**数脉医联** 是一个集"智能中医问诊 + 舌诊 / 穴位识别 + 体质辨识 + 社区医疗 + 健康商品商城"为一体的 H5 健康管理平台，同时在同一个 Node 服务内保留了项目最初的**自建图床**能力（上传图片/视频 → 得到公网可访问 URL）。

### 1.1 功能模块

| 模块 | 入口 | 核心能力 |
|---|---|---|
| AI 问诊 | `/chat` | 接入百度千帆 Agent，SSE 流式对话；支持图片/语音输入；按 mode 区分问诊/舌诊/穴位/健康监测等场景 |
| 舌诊分析 | `/tongue` | 上传舌头正/背面照 → 调第三方 API 出舌象指标 + 健康评分 |
| 穴位识别 | `/acupoint` | 上传身体部位照 → AI 识别并标注穴位坐标 |
| 体质辨识 | `/chat?mode=constitution` | 本地题库（GB/T 46939-2025 九型体质）+ 本地评分 + WebAI 生成报告 |
| 健康报告 | `/reports` | 汇总上述各类报告；支持"分享全文"（文本）和打印 |
| 社区联动 | `/community` | 百度地图 POI 搜附近医院 / 社区中心 + AI 排序；结果本地缓存 |
| 健康商城 | `/shop` | 中医器具商品展示 / 加购 / 模拟下单；支持分类 Tab + 搜索 + 图片展示 |
| 图床管理 | `/admin/files` | 上传 / 列表 / 重命名 / 删除文件，对外 URL `/files/:name` |

### 1.2 生产访问地址

- **主站（推荐）**：<https://kaguraaya.cn>（2026-04-21 上 HTTPS，Let's Encrypt 免费证书，certbot.timer 自动续期）
- 兼容入口：<http://119.23.74.244:3000>（IP 直连 3000 端口保留，历史集成不中断）
- 架构：`nginx(443/SSL) → node(127.0.0.1:3000)`；`http://kaguraaya.cn` 会 301 自动跳 https
- 登录后几乎所有模块才有完整能力；舌诊 `/detect`、穴位 `/identify` 可匿名使用，但不会保存记录

---

## 二、技术栈

### 后端

- **运行时**：Node.js ≥ 16（生产用 18.20.8）
- **框架**：Express 4
- **数据库**：SQLite（`better-sqlite3` 同步 API，WAL 模式）
- **鉴权**：JWT（`jsonwebtoken`），HS256，Token 7 天过期
- **密码**：`bcryptjs` 10 rounds
- **上传**：`multer` 内存存储 → `saveFile` 落盘到 `files/`
- **外部 HTTP**：`node-fetch` v2
- **环境变量**：`dotenv`
- **进程管理**：pm2（admin 用户下，应用名 `tcm-health-platform`）

### 前端

**严格要求：纯原生 HTML + CSS + JavaScript，不使用任何前端框架**（无 React / Vue / 构建工具）。

- 页面：`public/*.html`
- 逻辑：`public/assets/*.js`
- 样式：`public/assets/style.css`
- 公共：`app.js` 提供 `renderNav`/`escapeHtml`/`renderMarkdown`/`apiGet`/`apiPost`/`toast`/`CartStore`/`Auth` 等

### 外部服务

| 服务 | 用途 | 必需？ |
|---|---|---|
| 百度千帆 Agent | `/chat` 主问诊 | 是 |
| "老张 API"（OpenAI 兼容） | 体质辨识报告 / 追问 / 首页健康分析 | 是 |
| 舌诊 API（`ai.poweruser.link`） | 舌诊分析 | 是 |
| 穴位识别 API | 穴位坐标 | 是 |
| 百度短语音 ASR | 手机端语音输入 | 否 |
| 百度地图 Place API | 社区附近机构 | 否 |

### 部署辅助

- **Python** 3.12 + **paramiko** 4.0（部署自动化，见 `scripts/deploy.py`）

---

## 三、目录结构

```
图床api测试/
├─ server.js                主入口：装配路由，启动监听
├─ db.js                    数据库初始化 + schema + 迁移
├─ package.json             依赖声明
├─ .env / .env.example      环境变量（.env 在 .gitignore 中）
├─ tcm.db                   SQLite 数据文件（.gitignore）
├─ files/                   图床物理文件（.gitignore）
├─ imgs/                    项目内置图片
│
├─ routes/                  ⚡ HTTP 路由层（每文件对应一个业务域）
│  ├─ auth.js                 用户注册/登录/profile
│  ├─ qianfan.js              千帆 Agent 对话 / 会话 / 报告
│  ├─ webai.js                "网页AI"（老张 API）SSE 追问
│  ├─ ai.js                   首页健康分析（调 webai）
│  ├─ tongue.js               舌诊
│  ├─ acupoint.js             穴位识别
│  ├─ constitution.js         体质辨识（本地题库 + webai）
│  ├─ reports.js              健康报告 CRUD
│  ├─ community.js            社区附近机构 + AI 推荐
│  ├─ shop.js                 商品 / 订单
│  ├─ asr.js                  语音识别
│  └─ filehost.js             图床（/upload-file /delete /files/:name 等）
│
├─ services/               业务逻辑层（被 routes 调用）
│  ├─ qianfan.js              千帆 SDK 封装（会话管理 + 流式解析 + 去重）
│  ├─ webai.js                OpenAI 兼容接口封装（流式 + 非流式）
│  ├─ tongue.js               舌诊 API 客户端
│  ├─ acupoint.js             穴位识别 + BODY_PARTS 常量表
│  ├─ health-report.js        健康报告聚合 / 用户档案读取
│  └─ baidu-asr.js            百度 ASR 客户端
│
├─ middleware/
│  └─ auth.js                requireAuth / optionalAuth / signToken
│
├─ utils/
│  ├─ files.js               saveFile/deleteFile/buildFileUrl/listFiles
│  └─ solar-term.js          当前节气计算（用于首页分析 prompt）
│
├─ data/
│  └─ constitution-questionnaire.js
│                              九型体质题库 + 评分逻辑（本地算分）
│
├─ docs/
│  ├─ HANDOVER.md            ← 你正在看的这个
│  ├─ qianfan-chatflow-interrupt.md
│  ├─ qianfan-file-upload.md
│  └─ workflow-end-api.md
│
├─ public/                 前端静态资源（Express 从这里 sendFile）
│  ├─ index.html / login.html / profile.html
│  ├─ chat.html            智能问诊聊天
│  ├─ tongue.html          舌诊
│  ├─ acupoint.html        穴位识别
│  ├─ community.html       社区联动（含缓存逻辑）
│  ├─ reports.html         健康报告列表
│  ├─ shop.html            商店
│  ├─ admin-files.html     图床管理
│  ├─ favicon.svg
│  └─ assets/
│     ├─ app.js               公共工具（Auth/CartStore/renderMarkdown/视频播放器等）
│     ├─ style.css            全站样式
│     ├─ chat-enhanced.js     聊天核心（SSE / 图片 / 商品卡）
│     ├─ chat.js              聊天旧版（保留）
│     ├─ constitution.js      体质辨识 UI
│     ├─ reports.js           报告详情 / 分享全文
│     ├─ shop-enhanced.js     商店（Tab + 搜索 + 图片）
│     ├─ shop.js              商店旧版（保留）
│     ├─ share.js             原生 canvas 海报分享
│     ├─ home.js              首页 AI 分析
│     ├─ profile.js           个人档案
│     └─ asr-recorder.js      录音器封装
│
└─ scripts/               运维脚本（Python / JS / PS1）
   ├─ deploy.py              paramiko 自动化部署主脚本
   ├─ verify.py              部署后 HTTP 健康检查
   ├─ probe2.py              服务器环境探测（一次性）
   ├─ parse-xlsx.js          解析 WPS 嵌入图片的 xlsx
   ├─ import-products.js     服务器端商品入库
   ├─ clean-legacy-products.py  清掉旧硬编码商品（一次性）
   ├─ fix-db-patch.py        上传 db.js 并清理 + 重启（一次性）
   ├─ install-key-once-v2.ps1   配 SSH 免密（本地一次性）
   └─ products-parsed.json   最近一次解析的商品 JSON
```

---

## 四、数据库设计

SQLite 文件：`<项目根>/tcm.db`（WAL 模式，外键打开）。

### 4.1 表结构

**`users`**：用户账号
```sql
id INTEGER PK, username TEXT UNIQUE, password_hash TEXT,
phone TEXT, age INTEGER, height_cm REAL, weight_kg REAL, gender TEXT,
created_at DATETIME
```

**`conversations`**：会话
```sql
id INTEGER PK, user_id FK→users,
qianfan_conversation_id TEXT,         -- 千帆会话 ID
mode TEXT DEFAULT 'consultation',      -- consultation/tongue/acupoint/health/constitution
title TEXT, created_at, updated_at,
pending_interrupt TEXT                 -- 千帆工作流中断事件 JSON
```

**`messages`**：消息明细
```sql
id, conversation_id FK, role (user|assistant|system),
content TEXT, image_urls TEXT='[]', meta TEXT='{}',
created_at
```
- `meta` 存 followUps / cards / severity / auto_options 等结构化 payload

**`tongue_reports`**：舌诊原始记录
```sql
id, user_id FK, phone, front_url, back_url,
raw_result TEXT='{}', health_score INT, health_result TEXT,
created_at
```

**`acupoint_records`**：穴位识别记录
```sql
id, user_id FK, image_url, marked_image_url,
point_name, bodyid, coordinates TEXT='[]', raw_result TEXT='{}',
created_at
```

**`health_reports`**：汇总健康报告（`/reports` 页面的数据源）
```sql
id, user_id FK, type, title, summary, content, score,
raw_data TEXT='{}', created_at, updated_at
```
- `type` 常见值：`tongue` / `acupoint` / `constitution` / `consultation` / `general`

**`products`**：商品
```sql
id, name, category, price,
image_url TEXT,
suitable_constitution, suitable_symptoms,
description, usage_text
```

**`orders`**：订单（模拟支付，不对接真实支付）
```sql
id, user_id FK, receiver_name, phone, address,
items TEXT='[]' (JSON), total_price, status, created_at
```

### 4.2 迁移策略

`db.js` 启动时用 `ensureColumn()` 幂等添加新列（`ALTER TABLE ADD COLUMN` 仅在列不存在时执行），**加字段不会破坏旧数据**。

**重要**：商品数据不再由 `db.js` 注入，统一通过 `scripts/import-products.js` 维护。改字段可直接改 `db.js` 的 CREATE TABLE 语句 + 加一行 `ensureColumn`。

---

## 五、环境变量

在服务器项目根目录放 `.env`（本地开发同样）。模板见 `.env.example`。

| 变量 | 说明 | 必填 |
|---|---|---|
| `PORT` | 监听端口，默认 3000 | 否 |
| `BASE_URL` | 对外域名，生成分享链接、上传文件 URL 时用 | 建议填 |
| `SESSION_SECRET` | JWT 签名密钥，**生产必须改成 32 位以上随机串** | 是 |
| `QIANFAN_APP_ID` | 百度千帆 Agent AppID | 问诊必需 |
| `QIANFAN_BEARER_TOKEN` | 千帆 Bearer Token（含 `Bearer ` 前缀） | 问诊必需 |
| `TONGUE_APP_KEY` | 舌诊 API key | 舌诊必需 |
| `TONGUE_APP_SECRET` | 舌诊 API secret | 舌诊必需 |
| `WEB_AI_API_BASE` | OpenAI 兼容网关，默认 `https://api.laozhang.ai/v1` | 体质/追问必需 |
| `WEB_AI_MODEL` | 默认模型，如 `gpt-5.2` | 体质/追问必需 |
| `WEB_AI_API_KEY` | 上述网关的 Key | 体质/追问必需 |
| `BAIDU_ASR_API_KEY` | 百度 ASR | 手机语音输入 |
| `BAIDU_ASR_SECRET_KEY` | 百度 ASR | 手机语音输入 |
| `BAIDU_MAP_AK` | 百度地图 Place API，服务端类型 Key | 附近机构 |
| `COMMUNITY_AI_MODEL` | 附近机构 AI 推荐的模型（留空则用 `WEB_AI_MODEL`） | 否 |
| `MAX_FILE_SIZE` | 上传大小限制（字节），默认 500MB | 否 |
| `DEPLOY_SSH_PW` | 仅本地部署脚本读取，**不要写到服务器 .env** | 否 |

生产当前真实 `.env` 在 `/home/admin/picui-proxy/.env`，密钥已配好。

---

## 六、本地启动

### 6.1 依赖

```powershell
# Windows PowerShell
cd C:\Users\Administrator\Documents\Project\图床api测试
npm install
```

### 6.2 配环境变量

```powershell
Copy-Item .env.example .env
notepad .env
```
至少填上：`SESSION_SECRET` / `QIANFAN_APP_ID` / `QIANFAN_BEARER_TOKEN` / `WEB_AI_API_KEY` / `WEB_AI_MODEL`。

### 6.3 运行

```powershell
npm start           # = node server.js
# 或热重载
npm run dev         # = nodemon server.js
```

浏览器打开 <http://localhost:3000>。

### 6.4 Windows 下的 `better-sqlite3`

这个包有 C++ 原生代码，首次 `npm install` 需要 VS Build Tools。若失败：
```powershell
npm install --global --production windows-build-tools
# 或者仅安装预编译版本
npm install better-sqlite3 --build-from-source=false
```

---

## 七、服务器部署

### 7.1 服务器基本信息

| 项 | 值 |
|---|---|
| IP | `119.23.74.244` |
| SSH 端口 | 22 |
| 系统 | Ubuntu 22.04（阿里云 ECS） |
| **登录用户** | **`admin`**（项目属主 + pm2 owner，SSH 密钥已配；root 通道保留但日常不用） |
| **项目路径** | `/home/admin/picui-proxy` |
| **服务管理** | `admin` 用户的 pm2（`/home/admin/.pm2`），应用名 `tcm-health-platform` |
| Node | `/usr/bin/node` v18.20.8 |
| pm2 | `/usr/bin/pm2` v6.0.14 |
| 数据库 | `/home/admin/picui-proxy/tcm.db` |
| 图床目录 | `/home/admin/picui-proxy/files/` |
| `.env` | `/home/admin/picui-proxy/.env`（**不要覆盖**，所有密钥在里面） |
| 对外端口 | 3000（阿里云安全组已放行） |

### 7.2 首次：配 SSH 免密

本机 `~/.ssh/id_ed25519`（私钥）+ `id_ed25519.pub`（公钥）。运行一次：

```powershell
powershell -ExecutionPolicy Bypass -File "scripts\install-key-once-v2.ps1"
```

会提示输入 `admin` 和 `root` 的密码各一次（自动跳过已配通的账号）。之后永久免密。

> **注意**：该脚本现在是**纯英文**（之前中文注释在 PowerShell 5.1 下会因 ANSI/UTF-8 冲突崩）。改动时保持 ASCII-only。

### 7.3 日常部署：一条命令

所有部署都走 `scripts/deploy.py`，它以 `admin` 身份直接 SSH（无需 sudo）。前置：

```powershell
python -m pip install paramiko
```

#### 最常用

```powershell
python scripts\deploy.py deploy
```

这一条 = **pack + upload + install + verify**，用时 5~15 秒。

整个流程：
1. 本地 `tar` 打 `$env:TEMP\tcm-code.tgz`（~3 MB，排除 node_modules / .env / tcm.db / files/）
2. sftp 上传到服务器 `/tmp/tcm-code.tgz`
3. 服务器 `tar -xzf` 到 `/home/admin/picui-proxy/` + `pm2 restart all --update-env`
4. HTTP GET 5 个关键页面确认 200

#### 带商品库重建

```powershell
# 1) 先本地解析 xlsx（前提：健康商品.xlsx 放在项目根）
node scripts\parse-xlsx.js

# 2) 部署 + 商品重建
python scripts\deploy.py deploy --imgs
```

`--imgs` 会额外：把商品图包上传 → 清空 `products` 表 → 复制图到 `files/` → 重插 24 条商品 → 然后再重启。

#### 细分子命令（调试用）

```powershell
python scripts\deploy.py probe      # 只探测：whoami / pm2 / Node 版本 / 磁盘
python scripts\deploy.py pack       # 只本地打包
python scripts\deploy.py upload     # 只 sftp 上传
python scripts\deploy.py install    # 只服务器解压 + 重启
python scripts\deploy.py verify     # 只 HTTP 检查
python scripts\deploy.py probe, pack, upload, install 都支持 --imgs
```

### 7.4 添加新文件的部署清单

`scripts/deploy.py` 里的 `CODE_PATHS` 决定打包什么。当前是：

```
server.js / db.js / package.json
routes/ services/ middleware/ utils/ data/
public/ scripts/ docs/
```

- 加了新**文件**（路径已在上述目录下）→ 无需改 deploy.py，直接部署即可
- 加了新**顶级目录**（比如 `config/`）→ 在 `CODE_PATHS` 里补一行

### 7.5 手工 SSH 操作（应急）

```bash
# 登录
ssh admin@119.23.74.244

# 看服务
pm2 list
pm2 logs tcm-health-platform --lines 100
pm2 restart tcm-health-platform

# 改文件（admin 就是属主）
vim /home/admin/picui-proxy/.env

# 对 SQLite 数据库做操作（服务器没有 sqlite3 CLI，用 node）
cd /home/admin/picui-proxy && node -e "
  const db = require('./db');
  console.log(db.prepare('SELECT COUNT(*) as n FROM products').get());
"
```

> **给 Cascade 的部署 SOP**：见 `docs/DEPLOY-FOR-CASCADE.md`（包含决策流程 / 故障自救 / 真实 log 样例）。

### 7.6 备份与配置安全推送（三层防护）

**起因**：2026-04-21 晚，Cascade 把只含 80 端口的 nginx 配置误覆盖到已被 certbot 改造为 443+80 结构的生产配置上，HTTPS 挂掉约 1 分钟（手动快速 cp 回 `.bak` 恢复）。事后搭建以下三层防护，此后类似事故不应再发生。

#### 7.6.1 L1：改配置不翻车（自动快照 + 回滚）

改 `/etc/nginx/sites-available/kaguraaya.cn` 或 `/home/admin/picui-proxy/.env` 只能走 **deploy.py 的 push 子命令**，不要再手工 `scp + sudo cp`。每次推送自动六步：
`[1]拉线上快照 → [2]diff 展示 → [3]确认 → [4]上传+备份 → [5] nginx -t / pm2 restart → [6] curl /health 验活`；任何一步失败都会自动 `cp 回 .bak` 回滚。

```powershell
# 推 nginx
python scripts\deploy.py nginx-pull                # 只拉快照到 deploy/snapshots/
python scripts\deploy.py nginx-push                # 推 deploy/nginx-kaguraaya.conf，会弹出 diff + 问 y/N
python scripts\deploy.py nginx-push -y             # 自动化场景跳过确认

# 推 .env（会自动 pm2 restart）
python scripts\deploy.py env-pull                  # 拉到 deploy/snapshots/env.YYYYMMDD-HHMMSS.txt
python scripts\deploy.py env-push C:\path\to\.env  # 推任意本地文件（含改过的 .env）
```

产物：
- 本地：`deploy/snapshots/nginx-kaguraaya.<时间戳>.conf` / `env.<时间戳>.txt`（已 gitignore）
- 服务器：`/etc/nginx/sites-available/kaguraaya.cn.bak.<时间戳>` / `/home/admin/picui-proxy/.env.bak.<时间戳>`

**安全性基础**：admin 用户通过 `/etc/sudoers.d/admin-nginx` 对以下命令免密 sudo：
`cp`（限定路径）、`nginx -t`、`systemctl reload nginx`。所以 push 流程全程不需要 root 密码。

#### 7.6.2 L2：服务器端每日自动备份（3:30 AM，保留 14 天）

服务器上 `/home/admin/backups/backup-daily.js` 由 crontab 每天 3:30 AM 自动跑一次，产出：

```
/home/admin/backups/daily/YYYY-MM-DD/
├── tcm.db              # SQLite 热备（better-sqlite3 .backup() API，不怕 WAL 半写）
├── env.txt             # 权限 600
├── nginx-kaguraaya.conf
├── files.tar.gz        # 用户上传的所有文件
└── manifest.json       # 本次备份的元信息（每项的大小、耗时、是否成功）
```

老于 14 天的日目录自动删除。cron 日志在 `/home/admin/backups/logs/cron.log`。

**首次部署**（或脚本改动后重装）：
```powershell
python scripts\deploy.py setup-backup     # 上传脚本 + 手动跑一次验证 + 设置 crontab（幂等）
```

**手动触发**（比如在做重大操作前想先做一次快照）：
```bash
ssh admin@119.23.74.244 "cd /home/admin/picui-proxy && /usr/bin/node /home/admin/backups/backup-daily.js"
```

#### 7.6.3 L3：本地拉镜像（增量同步）

只有服务器上有备份不够 —— 服务器整体挂了就全没了。所以本地电脑定期拉一份：

```powershell
python scripts\deploy.py pull-backup
# 从服务器 /home/admin/backups/ 增量 sftp 到本地 backups/
# 同时清理本地超过 14 天的日目录
```

增量判据：文件大小一致就跳过（daily 目录里的文件写完后就不再变）。现在每天约 113 MB（绝大部分是 files.tar.gz），14 天累计约 1.6 GB，本地磁盘不大就够。

**建议手动周期**：每周一次 或 做重大操作前，在 PowerShell 里跑一下 `python scripts\deploy.py pull-backup`。

#### 7.6.4 恢复流程（真出问题时怎么办）

**配置误改（nginx / .env）**：服务器上的 `.bak.<时间戳>` 备份秒回滚：
```bash
ssh admin@119.23.74.244
# nginx
sudo cp /etc/nginx/sites-available/kaguraaya.cn.bak.20260422-xxxxxx \
        /etc/nginx/sites-available/kaguraaya.cn
sudo nginx -t && sudo systemctl reload nginx
# .env
cp /home/admin/picui-proxy/.env.bak.20260422-xxxxxx /home/admin/picui-proxy/.env
pm2 restart all --update-env
```

**SQLite 数据库坏了**：从任意一天的 daily 备份恢复：
```bash
ssh admin@119.23.74.244
pm2 stop tcm-health-platform
cp /home/admin/backups/daily/2026-04-22/tcm.db /home/admin/picui-proxy/tcm.db
# 会同时带着 WAL checkpoint 后的一致状态，不需要管 .wal / .shm 文件
pm2 start tcm-health-platform
```

**用户上传文件丢了**：
```bash
cd /home/admin/picui-proxy
tar -xzf /home/admin/backups/daily/2026-04-22/files.tar.gz -C .
# files.tar.gz 里是 "files/..." 相对路径，-C . 即可解到 /home/admin/picui-proxy/files/
```

**服务器整体重装**（最坏情况）：
1. 本地已经有 `backups/YYYY-MM-DD/` 镜像（L3 定期拉）
2. 新机器装好 Node + pm2 + nginx + certbot（证书丢了 `certbot --nginx -d kaguraaya.cn` 重申）
3. 本地 `python scripts\deploy.py deploy` 推代码
4. 本地 scp 最新的 `tcm.db`、`env.txt`、`files.tar.gz` 到对应位置（见上三个命令）
5. `pm2 start ecosystem.config.js` 起服务

---

## 八、HTTP API 文档

### 8.1 通用约定

- 所有业务 API 统一返回：
  ```json
  { "success": true,  "data": <any> }
  { "success": false, "error": "错误描述" }
  ```
- 需登录的接口：请求头 `Authorization: Bearer <JWT>`
- JWT 由 `/api/auth/login` 或 `/api/auth/register` 颁发，有效期 7 天
- `optionalAuth` 接口：带 Token 则保存记录（关联 user_id），不带 Token 也能用

### 8.2 鉴权（`/api/auth`）

| 方法 | 路径 | 认证 | body / 返回 |
|---|---|---|---|
| POST | `/api/auth/register` | - | `{ username, password }` → `{ token, ...user }` |
| POST | `/api/auth/login` | - | `{ username, password }` → `{ token, ...user }` |
| POST | `/api/auth/logout` | - | `{}` → `{ success: true }` |
| GET  | `/api/auth/me` | ✓ | → `{ userId, username, phone, age, ... }` |
| PUT  | `/api/auth/profile` | ✓ | `{ phone, age, height_cm, weight_kg, gender }` → user |

### 8.3 千帆智能对话（`/api/qianfan`）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| POST | `/api/qianfan/conversation` | ✓ | 创建空会话 `{ mode }` → `{ conversationId, qianfanConversationId }` |
| POST | `/api/qianfan/start` | ✓ | 创建 + 自动发首条欢迎语 |
| POST | `/api/qianfan/chat` | ✓ | 非流式 `{ message, conversationId, imageUrls?, extraContext? }` → `{ reply, meta }` |
| POST | `/api/qianfan/chat-stream` | ✓ | SSE 流式（推荐用这个）；事件：`delta`/`done`/`error` |
| POST | `/api/qianfan/end` | - | 结束会话，触发千帆工作流 end 事件 |
| POST | `/api/qianfan/save-report` | ✓ | 把会话保存为 health_report |
| GET  | `/api/qianfan/conversations` | ✓ | 列会话；query：`mode`, `onlyWithUser=1`, `minUserMessages=2`, `limit=20` |
| GET  | `/api/qianfan/conversations/:id/messages` | ✓ | 读会话消息 |

#### SSE 协议

响应头：`Content-Type: text/event-stream`，事件格式：
```
event: delta
data: {"delta":"这段...","fullText":"到目前为止的完整文本"}

event: done
data: {"reply":"完整回复","meta":{"followUps":[...],"cards":[...],"severity":"high"}}

event: error
data: {"error":"..."}
```

### 8.4 网页 AI 追问（`/api/webai`）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| POST | `/api/webai/chat-stream` | ✓ | SSE 流式。`{ message, conversationId?, mode, context?, contextText? }`<br>`mode` ∈ `follow-up` / `constitution-qa` / `tcm-qa` |

### 8.5 首页健康分析（`/api/ai`）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| GET | `/api/ai/home-analysis?cacheKey=xxx` | ✓ | 基于用户档案 + 最近报告 + 节气生成首页洞察；前端传 `cacheKey` 等于上次返回则直接 304 |

### 8.6 舌诊（`/api/tongue`）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| POST | `/api/tongue/detect` | optional | `{ phone, front_url, back_url }` → 舌诊结果 + 保存记录（登录时） |
| GET  | `/api/tongue/reports` | ✓ | 我的舌诊历史 |

### 8.7 穴位识别（`/api/acupoint`）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| GET  | `/api/acupoint/body-parts` | - | 身体部位对照表（bodyid + 穴位清单） |
| GET  | `/api/acupoint/lookup?name=X` | - | 按穴位名反查 bodyid |
| POST | `/api/acupoint/identify` | optional | `{ image_url, point_name, bodyid }` → 坐标 + 标注图 |
| POST | `/api/acupoint/save-marked-image` | optional | 保存前端合成的标注图（base64 或 URL） |
| GET  | `/api/acupoint/records` | ✓ | 我的穴位识别历史 |

### 8.8 体质辨识（`/api/constitution`）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| GET  | `/api/constitution/questionnaire?scale=full\|short` | ✓ | 题库 + 题数统计 + 9 型元数据 |
| POST | `/api/constitution/assess` | ✓ | 非流式评测 `{ scaleType, items: { id: 1..5 } }` |
| POST | `/api/constitution/assess-stream` | ✓ | SSE 流式；事件：`scoring` → `chunk` → `done` |

### 8.9 健康报告（`/api/reports`）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| GET  | `/api/reports` | ✓ | 我的报告列表（按更新时间倒序） |
| GET  | `/api/reports/:id` | ✓ | 报告详情（含 raw_data JSON） |
| POST | `/api/reports` | ✓ | 创建 `{ type, title, summary, content, score, raw_data }` |
| DELETE | `/api/reports/:id` | ✓ | 删除 |

### 8.10 社区联动（`/api/community`）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| POST | `/api/community/nearby` | ✓ | `{ lat, lng, cityName?, radius? }` → `{ hospitals, communityCenters, overall, aiProvided }` |

### 8.11 商城（无前缀，直接挂在 `/api/products` / `/api/orders`）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| GET  | `/api/products` | - | 所有商品列表（按 id） |
| GET  | `/api/orders` | ✓ | 我的订单；自动按 created_at 决定阶段（paid→packing→shipping→delivered） |
| POST | `/api/orders` | ✓ | 下单 `{ receiver_name, phone, address, items, total_price }` |
| DELETE | `/api/orders/:id` | ✓ | 删除订单 |

### 8.12 语音识别（`/api/asr`）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| POST | `/api/asr/recognize` | ✓ | multipart 字段 `audio`（16kHz WAV） → `{ text }` |

### 8.13 图床（无 `/api` 前缀，为保持向后兼容）

| 方法 | 路径 | 认证 | 说明 |
|---|---|---|---|
| GET  | `/files/:filename` | - | 直接访问文件（静态+`Content-Type`） |
| POST | `/upload-file` | - | multipart `file` → `{ success, url, filename }` |
| POST | `/upload-from-url` | - | `{ url }` → 把外网图拉取后落盘 |
| GET  | `/api/files` | - | 所有文件列表 |
| POST | `/delete` | - | `{ filename }` 删除 |
| POST | `/rename` | - | `{ filename, newname }` 重命名 |
| GET  | `/health` | - | `{ status: 'ok' }` 健康检查 |

---

## 九、前端页面与模块

### 9.1 页面一览

| 路径 | 文件 | 对应 JS | 主要依赖的 API |
|---|---|---|---|
| `/` | `index.html` | `home.js` | `/api/ai/home-analysis`, `/api/reports` |
| `/login` | `login.html` | — | `/api/auth/*` |
| `/profile` | `profile.html` | `profile.js` | `/api/auth/me`, `/api/auth/profile` |
| `/chat` | `chat.html` | `chat-enhanced.js` + `chat.js` | `/api/qianfan/*`, `/api/webai/chat-stream`, `/api/asr/*` |
| `/tongue` | `tongue.html` | （内联） | `/api/tongue/*`, `/upload-file` |
| `/acupoint` | `acupoint.html` | （内联） | `/api/acupoint/*` |
| `/community` | `community.html` | （内联，含本地缓存） | `/api/community/nearby` |
| `/reports` | `reports.html` | `reports.js` | `/api/reports/*` |
| `/shop` | `shop.html` | `shop-enhanced.js` | `/api/products`, `/api/orders` |
| `/admin/files` | `admin-files.html` | — | `/api/files`, `/upload-file`, `/delete`, `/rename` |

### 9.2 `app.js` 暴露的公共工具

```js
Auth.isLoggedIn()      Auth.getToken()      Auth.getUser()
Auth.requireLogin()    Auth.logout()

apiGet(url)            apiPost(url, body)   apiPut(url, body)
apiDelete(url)                              // 自动加 Bearer Token

CartStore.add(p)       CartStore.get()      CartStore.count()
CartStore.updateQty(id, n)                  CartStore.remove(id)
CartStore.total()      CartStore.clear()    // localStorage 持久化

renderNav(active)      renderMarkdown(str)  escapeHtml(str)
fmtDate(dt)            toast(msg)
```

### 9.3 `chat-enhanced.js` 结构

- `state` — 全局状态（当前会话、流式气泡引用、待发图片等）
- `renderUserMessage` / `renderAssistantMessage` / `renderSystemMessage`
- `renderAssistantExtras` — **气泡"附加内容"渲染**：后续追问气泡、商品卡、严重症状救急卡。**商品卡是"展示+点击加购物车"而不是自动加**，详见代码注释
- `sendMessage` / `sendMessageStream` — 聊天主流程
- `sendFollowUpViaWebAi` — 追问气泡走 webai（SSE）
- `speakText` / ASR 录音按钮等

### 9.4 `share.js` — 原生 canvas 海报分享

不使用 html2canvas，避免 CORS 和字体问题。所有元素（背景、卡片、二维码）直接画在 canvas 上，然后 `toBlob` → 原生分享 API / 下载。

**注意**：健康报告分享走的**不是** `share.js`，而是 `reports.js` 的"分享全文纯文本"逻辑（优先 `navigator.share`，回退剪贴板）。其他场景（体质卡、运动打卡）仍用 canvas 海报。

---

## 十、运维脚本与工具

| 脚本 | 语言 | 作用 | 何时用 |
|---|---|---|---|
| `scripts/deploy.py` | Python | **核心部署脚本**（paramiko 自动化 probe/upload/install） | 每次部署 |
| `scripts/verify.py` | Python | 部署后 HTTP 验证：主页 / `/api/products` / 图片可访问 | 部署后必跑 |
| `scripts/probe2.py` | Python | 服务器环境深度探测 | 首次接手 / 诊断问题 |
| `scripts/parse-xlsx.js` | Node | 解析 WPS 嵌入图片的 xlsx（`健康商品.xlsx`） | 商品更新时 |
| `scripts/import-products.js` | Node | 服务器端商品入库（清空 + 插入 + 图片复制到 files/） | 由 `deploy.py install` 自动触发 |
| `scripts/install-key-once-v2.ps1` | PowerShell | 一次性把本机 SSH 公钥装到服务器 | 首次接手 |
| `scripts/clean-legacy-products.py` | Python | 删除旧 db.js 硬编码塞进去的商品 | 一次性（已跑过） |
| `scripts/fix-db-patch.py` | Python | 上传 db.js + 重启 | 一次性（已跑过） |
| `scripts/deploy-fix6.ps1` | PowerShell | 非 paramiko 备选（要手动输 3 次密码） | 备用 |

### 10.1 典型脚本模板（新建一次性服务器操作）

```python
# scripts/your-operation.py
import os, paramiko

PW = os.environ.get("DEPLOY_SSH_PW") or ""
c = paramiko.SSHClient()
c.set_missing_host_key_policy(paramiko.AutoAddPolicy())
kw = dict(hostname="119.23.74.244", username="root", timeout=15)
c.connect(password=PW, **kw) if PW else c.connect(**kw)

# 一切修改文件/运行 node 都要 sudo -u admin
_, o, _ = c.exec_command(
    "sudo -u admin bash -c 'cd /home/admin/picui-proxy && <your cmd>'"
)
print(o.read().decode())

c.close()
```

---

## 十一、常见问题 / 踩坑

### 11.1 pm2 里看不到进程？

```bash
pm2 list      # root 看到的是 root 的 pm2 实例，是空的！
sudo -u admin pm2 list   # 这才是真正跑服务的 pm2
```

### 11.2 修改文件后服务没变

1. 文件属主变了：`ls -la <file>` 若是 `root root` → `sudo chown admin:admin <file>`
2. 没重启：`sudo -u admin pm2 restart all --update-env`
3. 改到错的 `.env`：服务器唯一生效的是 `/home/admin/picui-proxy/.env`

### 11.3 SQLite 操作

服务器**没装 sqlite3 CLI**。只能：
```bash
sudo -u admin bash -c 'cd /home/admin/picui-proxy && node -e "const db=require(\"./db\"); /*...*/"'
```
或把 SQL 脚本写成 `.js` 文件 scp 上去后用 node 跑。

### 11.4 部署脚本"卡住"不动

大概率在等密码输入，但终端无交互 tty：
- 检查：`ssh -o BatchMode=yes root@119.23.74.244 echo OK`
- 如果报 `Permission denied` → 跑 `install-key-once-v2.ps1` 配密钥
- 如果通 → 问题在其他地方

### 11.5 购物车数量"凭空变多"

**历史 bug**，根因：`chat-enhanced.js` 的 `renderAssistantExtras` 曾在渲染商品卡时自动 `CartStore.add`，每次用户切换历史会话或刷新都会再加一次。

**已修**：现在商品卡只展示，点击"加入购物车"按钮才入 cart。若回退，检查 `@c:\Users\Administrator\Documents\Project\图床api测试\public\assets\chat-enhanced.js:536-570` 是否被改回自动加。

### 11.6 千帆工作流"反复开场白"

千帆 Agent 的 chatflow_interrupt 事件**必须**在下一轮请求体里带回 `action.action_type="resume"` + `parameters.interrupt_event={id,type}`。

本项目在 `conversations.pending_interrupt` 字段存 JSON，`services/qianfan.js` 发请求前会自动恢复。详见 `docs/qianfan-chatflow-interrupt.md`。

### 11.7 `better-sqlite3` 原生模块报错

服务器重启 Node 版本后需要 `npm rebuild better-sqlite3`。若 Windows 本地装不上：
```powershell
npm install better-sqlite3 --build-from-source=false
```

### 11.8 图片 404

图片 URL 规则：`/files/<时间戳>_<md5前12位>.<ext>`。`import-products.js` 用 `saveFile` 生成文件名，若 owner 是 root 但 pm2 进程是 admin → node 可能读不到。**必须以 admin 身份跑 import 脚本**。

### 11.9 阿里云安全组

端口 **3000**（服务）、**22**（SSH）必须在阿里云控制台 ECS → 安全组入站规则里放行。

---

## 十二、外部依赖密钥

所有密钥都由运营方申请，当前生产已配置。续期/替换时需对应修改服务器 `.env`。

### 12.1 百度千帆
- 控制台：<https://qianfan.cloud.baidu.com/>
- 取：AppID 和 `Bearer` Token
- 文档：`docs/qianfan-chatflow-interrupt.md`, `docs/workflow-end-api.md`, `docs/qianfan-file-upload.md`

### 12.2 老张 API（OpenAI 兼容）
- 控制台：<https://api.laozhang.ai/>
- 取：API Key
- 当前用模型：`gpt-5.2` / `qwen-plus`

### 12.3 舌诊 API
- 服务商：`ai.poweruser.link`
- 取：AppKey + AppSecret

### 12.4 百度 ASR
- 控制台：<https://console.bce.baidu.com/ai/#/ai/speech/app/list>
- 取：API Key + Secret Key

### 12.5 百度地图
- 控制台：<https://lbsyun.baidu.com/apiconsole/key>
- 应用类型勾选"服务端"
- 用到：Place API v2 search

---

## 十三、快速接手 Checklist

新同学按这个顺序来：

- [ ] 从 git 拉项目
- [ ] 读本文件 §1~§2 了解产品和技术栈
- [ ] 本地 `npm install && npm run dev`，`.env` 至少配千帆 + WebAI
- [ ] 用浏览器跑一遍 `/chat` / `/reports` / `/shop` / `/community` 主流程
- [ ] 读 `server.js`（30 行）+ `db.js`（130 行）看数据模型
- [ ] 挑一个熟悉的模块（建议 `shop.js`，最简单）把 route→service→前端顺一遍
- [ ] `python -m pip install paramiko`
- [ ] 跑 `scripts\install-key-once-v2.ps1` 配 SSH 免密
- [ ] `python scripts\deploy.py probe` 验证能连服务器
- [ ] 改一小行注释，走一次完整部署流程试水
- [ ] `python scripts\verify.py` 确认线上可用

遇到本文没覆盖的情况，先看：
1. 对应 route/service 文件开头的大段 JSDoc 注释（几乎每个文件都有）
2. `docs/` 下的千帆相关专题文档
3. `.env.example` 的注释

---

*本文档维护约定：每次新增/重构重要模块时同步更新本文件，尤其是 §3 目录结构、§4 数据库表、§8 API 列表。*
