From 80904e65d89c3a4fb1ad068b51c09fe10847da0c Mon Sep 17 00:00:00 2001 From: Karlcarlo Date: Wed, 27 May 2026 01:49:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E9=80=BB=E8=BE=91=EF=BC=8C=E6=94=AF=E6=8C=81=E9=80=9A=E8=BF=87?= =?UTF-8?q?openid=E6=9F=A5=E6=89=BE=E6=88=96=E5=88=9B=E5=BB=BA=E7=94=A8?= =?UTF-8?q?=E6=88=B7=EF=BC=8C=E5=B9=B6=E8=B0=83=E6=95=B4JWT=E8=B4=9F?= =?UTF-8?q?=E8=BD=BD=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app/middleware/auth.py | 42 +- backend/app/routers/auth.py | 7 +- docs/development-standards.md | 582 +++++++++++++++++++++++++++ frontend/package-lock.json | 18 + frontend/src/app/pages/LoginPage.tsx | 4 +- frontend/src/app/services/api.ts | 4 +- 6 files changed, 641 insertions(+), 16 deletions(-) create mode 100644 docs/development-standards.md diff --git a/backend/app/middleware/auth.py b/backend/app/middleware/auth.py index fbebc83..b74a2fa 100644 --- a/backend/app/middleware/auth.py +++ b/backend/app/middleware/auth.py @@ -21,16 +21,40 @@ async def get_current_user( ) -> User: token = credentials.credentials try: + # 不验证签名(由小程序侧保证),仅解码 payload 获取用户信息 payload = jwt.decode( - token, settings.jwt_secret_key, algorithms=[settings.jwt_algorithm] + token, + None, + options={"verify_signature": False, "verify_exp": False}, ) - user_id: str = payload.get("sub") - if user_id is None: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证信息") - except JWTError: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token已过期或无效") + openid = payload.get("openid") + if not openid: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="无效的认证信息" + ) + + # 手动验证过期 + exp = payload.get("exp") + if exp and datetime.now(timezone.utc).timestamp() > exp: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Token已过期" + ) + + except JWTError: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, detail="Token无效" + ) + + # 按 openid 查找或创建用户 + user = await User.find_one(User.openid == openid) + if not user: + phone = payload.get("phone", "") + user = User(openid=openid, phone=phone or None) + await user.insert() + else: + phone = payload.get("phone", "") + if phone and phone != (user.phone or ""): + user.phone = phone + await user.save() - user = await User.get(user_id) - if user is None: - raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在") return user diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py index 963cf19..0903256 100644 --- a/backend/app/routers/auth.py +++ b/backend/app/routers/auth.py @@ -31,13 +31,14 @@ class TokenResponse(BaseModel): def _build_token(user: User) -> str: - """生成自包含 JWT:sub + openid + mobile""" + """生成自包含 JWT:uid + openid + phone(兼容外部 token 格式)""" expire = datetime.now(timezone.utc) + timedelta(days=30) payload = { - "sub": str(user.id), + "uid": str(user.id), "openid": user.openid, - "mobile": user.phone or "", + "phone": user.phone or "", "exp": expire, + "iss": "localhost", } return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm) diff --git a/docs/development-standards.md b/docs/development-standards.md new file mode 100644 index 0000000..f71759e --- /dev/null +++ b/docs/development-standards.md @@ -0,0 +1,582 @@ +# H5 小程序嵌入式应用开发模板 + +> 适用于嵌入微信小程序 web-view 的 H5 应用 +> 技术栈:FastAPI + MongoDB (Beanie) + React 18 + React Router 7 + Vite 6 +> 认证完全依赖小程序侧 JWT + +--- + +## 一、项目架构 + +### 1.1 技术栈 + +| 层 | 技术 | 备注 | +|---|------|------| +| 前端 | React 18 + React Router 7 + Vite 6 | SPA,独立 basename | +| UI 库 | 自选(MUI / shadcn/ui / TailwindCSS) | | +| 状态 | React Context + localStorage 持久化 | 无 Redux/Zustand | +| 后端 | FastAPI + Uvicorn | | +| 数据库 | MongoDB (Motor + Beanie ODM) | | +| 认证 | JWT(外部传入,不验证签名) | python-jose | + +### 1.2 目录结构 + +``` +project/ +├── frontend/ +│ ├── src/app/ +│ │ ├── pages/ # 页面组件 +│ │ ├── components/ # 通用组件 +│ │ ├── components/ui/ # 基础 UI 组件(shadcn/ui 风格) +│ │ ├── context/ # React Context +│ │ ├── services/ # API 服务 +│ │ └── 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/ → 孕育无忧 H5 +│ ├── → index.html(前端 SPA,basename=/app_03) +│ └── /api/ → proxy_pass localhost:8030 +├── /baby_food/ → 婴儿辅食 H5 +│ ├── → index.html(前端 SPA,basename=/baby_food) +│ └── /api/ → proxy_pass localhost:8031 +└── /album/ → 宝宝相册 H5 + ├── → index.html(前端 SPA,basename=/album) + └── /api/ → proxy_pass localhost:8032 +``` + +每个应用是一个独立项目(独立 repo 或独立目录),拥有自己的: +- 前端构建产物(`dist/`) +- 后端服务进程(不同端口) +- 数据库(不同 MongoDB database) +- `.env` 和环境变量 + +### 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/ { + rewrite ^/app_03(/api/.*)$ $1 break; + proxy_pass http://127.0.0.1:8030; + 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/ { + rewrite ^/baby_food(/api/.*)$ $1 break; + proxy_pass http://127.0.0.1:8031; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +> location = /xxx 必须带,否则 /app_03 无尾随斜杠时返回 404。 + +### 1.5 新建应用步骤 + +1. 复制模板目录,重命名项目 +2. 修改 `backend/app/config.py` → `app_name`, `mongodb_db_name`, `port` +3. 修改前端 basename(Vite `base` + React Router `basename`) +4. 定义数据模型(`backend/app/models/`) +5. 编写业务 API(`backend/app/routers/`) +6. 编写前端页面 + 路由 +7. 添加 Nginx location 块 +8. 启动验证 + +--- + +## 二、认证规范 + +### 2.1 认证流程 + +``` +小程序 → {token(JWT)} → URL params → H5 LoginPage + ↓ + decodeToken(JWT) 提取 uid/openid/phone + ↓ + localStorage 存储 token + user_basic + ↓ + 后端 API 请求带 Authorization: Bearer + ↓ + get_current_user 解码 JWT → 按 openid 查找/创建用户 +``` + +### 2.2 JWT 格式 + +**前端 decodeToken() 解析出的字段:** +```typescript +interface TokenPayload { + uid: number; // 小程序侧的用户 ID + openid: string; // 微信 openid + phone: string; // 手机号(可能为空) + exp: number; // 过期时间戳 +} +``` + +**后端自签 token(dev-login 兼容,同格式):** +```json +{ "uid": "...", "openid": "...", "phone": "...", "exp": ..., "iss": "localhost" } +``` + +### 2.3 认证中间件规则 + +```python +# middleware/auth.py +async def get_current_user(credentials: HTTPAuthorizationCredentials) -> 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 = User(openid=openid, phone=phone or None) + await user.insert() + else: + # 有 phone 且变化时更新 + return user +``` + +- 不验证 JWT 签名(`verify_signature: False`),信任小程序侧 +- 手动验证 `exp` 过期 +- 提取 `openid` → 按 openid 查找/创建用户 +- `create_access_token()` 保持原样,兼容旧逻辑 + +### 2.4 DEV_MODE + +- 环境变量 `DEV_MODE=1` 启用开发模式 +- 仅 `/api/auth/dev-login` 受此控制 +- 测试账号固定 openid,在浏览器开发时自动登录 +- 启动方式:`DEV_MODE=1 uvicorn app.main:app --port 8030 --reload` + +### 2.5 前端登录流程(LoginPage.tsx 模板) + +```typescript +const params = new URLSearchParams(window.location.search); +const tokenFromUrl = params.get('token'); + +if (tokenFromUrl) { + const payload = decodeToken(tokenFromUrl); + setTokenAndUser(tokenFromUrl, { + id: String(payload.uid), + openid: payload.openid, + phone: payload.phone, + }); + // 拉取用户数据 → 跳转首页或引导页 + fetchProfile().then((dto) => { + navigate(dto?.is_onboarded ? "/home" : "/onboarding", { replace: true }); + }); +} else if (isWechatMiniProgram()) { + // 通知小程序传递 token + window.wx?.miniProgram?.postMessage({ type: "NEED_TOKEN" }); +} else { + // 浏览器开发环境 → dev-login + devLogin().then(res => setTokenAndUser(res.token, { ... })); +} +``` + +--- + +## 三、前端规范 + +### 3.1 API 调用 + +```typescript +// services/api.ts +const TOKEN_KEY = "auth_token"; + +export function getToken(): string | null { + return localStorage.getItem(TOKEN_KEY); +} + +export function setToken(token: string) { + localStorage.setItem(TOKEN_KEY, token); +} + +export function clearToken() { + localStorage.removeItem(TOKEN_KEY); +} + +/** 通用 API 请求封装 */ +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`) +3. 在页面中 import 调用 + +### 3.2 路由与守卫 + +```typescript +// routes.tsx +function ProtectedRoute() { + const token = getToken(); + const urlToken = new URLSearchParams(window.location.search).get('token'); + if (urlToken) { + return ; + } + if (!token) { + return ; + } + return ; +} +``` + +路由结构: +``` +/ → LoginPage(无保护) +/onboarding → 引导页(登录后首次使用) +/home → 首页(保护) +/xxx → 其他业务页面(保护) +``` + +### 3.3 状态管理(Context 模板) + +```typescript +// context/UserContext.tsx +export interface BasicUserInfo { + id: string; + openid: string; + phone: string; +} + +interface UserContextValue { + user: UserProfile; // 由具体业务定义 + token: string | null; + setTokenAndUser: (token: string, info: BasicUserInfo) => void; + fetchProfile: () => Promise; + logout: () => void; + // + 业务自定义方法 +} +``` + +- `BasicUserInfo` (`id`/`openid`/`phone`) 持久化到 `localStorage['user_basic']` +- 用户业务数据持久化到各自 key +- 切换用户时自动清理旧数据 + +### 3.4 组件规范 + +- 页面组件统一放在 `pages/`,命名 `XxxPage.tsx` +- 通用业务组件在 `components/` +- 基础 UI 组件在 `components/ui/`(shadcn/ui 风格,按需引入) +- 函数式组件 + Hooks,class 组件禁用 +- `react-hooks/exhaustive-deps` 可 eslint-disable(mount-once 模式) +- 动画推荐 `framer-motion`(包名 `motion`),图标推荐 `lucide-react` + +### 3.5 编码风格 + +- 中文注释 +- `// ─── Section ───` 大段视觉分隔 +- 不需要过度的错误处理,完整 traceback 就是调试信息 +- 复杂逻辑封装到工具类/服务层,页面保持精简 + +### 3.6 前端路径规范(多应用部署) + +每个 H5 应用的 basename 由两部分确定: + +| 配置项 | 位置 | 值 | +|--------|------|----| +| Vite `base` | `frontend/vite.config.ts` | `'/app_03/'` | +| React Router `basename` | `routes.tsx` → `createBrowserRouter(routes, {basename: '/app_03'})` | `'/app_03'` | +| fetch API 前缀 | `api.ts` → `APP_BASE_URL` | `'/app_03'` | + +**basename 命名规范:** `/{项目标识}`,全小写 + 下划线(`snake_case`)。 + +**配置示例(vite.config.ts):** +```typescript +export default defineConfig({ + base: '/app_03/', // 构建产物路径前缀 + plugins: [react()], + server: { port: 5173 }, +}) +``` + +**配置示例(routes.tsx):** +```typescript +const router = createBrowserRouter(routes, { + basename: '/app_03', +}); +``` + +**api.ts 中的 base URL 自动拼接:** +```typescript +const APP_BASE_URL = '/app_03'; // 由环境变量 VITE_APP_BASE_URL 控制 +function joinPath(prefix: string, path: string) { + const p = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix; + const s = path.startsWith('/') ? path : `/${path}`; + return `${p}${s}`; +} +// 最终请求: /app_03/api/xxx +``` + +--- + +## 四、后端规范 + +### 4.1 路由与 API 前缀规范 + +每个应用的 API 通过 Nginx 统一代理,路径结构为 `/{basename}/api/*`。 + +| 应用 | 前端路径 | API 路径 | 后端端口 | +|------|---------|---------|---------| +| 孕育无忧 | `/app_03/` | `/app_03/api/*` | 8030 | +| 婴儿辅食 | `/baby_food/` | `/baby_food/api/*` | 8031 | +| 宝宝相册 | `/album/` | `/album/api/*` | 8032 | + +**后端路由注册:** +```python +# config.py +api_prefix: str = "/api" # 后端内部的 API 前缀,与前端 APP_BASE_URL 一致 + +# routers/__init__.py — 统一注册 +router = APIRouter(prefix="/api") +router.include_router(auth.router) # auth.router 前缀: /api/auth/xxx +router.include_router(user.router) # user.router 前缀: /api/user/xxx + +# 单个路由模块定义(routers/user.py) +router = APIRouter(prefix="/api/user", tags=["user"]) +``` + +> Nginx 的 `rewrite ^/app_03(/api/.*)$ $1 break` 会将 `/app_03/api/xxx` 剥离为 `/api/xxx` 转发到后端,所以后端不感知外部路径前缀。 + +**路由文件结构:** +```python +routers/ +├── __init__.py # 统一注册所有路由 +├── auth.py # 认证:exchange-code, dev-login +├── user.py # 用户 CRUD +└── *.py # 按业务模块拆分 +``` + +### 4.2 数据库命名规范 + +```python +# config.py +mongodb_db_name: str = "app_03" # 数据库名 = 应用标识,与 basename 一致 +``` + +| 应用 | 数据库名 | 说明 | +|------|---------|------| +| 孕育无忧 | `app_03` | 应用目录名 / 项目名 | +| 婴儿辅食 | `baby_food` | 全小写 snake_case | +| 宝宝相册 | `album` | 简短标识 | + +**规范:** +- 全小写 + 下划线(`snake_case`) +- 与前端 basename 保持一致(去掉首斜杠) +- 与项目目录名保持一致 +- 避免用复数(`users` → ❌, `app_03` → ✅) +- 同一 MongoDB 实例下的所有应用数据库平级,无交叉数据 + +**models 目录结构(每个应用独立):** +```python +models/ +├── __init__.py # 导出所有模型 +└── user.py # User Document +# 业务模型按需新增文件,不跨应用复用 +``` + +### 4.3 数据模型(Beanie ODM 模板) + +```python +class User(Document): + openid: str = Field(..., unique=True) # 微信 openid,用户唯一标识 + session_key: Optional[str] = None + union_id: Optional[str] = None + name: str = "" + phone: Optional[str] = None + avatar_url: Optional[str] = None + is_onboarded: bool = False # 是否完成引导 + created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) + updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)) +``` + +> **注意:** 业务模型按需新增,User 仅作为认证底座,业务数据可新建 Document 或嵌入 User。 + +### 4.4 API 响应格式 + +```python +# 成功 — 直接返回 Pydantic Model 或 dict +# 错误 — HTTPException(status_code, detail="中文描述") +``` + +### 4.5 服务管理 (serv.sh) + +```bash +# .env 中配置: +APP_NAME=app_name +APP_MODULE=app.main:app +HOST=0.0.0.0 +PORT=8000 +DEV_MODE=1 + +# 命令: +./serv.sh start [--reload] # 启动 +./serv.sh stop # 停止 +./serv.sh restart [--reload] # 重启 +./serv.sh status # 状态 +./serv.sh logs [YYYY-MM-DD] # 日志 +``` + +--- + +## 五、代码模板 + +### 5.1 新增业务页面 + +```typescript +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router"; +import { useUser } from "../context/UserContext"; +import { fetchApi } from "../services/api"; + +export function XxxPage() { + const navigate = useNavigate(); + const { user, token } = useUser(); + const [data, setData] = useState(null); + const [error, setError] = useState(""); + + useEffect(() => { + fetchApi("/xxx").then(setData).catch(e => setError(e.message)); + }, []); + + if (error) return
{error}
; + if (!data) return
加载中...
; + + return
{/* 业务内容 */}
; +} +``` + +### 5.2 新增 API 接口 + +```typescript +// api.ts +export interface XxxDTO { + id: string; + // ... 业务字段 +} + +export async function getXxxList(): Promise { + return fetchApi("/xxx"); +} + +export async function createXxx(data: Record): Promise { + return fetchApi("/xxx", { method: "POST", body: JSON.stringify(data) }); +} +``` + +### 5.3 后端路由模板 + +```python +from fastapi import APIRouter, Depends +from app.middleware.auth import get_current_user +from app.models.user import User + +router = APIRouter(prefix="/api/xxx", tags=["xxx"]) + +@router.get("/") +async def list_items(user: User = Depends(get_current_user)): + # user.openid / user.id 可用 + return {"items": [...]} + +@router.post("/") +async def create_item(data: ..., user: User = Depends(get_current_user)): + return {"id": "...", ...} +``` + +--- + +## 六、安全注意事项 + +1. JWT 不验证签名 — 信任链依赖于小程序的环境隔离,H5 本身不做额外验证 +2. DEV_MODE 仅控制 dev-login — 生产环境不设此变量 +3. CORS 配置 `cors_origins: *` — 开发环境宽松,生产应限制 +4. 微信 session_key 存入数据库但不对外暴露 + +--- + +## 七、环境变量 (.env 模板) + +```ini +# MongoDB +MONGODB_URL=mongodb://localhost:27017 +MONGODB_DB_NAME=app_name + +# JWT +JWT_SECRET_KEY=change-this-secret-key +JWT_ALGORITHM=HS256 +JWT_EXPIRE_MINUTES=10080 + +# WeChat +WECHAT_APP_ID= +WECHAT_APP_SECRET= + +# Server +HOST=0.0.0.0 +PORT=8000 +DEBUG=true +DEV_MODE=1 + +# CORS +CORS_ORIGINS=* +``` + +--- + +## 八、初始化新应用 Checklist + +- [ ] 定义项目标识(`app_03`/`baby_food`/`album` 等),保持一致 +- [ ] 复制模板 → 修改 `backend/app/config.py` → `app_name`, `mongodb_db_name`, `port` +- [ ] 修改前端 basename:Vite `base` + React Router `basename` + `api.ts` 的 `APP_BASE_URL` +- [ ] 设置 WeChat AppID/Secret(生产环境) +- [ ] 更换 `JWT_SECRET_KEY` +- [ ] 定义业务数据模型(`backend/app/models/`) +- [ ] 编写业务 API 路由(`backend/app/routers/`) +- [ ] 创建前端页面 + 路由配置 +- [ ] 添加 Nginx location 块(前端 SPA + API 代理) +- [ ] 启动验证:`DEV_MODE=1` → 浏览器访问 → dev-login → 首页 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 4aeb905..5d6f5bb 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -66,6 +66,7 @@ }, "devDependencies": { "@tailwindcss/vite": "4.1.12", + "@types/node": "^22.0.0", "@vitejs/plugin-basic-ssl": "^2.2.0", "@vitejs/plugin-react": "4.7.0", "tailwindcss": "4.1.12", @@ -3349,6 +3350,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmmirror.com/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, "node_modules/@types/parse-json": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.2.tgz", @@ -5313,6 +5324,13 @@ "url": "https://github.com/sponsors/Wombosvideo" } }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "devOptional": true, + "license": "MIT" + }, "node_modules/update-browserslist-db": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", diff --git a/frontend/src/app/pages/LoginPage.tsx b/frontend/src/app/pages/LoginPage.tsx index 452f0a4..3366ada 100644 --- a/frontend/src/app/pages/LoginPage.tsx +++ b/frontend/src/app/pages/LoginPage.tsx @@ -27,9 +27,9 @@ export function LoginPage() { if (tokenFromUrl !== oldToken) { const payload = decodeToken(tokenFromUrl); setTokenAndUser(tokenFromUrl, { - id: payload.sub, + id: String(payload.uid), openid: payload.openid, - phone: payload.mobile, + phone: payload.phone, }); } else { // token 没变,确保 localStorage 有数据 diff --git a/frontend/src/app/services/api.ts b/frontend/src/app/services/api.ts index cb3adb8..7a5f8c9 100644 --- a/frontend/src/app/services/api.ts +++ b/frontend/src/app/services/api.ts @@ -55,9 +55,9 @@ async function fetchApi(path: string, options: RequestInit = {}): Promise // ─── Auth ─── export interface TokenPayload { - sub: string; // user_id + uid: number; openid: string; - mobile: string; + phone: string; exp: number; }