Browse Source

📝 docs(wechat-auth): add wechat authorization login development documentation

- add detailed implementation guide for wechat authorization login feature
- include user entity schema updates with wechat-related fields
- document wechat auth service, API routes and frontend components
- provide database migration scripts and environment configuration instructions
- add test cases, deployment guidelines and troubleshooting section

📦 build(wechat): add wechat verification file

- add MP_verify_zpDcBHcFYiSQydAo.txt for wechat domain verification
yourname 6 months ago
parent
commit
948198f05b

+ 643 - 0
.roo/commands/wechat-auth-微信服务号网页授权登录开发.md

@@ -0,0 +1,643 @@
+# 微信服务号网页授权登录开发指令
+
+## 描述
+
+本指令指导如何实现微信服务号网页授权登录功能,包括后端API开发、前端组件集成和移动端适配。
+
+## 前置要求
+
+1. 已注册微信开放平台服务号
+2. 已配置网页授权域名
+3. 获取AppID和AppSecret
+
+## 开发步骤
+
+### 1. 用户实体字段添加
+
+在用户实体中添加微信相关字段:
+
+```typescript
+// src/server/modules/users/user.entity.ts
+@Column({ name: 'wechat_openid', type: 'varchar', length: 255, nullable: true, comment: '微信开放平台openid' })
+wechatOpenid!: string | null;
+
+@Column({ name: 'wechat_unionid', type: 'varchar', length: 255, nullable: true, comment: '微信开放平台unionid' })
+wechatUnionid!: string | null;
+
+@Column({ name: 'wechat_nickname', type: 'varchar', length: 255, nullable: true, comment: '微信昵称' })
+wechatNickname!: string | null;
+
+@Column({ name: 'wechat_avatar', type: 'varchar', length: 500, nullable: true, comment: '微信头像URL' })
+wechatAvatar!: string | null;
+
+@Column({ name: 'wechat_sex', type: 'tinyint', nullable: true, comment: '微信性别(1:男,2:女,0:未知)' })
+wechatSex!: number | null;
+
+@Column({ name: 'wechat_province', type: 'varchar', length: 100, nullable: true, comment: '微信省份' })
+wechatProvince!: string | null;
+
+@Column({ name: 'wechat_city', type: 'varchar', length: 100, nullable: true, comment: '微信城市' })
+wechatCity!: string | null;
+
+@Column({ name: 'wechat_country', type: 'varchar', length: 100, nullable: true, comment: '微信国家' })
+wechatCountry!: string | null;
+```
+
+### 2. 用户Schema更新
+
+在用户Schema中添加微信字段定义:
+
+```typescript
+// src/server/modules/users/user.schema.ts
+wechatOpenid: z.string().max(255).nullable().openapi({
+  example: 'o6_bmjrPTlm6_2sgVt7hMZOPfL2M',
+  description: '微信开放平台openid'
+}),
+wechatUnionid: z.string().max(255).nullable().openapi({
+  example: 'o6_bmasdasdsad6_2sgVt7hMZOPfL2M',
+  description: '微信开放平台unionid'
+}),
+wechatNickname: z.string().max(255).nullable().openapi({
+  example: '微信用户',
+  description: '微信昵称'
+}),
+wechatAvatar: z.string().max(500).nullable().openapi({
+  example: 'http://thirdwx.qlogo.cn/mmopen/g3MonUZtNHkdmzicIlibx6iaFqAc56vxLSUfpb6n5WKSYVY0ChQKkiaJSgQ1dZuTOgvLLrhJbERQQ4eMsv84eavHiaiceqxibJxCfHe/46',
+  description: '微信头像URL'
+}),
+wechatSex: z.number().int().min(0).max(2).nullable().openapi({
+  example: 1,
+  description: '微信性别(1:男,2:女,0:未知)'
+}),
+wechatProvince: z.string().max(100).nullable().openapi({
+  example: '广东省',
+  description: '微信省份'
+}),
+wechatCity: z.string().max(100).nullable().openapi({
+  example: '深圳市',
+  description: '微信城市'
+}),
+wechatCountry: z.string().max(100).nullable().openapi({
+  example: '中国',
+  description: '微信国家'
+})
+```
+
+### 3. 微信认证服务类
+
+创建微信认证服务:
+
+```typescript
+// src/server/modules/wechat/wechat-auth.service.ts
+import axios from 'axios';
+import { UserService } from '../users/user.service';
+import { AuthService } from '../auth/auth.service';
+
+export class WechatAuthService {
+  private readonly appId: string = process.env.WECHAT_APP_ID || '';
+  private readonly appSecret: string = process.env.WECHAT_APP_SECRET || '';
+
+  constructor(
+    private readonly userService: UserService,
+    private readonly authService: AuthService
+  ) {}
+
+  // 获取授权URL
+  getAuthorizationUrl(redirectUri: string, scope: 'snsapi_base' | 'snsapi_userinfo' = 'snsapi_userinfo'): string {
+    const state = Math.random().toString(36).substring(2);
+    return `https://open.weixin.qq.com/connect/oauth2/authorize?appid=${this.appId}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=${scope}&state=${state}#wechat_redirect`;
+  }
+
+  // 通过code获取access_token
+  async getAccessToken(code: string): Promise<any> {
+    const url = `https://api.weixin.qq.com/sns/oauth2/access_token?appid=${this.appId}&secret=${this.appSecret}&code=${code}&grant_type=authorization_code`;
+    const response = await axios.get(url);
+    return response.data;
+  }
+
+  // 获取用户信息
+  async getUserInfo(accessToken: string, openid: string): Promise<any> {
+    const url = `https://api.weixin.qq.com/sns/userinfo?access_token=${accessToken}&openid=${openid}&lang=zh_CN`;
+    const response = await axios.get(url);
+    return response.data;
+  }
+
+  // 微信登录/注册
+  async wechatLogin(code: string): Promise<{ token: string; user: any }> {
+    // 1. 获取access_token
+    const tokenData = await this.getAccessToken(code);
+    if (tokenData.errcode) {
+      throw new Error(`微信认证失败: ${tokenData.errmsg}`);
+    }
+
+    const { access_token, openid, unionid } = tokenData;
+
+    // 2. 检查用户是否存在
+    let user = await this.userService.findByWechatOpenid(openid);
+
+    if (!user) {
+      // 3. 获取用户信息(首次登录)
+      const userInfo = await this.getUserInfo(access_token, openid);
+      
+      // 4. 创建新用户
+      user = await this.userService.createUser({
+        username: `wx_${openid.substring(0, 8)}`,
+        password: Math.random().toString(36).substring(2),
+        wechatOpenid: openid,
+        wechatUnionid: unionid,
+        wechatNickname: userInfo.nickname,
+        wechatAvatar: userInfo.headimgurl,
+        wechatSex: userInfo.sex,
+        wechatProvince: userInfo.province,
+        wechatCity: userInfo.city,
+        wechatCountry: userInfo.country,
+        nickname: userInfo.nickname
+      });
+    }
+
+    // 5. 生成JWT token
+    const token = this.authService.generateToken(user);
+    
+    return { token, user };
+  }
+}
+```
+
+### 4. 微信认证路由
+
+创建微信认证API路由:
+
+```typescript
+// src/server/api/auth/wechat/
+// wechat-authorize.ts - 微信授权URL获取路由
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { WechatAuthService } from '@/server/modules/wechat/wechat-auth.service';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { UserService } from '@/server/modules/users/user.service';
+import { AuthService } from '@/server/modules/auth/auth.service';
+
+const AuthorizeSchema = z.object({
+  redirectUri: z.string().url().openapi({
+    example: 'https://example.com/auth/callback',
+    description: '回调地址'
+  }),
+  scope: z.enum(['snsapi_base', 'snsapi_userinfo']).default('snsapi_userinfo').openapi({
+    example: 'snsapi_userinfo',
+    description: '授权范围'
+  })
+});
+
+const AuthorizeResponseSchema = z.object({
+  url: z.string().url().openapi({
+    example: 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=xxx',
+    description: '微信授权URL'
+  })
+});
+
+const userService = new UserService(AppDataSource);
+const authService = new AuthService(userService);
+const wechatAuthService = new WechatAuthService(userService, authService);
+
+const authorizeRoute = createRoute({
+  method: 'post',
+  path: '/authorize',
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: AuthorizeSchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '获取微信授权URL成功',
+      content: {
+        'application/json': {
+          schema: AuthorizeResponseSchema
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono().openapi(authorizeRoute, async (c) => {
+  try {
+    const { redirectUri, scope } = await c.req.json();
+    const url = wechatAuthService.getAuthorizationUrl(redirectUri, scope);
+    return c.json({ url }, 200);
+  } catch (error) {
+    return c.json({ 
+      code: 400, 
+      message: error instanceof Error ? error.message : '获取授权URL失败' 
+    }, 400);
+  }
+});
+
+export default app;
+```
+
+```typescript
+// src/server/api/auth/wechat/wechat-login.ts - 微信登录路由
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi';
+import { z } from '@hono/zod-openapi';
+import { WechatAuthService } from '@/server/modules/wechat/wechat-auth.service';
+import { ErrorSchema } from '@/server/utils/errorHandler';
+import { AppDataSource } from '@/server/data-source';
+import { UserService } from '@/server/modules/users/user.service';
+import { AuthService } from '@/server/modules/auth/auth.service';
+import { TokenResponseSchema } from '../login/password';
+
+const WechatLoginSchema = z.object({
+  code: z.string().min(1).openapi({
+    example: '0816TxlL1Qe3QY0qgDlL1pQxlL16TxlO',
+    description: '微信授权code'
+  })
+});
+
+const userService = new UserService(AppDataSource);
+const authService = new AuthService(userService);
+const wechatAuthService = new WechatAuthService(userService, authService);
+
+const wechatLoginRoute = createRoute({
+  method: 'post',
+  path: '/login',
+  request: {
+    body: {
+      content: {
+        'application/json': {
+          schema: WechatLoginSchema
+        }
+      }
+    }
+  },
+  responses: {
+    200: {
+      description: '微信登录成功',
+      content: {
+        'application/json': {
+          schema: TokenResponseSchema
+        }
+      }
+    },
+    400: {
+      description: '请求参数错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    401: {
+      description: '微信认证失败',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+});
+
+const app = new OpenAPIHono().openapi(wechatLoginRoute, async (c) => {
+  try {
+    const { code } = await c.req.json();
+    const result = await wechatAuthService.wechatLogin(code);
+    return c.json(result, 200);
+  } catch (error) {
+    return c.json({ 
+      code: 401, 
+      message: error instanceof Error ? error.message : '微信登录失败' 
+    }, 401);
+  }
+});
+
+export default app;
+```
+
+```typescript
+// src/server/api/auth/wechat/index.ts - 路由聚合
+import { OpenAPIHono } from '@hono/zod-openapi';
+import authorizeRoute from './wechat-authorize';
+import loginRoute from './wechat-login';
+
+const app = new OpenAPIHono()
+  .route('/', authorizeRoute)
+  .route('/', loginRoute);
+
+export default app;
+```
+
+### 5. 注册微信路由
+
+在API入口文件中注册微信路由:
+
+```typescript
+// src/server/api.ts
+import wechatRoutes from '@/server/api/auth/wechat/index';
+
+// 注册路由
+api.route('/api/v1/auth/wechat', wechatRoutes);
+```
+
+### 6. 前端微信登录组件
+
+创建React微信登录组件:
+
+```typescript
+// src/client/components/WechatLoginButton.tsx
+import React from 'react';
+import { Button } from '@/client/components/ui/button';
+import { Wechat } from 'lucide-react';
+import { authClient } from '@/client/api';
+
+interface WechatLoginButtonProps {
+  redirectUri: string;
+  scope?: 'snsapi_base' | 'snsapi_userinfo';
+  onSuccess?: (data: any) => void;
+  onError?: (error: Error) => void;
+}
+
+const WechatLoginButton: React.FC<WechatLoginButtonProps> = ({
+  redirectUri,
+  scope = 'snsapi_userinfo',
+  onSuccess,
+  onError,
+  children = '微信登录'
+}) => {
+  const handleWechatLogin = async () => {
+    try {
+      // 使用RPC客户端获取微信授权URL
+      const response = await authClient.wechat.authorize.$post({
+        json: { redirectUri, scope }
+      });
+
+      if (response.status !== 200) {
+        throw new Error('获取微信授权URL失败');
+      }
+
+      const { url } = await response.json();
+      
+      // 重定向到微信授权页面
+      window.location.href = url;
+    } catch (error) {
+      onError?.(error as Error);
+    }
+  };
+
+  return (
+    <Button
+      variant="default"
+      onClick={handleWechatLogin}
+      className="bg-[#07C160] text-white hover:bg-[#06B456] border-none"
+    >
+      <Wechat className="w-4 h-4 mr-2" />
+      {children}
+    </Button>
+  );
+};
+
+export default WechatLoginButton;
+```
+
+### 7. 移动端微信登录集成
+
+更新移动端认证页面:
+
+```typescript
+// src/client/mobile/pages/AuthPage.tsx - 添加微信登录选项
+import WechatLoginButton from '@/client/components/WechatLoginButton';
+
+// 在表单后添加微信登录按钮
+<div className="mt-6">
+  <div className="relative">
+    <div className="absolute inset-0 flex items-center">
+      <div className="w-full border-t border-gray-300"></div>
+    </div>
+    <div className="relative flex justify-center text-sm">
+      <span className="px-2 bg-white text-gray-500">或使用以下方式登录</span>
+    </div>
+  </div>
+  
+  <div className="mt-4">
+    <WechatLoginButton
+      redirectUri={`${window.location.origin}/mobile/auth/callback`}
+      onSuccess={(data) => {
+        localStorage.setItem('token', data.token);
+        navigate(getReturnUrl(), { replace: true });
+      }}
+      onError={(error) => {
+        alert(error.message);
+      }}
+    >
+      微信一键登录
+    </WechatLoginButton>
+  </div>
+</div>
+```
+
+### 8. 环境变量配置
+
+在.env文件中添加微信配置:
+
+```bash
+# 微信服务号配置
+WECHAT_APP_ID=your_wechat_app_id
+WECHAT_APP_SECRET=your_wechat_app_secret
+WECHAT_REDIRECT_URI=https://yourdomain.com/auth/wechat/callback
+```
+
+### 9. 数据库迁移
+
+创建数据库迁移脚本添加微信字段:
+
+```typescript
+// src/server/migrations/AddWechatFields.ts
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddWechatFields1712345678901 implements MigrationInterface {
+  public async up(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+      ALTER TABLE users 
+      ADD COLUMN wechat_openid VARCHAR(255) NULL COMMENT '微信开放平台openid',
+      ADD COLUMN wechat_unionid VARCHAR(255) NULL COMMENT '微信开放平台unionid',
+      ADD COLUMN wechat_nickname VARCHAR(255) NULL COMMENT '微信昵称',
+      ADD COLUMN wechat_avatar VARCHAR(500) NULL COMMENT '微信头像URL',
+      ADD COLUMN wechat_sex TINYINT NULL COMMENT '微信性别(1:男,2:女,0:未知)',
+      ADD COLUMN wechat_province VARCHAR(100) NULL COMMENT '微信省份',
+      ADD COLUMN wechat_city VARCHAR(100) NULL COMMENT '微信城市',
+      ADD COLUMN wechat_country VARCHAR(100) NULL COMMENT '微信国家',
+      ADD UNIQUE INDEX idx_wechat_openid (wechat_openid),
+      ADD INDEX idx_wechat_unionid (wechat_unionid)
+    `);
+  }
+
+  public async down(queryRunner: QueryRunner): Promise<void> {
+    await queryRunner.query(`
+      ALTER TABLE users
+      DROP COLUMN wechat_openid,
+      DROP COLUMN wechat_unionid,
+      DROP COLUMN wechat_nickname,
+      DROP COLUMN wechat_avatar,
+      DROP COLUMN wechat_sex,
+      DROP COLUMN wechat_province,
+      DROP COLUMN wechat_city,
+      DROP COLUMN wechat_country
+    `);
+  }
+}
+```
+
+## 测试验证
+
+### 1. 单元测试
+
+创建微信认证服务的单元测试:
+
+```typescript
+// src/server/modules/wechat/__tests__/wechat-auth.service.spec.ts
+import { WechatAuthService } from '../wechat-auth.service';
+import { UserService } from '../../users/user.service';
+import { AuthService } from '../../auth/auth.service';
+import axios from 'axios';
+
+jest.mock('axios');
+
+describe('WechatAuthService', () => {
+  let service: WechatAuthService;
+  let userService: UserService;
+  let authService: AuthService;
+
+  beforeEach(() => {
+    userService = {
+      findByWechatOpenid: jest.fn(),
+      createUser: jest.fn()
+    } as any;
+
+    authService = {
+      generateToken: jest.fn()
+    } as any;
+
+    service = new WechatAuthService(userService, authService);
+  });
+
+  it('should be defined', () => {
+    expect(service).toBeDefined();
+  });
+
+  // 添加更多测试用例...
+});
+```
+
+### 2. 集成测试
+
+测试微信认证API端点:
+
+```typescript
+// src/server/api/auth/wechat/__tests__/wechat-auth.e2e-spec.ts
+import { createServer } from 'http';
+import { api } from '@/server/api';
+import * as request from 'supertest';
+
+describe('Wechat Auth (e2e)', () => {
+  let server: any;
+
+  beforeAll(async () => {
+    server = createServer(api.fetch);
+  });
+
+  afterAll(() => {
+    server.close();
+  });
+
+  it('/auth/wechat (POST) - should return 400 for invalid code', () => {
+    return request(server)
+      .post('/auth/wechat')
+      .send({ code: '' })
+      .expect(400);
+  });
+
+  // 添加更多集成测试...
+});
+```
+
+## 部署说明
+
+### 1. 微信服务号配置
+
+1. 登录微信公众平台
+2. 进入「开发」->「基本配置」
+3. 获取AppID和AppSecret
+4. 进入「设置」->「公众号设置」->「功能设置」
+5. 配置「网页授权域名」
+
+### 2. 服务器配置
+
+确保服务器能够访问微信API:
+- 开放对外网络访问
+- 配置正确的回调域名
+- 设置HTTPS(微信要求)
+
+### 3. 安全考虑
+
+1. **Token安全**: JWT token设置合理的过期时间
+2. **防CSRF**: 使用state参数防止CSRF攻击
+3. **错误处理**: 妥善处理微信API的错误响应
+4. **日志记录**: 记录微信认证过程中的关键操作
+
+## 故障排除
+
+### 常见问题
+
+1. **redirect_uri参数错误**
+   - 检查网页授权域名配置
+   - 确保回调地址与配置一致
+
+2. **invalid code**
+   - code只能使用一次
+   - code有效期5分钟
+
+3. **access_token过期**
+   - access_token有效期为2小时
+   - refresh_token有效期为30天
+
+4. **API调用频率限制**
+   - 微信API有调用频率限制
+   - 建议添加缓存机制
+
+## 性能优化
+
+1. **缓存access_token**: 使用Redis缓存微信access_token
+2. **数据库索引**: 为wechat_openid字段添加唯一索引
+3. **异步处理**: 用户信息获取可异步处理
+4. **CDN加速**: 微信头像使用CDN加速
+
+## 监控告警
+
+1. **微信API调用监控**: 监控微信API调用成功率
+2. **登录成功率监控**: 监控微信登录成功率
+3. **错误告警**: 设置微信认证错误告警
+4. **性能监控**: 监控微信认证接口响应时间
+
+## 版本历史
+
+- v1.0.0 (2024-01-01): 初始版本,支持微信网页授权登录
+- v1.1.0 (2024-02-01): 添加UnionID支持,优化错误处理
+- v1.2.0 (2024-03-01): 添加移动端适配,性能优化
+
+## 相关文档
+
+- [微信开放平台文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html)
+- [网页授权获取用户信息](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html)
+- [微信JS-SDK说明文档](https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html)

+ 1 - 0
public/MP_verify_zpDcBHcFYiSQydAo.txt

@@ -0,0 +1 @@
+zpDcBHcFYiSQydAo