基于对现有认证系统的分析,我来为您设计小程序 code-openid 自动注册登录的实现方案。
username, password, phone, email, nickname 等字段需要扩展用户实体以支持小程序登录:
// 在 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;
}
创建小程序认证服务:
// 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<UserEntity>;
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<UserEntity> {
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<UserEntity> {
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);
}
}
// 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;
// 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 (
<View className="min-h-screen flex items-center justify-center bg-gray-50">
<View className="w-80">
<Text className="text-2xl font-bold text-center mb-8">欢迎使用</Text>
<Button
type="primary"
loading={loading}
onClick={handleLogin}
className="w-full"
>
微信一键登录
</Button>
</View>
</View>
)
}
# .env
WX_MINI_APP_ID=your_mini_app_id
WX_MINI_APP_SECRET=your_mini_app_secret
// 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
MiniAuthService 处理小程序登录逻辑/api/v1/auth/mini-login 端点这个方案实现了: