Files
app_03_pregnancy/docs/development-standards.md

412 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# H5 小程序嵌入式应用开发模板
> 适用于嵌入微信小程序 web-view 的 H5 应用
> 技术栈:FastAPI + MongoDB (Beanie) + React 18 + React Router 7 + Vite 6
> 认证:小程序传 openid → 后端调 CRMEB 接口获取用户信息 → 自动补齐昵称/头像
---
## 一、项目架构
### 1.1 技术栈
| 层 | 技术 | 备注 |
|---|------|------|
| 前端 | React 18 + React Router 7 + Vite 6 | SPA,独立 basename |
| UI 库 | MUI 7 + shadcn/ui (Radix) + TailwindCSS 4 | |
| 状态 | React Context + localStorage 持久化 | 无 Redux/Zustand |
| 后端 | FastAPI + Uvicorn | |
| 数据库 | MongoDB (Motor + Beanie ODM) | |
| 认证 | openid → 后端调 CRMEB 内部接口 | 无 JWT 签名验证 |
### 1.2 目录结构
```
project/
├── frontend/
│ ├── src/app/
│ │ ├── pages/ # 页面组件
│ │ ├── components/ # 通用组件
│ │ ├── components/ui/ # 基础 UI 组件(shadcn/ui 风格)
│ │ ├── context/ # React Context
│ │ ├── services/ # API 服务 + debug 工具
│ │ └── data/ # 静态数据
│ │ ├── routes.tsx # 路由配置
│ │ └── App.tsx # 根组件
│ ├── package.json
│ └── dist/ # 构建产物
├── backend/
│ ├── app/
│ │ ├── routers/ # API 路由模块
│ │ ├── models/ # Beanie 数据模型
│ │ ├── services/ # 业务逻辑层
│ │ ├── middleware/ # 认证中间件
│ │ └── config.py # 配置
│ ├── requirements.txt
│ ├── serv.sh # 服务管理脚本
│ └── .env # 环境变量
└── docs/
└── development-standards.md
```
### 1.3 多应用同域名部署架构
```
同一个 Nginx 下代理多个 H5 应用,每个应用独立后端进程:
https://mp.yizhezhe.com/
├── /app_03/ → 前端 SPA(basename=/app_03)
│ └── /app_03/api/ → proxy_pass localhost:8030/api/
├── /baby_food/ → 前端 SPA
│ └── /baby_food/api/ → proxy_pass localhost:8031/api/
└── /album/ → 前端 SPA
└── /album/api/ → proxy_pass localhost:8032/api/
```
### 1.4 Nginx 配置模板
```nginx
server {
listen 443 ssl;
server_name mp.yizhezhe.com;
# ─── 应用: app_03 ───
location = /app_03 { return 301 /app_03/; }
location /app_03/ {
alias /www/app_03/frontend/dist/;
try_files $uri $uri/ /app_03/index.html;
}
location /app_03/api/ {
proxy_pass http://127.0.0.1:8030/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# ─── 应用: baby_food ───
location = /baby_food { return 301 /baby_food/; }
location /baby_food/ {
alias /www/baby_food/frontend/dist/;
try_files $uri $uri/ /baby_food/index.html;
}
location /baby_food/api/ {
proxy_pass http://127.0.0.1:8031/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
> `location = /app_03` 处理无尾随斜杠的 301 跳转。
> `location /app_03/api/` proxy_pass 带尾斜杠 = 匹配部分被完整替换。
### 1.5 新建应用步骤
1. 定义项目标识(全小写 snake_case,如 `baby_food`)
2. 复制模板 → 修改 `config.py` → `app_name`, `mongodb_db_name`, `port`
3. 修改前端 basename:Vite `base` + React Router `basename` + `APP_BASE_URL`
4. 添加 Nginx location 块(前端 + API 代理)
5. 定义数据模型 → 编写 API → 编写前端页面
6. 启动验证
---
## 二、认证规范
### 2.1 认证流程
```
小程序 URL: ?openid=oauWC1zc7hVn3j60QwkuHyjGn03Y
↓
H5 LoginPage → 前端无 token 处理
↓
POST /api/auth/openid-login { openid }
↓
app_03 后端 → GET CRMEB /api/internal/user-info?openid=xxx
↓ ↓ (X-Internal-Key 验证)
{ nickname, phone, avatar }
↓
创建/更新 User(MongoDB)
↓
返回自签 JWT token
↓
setTokenAndUser → fetchProfile → 跳转首页/引导页
```
### 2.2 登录流程(LoginPage.tsx)
```typescript
const openidParam = params.get('openid');
if (openidParam && openidParam.trim()) {
const res = await fetchApi("/auth/openid-login", {
method: "POST",
body: JSON.stringify({ openid: openidParam }),
});
setTokenAndUser(res.token, { id: res.user_id, openid: res.openid, phone: res.mobile });
// fetchProfile 自动拿到 name/avatar_url(已从 CRMEB 同步)
}
```
**小程序端 URL 格式:**
```javascript
const url = 'https://mp.yizhezhe.com/app_03/?openid=' + encodeURIComponent(openid);
```
### 2.3 后端 openid-login 接口(routers/auth.py)
```python
@router.post("/openid-login")
async def openid_login(req: OpenidRequest) -> TokenResponse:
# 1. 调 CRMEB 内部接口获取用户信息
async with httpx.AsyncClient() as client:
resp = await client.get(
f"{settings.crmeb_base_url}/api/internal/user-info",
params={"openid": req.openid},
headers={"X-Internal-Key": settings.crmeb_api_key},
)
info = resp.json()["data"]
# 2. 查找或创建用户(昵称/手机号/头像自动同步)
user = await User.find_one(User.openid == req.openid)
if not user:
user = User(openid=req.openid, name=info["nickname"], ...)
await user.insert()
else:
user.name = info["nickname"]; user.phone = info["phone"]
await user.save()
return TokenResponse(token=_build_token(user), ...)
```
### 2.4 认证中间件(middleware/auth.py)
```python
async def get_current_user(credentials) -> User:
payload = jwt.decode(token, None,
options={"verify_signature": False, "verify_exp": False})
openid = payload.get("openid")
# 手动验证 exp
user = await User.find_one(User.openid == openid)
if not user: 创建 User(openid, phone)
else: 更新 phone 等
return user
```
- 不验证 JWT 签名(`verify_signature: False`),信任自身生成或小程序传入的 token
- 手动验证 `exp` 过期
- 按 openid 查找/创建用户
### 2.5 DEV_MODE
- 环境变量 `DEV_MODE=1` 启用开发模式
- 仅 `POST /api/auth/dev-login` 受此控制
- 测试账号:openid=`dev_test_account_001`, phone=`13800000000`
- 启动方式:`DEV_MODE=1 uvicorn app.main:app --port 8030 --reload`
---
## 三、前端规范
### 3.1 API 调用(services/api.ts)
```typescript
/** 通用 API 请求封装(已 export) */
export async function fetchApi<T>(path: string, options: RequestInit = {}): Promise<T> {
const token = getToken();
const headers: Record<string, string> = { "Content-Type": "application/json" };
if (token) headers["Authorization"] = `Bearer ${token}`;
const res = await fetch(apiPath(path), { ...options, headers });
if (res.status === 401) { clearToken(); window.location.href = APP_BASE_URL; }
if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.detail || `请求失败 (${res.status})`); }
return res.json();
}
```
**新增 API 的步骤:**
1. 在 `api.ts` 定义接口(`export interface XxxResponse`)
2. 定义函数(`export async function xxx(): Promise<T>`),内部调 `fetchApi<T>()`
3. 在页面中 import 调用
### 3.2 调试模块(services/debug.ts)
```typescript
// VITE_DEBUG=true 时启用调试日志和面板
export function isDebug(): boolean
export function debugLog(msg: string): void // 写入 localStorage
export function getDebugLogs(): string[] // 读取日志
export function clearDebugLogs(): void
```
- 由 `.env` 中 `VITE_DEBUG=true` 控制
- 开发环境默认开启,生产环境设为 false 关闭
- LoginPage 自动记录登录流程
- ProfilePage 显示「展开调试日志」按钮
### 3.3 路由与守卫
```typescript
// routes.tsx
function ProtectedRoute() {
const token = getToken();
// 无 token → Navigate to /?redirect=当前路径
if (!token) return <Navigate to={`/?redirect=...`} replace />;
return <MobileLayout />;
}
```
路由结构:
```
/ → LoginPage(无保护)
/onboarding → 引导页(登录后首次使用)
/home → 首页(保护)
/xxx → 其他业务页面(保护)
```
### 3.4 状态管理(UserContext)
```typescript
interface BasicUserInfo {
id: string;
openid: string;
phone: string;
}
```
- `BasicUserInfo` 持久化到 `localStorage['user_basic']`
- 切换用户时自动清理旧数据
### 3.5 前端路径规范
| 配置项 | 位置 | 值 |
|--------|------|----|
| Vite `base` | `vite.config.ts` | `'/app_03/'` |
| React Router `basename` | `routes.tsx` | `'/app_03'` |
| `VITE_API_PREFIX` | `.env` | `'/app_03/api'` |
**`.env` 配置:**
```ini
VITE_API_PREFIX=/app_03/api
VITE_UPLOADS_PREFIX=/uploads
VITE_DEBUG=true
```
**Vite proxy(开发环境):**
```typescript
server: {
proxy: {
[apiPrefix]: { // "/app_03/api"
target: 'http://localhost:8030',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/app_03\/api/, '/api'),
},
},
}
```
### 3.6 组件规范
- 页面组件 `pages/XxxPage.tsx`,业务组件 `components/`
- 基础 UI 组件 `components/ui/`(shadcn/ui 风格)
- 函数式组件 + Hooks,中文注释
- 动画用 `motion`(framer-motion),图标用 `lucide-react`
---
## 四、后端规范
### 4.1 路由与 API 前缀
| 应用 | API 路径 | 后端端口 |
|------|---------|---------|
| 孕育无忧 | `/app_03/api/*` | 8030 |
| 婴儿辅食 | `/baby_food/api/*` | 8031 |
后端统一注册:
```python
# routers/__init__.py
router = APIRouter(prefix="/api")
router.include_router(auth.router) # /api/auth/xxx
router.include_router(user.router) # /api/user/xxx
```
### 4.2 数据库命名
```
app_03 / baby_food / album # 全小写 snake_case = 项目标识
```
同一 MongoDB 实例下各应用数据库平级,不交叉。
### 4.3 数据模型
```python
class User(Document):
openid: str = Field(..., unique=True) # 用户唯一标识
name: str = ""
phone: Optional[str] = None
avatar_url: Optional[str] = None
is_onboarded: bool = False
```
> User 仅作为认证底座。业务数据可新建 Document 或嵌入 User。
### 4.4 `.env` 完整配置
```ini
# MongoDB
MONGODB_URL=mongodb://localhost:27017
MONGODB_DB_NAME=app_03_dev
# JWT
JWT_SECRET_KEY=change-this-secret-key
# CRMEB 内部接口
CRMEB_BASE_URL=https://shop.douba.top
CRMEB_API_KEY=sk_...
# Server
HOST=0.0.0.0
PORT=8030
DEV_MODE=1
```
---
## 五、CRMEB 内部接口规范
### 5.1 接口定义
```
GET /api/internal/user-info?openid=xxx
Header: X-Internal-Key: xxx
```
### 5.2 响应格式
```json
{
"code": 200,
"data": {
"uid": 7,
"nickname": "枫逸",
"phone": "13693687880",
"avatar": "https://..."
}
}
```
### 5.3 认证方式
服务间共享密钥,通过 `X-Internal-Key` Header 传递。密钥存入 CRMEB 配置文件或系统设置。
---
## 六、初始化新应用 Checklist
- [ ] 定义项目标识,三处保持一致:Vite base / basename / 数据库名
- [ ] 复制模板 → 修改 `config.py` → `app_name`、`mongodb_db_name`、`port`
- [ ] 配置 `.env`:MongoDB / JWT / CRMEB(如果有)/ CORS
- [ ] 添加 Nginx location 块(`location = /xxx` + `location /xxx/` + `location /xxx/api/`)
- [ ] 定义业务数据模型 + 编写 API + 编写前端页面
- [ ] 启动后端 → 构建前端 → 部署 → 验证