# 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(path: string, options: RequestInit = {}): Promise { const token = getToken(); const headers: Record = { "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`),内部调 `fetchApi()` 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 ; return ; } ``` 路由结构: ``` / → 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 + 编写前端页面 - [ ] 启动后端 → 构建前端 → 部署 → 验证