40 KiB
明缘云道场小程序 + 后端 API · 需求规格说明书 V4.0
基于
miniprogram/(微信小程序前端)和server/(FastAPI 后端)实际代码反向整理。 与 V2.0/V3.0(涵盖 Web 主站 + 学员网站)平行存在;本文档 仅覆盖这两个子系统。· 适用范围:
miniprogram/、server/、server/migrations/· 反向工程日期:2026-09-10 · 反向基线:git工作树(HEAD = 当前)
目录
- 文档信息
- 项目概述
- 用户角色与权限
- 术语表
- 小程序前端(miniprogram/)
- 后端 API(server/)
- 数据模型
- 部署与配置
- 安全与合规
- 非功能性需求
- 错误约定
- 已知问题与 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 业务目标
为「明缘进阶场」体系的成员提供基于微信的 轻量化学习管理工具,核心是:
-
每周打卡(周日上课日)
-
周 / 月 / 年计划 三级管理
-
小组统计(学员互看,管理员全览)
-
个人主页(资料、头像、登录方式)
-
内嵌 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)
判定顺序:
Authorization: Bearer xxx(小程序 API 默认)Cookie: auth_token=xxx(H5 走 webview 桥接后自动带)- 都无 → 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 |
⚠ 已知问题:
/feedbackGET 当前无鉴权,任何人能看所有反馈(安全风险)
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": "..."}
- FastAPI 默认:
- 前端解析顺序(
pages/checkin/checkin.js):err.detail → err.error → err.message
文档结束。如有功能/接口改动,请同步更新本文件章节 5/6 并在文末「更新日志」追加条目。