forked from wangziqi/gongxue-base
docs: organize legacy reference project
This commit is contained in:
@@ -35,6 +35,7 @@
|
|||||||
- `docs/refactor/backend-handoff-roadmap.md`
|
- `docs/refactor/backend-handoff-roadmap.md`
|
||||||
- `docs/refactor/content-import-contract.md`
|
- `docs/refactor/content-import-contract.md`
|
||||||
- `docs/refactor/object-storage.md`
|
- `docs/refactor/object-storage.md`
|
||||||
|
- `docs/refactor/project-structure.md`
|
||||||
- `docs/refactor/next-development-todo.md`
|
- `docs/refactor/next-development-todo.md`
|
||||||
- `docs/refactor/blueprint-coverage.md`
|
- `docs/refactor/blueprint-coverage.md`
|
||||||
- `docs/refactor/api-structure.md`
|
- `docs/refactor/api-structure.md`
|
||||||
|
|||||||
123
docs/refactor/project-structure.md
Normal file
123
docs/refactor/project-structure.md
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
# 项目结构说明
|
||||||
|
|
||||||
|
更新时间:2026-06-28
|
||||||
|
|
||||||
|
这份文档用于区分新 Supabase/PostgreSQL 重构项目和旧 PocketBase 题库参考项目,避免后续提交 Gitea 时把旧项目文件混入新仓库。
|
||||||
|
|
||||||
|
## 当前仓库原则
|
||||||
|
|
||||||
|
- Git 跟踪的新项目代码,是商用 SaaS 重构后的后端、数据库迁移、导入工具、测试脚本和重构文档。
|
||||||
|
- 旧 PocketBase/React 题库项目只作为功能对照、数据迁移参考和前端样式参考,不作为当前 Gitea 仓库的源码主体。
|
||||||
|
- 旧项目参考文件集中放在 `参考/旧题库项目/`,该目录被 `.gitignore` 忽略,不会进入提交。
|
||||||
|
|
||||||
|
## 新重构项目目录
|
||||||
|
|
||||||
|
```text
|
||||||
|
F:\project
|
||||||
|
apps/
|
||||||
|
api/ Node.js 业务 API
|
||||||
|
src/
|
||||||
|
core/ 配置、HTTP、路由、数据库访问
|
||||||
|
features/ 业务模块
|
||||||
|
auth/ 迁移期登录、短信 mock、OAuth 占位
|
||||||
|
catalog/ 学生端目录、题库、资料、商品只读接口
|
||||||
|
commerce/ 订单、支付确认、激活码、权益
|
||||||
|
health/ 健康检查
|
||||||
|
learning/ 练习、答题、错题、收藏、单词进度
|
||||||
|
platform-admin/ 平台租户、套餐、账单、用量
|
||||||
|
profile/ 学生个人中心
|
||||||
|
referral/ 销售/代理/CRM 增长链路
|
||||||
|
scoreline/ 分数线
|
||||||
|
storage/ 对象存储签名 provider
|
||||||
|
tenant/ 租户解析
|
||||||
|
tenant-admin/ 租户后台配置、成员权限、审计
|
||||||
|
tenant-content/ 租户内容后台、导入、资源台账
|
||||||
|
video/ 题目视频讲解
|
||||||
|
types/ 第三方 SDK 窄类型声明
|
||||||
|
Dockerfile
|
||||||
|
package.json
|
||||||
|
tsconfig.json
|
||||||
|
|
||||||
|
packages/
|
||||||
|
config/ 共享配置和 env 工具
|
||||||
|
db/ PostgreSQL 连接池和查询封装
|
||||||
|
domain/ 领域常量和共享类型
|
||||||
|
|
||||||
|
supabase/
|
||||||
|
migrations/ PostgreSQL schema、RLS、索引、触发器
|
||||||
|
seed.sql 本地最小 seed
|
||||||
|
config.toml Supabase local 配置
|
||||||
|
|
||||||
|
scripts/
|
||||||
|
import-pocketbase/ PocketBase schema/数据导入器和校验器
|
||||||
|
api-integration-test.js API 集成测试
|
||||||
|
smoke-seed.js 本地 smoke seed
|
||||||
|
smoke-core-api.js 轻量核心 API 烟测
|
||||||
|
|
||||||
|
docs/
|
||||||
|
pb_schema.json 旧 PocketBase schema 输入文件
|
||||||
|
refactor/ 新架构、进度、交付和 TODO 文档
|
||||||
|
|
||||||
|
docker-compose.api.yml API 容器运行配置
|
||||||
|
package.json 工作区脚本入口
|
||||||
|
README.md 中文项目总览
|
||||||
|
```
|
||||||
|
|
||||||
|
## 旧项目参考目录
|
||||||
|
|
||||||
|
旧项目已经整理到:
|
||||||
|
|
||||||
|
```text
|
||||||
|
F:\project\参考\旧题库项目
|
||||||
|
```
|
||||||
|
|
||||||
|
其中主要内容:
|
||||||
|
|
||||||
|
```text
|
||||||
|
参考/旧题库项目/
|
||||||
|
src/ 旧 React/Vite 前端
|
||||||
|
pb_hooks/ 旧 PocketBase hooks
|
||||||
|
pb_migrations/ 旧 PocketBase migrations
|
||||||
|
docs/ 旧项目功能、导入、对接、部署文档
|
||||||
|
public/ 旧前端静态资源
|
||||||
|
scripts/ 旧项目迁移、部署、统计、卫星站脚本
|
||||||
|
setup/ 旧项目安装配置
|
||||||
|
DEPLOY.md 旧宝塔/PocketBase 部署说明
|
||||||
|
index.html 旧 Vite 入口
|
||||||
|
vite.config.ts 旧 Vite 配置
|
||||||
|
tailwind.config.js 旧 Tailwind 配置
|
||||||
|
tsconfig.json 旧前端 TS 配置
|
||||||
|
```
|
||||||
|
|
||||||
|
## 后续提交规范
|
||||||
|
|
||||||
|
提交前建议先看:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short --branch
|
||||||
|
```
|
||||||
|
|
||||||
|
正常情况下,后续提交应只包含这些路径:
|
||||||
|
|
||||||
|
- `apps/api/**`
|
||||||
|
- `packages/**`
|
||||||
|
- `supabase/**`
|
||||||
|
- `scripts/import-pocketbase/**`
|
||||||
|
- `scripts/api-integration-test.js`
|
||||||
|
- `scripts/smoke-seed.js`
|
||||||
|
- `scripts/smoke-core-api.js`
|
||||||
|
- `docs/refactor/**`
|
||||||
|
- `docs/pb_schema.json`
|
||||||
|
- 根目录的 `.env.example`、`.gitignore`、`README.md`、`package.json`、`package-lock.json`、`docker-compose.api.yml`
|
||||||
|
|
||||||
|
如果看到 `参考/旧题库项目/**`,说明 `.gitignore` 被改坏了,必须先修复再提交。
|
||||||
|
|
||||||
|
## 前端重构建议
|
||||||
|
|
||||||
|
后续 Taro 前端建议新建:
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/taro/
|
||||||
|
```
|
||||||
|
|
||||||
|
不要把旧 `src/` 重新搬回根目录继续开发。旧前端只作为视觉、页面、交互和字段迁移参考;新 Taro 应统一调用 `apps/api`,并把跨端 API client、租户解析、主题配置、登录、支付、资料下载和刷题链路放在新工程内。
|
||||||
117
scripts/smoke-core-api.js
Normal file
117
scripts/smoke-core-api.js
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
const apiBase = process.env.API_BASE || 'http://127.0.0.1:8787';
|
||||||
|
const tenantId = process.env.TENANT_ID || '00000000-0000-0000-0000-000000000001';
|
||||||
|
const userId = process.env.USER_ID || '00000000-0000-0000-0000-000000000101';
|
||||||
|
|
||||||
|
const ids = {
|
||||||
|
region: '00000000-0000-0000-0000-000000000301',
|
||||||
|
question: '00000000-0000-0000-0000-000000000401',
|
||||||
|
vocabularyUnit: '00000000-0000-0000-0000-000000000811',
|
||||||
|
vocabularyWord: '00000000-0000-0000-0000-000000000812',
|
||||||
|
scorelineSchool: '00000000-0000-0000-0000-000000000831',
|
||||||
|
};
|
||||||
|
|
||||||
|
function url(path, query = {}) {
|
||||||
|
const target = new URL(path, apiBase);
|
||||||
|
for (const [key, value] of Object.entries(query)) {
|
||||||
|
if (value !== undefined && value !== null && value !== '') {
|
||||||
|
target.searchParams.set(key, String(value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function request(path, options = {}) {
|
||||||
|
const response = await fetch(url(path, options.query), {
|
||||||
|
method: options.method || 'GET',
|
||||||
|
headers: {
|
||||||
|
'content-type': 'application/json',
|
||||||
|
'x-tenant-id': tenantId,
|
||||||
|
'x-user-id': userId,
|
||||||
|
...(options.headers || {}),
|
||||||
|
},
|
||||||
|
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const payload = await response.json().catch(() => ({}));
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${options.method || 'GET'} ${path} failed: ${response.status} ${JSON.stringify(payload)}`);
|
||||||
|
}
|
||||||
|
return payload;
|
||||||
|
}
|
||||||
|
|
||||||
|
function assert(condition, message) {
|
||||||
|
if (!condition) throw new Error(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function check(name, fn) {
|
||||||
|
const result = await fn();
|
||||||
|
console.log(`[PASS] ${name}`);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await check('health', async () => {
|
||||||
|
const payload = await request('/health');
|
||||||
|
assert(payload.ok === true, 'health response should be ok');
|
||||||
|
});
|
||||||
|
|
||||||
|
await check('profile me', async () => {
|
||||||
|
const payload = await request('/api/profile/me');
|
||||||
|
assert(payload.item?.userId === userId, 'profile should belong to smoke user');
|
||||||
|
assert(payload.item?.stats?.vocabulary?.totalWords >= 1, 'profile should include vocabulary stats');
|
||||||
|
});
|
||||||
|
|
||||||
|
await check('scoreline fields', async () => {
|
||||||
|
const payload = await request('/api/scoreline/fields', { query: { regionId: ids.region } });
|
||||||
|
assert(payload.items?.some(item => item.fieldKey === 'minScore'), 'scoreline fields should include minScore');
|
||||||
|
});
|
||||||
|
|
||||||
|
await check('scoreline schools and records', async () => {
|
||||||
|
const schools = await request('/api/scoreline/schools', { query: { regionId: ids.region } });
|
||||||
|
assert(schools.items?.some(item => item.id === ids.scorelineSchool), 'scoreline school should exist');
|
||||||
|
|
||||||
|
const records = await request('/api/scoreline/records', { query: { regionId: ids.region, pageSize: 5 } });
|
||||||
|
assert(records.total >= 1, 'scoreline records should have data');
|
||||||
|
|
||||||
|
const years = await request('/api/scoreline/years', { query: { regionId: ids.region } });
|
||||||
|
assert(years.items?.includes(2026), 'scoreline years should include 2026');
|
||||||
|
});
|
||||||
|
|
||||||
|
await check('question videos', async () => {
|
||||||
|
const single = await request(`/api/questions/${ids.question}/videos`);
|
||||||
|
assert(single.total >= 1, 'question should have videos');
|
||||||
|
|
||||||
|
const batch = await request('/api/questions/videos/batch', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { questionIds: [ids.question] },
|
||||||
|
});
|
||||||
|
assert(batch.data?.[ids.question]?.hasVideo === true, 'batch video lookup should mark question as having video');
|
||||||
|
});
|
||||||
|
|
||||||
|
await check('vocabulary progress and favorite', async () => {
|
||||||
|
const stats = await request('/api/learning/vocabulary/stats', { query: { unitId: ids.vocabularyUnit } });
|
||||||
|
assert(stats.item?.totalWords >= 1, 'word stats should count smoke word');
|
||||||
|
|
||||||
|
const progress = await request('/api/learning/vocabulary/progress', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { userId, wordId: ids.vocabularyWord, status: 'mastered', correctDelta: 1 },
|
||||||
|
});
|
||||||
|
assert(progress.item?.status === 'mastered', 'word progress should update to mastered');
|
||||||
|
|
||||||
|
const favorite = await request('/api/learning/vocabulary/favorites', {
|
||||||
|
method: 'POST',
|
||||||
|
body: { userId, wordId: ids.vocabularyWord, favorite: true },
|
||||||
|
});
|
||||||
|
assert(favorite.favorite === true, 'word favorite should be true');
|
||||||
|
|
||||||
|
const favorites = await request('/api/learning/vocabulary/favorites', { query: { unitId: ids.vocabularyUnit } });
|
||||||
|
assert(favorites.items?.some(item => item.wordId === ids.vocabularyWord), 'favorite list should include smoke word');
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('Core API smoke complete.');
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error(error);
|
||||||
|
process.exitCode = 1;
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user