12 KiB
12 KiB
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 配置模板
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 新建应用步骤
- 定义项目标识(全小写 snake_case,如
baby_food) - 复制模板 → 修改
config.py→app_name,mongodb_db_name,port - 修改前端 basename:Vite
base+ React Routerbasename+APP_BASE_URL - 添加 Nginx location 块(前端 + API 代理)
- 定义数据模型 → 编写 API → 编写前端页面
- 启动验证
二、认证规范
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)
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 格式:
const url = 'https://mp.yizhezhe.com/app_03/?openid=' + encodeURIComponent(openid);
2.3 后端 openid-login 接口(routers/auth.py)
@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)
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)
/** 通用 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 的步骤:
- 在
api.ts定义接口(export interface XxxResponse) - 定义函数(
export async function xxx(): Promise<T>),内部调fetchApi<T>() - 在页面中 import 调用
3.2 调试模块(services/debug.ts)
// 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 路由与守卫
// 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)
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 配置:
VITE_API_PREFIX=/app_03/api
VITE_UPLOADS_PREFIX=/uploads
VITE_DEBUG=true
Vite proxy(开发环境):
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 |
后端统一注册:
# 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 数据模型
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 完整配置
# 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 响应格式
{
"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 + 编写前端页面
- 启动后端 → 构建前端 → 部署 → 验证