docs(auth): 完善微信登录模块注释 — 含申请移动应用完整步骤
wechat.provider.ts:
- 补充微信开放平台申请移动应用的完整流程(企业资质、创建应用、
获取 AppID/AppSecret、启用 unionid、获取 Android 签名 MD5)
- 说明常见 errcode 含义(40029/42003/40163)
- 安全注意事项(AppSecret 保密、服务端换 token、防重放建议)
wechat.service.ts:
- 完整业务流程注释(6步,含新老用户分支逻辑)
- unionid 优先策略原理(跨 App 唯一性,防重复注册)
- 每次登录同步微信信息说明
- 自动生成账号字段说明(nickname/email/phone/password/kycLevel)
.env.example:
- 微信开放平台申请步骤(注册/认证/创建/获取/启用 unionid)
- Android 签名 MD5 获取命令
- Flutter --dart-define 构建参数说明
- Universal Links 说明
WXEntryActivity.kt:
- 包名路径规范说明(applicationId vs namespace 区别)
- AndroidManifest 配置要求
- 回调未触发的排查步骤(签名/包名/debug包名)
- 获取 Android 签名 MD5 命令
main.dart:
- registerWxApi 传参说明(--dart-define 用法)
- 未配置 WECHAT_APP_ID 时的降级行为
- Universal Links 配置说明
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c9100d3262
commit
44de21a733
|
|
@ -47,9 +47,44 @@ EMAIL_MAX_VERIFY_ATTEMPTS=5
|
|||
# GMAIL_APP_PASSWORD=xxxxxxxxxxxxxx (16位,填写时不含空格)
|
||||
# EMAIL_FROM_NAME=Genex
|
||||
|
||||
# ── WeChat OAuth (微信开放平台移动应用) ──
|
||||
# 在微信开放平台 (open.weixin.qq.com) 创建移动应用后获取
|
||||
# AppID 以 wx 开头,16位;AppSecret 32位,严格保密,不可泄露到客户端
|
||||
# ── WeChat OAuth (微信开放平台移动应用) ──────────────────────────────────────
|
||||
#
|
||||
# 申请步骤(需企业资质,个人无法申请移动应用 OAuth):
|
||||
#
|
||||
# 1. 注册/登录微信开放平台
|
||||
# https://open.weixin.qq.com
|
||||
# → 账号中心 → 开发者资质认证(年费 300 元,需营业执照/组织机构代码证)
|
||||
#
|
||||
# 2. 创建移动应用
|
||||
# 管理中心 → 移动应用 → 创建移动应用
|
||||
# 填写基本信息:应用名称、简介、图标(512×512 PNG,无圆角)
|
||||
# 填写平台信息:
|
||||
# Android — 应用包名: cn.gogenex.consumer
|
||||
# 应用签名: release keystore 的 MD5(32位小写,无冒号分隔)
|
||||
# 获取签名: keytool -exportcert -keystore release.jks \
|
||||
# -alias release -storepass <密码> | md5sum
|
||||
# iOS — Bundle ID: cn.gogenex.consumer
|
||||
# 提交审核,等待 1-7 个工作日
|
||||
#
|
||||
# 3. 审核通过后在「应用详情」页面获取:
|
||||
# AppID (16位,wx 开头) — 填入 WECHAT_APP_ID,客户端和服务端均需要
|
||||
# AppSecret (32位) — 填入 WECHAT_APP_SECRET,仅服务端使用,严格保密
|
||||
#
|
||||
# 4. 启用 unionid(强烈建议)
|
||||
# 开放平台 → 账号中心 → 公众号/小程序/移动应用 绑定到同一开放平台账号
|
||||
# 绑定后,同一微信用户在所有绑定应用中 unionid 相同,可跨 App 识别用户
|
||||
# 若不绑定,不同 App 下同一用户的 openid 不同,会注册出多个账号
|
||||
#
|
||||
# 5. Flutter 构建时传入 AppID(在 Android Studio / Xcode / CI 脚本中配置):
|
||||
# flutter build apk --dart-define=WECHAT_APP_ID=wx0000000000000000
|
||||
# flutter build ipa --dart-define=WECHAT_APP_ID=wx0000000000000000
|
||||
#
|
||||
# 注意事项:
|
||||
# - AppSecret 严禁写入客户端代码或提交到 Git,只在服务端 .env 中配置
|
||||
# - Android 发布正式版本时,签名 MD5 必须与申请时一致,否则微信授权失败
|
||||
# - iOS Universal Links 需要在开放平台填写 https://www.gogenex.com/wechat/
|
||||
# 并确保该 URL 可访问 apple-app-site-association 文件
|
||||
#
|
||||
# WECHAT_APP_ID=wx0000000000000000
|
||||
# WECHAT_APP_SECRET=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,53 @@
|
|||
// ============================================================
|
||||
// WechatService — 微信登录 / 注册业务逻辑
|
||||
//
|
||||
// 流程 (行业标准方案):
|
||||
// 1. 客户端通过微信 SDK 获取 code(一次性,5 分钟有效)
|
||||
// 2. 后端用 code 换取 access_token + openid + unionid
|
||||
// 3. 用 access_token + openid 获取微信用户信息(昵称、头像)
|
||||
// 4. 按 unionid(优先)或 openid 查找 social_accounts 表
|
||||
// - 已存在 → 老用户,同步最新 nickname/avatar,返回 JWT
|
||||
// - 不存在 → 新用户,自动注册(创建 user + social_account)
|
||||
// 5. 发布 UserRegistered 事件(仅新注册时,携带 referralCode)
|
||||
// ── 完整流程 ──────────────────────────────────────────────
|
||||
// 1. Flutter 客户端调用 fluwx.sendWeChatAuth(scope: 'snsapi_userinfo')
|
||||
// 拉起微信 App → 用户确认授权 → 微信回传 code(WXAuthResp.code)
|
||||
//
|
||||
// unionid 优先策略:
|
||||
// 微信的 openid 是每个 App 不同的,同一用户在不同 App 下 openid 不同。
|
||||
// unionid 是绑定到微信开放平台账号后跨 App 统一的。
|
||||
// 用 unionid 可避免:用户同时安装 genex-mobile 和 admin-app,
|
||||
// 被识别为两个不同用户。
|
||||
// 2. 客户端将 code 发送到 POST /api/v1/auth/wechat
|
||||
// (可同时传 referralCode 推荐码,仅新用户注册时有效)
|
||||
//
|
||||
// 自动生成账号信息:
|
||||
// - nickname: 来自微信昵称,若为空则生成 "wx_{openid 前8位}"
|
||||
// - passwordHash: 随机生成(用户无法用密码登录,只能微信登录)
|
||||
// 3. 本服务调用 WechatProvider.exchangeCodeForToken(code)
|
||||
// → 获得 access_token、openid、unionid
|
||||
//
|
||||
// 4. 调用 WechatProvider.getUserInfo(access_token, openid)
|
||||
// → 获得 nickname、headimgurl(头像 URL)
|
||||
//
|
||||
// 5. 在 social_accounts 表查找已有绑定:
|
||||
// - 优先用 unionid 查找(跨 App 唯一,避免重复注册)
|
||||
// - 若无 unionid 则 fallback 到 openid
|
||||
//
|
||||
// 6a. 老用户(找到 social_account):
|
||||
// - 同步最新 nickname / avatarUrl(头像 URL 可能变化)
|
||||
// - 若原记录无 unionid 但本次有,则补充 unionid
|
||||
// - 返回 JWT access_token + refresh_token
|
||||
//
|
||||
// 6b. 新用户(未找到 social_account):
|
||||
// - 创建 users 记录(phone/email 为 null,随机密码哈希)
|
||||
// - 创建 social_accounts 绑定记录
|
||||
// - 发布 UserRegistered Kafka 事件(含 referralCode)
|
||||
// - 返回 JWT access_token + refresh_token
|
||||
//
|
||||
// ── unionid 优先策略原理 ───────────────────────────────────
|
||||
// 微信 openid 是「每个应用 × 每个用户」独立的,同一人在 genex-mobile
|
||||
// 和 admin-app 下 openid 不同,如果只用 openid 会产生两个账号。
|
||||
// unionid 是「同一开放平台账号下所有 App × 用户」统一的,可以跨 App
|
||||
// 识别同一用户 → 因此优先用 unionid 查找,防止同一人注册两次。
|
||||
// 注意: unionid 只在应用接入了微信开放平台且 scope=snsapi_userinfo 时才有。
|
||||
//
|
||||
// ── 自动生成账号字段 ───────────────────────────────────────
|
||||
// - nickname : 取自微信昵称;若昵称为空则生成 "wx_{openid 前8位}"
|
||||
// - email : null(微信不提供邮箱,可由用户事后在设置中绑定)
|
||||
// - phone : null(微信仅在特定场景授权,当前不请求手机号权限)
|
||||
// - password : 随机 32 字节 hex → bcrypt 哈希(用户无法用密码登录)
|
||||
// - walletMode: 默认 'standard'
|
||||
// - kycLevel : 默认 0(需用户主动完成实名认证)
|
||||
//
|
||||
// ── 每次登录同步微信信息 ───────────────────────────────────
|
||||
// 微信用户可能修改昵称/头像,每次登录时同步到 social_accounts,
|
||||
// 上层业务可从 social_accounts 获取最新头像 URL 展示。
|
||||
// (users 表的 avatarUrl 不自动更新,避免覆盖用户手动设置的头像)
|
||||
// ============================================================
|
||||
|
||||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||||
|
|
|
|||
|
|
@ -1,26 +1,62 @@
|
|||
// ============================================================
|
||||
// WechatProvider — 微信开放平台 HTTP API 封装
|
||||
//
|
||||
// 实现微信 OAuth 2.0 授权码模式(移动应用):
|
||||
// ── 功能 ──────────────────────────────────────────────────
|
||||
// 实现微信 OAuth 2.0 授权码模式(移动应用),完整流程如下:
|
||||
//
|
||||
// Step 1 (客户端): 微信 SDK 向微信 App 发起授权 → 获得 code
|
||||
// Step 2 (本文件): code → access_token + openid + unionid
|
||||
// Step 3 (本文件): access_token + openid → 用户信息(昵称、头像等)
|
||||
// Step 1 (客户端 fluwx):
|
||||
// 调用 sendWeChatAuth(scope: 'snsapi_userinfo') 拉起微信 App
|
||||
// 用户点「允许」→ 微信 App 通过 URL Scheme (wx{AppID}://) 回传 code
|
||||
// code 一次性,5 分钟有效,只能在服务端使用一次
|
||||
//
|
||||
// 微信 API 特点:
|
||||
// - 返回格式始终是 200 OK;错误通过 errcode 字段区分
|
||||
// - access_token 有效期 7200 秒(2小时)
|
||||
// - code 一次性使用,5 分钟内有效
|
||||
// - unionid 仅在 snsapi_userinfo scope 且应用已接入开放平台时有值
|
||||
// Step 2 (本文件 exchangeCodeForToken):
|
||||
// GET https://api.weixin.qq.com/sns/oauth2/access_token
|
||||
// ?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code
|
||||
// 成功返回: access_token, refresh_token, openid, scope, unionid
|
||||
//
|
||||
// 必要环境变量:
|
||||
// WECHAT_APP_ID — 微信开放平台移动应用 AppID(wx 开头,16位)
|
||||
// WECHAT_APP_SECRET — 应用密钥(32位,严格保密,不可泄露到客户端)
|
||||
// Step 3 (本文件 getUserInfo):
|
||||
// GET https://api.weixin.qq.com/sns/userinfo
|
||||
// ?access_token=TOKEN&openid=OPENID&lang=zh_CN
|
||||
// 返回: nickname, headimgurl, sex, province, city, country, unionid
|
||||
//
|
||||
// 安全注意事项:
|
||||
// - APP_SECRET 只在服务端使用,绝不下发给 App
|
||||
// - code 交换 access_token 在服务端完成(防 MitM 攻击)
|
||||
// - 生产环境应对 code 做防重放检测(Redis 标记已使用的 code)
|
||||
// ── 微信 AppID 申请步骤 ────────────────────────────────────
|
||||
// 1. 注册微信开放平台账号 (需企业认证,个人无法申请移动应用)
|
||||
// https://open.weixin.qq.com → 注册/登录 → 完成开发者资质认证(300元/年)
|
||||
//
|
||||
// 2. 创建移动应用
|
||||
// 管理中心 → 移动应用 → 创建移动应用
|
||||
// 填写: 应用名称、应用简介、应用图标 (512x512)、应用官网
|
||||
// 平台信息:
|
||||
// Android: 填写应用包名 (cn.gogenex.consumer) + 应用签名 (keystore MD5,32位小写无冒号)
|
||||
// iOS: 填写 Bundle ID (cn.gogenex.consumer)
|
||||
// 提交审核(1-7 个工作日)
|
||||
//
|
||||
// 3. 审核通过后获得:
|
||||
// AppID (16位,wx 开头) — 公开,可在客户端使用
|
||||
// AppSecret (32位) — 严格保密,只在服务端使用
|
||||
//
|
||||
// 4. 接入微信开放平台(启用 unionid)
|
||||
// 开放平台账号绑定微信公众号 / 小程序 / 移动应用后,
|
||||
// 同一用户在所有绑定应用中 unionid 相同,可跨 App 识别用户
|
||||
//
|
||||
// ── 环境变量配置 ───────────────────────────────────────────
|
||||
// WECHAT_APP_ID — wx 开头的16位 AppID(客户端和服务端都需要)
|
||||
// WECHAT_APP_SECRET — 32位应用密钥(严格保密,仅服务端使用)
|
||||
//
|
||||
// ── API 特点 ───────────────────────────────────────────────
|
||||
// - HTTP 响应始终 200 OK;错误通过响应体 errcode 字段区分
|
||||
// - 常见 errcode:
|
||||
// 40029 = code 无效(已使用或已过期)
|
||||
// 42003 = code 过期(超过 5 分钟)
|
||||
// 40163 = code 已被使用
|
||||
// - access_token 有效期 7200 秒(2小时),可用 refresh_token 续期
|
||||
// - unionid 仅在 snsapi_userinfo scope 且应用已接入开放平台时返回
|
||||
//
|
||||
// ── 安全注意事项 ───────────────────────────────────────────
|
||||
// - APP_SECRET 只在服务端使用,绝不下发给 App(防止密钥泄露)
|
||||
// - code 换 access_token 在服务端完成(防中间人攻击)
|
||||
// - state 参数用于防 CSRF:客户端生成随机串,服务端验证(当前简化实现)
|
||||
// - 生产环境建议对 code 做防重放:Redis SET NX 标记已用 code,TTL 5 分钟
|
||||
// ============================================================
|
||||
|
||||
import { Injectable, Logger, BadRequestException } from '@nestjs/common';
|
||||
|
|
|
|||
|
|
@ -1,26 +1,48 @@
|
|||
// ============================================================
|
||||
// WXEntryActivity — 微信 OAuth 授权回调 Activity
|
||||
//
|
||||
// 微信 SDK 要求:授权完成后,微信会唤起宿主 App 的
|
||||
// {applicationId}.wxapi.WXEntryActivity 来传递授权结果(code)。
|
||||
// ── 作用 ─────────────────────────────────────────────────
|
||||
// 微信 SDK 规范:用户在微信 App 完成授权后,微信会通过 Intent 唤起
|
||||
// 宿主 App 中固定路径的 WXEntryActivity,将授权结果(code / errCode)
|
||||
// 回传给宿主 App。
|
||||
//
|
||||
// fluwx 已封装好处理逻辑,只需继承 FluwxWXEntryActivity 即可,
|
||||
// 无需额外代码。
|
||||
// ── 路径规范(必须严格遵守,否则微信无法回调)───────────────
|
||||
// 包名路径: {applicationId}.wxapi.WXEntryActivity
|
||||
// 本应用: cn.gogenex.consumer.wxapi.WXEntryActivity
|
||||
//
|
||||
// 注意:
|
||||
// 1. AndroidManifest 中必须声明 android:exported="true"
|
||||
// 2. launchMode 必须为 singleTask(微信规范要求)
|
||||
// 3. 此文件路径必须严格遵循 {applicationId}/wxapi/WXEntryActivity
|
||||
// 注意: applicationId 与 namespace 可能不同:
|
||||
// namespace = "cn.gogenex.genex_consumer"(Kotlin 源码包名)
|
||||
// applicationId = "cn.gogenex.consumer"(build.gradle 中定义的 App ID)
|
||||
// 微信按 applicationId 查找 WXEntryActivity,因此此文件放在
|
||||
// cn/gogenex/consumer/wxapi/ 目录,package 声明也必须是 cn.gogenex.consumer.wxapi
|
||||
//
|
||||
// ── fluwx 集成方式 ────────────────────────────────────────
|
||||
// fluwx 插件已提供 com.jarvanmo.fluwx.WXEntryActivity 基类,
|
||||
// 内部实现了 IWXAPIEventHandler 接口并通过 EventChannel 将结果
|
||||
// 传递给 Dart 层的 weChatResponseEventHandler Stream。
|
||||
// 只需继承该基类,无需任何额外代码。
|
||||
//
|
||||
// ── AndroidManifest 配置要求 ──────────────────────────────
|
||||
// android:exported="true" — 允许微信 App 从外部唤起
|
||||
// android:launchMode="singleTask" — 微信规范要求,防止多实例
|
||||
// android:taskAffinity="${applicationId}" — 确保在正确任务栈
|
||||
//
|
||||
// ── 调试常见问题 ──────────────────────────────────────────
|
||||
// 1. 微信授权后 App 没有收到回调:
|
||||
// - 检查 applicationId 是否与微信开放平台填写的包名一致
|
||||
// - 检查签名 MD5 是否与开放平台填写的一致(debug/release 签名不同)
|
||||
// - debug 构建的 applicationId 为 cn.gogenex.consumer.debug,
|
||||
// 开放平台通常只填 release 包名,需要单独添加 debug 包名
|
||||
// - 可安装「微信开放平台助手」App 验证签名是否正确
|
||||
//
|
||||
// 2. 获取 Android 签名 MD5 的方法:
|
||||
// keytool -exportcert -keystore release.jks -alias release \
|
||||
// -storepass <密码> | md5sum
|
||||
// 输出的 32 位小写 hex 即为需要填写到微信开放平台的「应用签名」
|
||||
// ============================================================
|
||||
|
||||
package cn.gogenex.consumer.wxapi
|
||||
|
||||
import com.tencent.mm.opensdk.openapi.IWXAPIEventHandler
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import com.tencent.mm.opensdk.modelbase.BaseReq
|
||||
import com.tencent.mm.opensdk.modelbase.BaseResp
|
||||
import com.jarvanmo.fluwx.WXEntryActivity
|
||||
|
||||
class WXEntryActivity : WXEntryActivity()
|
||||
|
|
|
|||
|
|
@ -43,13 +43,22 @@ import 'features/profile/presentation/pages/share_page.dart';
|
|||
Future<void> main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// 初始化微信 SDK(WECHAT_APP_ID 在构建时注入,通过 --dart-define 传入)
|
||||
// iOS: CFBundleURLSchemes 中必须已配置 wx{AppID}
|
||||
// Android: WXEntryActivity 必须在 AndroidManifest 中声明
|
||||
// ── 微信 SDK 初始化 ────────────────────────────────────────────────────
|
||||
// WECHAT_APP_ID 在构建时通过 --dart-define 注入,例如:
|
||||
// flutter build apk --dart-define=WECHAT_APP_ID=wx0000000000000000
|
||||
// flutter build ipa --dart-define=WECHAT_APP_ID=wx0000000000000000
|
||||
//
|
||||
// 未传入 WECHAT_APP_ID 时(本地开发 / CI 未配置),跳过初始化,
|
||||
// WelcomePage 中点击微信按钮会提示「请先安装微信 App」(isWeChatInstalled=false)。
|
||||
//
|
||||
// universalLink: iOS Universal Links 地址,需在微信开放平台填写并配置
|
||||
// apple-app-site-association 文件(路径: https://www.gogenex.com/wechat/apple-app-site-association)
|
||||
// 详见: https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Universal_Links/Universal_Links.html
|
||||
const wechatAppId = String.fromEnvironment('WECHAT_APP_ID', defaultValue: '');
|
||||
if (wechatAppId.isNotEmpty) {
|
||||
await registerWxApi(appId: wechatAppId, universalLink: 'https://www.gogenex.com/wechat/');
|
||||
}
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
// 初始化升级服务(走 Nginx 反向代理 → Kong 网关)
|
||||
UpdateService().initialize(UpdateConfig.selfHosted(
|
||||
|
|
|
|||
Loading…
Reference in New Issue