|
|
@@ -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` / 种子,上线必改)
|