2
0

mini-code-login.md 9.8 KB

基于对现有认证系统的分析,我来为您设计小程序 code-openid 自动注册登录的实现方案。

当前系统分析

现有认证流程

  1. 用户实体:包含 username, password, phone, email, nickname 等字段
  2. 认证方式:基于用户名+密码的传统登录
  3. 注册流程:需要手动填写用户名、密码、邮箱
  4. 令牌机制:JWT Token 认证

小程序自动注册登录方案

1. 数据库模型扩展

需要扩展用户实体以支持小程序登录:

// 在 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. 小程序登录服务

创建小程序认证服务:

// 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);
  }
}

3. API路由实现

// 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. 小程序端实现

// 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>
  )
}

5. 环境配置

# .env
WX_MINI_APP_ID=your_mini_app_id
WX_MINI_APP_SECRET=your_mini_app_secret

6. 路由注册

// 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验证用户身份