基于对现有认证系统的分析,我来为您设计小程序 code-openid 自动注册登录的实现方案。 ## 当前系统分析 ### 现有认证流程 1. **用户实体**:包含 `username`, `password`, `phone`, `email`, `nickname` 等字段 2. **认证方式**:基于用户名+密码的传统登录 3. **注册流程**:需要手动填写用户名、密码、邮箱 4. **令牌机制**:JWT Token 认证 ### 小程序自动注册登录方案 #### 1. 数据库模型扩展 需要扩展用户实体以支持小程序登录: ```typescript // 在 UserEntity 中添加小程序相关字段 @Entity({ name: 'users' }) export class UserEntity { // ... 现有字段 @Column({ name: 'openid', type: 'varchar', length: 255, nullable: true, unique: true, comment: '微信小程序openid' }) openid!: string | null; @Column({ name: 'unionid', type: 'varchar', length: 255, nullable: true, comment: '微信unionid' }) unionid!: string | null; @Column({ name: 'miniapp_avatar', type: 'varchar', length: 500, nullable: true, comment: '小程序头像URL' }) miniappAvatar!: string | null; @Column({ name: 'registration_source', type: 'varchar', length: 20, default: 'web', comment: '注册来源: web, miniapp' }) registrationSource!: string; } ``` #### 2. 小程序登录服务 创建小程序认证服务: ```typescript // src/server/modules/auth/mini-auth.service.ts import { DataSource, Repository } from 'typeorm'; import { UserEntity } from '../users/user.entity'; import jwt from 'jsonwebtoken'; import axios from 'axios'; export class MiniAuthService { private userRepository: Repository; constructor(private dataSource: DataSource) { this.userRepository = dataSource.getRepository(UserEntity); } async miniLogin(code: string): Promise<{ token: string; user: UserEntity; isNewUser: boolean }> { // 1. 通过code获取openid const openidInfo = await this.getOpenIdByCode(code); // 2. 查找或创建用户 let user = await this.userRepository.findOne({ where: { openid: openidInfo.openid } }); let isNewUser = false; if (!user) { // 自动注册新用户 user = await this.createMiniUser(openidInfo); isNewUser = true; } // 3. 生成token const token = this.generateToken(user); return { token, user, isNewUser }; } private async getOpenIdByCode(code: string): Promise<{ openid: string; unionid?: string; session_key: string }> { const appId = process.env.WX_MINI_APP_ID!; const appSecret = process.env.WX_MINI_APP_SECRET!; const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code`; const response = await axios.get(url); if (response.data.errcode) { throw new Error(`微信API错误: ${response.data.errmsg}`); } return { openid: response.data.openid, unionid: response.data.unionid, session_key: response.data.session_key }; } private async createMiniUser(openidInfo: { openid: string; unionid?: string }): Promise { const user = this.userRepository.create({ username: `wx_${Date.now()}_${Math.random().toString(36).substr(2, 6)}`, password: '', // 小程序用户不需要密码 openid: openidInfo.openid, unionid: openidInfo.unionid, nickname: '微信用户', registrationSource: 'miniapp', isDisabled: 0 }); return await this.userRepository.save(user); } private generateToken(user: UserEntity): string { const payload = { id: user.id, username: user.username, openid: user.openid }; return jwt.sign(payload, process.env.JWT_SECRET!, { expiresIn: '7d' }); } async updateUserProfile(userId: number, profile: { nickname?: string; avatar?: string }): Promise { const user = await this.userRepository.findOne({ where: { id: userId } }); if (!user) throw new Error('用户不存在'); if (profile.nickname) user.nickname = profile.nickname; if (profile.avatar) user.miniappAvatar = profile.avatar; return await this.userRepository.save(user); } } ``` #### 3. API路由实现 ```typescript // src/server/api/auth/mini-login/post.ts import { createRoute, OpenAPIHono } from '@hono/zod-openapi'; import { z } from '@hono/zod-openapi'; import { MiniAuthService } from '@/server/modules/auth/mini-auth.service'; import { AppDataSource } from '@/server/data-source'; import { ErrorSchema } from '@/server/utils/errorHandler'; const MiniLoginSchema = z.object({ code: z.string().openapi({ example: '08123456789012345678901234567890', description: '小程序登录code' }), userInfo: z.object({ nickName: z.string().optional(), avatarUrl: z.string().optional() }).optional() }); const MiniLoginResponseSchema = z.object({ token: z.string().openapi({ example: 'jwt.token.here', description: 'JWT Token' }), user: z.object({ id: z.number(), username: z.string(), nickname: z.string().nullable(), avatar: z.string().nullable() }), isNewUser: z.boolean().openapi({ example: true, description: '是否为新注册用户' }) }); const miniLoginRoute = createRoute({ method: 'post', path: '/mini-login', request: { body: { content: { 'application/json': { schema: MiniLoginSchema } } } }, responses: { 200: { description: '小程序登录成功', content: { 'application/json': { schema: MiniLoginResponseSchema } } }, 400: { description: '参数错误', content: { 'application/json': { schema: ErrorSchema } } } } }); const miniAuthService = new MiniAuthService(AppDataSource); const app = new OpenAPIHono().openapi(miniLoginRoute, async (c) => { try { const { code, userInfo } = c.req.valid('json'); const result = await miniAuthService.miniLogin(code); // 如果有用户信息,更新用户资料 if (userInfo && result.isNewUser) { await miniAuthService.updateUserProfile(result.user.id, { nickname: userInfo.nickName, avatar: userInfo.avatarUrl }); } return c.json({ token: result.token, user: { id: result.user.id, username: result.user.username, nickname: result.user.nickname, avatar: result.user.miniappAvatar }, isNewUser: result.isNewUser }); } catch (error) { return c.json({ code: 400, message: error instanceof Error ? error.message : '登录失败' }, 400); } }); export default app; ``` #### 4. 小程序端实现 ```typescript // mini/src/pages/login/index.tsx import { useState } from 'react' import Taro from '@tarojs/taro' import { View, Text, Button } from '@tarojs/components' import { useForm } from 'react-hook-form' import { zodResolver } from '@hookform/resolvers/zod' import { z } from 'zod' const miniLoginSchema = z.object({ code: z.string(), userInfo: z.object({ nickName: z.string(), avatarUrl: z.string() }).optional() }) export default function MiniLoginPage() { const [loading, setLoading] = useState(false) const handleLogin = async () => { setLoading(true) try { // 1. 获取用户信息 const userProfile = await Taro.getUserProfile({ desc: '用于完善用户资料' }) // 2. 获取登录code const loginRes = await Taro.login() // 3. 调用后端API const response = await Taro.request({ url: `${API_BASE_URL}/api/v1/auth/mini-login`, method: 'POST', data: { code: loginRes.code, userInfo: userProfile.userInfo } }) if (response.statusCode === 200) { const { token, user, isNewUser } = response.data // 4. 保存token Taro.setStorageSync('token', token) Taro.setStorageSync('userInfo', user) // 5. 跳转到首页 Taro.switchTab({ url: '/pages/index/index' }) Taro.showToast({ title: isNewUser ? '注册成功' : '登录成功', icon: 'success' }) } } catch (error) { Taro.showToast({ title: '登录失败', icon: 'none' }) } finally { setLoading(false) } } return ( 欢迎使用 ) } ``` #### 5. 环境配置 ```bash # .env WX_MINI_APP_ID=your_mini_app_id WX_MINI_APP_SECRET=your_mini_app_secret ``` #### 6. 路由注册 ```typescript // src/server/api/auth/index.ts import miniLoginRoute from './mini-login/post' const authRoutes = new OpenAPIHono() .route('/', loginRoute) .route('/', registerRoute) .route('/', miniLoginRoute) // 新增小程序登录路由 export default authRoutes ``` ### 实现步骤总结 1. **数据库迁移**:添加小程序相关字段 2. **创建服务**:`MiniAuthService` 处理小程序登录逻辑 3. **创建路由**:添加 `/api/v1/auth/mini-login` 端点 4. **小程序端**:实现微信登录按钮和API调用 5. **配置环境**:设置微信小程序的 AppID 和 AppSecret 这个方案实现了: - ✅ 自动注册:首次登录自动创建用户 - ✅ 自动登录:无需输入用户名密码 - ✅ 用户信息获取:支持获取微信昵称和头像 - ✅ 向后兼容:不影响现有的用户名密码登录 - ✅ 安全可靠:使用微信官方API验证用户身份