2
0
Эх сурвалжийг харах

📝 docs(mini-code-login): 添加小程序code-openid自动注册登录方案文档

- 分析现有认证系统,包括用户实体、认证方式、注册流程和令牌机制
- 设计数据库模型扩展,添加openid、unionid等小程序相关字段
- 提供小程序登录服务实现代码,包含code验证、用户创建和令牌生成
- 编写API路由实现,定义请求响应格式和错误处理
- 提供小程序端实现代码,包括登录流程和用户信息获取
- 补充环境配置和路由注册说明
- 总结实现步骤,确保自动注册登录功能的完整性和向后兼容性
yourname 4 сар өмнө
parent
commit
c5bbe1c68a
1 өөрчлөгдсөн 361 нэмэгдсэн , 0 устгасан
  1. 361 0
      docs/mini-code-login.md

+ 361 - 0
docs/mini-code-login.md

@@ -0,0 +1,361 @@
+基于对现有认证系统的分析,我来为您设计小程序 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<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路由实现
+
+```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 (
+    <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. 环境配置
+
+```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验证用户身份