Files
android-client/docs/需求规格说明书-V4.0-小程序与后端.md

40 KiB
Raw Blame History

明缘云道场小程序 + 后端 API · 需求规格说明书 V4.0

基于 miniprogram/(微信小程序前端)和 server/(FastAPI 后端)实际代码反向整理。 与 V2.0/V3.0(涵盖 Web 主站 + 学员网站)平行存在;本文档 仅覆盖这两个子系统。

· 适用范围:miniprogram/、server/、server/migrations/ · 反向工程日期:2026-09-10 · 反向基线:git 工作树(HEAD = 当前)


目录

  1. 文档信息
  2. 项目概述
  3. 用户角色与权限
  4. 术语表
  5. 小程序前端(miniprogram/)
  6. 后端 API(server/)
  7. 数据模型
  8. 部署与配置
  9. 安全与合规
  10. 非功能性需求
  11. 错误约定
  12. 已知问题与 TODO

1. 文档信息

项 内容
软件名称 明缘云道场小程序(前端)+ 明缘进阶场 API(后端)
版本 V1.0 / V4.0 文档
平台 前端:微信小程序 · 后端:Linux + Python 3.9 + FastAPI
关键依赖 微信开发者工具 · PyMySQL + DBUtils · httpx · python-dotenv
客户端入口 微信内搜索 / 扫码进入「明缘云道场」
服务端入口 uvicorn server.main:app 监听 :8000
文档目的 把代码事实定型为可验收的需求规格

2. 项目概述

2.1 子系统定位

子系统 代码路径 角色
小程序前端 miniprogram/ 用户交互入口(打卡 / 计划 / 个人),本地无状态,所有数据走后端 API
后端 API server/ 鉴权、业务逻辑、MySQL 读写、文件存储、WebView 桥接
微信生态 微信开放接口 wx.login 换取 openid、getuserphonenumber 绑手机号

2.2 业务目标

为「明缘进阶场」体系的成员提供基于微信的 轻量化学习管理工具,核心是:

  1. 每周打卡(周日上课日)

  2. 周 / 月 / 年计划 三级管理

  3. 小组统计(学员互看,管理员全览)

  4. 个人主页(资料、头像、登录方式)

  5. 内嵌 H5 复用既有 Web 复盘系统(通过 WebView + cookie 桥接)

    2.3 不在范围内(明确)

  • 不在前端代码内的子系统(Web 主站 xinhuo/19qi/18qi/、学员站)——见 V3.0
  • 不做支付、不做 IM、不做推送(小程序代码中无相关痕迹)
  • 不做服务端渲染 SSR

3. 用户角色与权限

3.1 角色矩阵

代码侧只识别两种角色(auth_users.role 字段):

角色键 中文 判定方式 用例
teacher 管理员 / 老师 (user.role or '').strip().lower() == 'teacher' 看所有小组、看未分组人数、看全部打卡统计
其他(含 student、member、leader、group_leader、admin) 成员 / 组长 非 teacher 走学员分支 只看自己所在小组的数据

注:ROLE_LABELS(pages/profile/profile.js)将 student/member → 成员、teacher/admin → 管理员、leader/group_leader → 组长,但后端权限只认 teacher,其他角色后端一律视作学员。

3.2 权限矩阵

接口前缀 学员 老师
/api/v1/auth/* ✅ 自助 ✅ 同左
/api/v1/profile ✅ 自己 ✅ 自己
/api/v1/profile/avatar ✅ 自己 ✅ 自己
/api/v1/checkin/weeks ✅ ✅
/api/v1/checkin/groups ✅(仅看自己小组) ✅(看全部 + 未分组数)
/api/v1/checkin/group 仅自己小组(传他组 → 403) 任意小组(不传看自己)
/api/v1/checkin POST ✅ 自己 ✅ 自己
/api/v1/checkin/list ✅ 自己历史 ✅ 自己历史
/api/v1/plan/* GET ✅ 看自己 / 自己小组 ✅ 看全部 + 未分组数
/api/v1/plan POST /checkin ✅ 自己当前周期 ✅ 自己当前周期(限当前周期)
/api/v1/plan/file/{sid}/... DELETE 仅自己 sid(403) 仅自己 sid(403)
/api/v1/webview/redirect 凭 token 凭 token(所有权限同 H5)
/api/v1/survey/* ✅ ✅(无鉴权)
/api/v1/feedback GET ⚠ 无鉴权(已知问题) ⚠ 无鉴权(已知问题)
/api/v1/course-vote ✅ 自己投 ✅ 自己投
/api/v1/yijing/* ✅ ✅

4. 术语表

术语 含义
openid 微信用户唯一标识,由 code_to_openid(code) 换取
token / session token 64 位 hex,会话标识,写入 auth_sessions 表(不是 JWT,名字沿用老 JWT 配置)
class_date 上课日 = 每周周日(周一 ~ 周日组成的自然周)
period_key 计划周期键:周=YYYY-Www、月=YYYY-MM、年=YYYY
小组(group) auth_users.group 字段,如「01组」
display_name 形如「01组张三」(仅当 group 形如 NN组 时拼接)
webview 桥接 通过 /api/v1/webview/redirect?token=...&target=... 把 URL 上的 token 换成 cookie 再 302

5. 小程序前端(miniprogram/)

5.1 全局结构

miniprogram/
├── app.js            App 入口:onLaunch 调 ensureLogin → /profile
├── app.json          7 个页面 + 4 tabBar + 导航主题色
├── app.wxss          全局基础样式(背景 #FDFBF7、主色 #C67B30)
├── project.config.json
├── project.private.config.json
├── sitemap.json
├── assets/           tabBar 图标(home/checkin/plan/profile × 2)
├── utils/
│   ├── api.js        request / planFileUrl / 环境切换 / 401 跳登录
│   └── auth.js       ensureLogin 单例 + loginWithPhone + token 存取
└── pages/
    ├── home/             首页(已登录 → 内嵌 H5;游客 → 原生介绍)
    ├── checkin/          打卡(老师总览+详情 / 学员小组+个人)
    ├── plan/             计划总览(周/月/年)
    ├── plan-submit/      计划提交(文本 + 至多 4 张图)
    ├── profile/          个人主页(资料 / 头像 / 退出)
    ├── webview/          通用 WebView(域名白名单)
    └── privacy/          隐私政策 + 用户协议(静态展示)

5.2 主题色与视觉规范

token 值 用途
主背景 #FDFBF7 全站冷色米白底
顶栏 #2C1810 渐变 → #6B3F24 Hero 卡背景
主交互色 #C67B30 主按钮、未选中状态、强调色
成功 #2E7D32(+ 浅底 #E8F4EB) 「正常」打卡、已完成
警示 #C0392B(+ 浅底 #FDEAEA) 「不参加讨论」、错误页
信息 #3A7BD5 「请假」
文本主 #2C1810 标题
文本辅 #8B7A6A / #999 描述、时间戳
圆角 12rpx / 16rpx / 24rpx 卡片、按钮、Hero
字号 22/24/26/28/30/32/34/40/48 rpx 时间戳到主标题

5.3 API 客户端(utils/api.js)

5.3.1 环境切换(多域名)

getAccountInfoSync().miniProgram.envVersion
├── 'develop' → baseUrl = https://test.muguaclub.cn/clock
├── 'trial'   → baseUrl = https://test.muguaclub.cn/clock
└── 'release' → baseUrl = https://m.muguaclub.cn/clock
  • 三个环境对应 STATIC_BASE_URL + H5_BASE_URL 同源

5.3.2 request 行为约定

入参 默认 说明
url 必填 必须以 /api/v1 开头
method GET 影响幂等重试
data {} 自动 JSON
auth true true 时自动从 storage 取 token 加 Authorization: Bearer xxx

行为:

  • 401 → 删 token + goProfile() 切到「我的」页(带 1.2s 防连点锁)
  • 幂等方法(GET / DELETE) 失败自动 800ms 后重试一次;命中 domain not 错误不重试
  • 非 2xx → reject 原 data({error/detail/message})

5.3.3 planFileUrl 路径约定

attachments/{user_id}/{plan_type}/{period_key}/{filename} → STATIC_BASE_URL + /clock/static/plan/<encoded/path>

前 4 段缺失则返回空串,强制调用方重新加载。

5.4 登录态(utils/auth.js)

5.4.1 单例 + 指数退避

  • loginPromise 全局单例,解决 App.onLaunch 与首页 onShow 同时调 ensureLogin 的竞态
  • wxLogin 内部失败重试 3 次(首次 600ms → 800ms → 1000ms)
  • 失败兜底:wechatLogin 整个流程失败再重试 1 次(500ms 后)
  • 任何情况下 loginPromise 完成后清空 → 让下一次重新发起(避免冷启动偶发失败后卡死)

5.4.2 双入口登录

入口 触发
微信授权登录 ensureLogin() → /api/v1/auth/wechat-login 用 code 换 token;后端返回 need_phone:true 时 resolve(null),前端引导到「我的」页绑定手机号
手机号登录 loginWithPhone(phoneCode) → /api/v1/auth/bind-phone 必须同时传 code 和 phone_code;绑定成功后写 token 并返回 is_new:true

5.5 页面清单

5.5.1 home 首页

  • 登录态判断:isLoggedIn = !!getToken()
  • 已登录 → <web-view src="/api/v1/webview/redirect?token=...&target=...">,key 自增强制重建(解决切 tab 白屏)
  • 未登录 → 原生介绍页(Hero 头图 + 4 个模块卡 + 4 个产品介绍卡 + 引导登录按钮)
  • 提供分享:onShareAppMessage / onShareTimeline

5.5.2 checkin 打卡(核心)

老师视图

  • view = 'overview':所有小组打卡进度 + 自己的打卡状态
  • view = 'detail':点开某小组看每个成员是否打卡 / 状态

学员视图

  • 顶部打卡卡:5 个状态按钮(正常/迟到/请假/不参加讨论/早退),单选切换
  • tab = 'group':本组成员列表
  • tab = 'personal':个人打卡历史(分页,每页 20 条)

通用交互

  • 周选择器:/api/v1/checkin/weeks 返 2022 年到当前的所有上课周六
  • 下拉刷新 + 上拉加载更多(个人历史 tab)
  • 周计划 / 月计划 / 年计划的周期切换(plan 页面公用同一套 picker)

打卡规则(来自 routes/checkin.py)

  • 状态:正常 / 迟到 / 请假 / 不参加讨论 / 早退
  • 默认状态 正常
  • 备注最大 500 字符(仅非「正常」时允许)
  • 打卡窗口:上课日所在自然周(周一 ~ 周日)与当前周相同 / 未来周可打卡

5.5.3 plan 计划(核心)

  • 类型切换 tab:周计划 / 月计划 / 年计划
  • 卡片:
    • 自己的状态(已提交/待提交 + 当前周期)
    • 「提交 / 修改」按钮,canSubmit=false 时 disabled
    • 窗口提示(如「周四截止,已过提交时间」)
  • 老师视图:所有小组列表
  • 学员视图:tab = 小组(自己成员)/ 个人(历史最多 20 条)
  • 周期 key:
    • 周 = YYYY-Www(ISO 周)
    • 月 = YYYY-MM
    • 年 = YYYY

提交时间窗(routes/plan.py)

  • 周:上一周五 00:00 ~ 本周四 23:59:59
  • 月 / 年:仅当前周期可提交,历史 / 未来都禁止

5.5.4 plan-submit 提交页

控件 限制
textarea maxlength 1000、disabled=!canSubmit
图片 最多 4 张、允许后缀 jpg/jpeg/png/gif/webp/bmp、>1MB 自动逐级压缩 80→60→45
删除 调 /api/v1/plan/file/{sid}/{plan_type}/{period}/{filename} DELETE
提交 文本或图片必有一项;已有记录弹覆盖确认

上传协议:wx.uploadFile,formData = { plan_type, period_key },name='files',支持 multipart 多文件

5.5.5 profile 个人主页

视图状态机:view ∈ { loading, profile, phoneLogin, loadError }

  • 已登录 + 有 token → 显示完整资料 + 头像 + 班级分组列表 + 退出登录
  • 未登录 / need_phone → 显示手机号登录卡(强依赖隐私协议勾选)
  • chooseAvatar:调 /api/v1/profile/avatar 单文件上传,avatar 文件名统一为 avatar.{ext},旧头像自动清理
  • 退出登录:调 /api/v1/auth/logout(尽力而为)+ 清本地 token + 清隐私同意标记 + reLaunch 回首页

5.5.6 webview 通用 web-view

  • 入参:url(必须 URL 编码)
  • 域名白名单(isAllowedUrl):仅允许 m.muguaclub.cn 和 muguaclub.cn 及其子域;非法 URL 渲染错误页

5.5.7 privacy 隐私 / 协议页

  • 两个静态文档:隐私政策 8 节、用户协议 8 节
  • 通过 query type 切换:privacy / agreement
  • 最后更新日期占位:2026 年 9 月

5.6 通用 UI 特性

  • 骨架屏:列表 / 表单 / 详情都有 sk-* 占位
  • 错误页:圆形「!」 + 失败文案 + 「重新加载」按钮
  • 空态:emoji + 简短说明
  • 分享:所有页面都实现 onShareAppMessage + onShareTimeline

6. 后端 API(server/)

6.1 工程骨架

server/
├── __init__.py
├── main.py           FastAPI() + 挂 9 个 router + /api/v1/health
├── config.py         从 .env 读所有密钥 / 路径
├── db.py             PyMySQL + DBUtils PooledDB,单例 per db
├── security.py       encrypt_phone(hmac-sha256) + create_session_token
├── wechat.py         code_to_openid + get_phone_number + _get_access_token
├── user.py           make_display_name + get_class_groups + user_payload
├── routes/
│   ├── __init__.py
│   ├── auth.py       /api/v1/auth/*
│   ├── checkin.py    /api/v1/checkin/*
│   ├── plan.py       /api/v1/plan/*
│   ├── profile.py    /api/v1/profile*
│   ├── webview.py    /api/v1/webview/redirect
│   ├── survey.py     /api/v1/survey/*  (无鉴权)
│   ├── feedback.py   /api/v1/feedback/* (含仅 ADMIN_KEY 的管理接口)
│   ├── course_vote.py /api/v1/course-vote*
│   └── yijing.py     /api/v1/yijing/* (从老 Flask 服务迁移)
└── migrations/
    ├── 001_add_wechat_openid_column.sql
    ├── 002_plan_perf_index.sql
    ├── 003_checkin_records_user_id.sql
    └── 004_plan_checkins_user_id.sql

⚠ 已知 bug(main.py:29):uvicorn.run("server_prd.main:app", ...) 引用不存在的包,启动会 ImportError。

6.2 全局运行时依赖

依赖 作用
FastAPI Web 框架
uvicorn ASGI 运行(默认 :8000,host 127.0.0.1,dev 模式 reload)
PyMySQL MySQL 驱动
DBUtils PooledDB 连接池:DB_POOL_MIN_CACHED=1 / MAX_CACHED=5 / MAX_CONNECTIONS=10 / MAX_USAGE=100、ping=1、connect_timeout=5s、read_timeout=5s
httpx 调用 api.weixin.qq.com(jscode2session / access_token / 手机号解密)
python-dotenv load_dotenv(dotenv_path, override=True)
pydantic 请求体 / 响应模型

6.3 配置项(config.py)

键 默认 用途
WECHAT_APPID / WECHAT_SECRET 空 微信 API 凭据
JWT_SECRET * 空 代码里没有真正用上 JWT,命名沿用历史,实际为 auth_sessions
JWT_ALGORITHM HS256 同上,遗留
JWT_TTL_SECONDS 90*24*3600(90 天) 会话有效期
ADMIN_KEY 空 /api/v1/feedback/resolve 管理密钥
MYSQL_HOST/PORT/USER/PASSWORD/DB localhost/3306/root/''/mys_info_v3 主业务库
SURVEY_DB myxhxc 问卷库(独立库,独立连接池)
PHONE_HMAC_KEY MingYuan2026AuthKey 手机号 HMAC-SHA256 盐
H5_BASE_URL http://127.0.0.1:8001 WebView 跳转上游(H5 主站)
FEEDBACK_DIR /home/introduction/feedback 用户反馈 JSON + 图片目录
COURSE_VOTES_FILE /home/introduction/test/course_votes.json 课程投票持久化文件
PLAN_ATTACH_DIR /home/static/plan 计划附件根目录
STATIC_ROOT / AVATAR_DIR /home/static[/avatars] 头像静态文件目录(nginx 直读)
DB_POOL_* 上表 连接池参数(仅从 os.environ 读,不走 config)

注:config.py 用 os.environ.get(name, default),但 db.py 用 os.environ.get("DB_POOL_*") 直接读。

6.4 数据库连接池(db.py)

  • 多库支持:_get_pool(database) 按 db 名缓存 pool,双数据库(主业务 + survey)
  • fetch_one / fetch_all / execute 三个 API,借 / 还连接(close() 让连接回池)
  • close_all() 用于测试 / reload hook

6.5 鉴权(get_current_user)

判定顺序:

  1. Authorization: Bearer xxx(小程序 API 默认)
  2. Cookie: auth_token=xxx(H5 走 webview 桥接后自动带)
  3. 都无 → 401

查表:

  • auth_sessions WHERE token=? AND expires_at>NOW() 拿到 user_id
  • 再 SELECT * FROM auth_users WHERE id=?

6.6 接口约定

6.6.1 /api/v1/auth

方法 路径 鉴权 说明
POST /wechat-login 否 code → openid → 查 user;命中即发 token,未命中返 need_phone:true
POST /bind-phone 否 code, phone_code → 拿手机号 → HMAC 查 user;命中即 update wechat_openid + 发 token;首次绑定返 is_new:true
GET /verify 是 返回 user_payload
POST /logout 是 删除 session + 清 user.wechat_openid + 删 cookie

6.6.2 /api/v1/profile

方法 路径 说明
GET /profile user_payload + phone_display(注意:原文里 user 表无 phone_display 字段,8.2 已知问题) + wechat_avatar + login_type
POST /profile/avatar multipart 单文件,4MB 上限,存 avatar.{ext},旧头像自动删;URL =/clock/static/avatars/{uid}/avatar{ext}

6.6.3 /api/v1/checkin

方法 路径 说明
GET /weeks 2022 年至当前的所有上课周六列表 + current_week
GET /week 已弃用,仅供旧客户端
GET /groups?class_date=YYYY-MM-DD 老师→全部小组 + 未分组数;学员→自己小组 + my_group
GET /group?class_date=&group_name= 单组成员 + 各自打卡状态;学员传他组→403
POST /checkin {status, class_date?, note?} upsert(按 user_id + class_date 唯一键)
GET /list?page=&size= 个人历史分页,默认 10,上限 50

6.6.4 /api/v1/plan

方法 路径 说明
GET /periods?type= 周/月/年周期列表 + current_period
GET /status?type=&period_key?= 看本期是否已提交 + can_submit + message
GET /groups?type=&period_key?= 老师看全部 + 未分组;学员看自己小组
GET /group?type=&group_name=&period_key?= 单组成员列表 + 各自 filled
POST /checkin {plan_type, period_key?, status?, content?, attachments[]?} upsert(按 user_id + plan_type + period_key)
GET /history?type=&page=&size= 个人历史分页
POST /upload multipart {plan_type, period_key, files[]} 至多 4 文件、每文件 ≤10MB、白名单扩展名、保存路径 attachments/{user_id}/{plan_type}/{period_key}/{filename}
GET /file/{sid}/{plan_type}/{period}/{filename} 直读静态文件
DELETE /file/{sid}/... 校验 sid == user.id,否则 403

6.6.5 /api/v1/webview

方法 路径 鉴权 说明
GET /redirect?token=&target= 否(凭 URL token) 校验 token → set HttpOnly cookie → 302 target

安全要点:

  • target 仅允许相对路径(/xxx)或白名单域 m.muguaclub.cn 及其子域
  • cookie:HttpOnly、Secure(scheme=https)、SameSite=Lax、MaxAge=JWT_TTL_SECONDS

6.6.6 /api/v1/survey(独立库 myxhxc,无鉴权)

方法 路径 说明
GET /tasks 状态为 active 的所有 task
GET /responses?task_id= 某 task 的所有 group 回答
POST /response {task_id, group_id, answers} upsert
PUT /response/{rid} {answers} 更新已有
POST /task 占位,功能预留

6.6.7 /api/v1/feedback

方法 路径 鉴权 说明
GET "" ❌ 无 读所有 json 文件,按 unsolved/solved 倒序
POST "" {name?, description?, image?} 否 写一个 4 位 hex id 的 JSON,可附 data:image base64 图片
GET /file/{name} 否 直读图片
POST /resolve?id=&token= ADMIN_KEY 标记为 solved

⚠ 已知问题:/feedback GET 当前无鉴权,任何人能看所有反馈(安全风险)

6.6.8 /api/v1/course_vote

方法 路径 鉴权 说明
POST /course-vote {votes[3-6], rating?, comment?} 是 写入 COURSE_VOTES_FILE JSON 文件
GET /course-votes 是 聚合(总票数 / 评论均分)+ 我的投票

用 JSON 文件存,无数据库,便于迁移自老 Flask。

6.6.9 /api/v1/yijing

方法 路径 鉴权 说明
GET /catalog 否 八卦 + 六十四卦元数据
GET /progress 是 5 个阶段(1..5)的学习进度
POST /progress {stage[1-5], item_type, item_id, status?} 是 upsert
POST /quiz {stage, item_type, item_id, correct, answer?} 是 记一次尝试,对应进度 score++;score ≥ 2 → mastered;写入 yijing_reviews 用于复习排期

6.7 后端非业务模块

模块 用途
start.sh / stop.sh 启停脚本(开发环境)
start_prd.sh / stop_prd.sh 生产环境启停(pid 文件管理)
index.html 部署目录下的 health 静态页
requirements.txt 依赖清单
.env 曾被 git 跟踪,含密钥(已知问题)
uvicorn.log 运行日志

7. 数据模型

由 auth_users / auth_sessions / classes / groups / class_groups / group_history / checkin_records_old / plan_checkins / yijing_trigrams / yijing_hexagrams / yijing_progress / yijing_attempts / yijing_reviews / survey_tasks / survey_responses 等表名 / SQL 反推;具体 DDL 不在 server/ 内。

7.1 主业务库(mys_info_v3)

表 主键 关键字段 用途
auth_users id student_id, name, phone_encrypted(HMAC-SHA256), wechat_openid(唯一键), wechat_avatar, login_type, role, \group`` 用户账号 + 资料
auth_sessions id user_id, token(唯一), expires_at 会话(90 天)
classes id code, name 班级(期/班次)
groups id group_code, sort 小组
class_groups (复合) class_id, group_id, user_id, role_in_group 班级 × 小组 × 学员 多对多
group_history (复合) user_id, class_id, old_group_id, changed_at 学员所在小组变迁
checkin_records_old (复合) uk_checkin_user_date(user_id, class_date) user_id, class_date, checked_at, status, note 打卡记录(注:表名保留 _old 后缀,迁移按 user_id,详见 migration 003)
plan_checkins (复合) uk_plan_user_period(user_id, plan_type, period_key) user_id, plan_type, period_key, status(已完成/未完成), checked_at, content, attachments(JSON), created_at, updated_at + 索引 idx_plan_period_user 计划记录
yijing_trigrams id name, symbol, nature, de, family, direction, xiantian, houtian, binary_code, class_xiang, summary 八卦(教材化元数据)
yijing_hexagrams id name, full_name, symbol, upper_trigram, lower_trigram, binary_code, judgment, gua_explanation, bagong, gong_wuxing 六十四卦
yijing_progress (复合 stage, item_type, item_id) user_id, stage, item_type, item_id, status(learning/mastered), score, attempts, correct 学习进度
yijing_attempts id user_id, stage, item_type, item_id, correct, answer 尝试明细
yijing_reviews (复合) user_id, item_type, item_id, due_at 复习到期时间

7.2 问卷库(myxhxc)

表 关键字段
survey_tasks task_no, status(active), ...
survey_responses task_id, group_id, answers(JSON)

7.3 文件存储

路径 用途
/home/static/avatars/{uid}/avatar{ext} 个人头像(nginx 直读)
/home/static/plan/attachments/{user_id}/{plan_type}/{period_key}/... 计划附件
/home/introduction/feedback/*.json + images/*.png 反馈 JSON + 图片
/home/introduction/test/course_votes.json 课程投票 JSON

7.4 迁移状态(migrations/)

  • 001:给 auth_users 加 wechat_openid 唯一键(绑微信前提)
  • 002:给 plan_checkins 加 (plan_type, period_key, student_id) 索引(性能,已被 004 替换)
  • 003:checkin_records_old 改按 user_id 关联,加唯一键 (user_id, class_date)
  • 004:plan_checkins 改按 user_id 关联,加 (user_id, plan_type, period_key) 唯一键 + 替换索引;附件相对路径从 student_id 改写为 user_id

8. 部署与配置

8.1 启动命令(开发)

pip install -r requirements.txt
# 配置 server/.env:WECHAT_APPID / WECHAT_SECRET / MYSQL_* / PHONE_HMAC_KEY / ADMIN_KEY / H5_BASE_URL
python -m uvicorn server.main:app --reload --port 8000

8.2 启动命令(生产)

# 启动
bash server/start.sh
# 停止
bash server/stop.sh

8.3 nginx 转发示例(推断)

# /api/v1/* → :8000 (uvicorn)
# /clock/static/* → /home/static/ (nginx 直读)
# /clock/api/v1/webview/* → :8000 (cookie 桥接)
# / → upstream muguaclub.cn / 8001 (H5 主站)

8.4 域名与端口

角色 域名 后端端口
正式 m.muguaclub.cn 8000 (FastAPI) + 8001 (H5)
测试 test.muguaclub.cn 同上

9. 安全与合规

9.1 鉴权

  • 单 token 体系:所有受保护接口统一用 get_current_user(Authorization 或 cookie)
  • 退出登录:删 session + 解绑 openid(下次需重新授权手机号)
  • WebView 桥接:URL 上短期 token → cookie,再 302 跳转同站域(防开放重定向)

9.2 传输安全

  • 头像直链强制 https(secure=request.url.scheme == "https")
  • Cookie HttpOnly + SameSite=Lax
  • OpenAPI 全 HTTPS(微信 API 仅 https)

9.3 数据安全

  • 手机号 HMAC-SHA256 存 phone_encrypted,原号不出库
  • 反馈、计划内容、明文入库(小程序端有 maxlength 1000 / 500 限制)

9.4 隐私合规

  • 「我的」页强制要求勾选隐私政策 + 用户协议才能调 getPhoneNumber
  • 隐私协议 8 节;用户协议 8 节;最后更新 2026 年 9 月(占位)
  • 客服邮箱 [此处填写客服邮箱] 占位需补充

10. 非功能性需求

10.1 性能

维度 要求
首屏冷启动 幂等请求(GET/DELETE)网络错误自动 800ms 重试 1 次
单实例 QPS 与连接池 maxconnections=10 匹配;超出排队
单次连接 connect_timeout=5s,read_timeout=5s
列表分页 打卡默认 10,最大 50;计划默认 10,最大 50
客户端压缩 计划图片 >1MB 自动 80→60→45 三级降质压缩
上传上限 单文件 10MB(计划)/ 4MB(头像)

10.2 可用性

  • 单会话 90 天有效,过期即失效(前端需重新登录)
  • 计划历史周期只读(can_submit=false)
  • 学员只能查自己所在小组
  • 切换 tab / 周选择器 / 周期选择器都有 fallback(数据为空时不刷)

10.3 可观测性(缺)

代码中未发现业务埋点 / 性能监控 / 错误聚合;uvicorn.log 仅记录关键 warn/error(如 微信登录 code 换取 openid 失败)。

10.4 兼容性

  • 小程序基础库 >=3.0.0(wx.getAccountInfoSync)
  • Python ≥3.9(FastAPI 最低版本)

11. 错误约定

  • HTTP 状态码:
    • 200 业务正常
    • 400 参数错误(含业务校验:「仅可填写当前周期」「已过打卡时间」等)
    • 401 缺 token / token 无效或过期
    • 403 无权限(学员查他组 / 删他人附件)
    • 404 资源不存在
    • 500 未捕获的运行时错误(FastAPI 默认)
  • 错误体:
    • FastAPI 默认:{"detail": "..."}
    • 业务自定义:{"error": "..."} 或 {"success": false, "error": "..."} 或 {"need_phone": true, "error": "..."}
  • 前端解析顺序(pages/checkin/checkin.js):err.detail → err.error → err.message

文档结束。如有功能/接口改动,请同步更新本文件章节 5/6 并在文末「更新日志」追加条目。