412 lines
12 KiB
Markdown
412 lines
12 KiB
Markdown
# 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 + 编写前端页面
|
||
- [ ] 启动后端 → 构建前端 → 部署 → 验证
|