浏览代码

docs: add ajyy feedback platform implementation plan

Break the approved design into 12 executable tasks covering backend, web-admin, and mobile H5.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 2 月之前
父节点
当前提交
f4aaf64177
共有 1 个文件被更改,包括 769 次插入 和 0 次删除
  1. 769 0
      docs/superpowers/plans/2026-09-06-ajyy-feedback-platform.md

+ 769 - 0
docs/superpowers/plans/2026-09-06-ajyy-feedback-platform.md

@@ -0,0 +1,769 @@
+# 安建医院纪检扫码反馈系统 Implementation Plan
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 交付可验收的纪检扫码反馈平台:微信 H5 提交反馈/建言 + Web 管理端台账办理与配置 + FastAPI 后端(含角色菜单授权)。
+
+**Architecture:** 单仓三端——`backend`(FastAPI)提供 `/api/public/*` 与 `/api/web/*`;`mobile-wx`(uni-app→H5)只调公开接口;`web-admin`(Vue3+Element Plus)JWT 登录后按 `sys_role_menu` 动态菜单。MySQL 库 `ajyy`,MinIO 独立 bucket `ajyy`。
+
+**Tech Stack:** Python 3.9+ / FastAPI 0.109 / SQLAlchemy 2 / PyMySQL / bcrypt / python-jose / minio;Vue 3.5 + Vite + Element Plus + Pinia + vue-router(hash) + Axios;uni-app Vue3 H5。
+
+**Spec:** `docs/superpowers/specs/2026-09-06-ajyy-feedback-platform-design.md`
+
+## Global Constraints
+
+- 统一响应:`{ code: 200, message, data, timestamp }`(毫秒时间戳)
+- 单院、无 `tenant_no`;软删除 `del_flag`(`0` 正常 / `1` 删除)
+- 主键 `id`:UUID 字符串;附件 URL 列表:JSON 数组
+- 反馈状态:`待处理` | `处理中` | `已办结`;建言状态:`待查阅` | `已采纳` | `已回复`
+- 品牌色移动端 `#a51f23`;视觉/字段对照 `原型图/index.html`、`原型图/admin.html`
+- 本期不上小程序;不做公开限流/脱敏/RSA/API 加密
+- 参考实现可看 `../shanxia2.0/backend` 等,但**禁止拷贝** CRM/报销等业务模块
+- 测试默认:pytest + FastAPI `TestClient`;本地可用 SQLite(`sqlite+pysqlite:///:memory:`)跑单测,联调再用 MySQL `ajyy`
+
+---
+
+## File Structure (锁死职责)
+
+```
+backend/
+  requirements.txt
+  .env.example
+  pytest.ini
+  app/
+    main.py                 # 挂载路由、CORS、启动种子
+    core/
+      config.py             # Settings
+      database.py           # engine, SessionLocal, get_db
+      response.py           # success_response / error_response
+      security.py           # hash/verify password, create/decode JWT
+      deps.py               # get_current_user, require_menu(*codes)
+      storage.py            # MinIO 上传(STORAGE_TYPE=local|minio)
+    models/
+      __init__.py
+      system.py             # SysUser, SysRole, SysMenu, SysRoleMenu, OperLog
+      business.py           # Feedback, Suggestion, HotlineConfig, QrcodeConfig, Attachment
+    schemas/
+      auth.py
+      system.py
+      business.py
+      common.py             # PageResult
+    crud/
+      user.py, role.py, menu.py, feedback.py, suggestion.py, config.py, attachment.py, oper_log.py
+    services/
+      auth_service.py
+      feedback_service.py
+      suggestion_service.py
+      config_service.py
+      dashboard_service.py
+      seed.py               # 幂等种子
+    api/
+      public.py             # /api/public/*
+      auth.py               # /api/web/auth/*
+      admin.py              # /api/web/admin/*
+  tests/
+    conftest.py
+    test_health.py
+    test_auth.py
+    test_public_feedback.py
+    test_admin_feedback.py
+    test_role_menus.py
+  docs/sql/
+    001_init.sql            # 可选:MySQL DDL 对照
+
+web-admin/
+  package.json
+  vite.config.ts
+  .env.development
+  index.html
+  src/
+    main.ts
+    App.vue
+    styles/index.css
+    utils/http.ts
+    api/auth.ts, admin.ts
+    stores/user.ts
+    router/index.ts
+    layouts/AdminLayout.vue
+    views/login/index.vue
+    views/dashboard/index.vue
+    views/feedback/index.vue
+    views/suggestion/index.vue
+    views/hotline/index.vue
+    views/qrcode/index.vue
+    views/user/index.vue
+    views/role/index.vue
+    views/audit/index.vue
+
+mobile-wx/
+  pages.json                # 注册全部 H5 页
+  config/api.js
+  utils/request.js
+  utils/upload.js
+  api/public.js
+  pages/home/index.vue
+  pages/notice/index.vue
+  pages/feedback/index.vue
+  pages/suggest/index.vue
+  pages/hotline/index.vue
+  (分享二维码可用 home 弹层,不必单独路由)
+  static/                   # logo 等,可从 原型图/assets 拷贝
+```
+
+---
+
+### Task 1: Backend 骨架 + 健康检查
+
+**Files:**
+- Create: `backend/requirements.txt`
+- Create: `backend/pytest.ini`
+- Create: `backend/app/__init__.py`
+- Create: `backend/app/main.py`
+- Create: `backend/app/core/config.py`
+- Create: `backend/app/core/response.py`
+- Create: `backend/tests/conftest.py`
+- Create: `backend/tests/test_health.py`
+
+**Interfaces:**
+- Produces: `success_response(data=None, message="success") -> dict`;`Settings` via `get_settings()`;`GET /api/health` → `{code:200, data:{status:"ok"}}`
+
+- [ ] **Step 1: Write failing health test**
+
+```python
+# backend/tests/test_health.py
+from fastapi.testclient import TestClient
+from app.main import app
+
+client = TestClient(app)
+
+def test_health():
+    r = client.get("/api/health")
+    assert r.status_code == 200
+    body = r.json()
+    assert body["code"] == 200
+    assert body["data"]["status"] == "ok"
+    assert "timestamp" in body
+```
+
+- [ ] **Step 2: Run test — expect fail (app missing)**
+
+Run: `cd backend && python -m pytest tests/test_health.py -v`  
+Expected: import/collection error or 404
+
+- [ ] **Step 3: Implement minimal app**
+
+`requirements.txt`(精简):
+
+```text
+fastapi==0.109.0
+uvicorn[standard]==0.27.0
+sqlalchemy==2.0.25
+pymysql==1.1.0
+pydantic==2.5.3
+pydantic-settings==2.1.0
+python-jose[cryptography]==3.3.0
+bcrypt==4.0.1
+python-multipart==0.0.6
+python-dotenv==1.0.0
+minio==7.2.20
+httpx==0.26.0
+pytest==8.0.0
+```
+
+`app/core/response.py`:照搬 shanxia 的 `success_response` / `error_response`(含毫秒 `timestamp`)。
+
+`app/core/config.py`:
+
+```python
+from functools import lru_cache
+from pydantic_settings import BaseSettings
+
+class Settings(BaseSettings):
+    APP_NAME: str = "ajyy"
+    API_V1_PREFIX: str = "/api"
+    DATABASE_URL: str = "sqlite+pysqlite:///:memory:"
+    SECRET_KEY: str = "change-me-in-production"
+    ACCESS_TOKEN_EXPIRE_MINUTES: int = 60 * 12
+    ALGORITHM: str = "HS256"
+    INIT_ADMIN_USERNAME: str = "admin"
+    INIT_ADMIN_PASSWORD: str = "Admin@123"
+    STORAGE_TYPE: str = "local"  # local | minio
+    LOCAL_STORAGE_DIR: str = "./uploads"
+    MINIO_ENDPOINT: str = "127.0.0.1:9000"
+    MINIO_ACCESS_KEY: str = ""
+    MINIO_SECRET_KEY: str = ""
+    MINIO_BUCKET: str = "ajyy"
+    MINIO_SECURE: bool = False
+    MINIO_BASE_URL: str = ""
+    CORS_ORIGINS: str = "*"
+
+    class Config:
+        env_file = ".env"
+        extra = "ignore"
+
+@lru_cache
+def get_settings() -> Settings:
+    return Settings()
+```
+
+`app/main.py`:创建 FastAPI,CORS,`GET /api/health` 返回 `success_response({"status": "ok"})`。
+
+- [ ] **Step 4: Install deps + pass test**
+
+Run: `cd backend && pip install -r requirements.txt && python -m pytest tests/test_health.py -v`  
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend
+git commit -m "feat(backend): scaffold FastAPI app with health check"
+```
+
+---
+
+### Task 2: 数据库模型 + 建表 + 幂等种子
+
+**Files:**
+- Create: `backend/app/core/database.py`
+- Create: `backend/app/models/system.py`
+- Create: `backend/app/models/business.py`
+- Create: `backend/app/models/__init__.py`
+- Create: `backend/app/services/seed.py`
+- Modify: `backend/app/main.py`(startup 建表+种子)
+- Modify: `backend/tests/conftest.py`
+- Create: `backend/tests/test_seed.py`
+- Create: `backend/docs/sql/001_init.sql`(MySQL 对照 DDL,可选但推荐)
+
+**Interfaces:**
+- Produces: ORM 模型名见上;`init_db()` 建表;`seed_if_empty(db)` 幂等写入 3 角色、菜单树、admin 用户、热线/二维码各 1 行
+- Menu codes(固定):`dashboard`,`feedback`,`suggestion`,`hotline`,`qrcode`,`user`,`role`,`audit`;按钮:`feedback:handle`,`suggestion:handle`,`hotline:write`,`qrcode:write`,`user:manage`,`role:grant`
+
+- [ ] **Step 1: Write seed test**
+
+```python
+# backend/tests/test_seed.py
+def test_seed_creates_admin_and_roles(db_session):
+    from app.services.seed import seed_if_empty
+    from app.models.system import SysUser, SysRole, SysMenu, SysRoleMenu
+    seed_if_empty(db_session)
+    seed_if_empty(db_session)  # idempotent
+    assert db_session.query(SysRole).count() == 3
+    assert db_session.query(SysUser).filter_by(username="admin").count() == 1
+    assert db_session.query(SysMenu).count() >= 8
+    assert db_session.query(SysRoleMenu).count() >= 1
+```
+
+- [ ] **Step 2: Run — expect fail**
+
+Run: `cd backend && python -m pytest tests/test_seed.py -v`  
+Expected: FAIL import/missing
+
+- [ ] **Step 3: Implement models + database + seed**
+
+`database.py`:`create_engine(settings.DATABASE_URL, ...)`;SQLite 时 `connect_args={"check_same_thread": False}`;`SessionLocal`;`get_db`;`Base = declarative_base()`;`init_db()` → `Base.metadata.create_all`。
+
+模型字段(必须):
+
+`SysUser`: id, username, password_hash, display_name, role_id, status(`0`启用/`1`停用), del_flag, create_time, update_time, create_by, update_by  
+
+`SysRole`: id, role_code, role_name, remark, del_flag, ...  
+
+`SysMenu`: id, parent_id, menu_code, menu_name, path, menu_type(`M`目录/`C`菜单/`F`按钮), sort, visible, del_flag, ...  
+
+`SysRoleMenu`: id, role_id, menu_id  
+
+`Feedback`: id, biz_no, fb_type(`投诉`|`表扬`), target_name, target_dept, target_post, content, reporter_name, reporter_phone, attachments(JSON), status, handle_remark, del_flag, timestamps  
+
+`Suggestion`: id, biz_no, content, contact_name, contact_unit, contact_post, contact_phone, attachments(JSON), status, handle_remark, del_flag, timestamps  
+
+`HotlineConfig`: id, phone, email, address, work_time, ...  
+
+`QrcodeConfig`: id, land_url, title, image_url, ...  
+
+`Attachment`: id, file_name, object_key, url, size, biz_type, biz_id, ...  
+
+`OperLog`: id, user_id, username, module, action, detail, ip, create_time  
+
+种子授权矩阵:
+
+| role_code | 菜单/按钮 |
+|---|---|
+| admin | 全部 |
+| handler | dashboard, feedback, suggestion, feedback:handle, suggestion:handle |
+| viewer | dashboard, feedback, suggestion(无 handle) |
+
+密码用 bcrypt hash(`passlib` 或直接 `bcrypt.hashpw`)。
+
+- [ ] **Step 4: Pass tests**
+
+Run: `cd backend && python -m pytest tests/test_seed.py -v`  
+Expected: PASS
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add backend
+git commit -m "feat(backend): add models, init_db, and idempotent seed data"
+```
+
+---
+
+### Task 3: 登录鉴权 JWT + current-user + menus
+
+**Files:**
+- Create: `backend/app/core/security.py`
+- Create: `backend/app/core/deps.py`
+- Create: `backend/app/schemas/auth.py`
+- Create: `backend/app/crud/user.py`
+- Create: `backend/app/crud/menu.py`
+- Create: `backend/app/services/auth_service.py`
+- Create: `backend/app/api/auth.py`
+- Modify: `backend/app/main.py`(include router)
+- Create: `backend/tests/test_auth.py`
+- Modify: `backend/tests/conftest.py`(提供已种子的 client)
+
+**Interfaces:**
+- Produces:
+  - `POST /api/web/auth/login` body `{username, password}` → `{token, tokenType:"Bearer"}`
+  - `GET /api/web/auth/current-user` Bearer → `{id, username, displayName, roleCode, roleName}`
+  - `GET /api/web/auth/menus` → 树形菜单列表(仅 `menu_type` in M/C,按 sort)
+  - `POST /api/web/auth/logout` → success
+  - `require_menu(*codes)`:当前用户角色若无任一 code → 403
+  - `get_current_user`:无效/过期 token → 401
+
+- [ ] **Step 1: Write auth tests**
+
+```python
+def test_login_and_menus(client):
+    r = client.post("/api/web/auth/login", json={"username": "admin", "password": "Admin@123"})
+    assert r.status_code == 200
+    token = r.json()["data"]["token"]
+    me = client.get("/api/web/auth/current-user", headers={"Authorization": f"Bearer {token}"})
+    assert me.json()["data"]["roleCode"] == "admin"
+    menus = client.get("/api/web/auth/menus", headers={"Authorization": f"Bearer {token}"})
+    codes = {m["menuCode"] for m in _flatten(menus.json()["data"])}
+    assert "dashboard" in codes
+    assert "role" in codes
+
+def test_login_wrong_password(client):
+    r = client.post("/api/web/auth/login", json={"username": "admin", "password": "bad"})
+    assert r.status_code == 200
+    assert r.json()["code"] == 401  # 或 HTTP 401;全项目统一一种,推荐 HTTP 401
+```
+
+(选定:**HTTP 状态码** 401/403,body 仍可用 `error_response`。)
+
+- [ ] **Step 2: Run — expect fail**
+
+- [ ] **Step 3: Implement security + auth routes**
+
+`create_access_token(sub: str, extra: dict) -> str`;`decode_token`;OAuth2 bearer。  
+登录失败写 `oper_log` 可选;成功登录写 `oper_log`(module=`auth`, action=`login`)。
+
+JSON 字段对外 **camelCase**(Pydantic `alias_generator` 或手动 dict)。
+
+- [ ] **Step 4: Pass tests + commit**
+
+```bash
+git add backend
+git commit -m "feat(backend): add JWT login, current-user, and menus API"
+```
+
+---
+
+### Task 4: 附件存储 + 公开上传
+
+**Files:**
+- Create: `backend/app/core/storage.py`
+- Create: `backend/app/crud/attachment.py`
+- Create: `backend/app/api/public.py`(先只 upload + 空路由壳)
+- Create: `backend/tests/test_upload.py`
+- Create: `backend/.env.example`
+
+**Interfaces:**
+- Produces: `storage.upload_file(content: bytes, filename: str, folder: str) -> dict` 含 `url`, `objectKey`
+- `POST /api/public/attachment/upload` multipart field `file`,可选 `folder` → `{id, url, fileName, size}`
+- `STORAGE_TYPE=local` 时落到 `LOCAL_STORAGE_DIR`,URL 形如 `/uploads/...`;并用 `StaticFiles` 挂载便于本地预览
+
+- [ ] **Step 1: Write upload test (local storage)**
+
+```python
+def test_public_upload(client, tmp_path, monkeypatch):
+    monkeypatch.setenv("STORAGE_TYPE", "local")
+    monkeypatch.setenv("LOCAL_STORAGE_DIR", str(tmp_path))
+    # 重新 load settings / app 若需要
+    files = {"file": ("a.png", b"fake-png-bytes", "image/png")}
+    r = client.post("/api/public/attachment/upload", files=files)
+    assert r.status_code == 200
+    data = r.json()["data"]
+    assert data["url"]
+    assert data["id"]
+```
+
+- [ ] **Step 2–4: Implement MinIO 分支(endpoint/bucket 来自 settings;bucket 不存在则创建)+ local;pass test;commit**
+
+```bash
+git commit -m "feat(backend): add local/minio storage and public upload API"
+```
+
+---
+
+### Task 5: 公开反馈 / 建言 / 热线 / 二维码
+
+**Files:**
+- Create: `backend/app/schemas/business.py`
+- Create: `backend/app/crud/feedback.py`
+- Create: `backend/app/crud/suggestion.py`
+- Create: `backend/app/crud/config.py`
+- Create: `backend/app/services/feedback_service.py`
+- Create: `backend/app/services/suggestion_service.py`
+- Create: `backend/app/services/config_service.py`
+- Modify: `backend/app/api/public.py`
+- Create: `backend/tests/test_public_feedback.py`
+
+**Interfaces:**
+- `GET /api/public/hotline` → 热线对象
+- `GET /api/public/qrcode` → 二维码配置
+- `POST /api/public/feedback` body:
+  `{fbType, targetName?, targetDept?, targetPost?, content, reporterName?, reporterPhone?, attachmentUrls?: string[]}`
+  → `{id, bizNo}`;默认 status=`待处理`;`biz_no`=`FK`+`YYYYMMDD`+3 位日序号
+- `POST /api/public/suggestion` 类似;`biz_no` 前缀 `JY`;status=`待查阅`
+- content 空 → 400
+
+- [ ] **Step 1: Tests**
+
+```python
+def test_submit_feedback(client):
+    r = client.post("/api/public/feedback", json={
+        "fbType": "投诉", "content": "测试投诉内容"
+    })
+    assert r.status_code == 200
+    assert r.json()["data"]["bizNo"].startswith("FK")
+
+def test_feedback_requires_content(client):
+    r = client.post("/api/public/feedback", json={"fbType": "投诉", "content": ""})
+    assert r.status_code == 400
+
+def test_get_hotline(client):
+    assert client.get("/api/public/hotline").json()["code"] == 200
+```
+
+- [ ] **Step 2–4: Implement + pass + commit**
+
+```bash
+git commit -m "feat(backend): add public feedback, suggestion, and config APIs"
+```
+
+---
+
+### Task 6: 管理端台账、办理、工作台、配置写入
+
+**Files:**
+- Create: `backend/app/api/admin.py`
+- Create: `backend/app/services/dashboard_service.py`
+- Create: `backend/app/crud/oper_log.py`
+- Create: `backend/tests/test_admin_feedback.py`
+
+**Interfaces:**
+- `GET /api/web/admin/dashboard` → `{feedbackTotal, complaintPending, suggestionTotal, weekNew, recentFeedback:[]}`
+- `GET /api/web/admin/feedbacks?status=&fbType=&keyword=&page=1&pageSize=10` → `{list, total, page, pageSize}`
+- `GET /api/web/admin/feedbacks/{id}`
+- `PUT /api/web/admin/feedbacks/{id}/status` body `{status, handleRemark?}` — 需 `feedback:handle`;非法状态迁移 → 400;写 oper_log
+- 建言对称:`/suggestions`,权限 `suggestion:handle`
+- `GET/PUT /api/web/admin/hotline` — PUT 需 `hotline:write`
+- `GET/PUT /api/web/admin/qrcode` — PUT 需 `qrcode:write`
+- `POST /api/web/admin/attachment/upload` — 需登录(任意已登录角色)
+
+状态迁移允许:
+
+- 反馈:`待处理→处理中|已办结`;`处理中→已办结`;同状态仅更新备注允许
+- 建言:`待查阅→已采纳|已回复`;已采纳/已回复之间互转允许
+
+- [ ] **Step 1: Tests(viewer 无办理权限;handler 可办理;admin 可改配置)**
+
+用种子额外创建 `handler`/`viewer` 测试用户,或在 test 里直接插用户。
+
+```python
+def test_viewer_cannot_handle(client, viewer_headers):
+    # 先造一条反馈
+    fb = client.post("/api/public/feedback", json={"fbType": "投诉", "content": "x"*4}).json()["data"]
+    r = client.put(f"/api/web/admin/feedbacks/{fb['id']}/status",
+                   headers=viewer_headers,
+                   json={"status": "处理中"})
+    assert r.status_code == 403
+```
+
+- [ ] **Step 2–4: Implement + pass + commit**
+
+```bash
+git commit -m "feat(backend): add admin dashboard, ledger, handle, and config APIs"
+```
+
+---
+
+### Task 7: 用户管理 + 角色授权 + 审计列表
+
+**Files:**
+- Create: `backend/app/schemas/system.py`
+- Create: `backend/app/crud/role.py`
+- Modify: `backend/app/api/admin.py`
+- Create: `backend/tests/test_role_menus.py`
+
+**Interfaces:**
+- `GET /api/web/admin/users` — 需 `user:manage`
+- `POST /api/web/admin/users` body `{username, password, displayName, roleId}` 
+- `PUT /api/web/admin/users/{id}` body `{displayName?, roleId?, status?}`
+- `PUT /api/web/admin/users/{id}/reset-password` body `{password}`
+- `GET /api/web/admin/roles`
+- `GET /api/web/admin/menus/tree` — 全量菜单树(授权页用)
+- `GET /api/web/admin/roles/{id}/menus` → `menuIds: string[]`
+- `PUT /api/web/admin/roles/{id}/menus` body `{menuIds: string[]}` — 需 `role:grant`;全量替换 `sys_role_menu`
+- `GET /api/web/admin/oper-logs?page&pageSize` — 需登录且菜单 `audit`(admin 种子有)
+
+- [ ] **Step 1: Test role grant changes menus for user**
+
+```python
+def test_role_grant_updates_menus(client, admin_headers, db_session):
+    roles = client.get("/api/web/admin/roles", headers=admin_headers).json()["data"]
+    viewer = next(r for r in roles if r["roleCode"] == "viewer")
+    # 去掉 suggestion 菜单后再查 viewer 用户 menus…
+```
+
+- [ ] **Step 2–4: Implement + pass + commit**
+
+```bash
+git commit -m "feat(backend): add user admin, role menu grant, and oper-log list"
+```
+
+---
+
+### Task 8: web-admin 工程骨架 + 登录 + 动态菜单布局
+
+**Files:**
+- Create: `web-admin/` 全套 Vite Vue3 TS 工程(可用 `npm create vite@latest web-admin -- --template vue-ts` 后加依赖)
+- Create: 见 File Structure 中 `web-admin/src/**` 登录/布局/路由/store/http
+
+**Interfaces:**
+- Consumes: Task 3 auth APIs
+- Produces: 可 `pnpm/npm run dev`;登录后按 menus 渲染侧栏;无 token 跳 `/login`
+
+- [ ] **Step 1: Scaffold**
+
+```bash
+cd /Users/wuzilong/work/00code/ajyy
+npm create vite@latest web-admin -- --template vue-ts
+cd web-admin && npm i && npm i element-plus vue-router@4 pinia axios @element-plus/icons-vue
+```
+
+`.env.development`:
+
+```text
+VITE_API_BASE_URL=/api
+VITE_API_PROXY_TARGET=http://127.0.0.1:8000
+```
+
+`vite.config.ts`:proxy `/api` → `VITE_API_PROXY_TARGET`;hash history。
+
+- [ ] **Step 2: http + auth store**
+
+`utils/http.ts`:axios 实例,请求头 `Authorization: Bearer ${token}`;`code!==200` 时 ElMessage;401 清 token 跳登录。
+
+`stores/user.ts`:`token`, `user`, `menus`, `login`, `fetchProfile`, `logout`;`hasPerm(code: string)` 递归 menus + 按钮(登录后可另存 `permCodes: string[]`——建议 `current-user` 或 menus 接口同时返回按钮 codes;**实现约定**:`GET /menus` 返回含按钮的扁平 `permCodes` 放在响应 `data.permCodes`,树在 `data.tree`)。
+
+若 Task 3 的 menus 仅返回树,则在本 Task **回头小改后端**:
+
+```json
+{ "tree": [...], "permCodes": ["feedback:handle", ...] }
+```
+
+并补一条后端测试断言 `permCodes`。
+
+- [ ] **Step 3: Login page + AdminLayout + router guard**
+
+路由表:静态 `/login`;业务路由 `meta.menuCode` 与菜单 path 对齐:
+
+| path | menuCode | component |
+|---|---|---|
+| `/dashboard` | dashboard | dashboard |
+| `/feedback` | feedback | feedback |
+| `/suggestion` | suggestion | suggestion |
+| `/hotline` | hotline | hotline |
+| `/qrcode` | qrcode | qrcode |
+| `/user` | user | user |
+| `/role` | role | role |
+| `/audit` | audit | audit |
+
+守卫:无 token → login;有 token 无对应 menuCode → `/dashboard` 或 403 页。
+
+侧栏主色可用纪检红接近色(管理端不必强制 `#a51f23`,但 logo/标题用「安徽建工集团医院纪检室」)。
+
+- [ ] **Step 4: Manual verify**
+
+Run backend: `uvicorn app.main:app --reload --port 8000`  
+Run admin: `npm run dev`  
+登录 `admin` / `Admin@123`,侧栏应出现全部菜单。
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add web-admin
+git commit -m "feat(web-admin): scaffold app with login and dynamic menus"
+```
+
+---
+
+### Task 9: web-admin 业务页(工作台/反馈/建言/热线/二维码)
+
+**Files:**
+- Create/Modify: `web-admin/src/views/dashboard/index.vue`
+- Create/Modify: `web-admin/src/views/feedback/index.vue`
+- Create/Modify: `web-admin/src/views/suggestion/index.vue`
+- Create/Modify: `web-admin/src/views/hotline/index.vue`
+- Create/Modify: `web-admin/src/views/qrcode/index.vue`
+- Create/Modify: `web-admin/src/api/admin.ts`
+
+**Interfaces:** Consumes Task 6 APIs。字段与交互对照 `原型图/admin.html`。
+
+- [ ] **Step 1: dashboard** — 四统计卡片 + 最近反馈表(编号、类型、摘要、时间、状态)
+
+- [ ] **Step 2: feedback list** — 状态下拉、类型、关键词;表格;详情 Drawer;办理区:状态下拉+备注+保存;无 `feedback:handle` 时隐藏保存
+
+- [ ] **Step 3: suggestion** — 对称实现
+
+- [ ] **Step 4: hotline + qrcode forms** — 保存调用 PUT;二维码支持上传自定义图(admin upload)+ 预览;无写权限则表单 disabled
+
+- [ ] **Step 5: Manual verify with handler/viewer accounts**(Task 7 可先用 SQL/接口创建)+ Commit
+
+```bash
+git commit -m "feat(web-admin): add dashboard, ledger, and config pages"
+```
+
+---
+
+### Task 10: web-admin 用户 / 角色授权 / 审计
+
+**Files:**
+- Create: `web-admin/src/views/user/index.vue`
+- Create: `web-admin/src/views/role/index.vue`
+- Create: `web-admin/src/views/audit/index.vue`
+
+- [ ] **Step 1: 用户列表** — 新建、启停、重置密码、改角色(Element Plus Table + Dialog)
+
+- [ ] **Step 2: 角色授权页** — 左侧角色列表,右侧 `el-tree` `show-checkbox` 绑定全量菜单树;保存 `PUT .../menus`
+
+- [ ] **Step 3: 审计列表** — 简单分页表
+
+- [ ] **Step 4: Verify** — 去掉 viewer 的 suggestion 菜单后,用 viewer 登录侧栏不再出现建言;恢复后出现
+
+- [ ] **Step 5: Commit**
+
+```bash
+git commit -m "feat(web-admin): add user admin, role grant, and audit pages"
+```
+
+---
+
+### Task 11: mobile-wx H5 页面 + 公开 API 对接
+
+**Files:**
+- Modify: `mobile-wx/pages.json`
+- Modify: `mobile-wx/App.vue`(全局样式变量 `--primary:#a51f23`)
+- Create: `mobile-wx/config/api.js`
+- Create: `mobile-wx/utils/request.js`
+- Create: `mobile-wx/utils/upload.js`
+- Create: `mobile-wx/api/public.js`
+- Create: `mobile-wx/pages/home/index.vue`
+- Create: `mobile-wx/pages/notice/index.vue`
+- Create: `mobile-wx/pages/feedback/index.vue`
+- Create: `mobile-wx/pages/suggest/index.vue`
+- Create: `mobile-wx/pages/hotline/index.vue`
+- Remove or repurpose: `mobile-wx/pages/index/index.vue`
+
+**Interfaces:** Consumes Task 4–5 public APIs。UI 对照 `原型图/index.html`(布局/文案/倒计时 5 秒)。
+
+- [ ] **Step 1: request/upload/config**
+
+```javascript
+// config/api.js
+export const BASE_URL = 'http://127.0.0.1:8000/api' // H5 开发可改;真机用局域网 IP
+```
+
+`request.js`:`uni.request` 封装,解析 `code===200`。  
+`upload.js`:`uni.chooseImage` / `uni.chooseFile` + `uni.uploadFile` → `/public/attachment/upload`。
+
+- [ ] **Step 2: pages.json 注册五页**;首页为 `pages/home/index`;navigationBar 红色
+
+- [ ] **Step 3: 实现各页**
+
+- home:四大入口;分享弹层读 `/public/qrcode`(有 `imageUrl` 用图,否则用 `landUrl` + 第三方或 canvas 生成二维码——可用纯图片配置优先,避免强依赖库;若无自定义图则展示 URL 文本 + 提示「请使用后台上传二维码图」或引入轻量 `uQRCode`)
+- notice:须知 + 5s countdown 后可点「已阅读并继续」
+- feedback:类型切换、被反馈人、内容、附件、匿名联系方式、提交成功 Toast/回首页
+- suggest:对称
+- hotline:展示配置,`tel:` 拨打
+
+- [ ] **Step 4: HBuilderX 运行到浏览器 / 微信开发者工具 H5**,走通提交 → 管理端可见
+
+- [ ] **Step 5: Commit**
+
+```bash
+git add mobile-wx
+git commit -m "feat(mobile-wx): add H5 feedback portal pages wired to public API"
+```
+
+---
+
+### Task 12: 环境接通文档 + 联调验收清单
+
+**Files:**
+- Create: `backend/.env.example`(若未完备则补全)
+- Create: `docs/deploy-test.md`
+- Create: `docs/acceptance-checklist.md`
+
+- [ ] **Step 1: 写测试环境说明**
+
+`docs/deploy-test.md` 内容须含:
+
+1. 创建 MySQL 库 `ajyy`(utf8mb4)
+2. `DATABASE_URL=mysql+pymysql://USER:PASS@HOST:3306/ajyy`
+3. MinIO:`STORAGE_TYPE=minio`,bucket `ajyy`,填写与 shanxia 测试环境相同 endpoint/密钥
+4. 启动:`uvicorn app.main:app --host 0.0.0.0 --port 8000`
+5. web-admin build / nginx 反代 `/api`
+6. mobile-wx H5 打包部署路径与二维码落地 URL 配置到 `qrcode_config.land_url`
+
+- [ ] **Step 2: 验收清单** — 逐项对应建设清单 1–17(功能),每项可勾选
+
+- [ ] **Step 3: 在测试环境实际跑通最小路径**(若凭证可用):提交一条反馈 + admin 办理 + 改热线在 H5 可见
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add docs backend/.env.example
+git commit -m "docs: add test deploy guide and acceptance checklist"
+```
+
+---
+
+## Spec coverage self-check
+
+| Spec 项 | Task |
+|---|---|
+| 仓库三端结构 | 1, 8, 11 |
+| MySQL ajyy / MinIO bucket | 2, 4, 12 |
+| sys_user/role/menu/role_menu | 2, 3, 7 |
+| feedback/suggestion/hotline/qrcode/attachment/oper_log | 2, 5, 6, 7 |
+| 公开 API | 4, 5 |
+| JWT 登录 / menus / 角色授权页 | 3, 7, 8, 10 |
+| 管理台账办理/工作台/配置 | 6, 9 |
+| 用户管理 | 7, 10 |
+| mobile 6 页能力 | 11 |
+| 不做限流脱敏/小程序 | 全局约束 + 未列入任务 |
+| 验收/部署说明 | 12 |
+
+## Placeholder / consistency notes (已在正文写死)
+
+- 对外 JSON:**camelCase**
+- 鉴权失败:**HTTP 401/403**
+- menus 响应:`{ tree, permCodes }`(Task 8 若需则回改 Task 3)
+- 附件字段:业务表 JSON 数组;上传返回 `id/url`
+- 默认管理员:`admin` / `Admin@123`(仅 `.env` / 种子,上线必改)