Files
app_03_pregnancy/docs/development-standards.md

12 KiB
Raw Permalink Blame History

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 新建应用步骤

  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)

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 的步骤:

  1. 在 api.ts 定义接口(export interface XxxResponse)
  2. 定义函数(export async function xxx(): Promise<T>),内部调 fetchApi<T>()
  3. 在页面中 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 + 编写前端页面
  • 启动后端 → 构建前端 → 部署 → 验证