504 lines
17 KiB
Markdown
504 lines
17 KiB
Markdown
新功能前要用分支发PR!禁止直接向main提交。
|
||
|
||
# Hulumath-Web 代码贡献指南
|
||
|
||
本指南面向人类开发者和 AI 编程助手。开始修改前,请完整阅读本文。
|
||
|
||
葫芦数学不是通用内容站,而是围绕 MathBTI、数学人生、比赛、工具箱、视频探索和长期成长构建的“数学人生宇宙”。本仓库已经进入可部署的 Django 生产形态,贡献时应优先保持业务一致性、数据安全和可回滚性,不要把它当作一次性 Demo。
|
||
|
||
## 1. 必须遵守的协作规则
|
||
|
||
以下规则没有例外:
|
||
|
||
1. **禁止直接向 `main` 推送。**
|
||
2. **所有改动必须通过 Pull Request(PR)提交。**
|
||
3. **所有 PR 必须由仓库负责人 Jacky 审查并明确批准后才能合并。**
|
||
4. CI 未通过时不得合并,不得通过删除测试、降低规则或跳过检查来“修复” CI。
|
||
5. 不得提交密码、Token、SSH 私钥、生产 `.env.production`、真实用户数据或数据库备份。
|
||
6. 不得直接在生产服务器上改源码后绕过 Git。紧急修复也应补回 PR。
|
||
7. 不得回退、覆盖或整理与当前任务无关的他人改动。
|
||
|
||
如果你没有仓库写权限,请从 fork 创建分支并向本仓库提交 PR。
|
||
|
||
## 2. AI 协作者五分钟快速入口
|
||
|
||
AI 在采取任何修改动作前,至少完成以下步骤:
|
||
|
||
1. 阅读本文件。
|
||
2. 阅读任务直接涉及的代码、测试和迁移。
|
||
3. 执行 `git status --short --branch`,确认工作区是否已有他人改动。
|
||
4. 用 `rg` 搜索现有实现、调用方和测试,不凭文件名猜测行为。
|
||
5. 对照下方“文档优先级”和“不可破坏的系统约束”。
|
||
6. 明确最小修改范围,避免顺手重构。
|
||
7. 修改后运行与风险相匹配的检查。
|
||
8. 总结行为变化、测试结果、迁移影响和未验证风险。
|
||
|
||
AI 不应:
|
||
|
||
- 在没有阅读上下文时批量重写模块。
|
||
- 因测试失败而删除断言、吞掉异常或放宽安全校验。
|
||
- 自行修改产品规则、奖励数值、剧情事实或历史人物设定。
|
||
- 使用字符串替换模拟结构化数据迁移。
|
||
- 为了“代码更现代”而更换框架、数据库或部署方式。
|
||
- 自动合并 PR。最终审查与合并权属于 Jacky。
|
||
|
||
## 3. 文档与事实来源优先级
|
||
|
||
遇到冲突时按以下顺序判断:
|
||
|
||
1. **当前任务中仓库负责人的明确要求。**
|
||
2. **本文件中的贡献与工程规则。**
|
||
3. **数据库迁移、当前代码、测试和 API 契约所表达的真实行为。**
|
||
4. **`Bible.md` 中的产品定位、核心循环和架构决策。**
|
||
5. **`docs/DEPLOYMENT.md` 与 `docs/BAOTA_UBUNTU_FROM_ZERO.md` 中的生产约束。**
|
||
6. **`README.md` 中的使用入口。**
|
||
7. **`docs/TUTORIAL.md` 及旧脚本只作为历史和内容迁移参考。**
|
||
|
||
旧 Flask、旧 SQLite 和旧前端文档不能覆盖当前 Django/MySQL 生产设计。
|
||
|
||
`docs/` 中可能包含 Word 导出、UTF-16 或历史格式文件。编辑器显示乱码时,不要直接重写或删除原文件;优先使用同名 Markdown 版本,必要时先确认编码和来源。
|
||
|
||
## 4. 当前技术栈
|
||
|
||
- Django 4.2
|
||
- Django REST Framework
|
||
- Django Channels
|
||
- MySQL 8.0.35
|
||
- Redis 7.x Channel Layer
|
||
- Gunicorn + UvicornWorker
|
||
- Nginx
|
||
- WhiteNoise
|
||
- 原生 HTML、CSS、JavaScript Web 客户端
|
||
- pytest、pytest-django、pytest-cov
|
||
- Ruff
|
||
- Gitea Actions
|
||
|
||
本地最低兼容 Python 版本为 3.9,建议使用 Python 3.11 或 3.12。生产环境使用 Python 3.12。
|
||
|
||
当前 Web 客户端没有 Node 构建步骤。不要仅为一个小功能引入 Node、前端框架或新的打包链。
|
||
|
||
## 5. 仓库结构与模块所有权
|
||
|
||
```text
|
||
backend/
|
||
├── accounts/ 用户、邀请码、会话、游客迁移、审计
|
||
├── math_life/ MathBTI、数学人格、剧情版本、存档、人物 Skill
|
||
├── contest/ 题库、比赛、实时匹配、判分、Rating、反作弊
|
||
├── progression/ 五维能力、数学精灵、卡牌、奖励流水
|
||
├── content/ 视频、知识卡片、人物内容、收藏、观看进度
|
||
├── latex_lab/ 公式文档、课程、练习与 LaTeX 判定
|
||
├── engagement/ 签到、通知等回访能力
|
||
├── common/ 通用 API、日志、健康检查、管理后台
|
||
├── config/ Django 设置、URL、ASGI/WSGI
|
||
├── templates/ 网站与 Django Admin 模板
|
||
└── static/ CSS、JavaScript 等静态资源源文件
|
||
|
||
scripts/ 部署、生产冒烟检查等运维脚本
|
||
docs/ 部署、迁移、产品和内容文档
|
||
.gitea/workflows/ PR CI 与合并后自动部署
|
||
```
|
||
|
||
### 静态资源特别说明
|
||
|
||
- `backend/static/` 是源文件,应在这里修改。
|
||
- `backend/staticfiles/` 是 `collectstatic` 产物,不应手工编辑。
|
||
- 普通功能 PR 不应提交 `backend/staticfiles/` 的意外变化。
|
||
- 生产部署会自动执行 `collectstatic` 并生成带指纹资源。
|
||
|
||
## 6. 核心请求路径
|
||
|
||
典型请求路径如下:
|
||
|
||
```text
|
||
浏览器
|
||
→ Nginx
|
||
→ Gunicorn/Uvicorn ASGI
|
||
→ Django URL / Channels 路由
|
||
→ View / Consumer
|
||
→ Service
|
||
→ Model / MySQL / Redis
|
||
```
|
||
|
||
职责建议:
|
||
|
||
- View/Consumer:鉴权、解析请求、返回响应。
|
||
- Service:事务、业务规则、判分、奖励、状态迁移。
|
||
- Model:数据结构、约束、索引和轻量领域属性。
|
||
- Serializer:输入校验与 API 表达。
|
||
- Template/JS:交互与展示,不承担正式判分和奖励真相。
|
||
|
||
复杂业务不要全部写进 View,也不要把正式规则只放在浏览器。
|
||
|
||
## 7. 不可破坏的系统约束
|
||
|
||
### 7.1 服务端是正式数据唯一事实来源
|
||
|
||
以下结果必须由服务端决定并持久化:
|
||
|
||
- MathBTI 正式结果
|
||
- 剧情存档、选择、关系和结局
|
||
- Contest 计时、答案、分数、Rating 和反作弊标记
|
||
- 视频完成状态
|
||
- 五维能力、精灵经验、卡牌和奖励
|
||
|
||
浏览器状态只能用于临时 UI,不得代替正式数据库记录。
|
||
|
||
### 7.2 幂等性
|
||
|
||
剧情选择、比赛提交、奖励发放、游客数据迁移等可重试写操作必须幂等。
|
||
|
||
- 优先使用数据库唯一约束和事务保证幂等。
|
||
- 客户端正式提交应提供 `Idempotency-Key`。
|
||
- 不要只依赖“按钮禁用”防止重复请求。
|
||
- HTTP 测试环境不是安全上下文,前端不能假设 `crypto.randomUUID()` 一定存在。
|
||
|
||
### 7.3 数据库
|
||
|
||
- 生产数据库必须为 MySQL 8.0.35 或更高兼容版本。
|
||
- 禁止把生产改回 SQLite、MySQL 5.7 或直接暴露数据库公网端口。
|
||
- 字符集必须支持 `utf8mb4`。
|
||
- 表引擎使用 InnoDB。
|
||
- 事务隔离级别为 `READ COMMITTED`。
|
||
- 实时匹配依赖 `SELECT ... FOR UPDATE SKIP LOCKED` 和匹配索引。
|
||
|
||
SQLite 只用于快速本地测试。涉及锁、排序规则、事务或 MySQL 特性的改动必须在 MySQL 上验证。
|
||
|
||
### 7.4 Redis 与 WebSocket
|
||
|
||
- Redis 是 Channels 的消息层,不是可随意移除的缓存依赖。
|
||
- ASGI 初始化顺序经过特殊处理,模型相关 Consumer 必须在 Django App Registry 初始化后导入。
|
||
- 修改 `config/asgi.py`、Consumer 或路由时,必须验证 ASGI 导入和 WebSocket。
|
||
|
||
### 7.5 内容版本
|
||
|
||
- 已发布题目和剧情内容使用版本模型,避免直接覆盖历史运行所依赖的内容。
|
||
- `StoryRun` 应指向具体 `StoryVersion`。
|
||
- 比赛题目应指向具体 `QuestionVersion`。
|
||
- 修改种子内容时保持命令可重复执行。
|
||
- 种子命令不能加入每次生产部署,以免覆盖运营修改。
|
||
|
||
### 7.6 用户和权限
|
||
|
||
- 使用项目自定义 UUID 用户模型,不要绕过 `AUTH_USER_MODEL`。
|
||
- 普通用户不能看到或访问运营后台。
|
||
- `is_staff`、`is_superuser` 等权限字段不得通过普通用户 API 写入。
|
||
- 任何后台入口可见性都不能替代服务端权限校验。
|
||
|
||
### 7.7 生产部署
|
||
|
||
- 生产进程由 systemd 管理,不使用宝塔 Python 项目管理器。
|
||
- 应用监听 `127.0.0.1:8000`,由 Nginx 对外代理。
|
||
- 当前测试生产入口使用 `4321`,不要开放应用内部 `8000`。
|
||
- 部署前自动备份 MySQL,执行迁移和静态资源收集。
|
||
- 部署后验证 HTTP、数据库、Redis、WebSocket、页面和视频目录。
|
||
- 不要在自动回退中执行破坏性数据库反向迁移。
|
||
|
||
## 8. API 与前端约定
|
||
|
||
### API
|
||
|
||
- HTTP API 使用 `/api/v1/` 前缀。
|
||
- WebSocket 使用 `/ws/`,业务 WebSocket 通常使用 `/ws/v1/`。
|
||
- API 异常使用统一结构:
|
||
|
||
```json
|
||
{
|
||
"error": {
|
||
"code": "request_error",
|
||
"message": "请求未能完成",
|
||
"details": {},
|
||
"request_id": "..."
|
||
}
|
||
}
|
||
```
|
||
|
||
- 新接口应遵循现有鉴权、CSRF、限流和 Request ID 约定。
|
||
- 不要在响应中泄露内部异常、密钥或敏感用户字段。
|
||
|
||
### 前端
|
||
|
||
- 复用 `backend/static/js/app.js` 中的 `api()`、状态和渲染模式。
|
||
- 使用 DOM API 和 `textContent` 表达不可信内容,避免直接拼接 HTML。
|
||
- 修改用户可见流程时同时检查桌面端和移动端。
|
||
- 当前生产可能运行在纯 HTTP IP 环境,不要无条件依赖安全上下文 API。
|
||
- 后台主题源文件为 `backend/static/admin/css/hulumath_admin.css`。
|
||
- CSS 修改要检查颜色对比度、禁用态、长内容滚动和系统深色偏好。
|
||
|
||
## 9. 本地开发
|
||
|
||
### 初始化
|
||
|
||
```bash
|
||
python3 -m venv .venv
|
||
.venv/bin/pip install -r requirements-dev.txt
|
||
make migrate
|
||
make seed
|
||
make run
|
||
```
|
||
|
||
本地地址:
|
||
|
||
```text
|
||
网站:http://127.0.0.1:8000/
|
||
后台:http://127.0.0.1:8000/admin/
|
||
邀请码:HULU2026
|
||
```
|
||
|
||
创建本地管理员:
|
||
|
||
```bash
|
||
cd backend
|
||
../.venv/bin/python manage.py createsuperuser
|
||
```
|
||
|
||
### 环境变量
|
||
|
||
从 `.env.example` 或 `.env.production.example` 复制本地文件,不要修改并提交真实值。
|
||
|
||
```bash
|
||
cp .env.example .env.local
|
||
set -a
|
||
source .env.local
|
||
set +a
|
||
```
|
||
|
||
生产要求 `DJANGO_DEBUG=false`、MySQL `DATABASE_URL`、Redis `REDIS_URL`、强随机 `DJANGO_SECRET_KEY` 和正确的 Host/CORS/CSRF 来源。
|
||
|
||
## 10. 推荐修改流程
|
||
|
||
### 10.1 创建分支
|
||
|
||
先同步 `main`,再创建语义清晰的分支:
|
||
|
||
```bash
|
||
git switch main
|
||
git pull --ff-only origin main
|
||
git switch -c feat/short-description
|
||
```
|
||
|
||
常用前缀:
|
||
|
||
- `feat/`:用户可见功能
|
||
- `fix/`:缺陷修复
|
||
- `ci/`:CI/CD
|
||
- `docs/`:文档
|
||
- `refactor/`:无行为变化的重构
|
||
- `test/`:测试改进
|
||
|
||
### 10.2 先读后改
|
||
|
||
查找实现和调用方:
|
||
|
||
```bash
|
||
rg "目标类名|函数名|API 路径" backend
|
||
rg --files backend/<app>
|
||
```
|
||
|
||
至少阅读:
|
||
|
||
- 目标模块的 model/service/view/serializer
|
||
- 对应 URL 或 routing
|
||
- 现有测试
|
||
- 相关迁移
|
||
- 调用该行为的前端代码
|
||
|
||
### 10.3 小步提交
|
||
|
||
- 一个 PR 解决一个明确问题。
|
||
- 优先提交可运行的垂直切片。
|
||
- 不夹带格式化整个仓库、目录重命名或无关依赖升级。
|
||
- 提交信息使用祈使式或清楚的类型前缀,例如:
|
||
|
||
```text
|
||
feat: add story resume endpoint
|
||
fix: make contest submission idempotent
|
||
ci: add production smoke check
|
||
docs: document content authoring flow
|
||
```
|
||
|
||
## 11. 数据模型与迁移
|
||
|
||
修改 Django Model 时:
|
||
|
||
```bash
|
||
cd backend
|
||
../.venv/bin/python manage.py makemigrations
|
||
../.venv/bin/python manage.py makemigrations --check --dry-run
|
||
../.venv/bin/python manage.py migrate
|
||
```
|
||
|
||
要求:
|
||
|
||
- 迁移文件必须随 Model 变更提交。
|
||
- 为唯一性、幂等性和高频查询使用数据库约束或索引。
|
||
- 高风险迁移采用“先扩展、后切换、再清理”。
|
||
- 不在同一发布中删除旧字段并立即依赖不可回退的新结构。
|
||
- 数据迁移必须可审查、可重复或明确记录一次性边界。
|
||
- 禁止直接复制 SQLite 文件、MySQL 数据目录或跨数据库 dump 作为迁移方案。
|
||
|
||
PR 描述中必须说明:
|
||
|
||
- 是否新增迁移
|
||
- 是否锁表或扫描大表
|
||
- 是否需要数据回填
|
||
- 应用代码如何兼容发布前后的 schema
|
||
- 回滚时数据库如何处理
|
||
|
||
## 12. 测试与质量门禁
|
||
|
||
### 最小本地检查
|
||
|
||
文档以外的代码改动至少运行:
|
||
|
||
```bash
|
||
.venv/bin/ruff check backend scripts
|
||
make check
|
||
make test
|
||
```
|
||
|
||
### 与 PR CI 对齐
|
||
|
||
```bash
|
||
.venv/bin/ruff check backend scripts
|
||
|
||
cd backend
|
||
../.venv/bin/python manage.py makemigrations --check --dry-run
|
||
../.venv/bin/python manage.py check
|
||
../.venv/bin/python -c \
|
||
"from config.asgi import application; print(type(application).__name__)"
|
||
cd ..
|
||
|
||
.venv/bin/python -m pytest -q \
|
||
--cov=backend \
|
||
--cov-config=.coveragerc \
|
||
--cov-report=term \
|
||
--cov-fail-under=75
|
||
```
|
||
|
||
Gitea CI 还会在隔离的 MySQL 8.0.35 容器中执行:
|
||
|
||
```bash
|
||
python backend/manage.py check --database default
|
||
python backend/manage.py check_mysql
|
||
python backend/manage.py migrate --noinput
|
||
pytest -q
|
||
```
|
||
|
||
### 测试原则
|
||
|
||
- Bug 修复必须先理解复现条件,并增加能防止回归的测试。
|
||
- Service 层规则优先写单元测试。
|
||
- API 权限和响应写请求测试。
|
||
- MySQL 锁、并发和事务行为不能只用 SQLite 测试。
|
||
- 前端缺陷至少增加静态资产断言;关键交互应进行浏览器验证。
|
||
- 用户流程、后台样式和响应式布局应附截图或录屏。
|
||
- 覆盖率是下限,不是目标;不要为了数字测试无意义代码。
|
||
|
||
## 13. PR 要求
|
||
|
||
PR 标题应描述结果,而不是过程:
|
||
|
||
```text
|
||
fix: prevent duplicate contest settlement
|
||
feat: add actuarial story import
|
||
```
|
||
|
||
PR 描述至少包含:
|
||
|
||
```markdown
|
||
## 背景
|
||
为什么需要修改。
|
||
|
||
## 变更
|
||
具体改变了什么行为和模块。
|
||
|
||
## 验证
|
||
执行了哪些测试,结果是什么。
|
||
|
||
## 数据与部署
|
||
是否有迁移、种子、环境变量、静态资源或回滚影响。
|
||
|
||
## 截图
|
||
涉及 UI 时提供修改前后截图。
|
||
```
|
||
|
||
提交 PR 后:
|
||
|
||
1. 等待 `CI / test` 全部通过。
|
||
2. 处理审查意见,不要无解释地关闭讨论。
|
||
3. 请求 Jacky 审查。
|
||
4. 只有 Jacky 明确批准后才可合并。
|
||
5. 合并后观察 `PR合并自动部署`:`release-check` 做无依赖轻量检查,`deploy` 执行生产发布与冒烟。
|
||
6. 部署失败时保留日志,先判断是代码、迁移、网络还是冒烟检查问题。
|
||
|
||
## 14. 安全与隐私
|
||
|
||
- 不记录真实密码、Cookie、Session、Token 或私钥。
|
||
- 日志中使用 Request ID,避免打印完整请求体和敏感字段。
|
||
- 最小化收集未成年人信息,不要求真实学校、姓名或精确年龄。
|
||
- 新的用户输入必须校验长度、类型和权限。
|
||
- 文件上传、富文本、外部 URL 和管理员批量操作需要单独安全评审。
|
||
- 不要通过前端隐藏代替后端权限控制。
|
||
- 不要关闭 CSRF、CORS、密码校验或生产安全检查来解决局部问题。
|
||
|
||
## 15. 内容贡献规范
|
||
|
||
剧情、数学人物和题目既是内容,也是生产数据。
|
||
|
||
### 剧情
|
||
|
||
- 使用版本化 Story 内容。
|
||
- 节点 ID 稳定且唯一。
|
||
- 每个 choice 的 `next` 必须存在。
|
||
- 结局节点不再提供 choice。
|
||
- 真实人物内容应列出事实来源和虚构边界。
|
||
- 不擅自改写已发布存档所依赖的版本。
|
||
|
||
### 题目
|
||
|
||
- 正确答案和解释属于服务端版本。
|
||
- 不把正式答案提前发送给未提交的客户端。
|
||
- 题号必须为正整数且不重复。
|
||
- 题目更新创建新版本,不覆盖历史正式尝试。
|
||
- 注意 Unicode 数学符号与普通 ASCII 输入的归一化边界。
|
||
|
||
### 视频与成长
|
||
|
||
- 五维能力固定为:眼光、人文、侦探、建模、联结。
|
||
- 视频完成奖励必须幂等。
|
||
- 内容筛选字段和能力映射保持后台、API、前端一致。
|
||
|
||
## 16. 不应出现在普通 PR 中的改动
|
||
|
||
除非任务明确要求,否则不要:
|
||
|
||
- 替换 Django、MySQL、Redis、Channels 或部署方案。
|
||
- 将模块化单体拆成微服务。
|
||
- 引入 Kubernetes。
|
||
- 新建长期并行的第二套前端或后端。
|
||
- 批量重写全部剧情和种子数据。
|
||
- 修改生产服务器路径、端口、systemd 服务名或 Gitea Secrets。
|
||
- 提交 `.env.production`、备份、数据库文件和运行日志。
|
||
- 手改 `backend/staticfiles/`。
|
||
- 重新运行生产种子命令覆盖运营数据。
|
||
- 降低覆盖率门槛、删除 Ruff 规则或跳过 MySQL CI。
|
||
|
||
需要做上述变更时,先提交设计说明并获得 Jacky 明确批准。
|
||
|
||
## 17. 完成定义
|
||
|
||
一个贡献只有同时满足以下条件才算完成:
|
||
|
||
- 需求行为已实现,且没有明显超出范围。
|
||
- 代码遵循现有模块边界。
|
||
- 数据约束、事务和幂等性得到处理。
|
||
- 测试覆盖新增行为和关键失败路径。
|
||
- Ruff、Django check、迁移检查和 pytest 通过。
|
||
- 涉及 MySQL、Redis、WebSocket 或部署时完成对应验证。
|
||
- 涉及 UI 时检查桌面端、移动端和颜色对比度。
|
||
- 文档、环境变量示例和迁移说明已同步。
|
||
- 没有提交秘密或生成垃圾。
|
||
- PR 已由 Jacky 审查并明确批准。
|
||
|
||
不确定时,不要猜测产品规则。把问题、已知事实、可选方案和影响写进 PR,请仓库负责人决策。
|