docs: 添加静默授权功能迁移指南和解耦方案文档
新增快速迁移指南、解耦方案文档及使用示例,提供将老来赛项目静默授权功能迁移到其他项目的详细方案
Showing
4 changed files
with
1337 additions
and
0 deletions
auth-manager-usage-example.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Description: 通用授权管理器使用示例 | ||
| 3 | + * @Date: 2025-01-25 | ||
| 4 | + */ | ||
| 5 | + | ||
| 6 | +import { UniversalAuthManager, TaroAdapters } from './universal-auth-manager.js' | ||
| 7 | +import request from '@/utils/request' | ||
| 8 | +import { getMyFamiliesAPI } from '@/api/family' | ||
| 9 | + | ||
| 10 | +/** | ||
| 11 | + * 老来赛项目的授权管理器配置 | ||
| 12 | + */ | ||
| 13 | +const laolaisaiAuthConfig = { | ||
| 14 | + // 页面路径配置 | ||
| 15 | + authPage: '/pages/auth/index', | ||
| 16 | + defaultPage: '/pages/Dashboard/index', | ||
| 17 | + | ||
| 18 | + // 测试环境配置 | ||
| 19 | + testOpenIds: ['h-008', 'h-009', 'h-010', 'h-011', 'h-012', 'h-013'], | ||
| 20 | + | ||
| 21 | + // 适配器配置 | ||
| 22 | + ...TaroAdapters.getAdapters(request), | ||
| 23 | + | ||
| 24 | + // 业务逻辑:检查用户是否已加入家庭 | ||
| 25 | + async checkUserStatus() { | ||
| 26 | + try { | ||
| 27 | + const { code, data } = await getMyFamiliesAPI() | ||
| 28 | + return code && data && data.length > 0 | ||
| 29 | + } catch (error) { | ||
| 30 | + console.error('检查用户家庭状态失败:', error) | ||
| 31 | + return false | ||
| 32 | + } | ||
| 33 | + }, | ||
| 34 | + | ||
| 35 | + // 业务逻辑:根据用户状态决定重定向路径 | ||
| 36 | + async getRedirectPath(savedPath, defaultPath) { | ||
| 37 | + const hasFamily = await this.checkUserStatus() | ||
| 38 | + if (!hasFamily) { | ||
| 39 | + return '/pages/Welcome/index' | ||
| 40 | + } | ||
| 41 | + return savedPath || defaultPath | ||
| 42 | + }, | ||
| 43 | + | ||
| 44 | + // 授权成功回调 | ||
| 45 | + onAuthSuccess: () => { | ||
| 46 | + // 授权成功处理逻辑 | ||
| 47 | + }, | ||
| 48 | + | ||
| 49 | + // 授权失败回调 | ||
| 50 | + onAuthError: () => { | ||
| 51 | + // 授权失败处理逻辑 | ||
| 52 | + } | ||
| 53 | +} | ||
| 54 | + | ||
| 55 | +// 创建授权管理器实例 | ||
| 56 | +export const authManager = new UniversalAuthManager(laolaisaiAuthConfig) | ||
| 57 | + | ||
| 58 | +/** | ||
| 59 | + * 在页面中使用授权管理器的示例 | ||
| 60 | + */ | ||
| 61 | + | ||
| 62 | +// 1. 在页面加载时进行静默授权 | ||
| 63 | +export async function handlePageLoad() { | ||
| 64 | + try { | ||
| 65 | + await authManager.silentAuth() | ||
| 66 | + // 页面授权检查完成 | ||
| 67 | + } catch (error) { | ||
| 68 | + // 页面授权失败,跳转到授权页面 | ||
| 69 | + await authManager.navigateToAuth() | ||
| 70 | + } | ||
| 71 | +} | ||
| 72 | + | ||
| 73 | +// 2. 处理分享页面的授权 | ||
| 74 | +export async function handleSharePage(options) { | ||
| 75 | + const isAuthorized = await authManager.handleSharePageAuth(options, () => { | ||
| 76 | + // 分享页面授权成功,可以继续执行业务逻辑 | ||
| 77 | + // 这里执行授权成功后的业务逻辑 | ||
| 78 | + }) | ||
| 79 | + | ||
| 80 | + if (!isAuthorized) { | ||
| 81 | + // 需要授权,已跳转到授权页面 | ||
| 82 | + } | ||
| 83 | +} | ||
| 84 | + | ||
| 85 | +// 3. 在授权页面完成授权后的跳转 | ||
| 86 | +export async function handleAuthComplete() { | ||
| 87 | + await authManager.returnToOriginalPage() | ||
| 88 | +} | ||
| 89 | + | ||
| 90 | +// 4. 检查是否需要授权 | ||
| 91 | +export function checkAuthStatus() { | ||
| 92 | + return !authManager.needAuth() | ||
| 93 | +} | ||
| 94 | + | ||
| 95 | +// 5. 手动触发授权流程 | ||
| 96 | +export async function manualAuth() { | ||
| 97 | + const result = await authManager.silentAuth() | ||
| 98 | + // 手动授权成功 | ||
| 99 | + return result | ||
| 100 | +} | ||
| 101 | + | ||
| 102 | +/** | ||
| 103 | + * 其他项目使用示例 | ||
| 104 | + */ | ||
| 105 | + | ||
| 106 | +// 示例1:简单的电商小程序 | ||
| 107 | +export function createEcommerceAuthManager() { | ||
| 108 | + return new UniversalAuthManager({ | ||
| 109 | + // 基础配置 | ||
| 110 | + authPage: '/pages/login/index', | ||
| 111 | + defaultPage: '/pages/home/index', | ||
| 112 | + | ||
| 113 | + // 适配器 | ||
| 114 | + ...TaroAdapters.getAdapters(yourHttpClient), | ||
| 115 | + | ||
| 116 | + // 简单的用户状态检查 | ||
| 117 | + async checkUserStatus() { | ||
| 118 | + try { | ||
| 119 | + const userInfo = await getUserInfo() | ||
| 120 | + return userInfo && userInfo.isActive | ||
| 121 | + } catch (error) { | ||
| 122 | + return false | ||
| 123 | + } | ||
| 124 | + }, | ||
| 125 | + | ||
| 126 | + // 简单的重定向逻辑 | ||
| 127 | + async getRedirectPath(savedPath, defaultPath) { | ||
| 128 | + return savedPath || defaultPath | ||
| 129 | + } | ||
| 130 | + }) | ||
| 131 | +} | ||
| 132 | + | ||
| 133 | +// 示例2:社交类小程序 | ||
| 134 | +export function createSocialAuthManager() { | ||
| 135 | + return new UniversalAuthManager({ | ||
| 136 | + authPage: '/pages/auth/index', | ||
| 137 | + defaultPage: '/pages/timeline/index', | ||
| 138 | + | ||
| 139 | + ...TaroAdapters.getAdapters(yourHttpClient), | ||
| 140 | + | ||
| 141 | + // 检查用户是否完成了个人资料设置 | ||
| 142 | + async checkUserStatus() { | ||
| 143 | + try { | ||
| 144 | + const profile = await getUserProfile() | ||
| 145 | + return profile && profile.isProfileComplete | ||
| 146 | + } catch (error) { | ||
| 147 | + return false | ||
| 148 | + } | ||
| 149 | + }, | ||
| 150 | + | ||
| 151 | + // 根据用户状态决定跳转 | ||
| 152 | + async getRedirectPath(savedPath, defaultPath) { | ||
| 153 | + const hasCompleteProfile = await this.checkUserStatus() | ||
| 154 | + if (!hasCompleteProfile) { | ||
| 155 | + return '/pages/profile-setup/index' | ||
| 156 | + } | ||
| 157 | + return savedPath || defaultPath | ||
| 158 | + } | ||
| 159 | + }) | ||
| 160 | +} | ||
| 161 | + | ||
| 162 | +// 示例3:企业应用小程序 | ||
| 163 | +export function createEnterpriseAuthManager() { | ||
| 164 | + return new UniversalAuthManager({ | ||
| 165 | + authPage: '/pages/login/index', | ||
| 166 | + defaultPage: '/pages/dashboard/index', | ||
| 167 | + | ||
| 168 | + ...TaroAdapters.getAdapters(yourHttpClient), | ||
| 169 | + | ||
| 170 | + // 检查用户权限和部门信息 | ||
| 171 | + async checkUserStatus() { | ||
| 172 | + try { | ||
| 173 | + const userAuth = await getUserAuthInfo() | ||
| 174 | + return userAuth && userAuth.hasValidRole | ||
| 175 | + } catch (error) { | ||
| 176 | + return false | ||
| 177 | + } | ||
| 178 | + }, | ||
| 179 | + | ||
| 180 | + // 根据用户角色决定跳转 | ||
| 181 | + async getRedirectPath(savedPath, defaultPath) { | ||
| 182 | + const userAuth = await getUserAuthInfo() | ||
| 183 | + | ||
| 184 | + if (!userAuth.hasValidRole) { | ||
| 185 | + return '/pages/no-permission/index' | ||
| 186 | + } | ||
| 187 | + | ||
| 188 | + // 根据用户角色跳转到不同的首页 | ||
| 189 | + if (userAuth.role === 'admin') { | ||
| 190 | + return savedPath || '/pages/admin-dashboard/index' | ||
| 191 | + } else if (userAuth.role === 'manager') { | ||
| 192 | + return savedPath || '/pages/manager-dashboard/index' | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | + return savedPath || defaultPath | ||
| 196 | + } | ||
| 197 | + }) | ||
| 198 | +} | ||
| 199 | + | ||
| 200 | +/** | ||
| 201 | + * 迁移指南:如何从现有代码迁移到通用授权管理器 | ||
| 202 | + */ | ||
| 203 | + | ||
| 204 | +// 步骤1:替换现有的授权函数调用 | ||
| 205 | +// 原来的代码: | ||
| 206 | +// import { silentAuth, needAuth, returnToOriginalPage } from '@/utils/authRedirect' | ||
| 207 | + | ||
| 208 | +// 新的代码: | ||
| 209 | +// import { authManager } from './auth-manager-usage-example' | ||
| 210 | + | ||
| 211 | +// 步骤2:更新函数调用 | ||
| 212 | +// 原来:silentAuth(onSuccess, onError) | ||
| 213 | +// 现在:authManager.silentAuth(onSuccess, onError) | ||
| 214 | + | ||
| 215 | +// 原来:needAuth() | ||
| 216 | +// 现在:authManager.needAuth() | ||
| 217 | + | ||
| 218 | +// 原来:returnToOriginalPage(defaultPath) | ||
| 219 | +// 现在:authManager.returnToOriginalPage(defaultPath) | ||
| 220 | + | ||
| 221 | +// 步骤3:更新页面中的授权逻辑 | ||
| 222 | +// 原来在页面的onLoad中: | ||
| 223 | +/* | ||
| 224 | +onLoad(options) { | ||
| 225 | + if (needAuth()) { | ||
| 226 | + silentAuth( | ||
| 227 | + () => { | ||
| 228 | + // 授权成功,继续执行业务逻辑 | ||
| 229 | + this.loadPageData() | ||
| 230 | + }, | ||
| 231 | + (error) => { | ||
| 232 | + // 授权失败,跳转到授权页面 | ||
| 233 | + navigateToAuth() | ||
| 234 | + } | ||
| 235 | + ) | ||
| 236 | + } else { | ||
| 237 | + // 已授权,直接执行业务逻辑 | ||
| 238 | + this.loadPageData() | ||
| 239 | + } | ||
| 240 | +} | ||
| 241 | +*/ | ||
| 242 | + | ||
| 243 | +// 现在: | ||
| 244 | +/* | ||
| 245 | +async onLoad(options) { | ||
| 246 | + try { | ||
| 247 | + await authManager.silentAuth() | ||
| 248 | + // 授权成功,继续执行业务逻辑 | ||
| 249 | + this.loadPageData() | ||
| 250 | + } catch (error) { | ||
| 251 | + // 授权失败,跳转到授权页面 | ||
| 252 | + await authManager.navigateToAuth() | ||
| 253 | + } | ||
| 254 | +} | ||
| 255 | +*/ | ||
| 256 | + | ||
| 257 | +/** | ||
| 258 | + * 打包为npm包的建议结构 | ||
| 259 | + */ | ||
| 260 | + | ||
| 261 | +// package.json | ||
| 262 | +/* | ||
| 263 | +{ | ||
| 264 | + "name": "@your-org/universal-auth-manager", | ||
| 265 | + "version": "1.0.0", | ||
| 266 | + "description": "通用的小程序静默授权管理器", | ||
| 267 | + "main": "dist/index.js", | ||
| 268 | + "module": "dist/index.esm.js", | ||
| 269 | + "types": "dist/index.d.ts", | ||
| 270 | + "files": [ | ||
| 271 | + "dist", | ||
| 272 | + "adapters" | ||
| 273 | + ], | ||
| 274 | + "scripts": { | ||
| 275 | + "build": "rollup -c", | ||
| 276 | + "dev": "rollup -c -w" | ||
| 277 | + }, | ||
| 278 | + "peerDependencies": { | ||
| 279 | + "@tarojs/taro": ">=3.0.0" | ||
| 280 | + }, | ||
| 281 | + "keywords": [ | ||
| 282 | + "taro", | ||
| 283 | + "miniprogram", | ||
| 284 | + "auth", | ||
| 285 | + "wechat" | ||
| 286 | + ] | ||
| 287 | +} | ||
| 288 | +*/ | ||
| 289 | + | ||
| 290 | +// 目录结构 | ||
| 291 | +/* | ||
| 292 | +universal-auth-manager/ | ||
| 293 | +├── src/ | ||
| 294 | +│ ├── index.js # 主入口文件 | ||
| 295 | +│ ├── auth-manager.js # 核心授权管理器 | ||
| 296 | +│ └── adapters/ | ||
| 297 | +│ ├── taro.js # Taro框架适配器 | ||
| 298 | +│ ├── uni-app.js # uni-app框架适配器 | ||
| 299 | +│ └── native.js # 原生小程序适配器 | ||
| 300 | +├── dist/ # 构建输出目录 | ||
| 301 | +├── examples/ # 使用示例 | ||
| 302 | +├── docs/ # 文档 | ||
| 303 | +├── package.json | ||
| 304 | +├── rollup.config.js | ||
| 305 | +└── README.md | ||
| 306 | +*/ | ||
| 307 | + | ||
| 308 | +export default authManager | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
universal-auth-manager.js
0 → 100644
| 1 | +/* | ||
| 2 | + * @Description: 通用静默授权管理器 - 解耦版本 | ||
| 3 | + * @Author: 基于老来赛项目的静默授权功能重构 | ||
| 4 | + * @Date: 2025-01-25 | ||
| 5 | + */ | ||
| 6 | + | ||
| 7 | +/** | ||
| 8 | + * 通用授权管理器类 | ||
| 9 | + * 通过配置参数适配不同项目的需求 | ||
| 10 | + */ | ||
| 11 | +export class UniversalAuthManager { | ||
| 12 | + constructor(config = {}) { | ||
| 13 | + // 默认配置 | ||
| 14 | + this.config = { | ||
| 15 | + // 授权接口配置 | ||
| 16 | + authUrl: '/srv/?a=openid', | ||
| 17 | + | ||
| 18 | + // 页面路径配置 | ||
| 19 | + authPage: '/pages/auth/index', | ||
| 20 | + defaultPage: '/pages/index/index', | ||
| 21 | + | ||
| 22 | + // 存储配置 | ||
| 23 | + sessionKey: 'sessionid', | ||
| 24 | + routeKey: 'saved_route', | ||
| 25 | + | ||
| 26 | + // 测试环境配置 | ||
| 27 | + testOpenIds: [], | ||
| 28 | + | ||
| 29 | + // 请求超时配置 | ||
| 30 | + timeout: 5000, | ||
| 31 | + | ||
| 32 | + // 加载提示配置 | ||
| 33 | + loadingText: '加载中...', | ||
| 34 | + | ||
| 35 | + // 自定义钩子函数 | ||
| 36 | + onAuthSuccess: null, | ||
| 37 | + onAuthError: null, | ||
| 38 | + checkUserStatus: null, // 用户状态检查函数 | ||
| 39 | + getRedirectPath: null, // 获取重定向路径函数 | ||
| 40 | + | ||
| 41 | + // 适配器接口 | ||
| 42 | + storageAdapter: null, | ||
| 43 | + httpAdapter: null, | ||
| 44 | + navigatorAdapter: null, | ||
| 45 | + platformAdapter: null, | ||
| 46 | + | ||
| 47 | + // 合并用户配置 | ||
| 48 | + ...config | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + // 验证必需的适配器 | ||
| 52 | + this._validateAdapters() | ||
| 53 | + } | ||
| 54 | + | ||
| 55 | + /** | ||
| 56 | + * 验证必需的适配器是否已提供 | ||
| 57 | + * @private | ||
| 58 | + */ | ||
| 59 | + _validateAdapters() { | ||
| 60 | + const requiredAdapters = ['storageAdapter', 'httpAdapter', 'navigatorAdapter', 'platformAdapter'] | ||
| 61 | + | ||
| 62 | + for (const adapter of requiredAdapters) { | ||
| 63 | + if (!this.config[adapter]) { | ||
| 64 | + throw new Error(`缺少必需的适配器: ${adapter}`) | ||
| 65 | + } | ||
| 66 | + } | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + /** | ||
| 70 | + * 检查是否需要授权 | ||
| 71 | + * @returns {boolean} 是否需要授权 | ||
| 72 | + */ | ||
| 73 | + needAuth() { | ||
| 74 | + try { | ||
| 75 | + const sessionid = this.config.storageAdapter.get(this.config.sessionKey) | ||
| 76 | + return !sessionid || sessionid === '' | ||
| 77 | + } catch (error) { | ||
| 78 | + console.error('检查授权状态失败:', error) | ||
| 79 | + return true | ||
| 80 | + } | ||
| 81 | + } | ||
| 82 | + | ||
| 83 | + /** | ||
| 84 | + * 静默授权 | ||
| 85 | + * @param {Function} onSuccess - 成功回调 | ||
| 86 | + * @param {Function} onError - 失败回调 | ||
| 87 | + * @returns {Promise} 授权结果 | ||
| 88 | + */ | ||
| 89 | + async silentAuth(onSuccess, onError) { | ||
| 90 | + return new Promise((resolve, reject) => { | ||
| 91 | + // 检查是否已经授权 | ||
| 92 | + if (!this.needAuth()) { | ||
| 93 | + const result = { code: 1, msg: '已授权' } | ||
| 94 | + if (onSuccess) onSuccess(result) | ||
| 95 | + resolve(result) | ||
| 96 | + return | ||
| 97 | + } | ||
| 98 | + | ||
| 99 | + // 显示loading提示 | ||
| 100 | + this.config.platformAdapter.showLoading({ | ||
| 101 | + title: this.config.loadingText, | ||
| 102 | + mask: true | ||
| 103 | + }) | ||
| 104 | + | ||
| 105 | + // 调用平台登录 | ||
| 106 | + this.config.platformAdapter.login({ | ||
| 107 | + success: (res) => { | ||
| 108 | + if (res.code) { | ||
| 109 | + this._handleAuthRequest(res.code, onSuccess, onError, resolve, reject) | ||
| 110 | + } else { | ||
| 111 | + this._handleAuthError('平台登录失败:' + res.errMsg, onError, reject) | ||
| 112 | + } | ||
| 113 | + }, | ||
| 114 | + fail: (error) => { | ||
| 115 | + this._handleAuthError('调用平台登录失败', onError, reject, error) | ||
| 116 | + } | ||
| 117 | + }) | ||
| 118 | + }) | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + /** | ||
| 122 | + * 处理授权请求 | ||
| 123 | + * @private | ||
| 124 | + */ | ||
| 125 | + async _handleAuthRequest(code, onSuccess, onError, resolve, reject) { | ||
| 126 | + try { | ||
| 127 | + // 构建请求数据 | ||
| 128 | + const requestData = { code } | ||
| 129 | + | ||
| 130 | + // 测试环境下添加测试openid | ||
| 131 | + if (process.env.NODE_ENV === 'development' && this.config.testOpenIds.length > 0) { | ||
| 132 | + requestData.openid = this.config.testOpenIds[0] | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + // 发起授权请求 | ||
| 136 | + const response = await this.config.httpAdapter.post(this.config.authUrl, requestData) | ||
| 137 | + | ||
| 138 | + this.config.platformAdapter.hideLoading() | ||
| 139 | + | ||
| 140 | + if (response.data.code) { | ||
| 141 | + const cookie = response.cookies && response.cookies[0] | ||
| 142 | + if (cookie) { | ||
| 143 | + // 保存sessionid | ||
| 144 | + this.config.storageAdapter.set(this.config.sessionKey, cookie) | ||
| 145 | + | ||
| 146 | + // 更新HTTP客户端的默认headers | ||
| 147 | + if (this.config.httpAdapter.updateHeaders) { | ||
| 148 | + this.config.httpAdapter.updateHeaders({ cookie }) | ||
| 149 | + } | ||
| 150 | + | ||
| 151 | + // 执行成功回调 | ||
| 152 | + if (onSuccess) onSuccess(response.data) | ||
| 153 | + if (this.config.onAuthSuccess) this.config.onAuthSuccess(response.data) | ||
| 154 | + | ||
| 155 | + resolve(response.data) | ||
| 156 | + } else { | ||
| 157 | + this._handleAuthError('授权失败:没有获取到有效的会话信息', onError, reject) | ||
| 158 | + } | ||
| 159 | + } else { | ||
| 160 | + this._handleAuthError(response.data.msg || '授权失败', onError, reject) | ||
| 161 | + } | ||
| 162 | + } catch (error) { | ||
| 163 | + this.config.platformAdapter.hideLoading() | ||
| 164 | + this._handleAuthError('网络请求失败,请稍后重试', onError, reject, error) | ||
| 165 | + } | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + /** | ||
| 169 | + * 处理授权错误 | ||
| 170 | + * @private | ||
| 171 | + */ | ||
| 172 | + _handleAuthError(message, onError, reject, originalError = null) { | ||
| 173 | + console.error('静默授权失败:', message, originalError) | ||
| 174 | + | ||
| 175 | + if (onError) onError(message) | ||
| 176 | + if (this.config.onAuthError) this.config.onAuthError(message, originalError) | ||
| 177 | + | ||
| 178 | + reject(new Error(message)) | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + /** | ||
| 182 | + * 获取当前页面完整路径(包含参数) | ||
| 183 | + * @returns {string} 完整的页面路径 | ||
| 184 | + */ | ||
| 185 | + getCurrentPageFullPath() { | ||
| 186 | + return this.config.platformAdapter.getCurrentPageFullPath() | ||
| 187 | + } | ||
| 188 | + | ||
| 189 | + /** | ||
| 190 | + * 保存当前页面路径 | ||
| 191 | + * @param {string} customPath - 自定义路径 | ||
| 192 | + */ | ||
| 193 | + saveCurrentPagePath(customPath) { | ||
| 194 | + const path = customPath || this.getCurrentPageFullPath() | ||
| 195 | + this.config.storageAdapter.set(this.config.routeKey, path) | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + /** | ||
| 199 | + * 跳转到授权页面 | ||
| 200 | + * @param {string} returnPath - 授权完成后要返回的页面路径 | ||
| 201 | + */ | ||
| 202 | + async navigateToAuth(returnPath) { | ||
| 203 | + // 保存返回路径 | ||
| 204 | + if (returnPath) { | ||
| 205 | + this.saveCurrentPagePath(returnPath) | ||
| 206 | + } else { | ||
| 207 | + this.saveCurrentPagePath() | ||
| 208 | + } | ||
| 209 | + | ||
| 210 | + // 跳转到授权页面 | ||
| 211 | + await this.config.navigatorAdapter.navigateTo({ | ||
| 212 | + url: this.config.authPage | ||
| 213 | + }) | ||
| 214 | + } | ||
| 215 | + | ||
| 216 | + /** | ||
| 217 | + * 授权完成后返回原页面 | ||
| 218 | + * @param {string} defaultPath - 默认返回路径 | ||
| 219 | + */ | ||
| 220 | + async returnToOriginalPage(defaultPath) { | ||
| 221 | + const finalDefaultPath = defaultPath || this.config.defaultPage | ||
| 222 | + | ||
| 223 | + try { | ||
| 224 | + // 获取保存的路径 | ||
| 225 | + const savedPath = this.config.storageAdapter.get(this.config.routeKey) | ||
| 226 | + | ||
| 227 | + // 清除保存的路径 | ||
| 228 | + this.config.storageAdapter.remove(this.config.routeKey) | ||
| 229 | + | ||
| 230 | + // 获取当前页面信息 | ||
| 231 | + const currentRoute = this.config.platformAdapter.getCurrentRoute() | ||
| 232 | + | ||
| 233 | + // 确定目标路径 | ||
| 234 | + let targetPath = finalDefaultPath | ||
| 235 | + if (savedPath && savedPath !== '') { | ||
| 236 | + targetPath = savedPath.startsWith('/') ? savedPath : `/${savedPath}` | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + // 如果配置了自定义重定向路径解析函数,使用它来确定最终路径 | ||
| 240 | + if (this.config.getRedirectPath) { | ||
| 241 | + targetPath = await this.config.getRedirectPath(savedPath, finalDefaultPath) | ||
| 242 | + } | ||
| 243 | + | ||
| 244 | + // 提取目标页面路由(去掉参数) | ||
| 245 | + const targetRoute = targetPath.split('?')[0].replace(/^\//, '') | ||
| 246 | + | ||
| 247 | + // 如果当前页面就是目标页面,不需要跳转 | ||
| 248 | + if (currentRoute === targetRoute) { | ||
| 249 | + return | ||
| 250 | + } | ||
| 251 | + | ||
| 252 | + // 根据目标路径选择跳转方式 | ||
| 253 | + if (targetRoute === this.config.defaultPage.replace(/^\//, '')) { | ||
| 254 | + // 如果是默认页面,使用 reLaunch | ||
| 255 | + await this.config.navigatorAdapter.reLaunch({ url: targetPath }) | ||
| 256 | + } else { | ||
| 257 | + // 其他页面使用 redirectTo | ||
| 258 | + await this.config.navigatorAdapter.redirectTo({ url: targetPath }) | ||
| 259 | + } | ||
| 260 | + } catch (error) { | ||
| 261 | + console.error('returnToOriginalPage 执行出错:', error) | ||
| 262 | + | ||
| 263 | + // 错误处理:使用默认路径或自定义错误处理逻辑 | ||
| 264 | + try { | ||
| 265 | + let fallbackPath = finalDefaultPath | ||
| 266 | + | ||
| 267 | + if (this.config.getRedirectPath) { | ||
| 268 | + fallbackPath = await this.config.getRedirectPath(null, finalDefaultPath) | ||
| 269 | + } | ||
| 270 | + | ||
| 271 | + await this.config.navigatorAdapter.reLaunch({ url: fallbackPath }) | ||
| 272 | + } catch (finalError) { | ||
| 273 | + console.error('最终降级方案也失败了:', finalError) | ||
| 274 | + } | ||
| 275 | + } | ||
| 276 | + } | ||
| 277 | + | ||
| 278 | + /** | ||
| 279 | + * 检查页面是否来自分享 | ||
| 280 | + * @param {Object} options - 页面参数 | ||
| 281 | + * @returns {boolean} 是否来自分享 | ||
| 282 | + */ | ||
| 283 | + isFromShare(options) { | ||
| 284 | + return options && (options.from_share === '1' || options.scene) | ||
| 285 | + } | ||
| 286 | + | ||
| 287 | + /** | ||
| 288 | + * 处理分享页面的授权逻辑 | ||
| 289 | + * @param {Object} options - 页面参数 | ||
| 290 | + * @param {Function} callback - 授权成功后的回调函数 | ||
| 291 | + * @returns {boolean} 是否已授权 | ||
| 292 | + */ | ||
| 293 | + async handleSharePageAuth(options, callback) { | ||
| 294 | + if (!this.needAuth()) { | ||
| 295 | + // 已授权,执行回调 | ||
| 296 | + if (callback && typeof callback === 'function') { | ||
| 297 | + callback() | ||
| 298 | + } | ||
| 299 | + return true | ||
| 300 | + } | ||
| 301 | + | ||
| 302 | + // 没有授权,需要先授权 | ||
| 303 | + if (this.isFromShare(options)) { | ||
| 304 | + // 来自分享,保存当前页面路径用于授权后返回 | ||
| 305 | + this.saveCurrentPagePath() | ||
| 306 | + } | ||
| 307 | + | ||
| 308 | + // 跳转到授权页面 | ||
| 309 | + await this.navigateToAuth() | ||
| 310 | + return false | ||
| 311 | + } | ||
| 312 | + | ||
| 313 | + /** | ||
| 314 | + * 为分享链接添加分享标识参数 | ||
| 315 | + * @param {string} path - 原始路径 | ||
| 316 | + * @returns {string} 添加分享标识后的路径 | ||
| 317 | + */ | ||
| 318 | + addShareFlag(path) { | ||
| 319 | + const separator = path.includes('?') ? '&' : '?' | ||
| 320 | + return `${path}${separator}from_share=1` | ||
| 321 | + } | ||
| 322 | +} | ||
| 323 | + | ||
| 324 | +/** | ||
| 325 | + * Taro框架适配器 | ||
| 326 | + */ | ||
| 327 | +export class TaroAdapters { | ||
| 328 | + /** | ||
| 329 | + * 存储适配器 | ||
| 330 | + */ | ||
| 331 | + static storageAdapter = { | ||
| 332 | + get(key) { | ||
| 333 | + try { | ||
| 334 | + return wx.getStorageSync(key) || null | ||
| 335 | + } catch (error) { | ||
| 336 | + console.error(`获取存储${key}失败:`, error) | ||
| 337 | + return null | ||
| 338 | + } | ||
| 339 | + }, | ||
| 340 | + | ||
| 341 | + set(key, value) { | ||
| 342 | + try { | ||
| 343 | + wx.setStorageSync(key, value) | ||
| 344 | + } catch (error) { | ||
| 345 | + console.error(`设置存储${key}失败:`, error) | ||
| 346 | + } | ||
| 347 | + }, | ||
| 348 | + | ||
| 349 | + remove(key) { | ||
| 350 | + try { | ||
| 351 | + wx.removeStorageSync(key) | ||
| 352 | + } catch (error) { | ||
| 353 | + console.error(`删除存储${key}失败:`, error) | ||
| 354 | + } | ||
| 355 | + } | ||
| 356 | + } | ||
| 357 | + | ||
| 358 | + /** | ||
| 359 | + * 导航适配器 | ||
| 360 | + */ | ||
| 361 | + static navigatorAdapter = { | ||
| 362 | + async navigateTo(options) { | ||
| 363 | + const Taro = await import('@tarojs/taro') | ||
| 364 | + return Taro.default.navigateTo(options) | ||
| 365 | + }, | ||
| 366 | + | ||
| 367 | + async redirectTo(options) { | ||
| 368 | + const Taro = await import('@tarojs/taro') | ||
| 369 | + return Taro.default.redirectTo(options) | ||
| 370 | + }, | ||
| 371 | + | ||
| 372 | + async reLaunch(options) { | ||
| 373 | + const Taro = await import('@tarojs/taro') | ||
| 374 | + return Taro.default.reLaunch(options) | ||
| 375 | + } | ||
| 376 | + } | ||
| 377 | + | ||
| 378 | + /** | ||
| 379 | + * 平台适配器 | ||
| 380 | + */ | ||
| 381 | + static platformAdapter = { | ||
| 382 | + showLoading(options) { | ||
| 383 | + wx.showLoading(options) | ||
| 384 | + }, | ||
| 385 | + | ||
| 386 | + hideLoading() { | ||
| 387 | + wx.hideLoading() | ||
| 388 | + }, | ||
| 389 | + | ||
| 390 | + login(options) { | ||
| 391 | + wx.login(options) | ||
| 392 | + }, | ||
| 393 | + | ||
| 394 | + getCurrentPageFullPath() { | ||
| 395 | + const pages = getCurrentPages() | ||
| 396 | + if (pages.length === 0) return '' | ||
| 397 | + | ||
| 398 | + const currentPage = pages[pages.length - 1] | ||
| 399 | + const route = currentPage.route | ||
| 400 | + const options = currentPage.options | ||
| 401 | + | ||
| 402 | + // 构建查询参数字符串 | ||
| 403 | + const queryParams = Object.keys(options) | ||
| 404 | + .map(key => `${key}=${encodeURIComponent(options[key])}`) | ||
| 405 | + .join('&') | ||
| 406 | + | ||
| 407 | + return queryParams ? `${route}?${queryParams}` : route | ||
| 408 | + }, | ||
| 409 | + | ||
| 410 | + getCurrentRoute() { | ||
| 411 | + const pages = getCurrentPages() | ||
| 412 | + if (pages.length === 0) return '' | ||
| 413 | + | ||
| 414 | + const currentPage = pages[pages.length - 1] | ||
| 415 | + return currentPage.route | ||
| 416 | + } | ||
| 417 | + } | ||
| 418 | + | ||
| 419 | + /** | ||
| 420 | + * 获取完整的Taro适配器配置 | ||
| 421 | + * @param {Object} httpAdapter - HTTP适配器实例 | ||
| 422 | + * @returns {Object} 完整的适配器配置 | ||
| 423 | + */ | ||
| 424 | + static getAdapters(httpAdapter) { | ||
| 425 | + return { | ||
| 426 | + storageAdapter: this.storageAdapter, | ||
| 427 | + navigatorAdapter: this.navigatorAdapter, | ||
| 428 | + platformAdapter: this.platformAdapter, | ||
| 429 | + httpAdapter: httpAdapter | ||
| 430 | + } | ||
| 431 | + } | ||
| 432 | +} | ||
| 433 | + | ||
| 434 | +/** | ||
| 435 | + * 创建老来赛项目的授权管理器实例 | ||
| 436 | + * @param {Object} customConfig - 自定义配置 | ||
| 437 | + * @returns {UniversalAuthManager} 授权管理器实例 | ||
| 438 | + */ | ||
| 439 | +export function createLaolaisaiAuthManager(customConfig = {}) { | ||
| 440 | + // 这里需要传入项目特定的HTTP适配器和业务逻辑 | ||
| 441 | + const defaultConfig = { | ||
| 442 | + authPage: '/pages/auth/index', | ||
| 443 | + defaultPage: '/pages/Dashboard/index', | ||
| 444 | + testOpenIds: ['h-008', 'h-009', 'h-010', 'h-011', 'h-012', 'h-013'], | ||
| 445 | + | ||
| 446 | + // 这些需要在实际使用时传入 | ||
| 447 | + // httpAdapter: request, | ||
| 448 | + // async checkUserStatus() { | ||
| 449 | + // const { code, data } = await getMyFamiliesAPI() | ||
| 450 | + // return code && data && data.length > 0 | ||
| 451 | + // }, | ||
| 452 | + // async getRedirectPath(savedPath, defaultPath) { | ||
| 453 | + // const hasFamily = await this.checkUserStatus() | ||
| 454 | + // return hasFamily ? (savedPath || defaultPath) : '/pages/Welcome/index' | ||
| 455 | + // } | ||
| 456 | + } | ||
| 457 | + | ||
| 458 | + return new UniversalAuthManager({ | ||
| 459 | + ...defaultConfig, | ||
| 460 | + ...customConfig | ||
| 461 | + }) | ||
| 462 | +} | ||
| 463 | + | ||
| 464 | +export default UniversalAuthManager | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
快速迁移指南.md
0 → 100644
| 1 | +# 静默授权功能快速迁移指南 | ||
| 2 | + | ||
| 3 | +## 概述 | ||
| 4 | + | ||
| 5 | +本指南提供了将老来赛项目的静默授权功能迁移到其他项目的最低成本方案。通过简单的配置和少量代码修改,你可以在新项目中快速复用这套成熟的授权机制。 | ||
| 6 | + | ||
| 7 | +## 最低成本方案:直接复制 + 配置化 | ||
| 8 | + | ||
| 9 | +### 第一步:复制核心文件 | ||
| 10 | + | ||
| 11 | +将以下文件复制到新项目中: | ||
| 12 | + | ||
| 13 | +1. **复制 `universal-auth-manager.js`** 到新项目的 `utils` 目录 | ||
| 14 | +2. **创建项目特定的配置文件** | ||
| 15 | + | ||
| 16 | +### 第二步:创建项目配置 | ||
| 17 | + | ||
| 18 | +在新项目中创建 `auth-config.js`: | ||
| 19 | + | ||
| 20 | +```javascript | ||
| 21 | +// auth-config.js | ||
| 22 | +import { UniversalAuthManager, TaroAdapters } from './utils/universal-auth-manager.js' | ||
| 23 | +import request from './utils/request' // 你的HTTP请求工具 | ||
| 24 | + | ||
| 25 | +// 项目特定配置 | ||
| 26 | +const authConfig = { | ||
| 27 | + // 必需配置:页面路径 | ||
| 28 | + authPage: '/pages/login/index', // 你的登录页面路径 | ||
| 29 | + defaultPage: '/pages/home/index', // 你的首页路径 | ||
| 30 | + | ||
| 31 | + // 必需配置:适配器 | ||
| 32 | + ...TaroAdapters.getAdapters(request), | ||
| 33 | + | ||
| 34 | + // 可选配置:测试环境openid(如果需要) | ||
| 35 | + testOpenIds: ['test-user-001'], | ||
| 36 | + | ||
| 37 | + // 可选配置:用户状态检查(如果有特殊业务逻辑) | ||
| 38 | + async checkUserStatus() { | ||
| 39 | + // 示例:检查用户是否完成了必要的设置 | ||
| 40 | + try { | ||
| 41 | + const userInfo = await getUserInfo() // 替换为你的用户信息接口 | ||
| 42 | + return userInfo && userInfo.status === 'active' | ||
| 43 | + } catch (error) { | ||
| 44 | + return false | ||
| 45 | + } | ||
| 46 | + }, | ||
| 47 | + | ||
| 48 | + // 可选配置:自定义重定向逻辑 | ||
| 49 | + async getRedirectPath(savedPath, defaultPath) { | ||
| 50 | + // 示例:根据用户状态决定跳转页面 | ||
| 51 | + const isUserActive = await this.checkUserStatus() | ||
| 52 | + if (!isUserActive) { | ||
| 53 | + return '/pages/setup/index' // 跳转到设置页面 | ||
| 54 | + } | ||
| 55 | + return savedPath || defaultPath | ||
| 56 | + } | ||
| 57 | +} | ||
| 58 | + | ||
| 59 | +// 导出授权管理器实例 | ||
| 60 | +export const authManager = new UniversalAuthManager(authConfig) | ||
| 61 | + | ||
| 62 | +// 导出常用方法(保持与原项目API一致) | ||
| 63 | +export const silentAuth = (onSuccess, onError) => authManager.silentAuth(onSuccess, onError) | ||
| 64 | +export const needAuth = () => authManager.needAuth() | ||
| 65 | +export const navigateToAuth = (returnPath) => authManager.navigateToAuth(returnPath) | ||
| 66 | +export const returnToOriginalPage = (defaultPath) => authManager.returnToOriginalPage(defaultPath) | ||
| 67 | +export const handleSharePageAuth = (options, callback) => authManager.handleSharePageAuth(options, callback) | ||
| 68 | +``` | ||
| 69 | + | ||
| 70 | +### 第三步:更新现有代码 | ||
| 71 | + | ||
| 72 | +将原来的导入语句: | ||
| 73 | +```javascript | ||
| 74 | +// 原来 | ||
| 75 | +import { silentAuth, needAuth, navigateToAuth } from '@/utils/authRedirect' | ||
| 76 | +``` | ||
| 77 | + | ||
| 78 | +替换为: | ||
| 79 | +```javascript | ||
| 80 | +// 现在 | ||
| 81 | +import { silentAuth, needAuth, navigateToAuth } from './auth-config' | ||
| 82 | +``` | ||
| 83 | + | ||
| 84 | +**其他代码无需修改!** | ||
| 85 | + | ||
| 86 | +## 极简方案:5分钟快速配置 | ||
| 87 | + | ||
| 88 | +如果你的项目非常简单,只需要基础的静默授权功能,可以使用这个极简配置: | ||
| 89 | + | ||
| 90 | +```javascript | ||
| 91 | +// simple-auth.js | ||
| 92 | +import { UniversalAuthManager, TaroAdapters } from './utils/universal-auth-manager.js' | ||
| 93 | +import request from './utils/request' | ||
| 94 | + | ||
| 95 | +// 极简配置 | ||
| 96 | +const authManager = new UniversalAuthManager({ | ||
| 97 | + authPage: '/pages/login/index', // 改为你的登录页 | ||
| 98 | + defaultPage: '/pages/home/index', // 改为你的首页 | ||
| 99 | + ...TaroAdapters.getAdapters(request) | ||
| 100 | +}) | ||
| 101 | + | ||
| 102 | +// 导出方法 | ||
| 103 | +export const silentAuth = (onSuccess, onError) => authManager.silentAuth(onSuccess, onError) | ||
| 104 | +export const needAuth = () => authManager.needAuth() | ||
| 105 | +export const navigateToAuth = () => authManager.navigateToAuth() | ||
| 106 | +export const returnToOriginalPage = () => authManager.returnToOriginalPage() | ||
| 107 | +``` | ||
| 108 | + | ||
| 109 | +## 常见使用场景 | ||
| 110 | + | ||
| 111 | +### 1. 页面加载时检查授权 | ||
| 112 | + | ||
| 113 | +```javascript | ||
| 114 | +// 在页面的 onLoad 方法中 | ||
| 115 | +async onLoad() { | ||
| 116 | + try { | ||
| 117 | + await silentAuth() | ||
| 118 | + // 授权成功,加载页面数据 | ||
| 119 | + this.loadData() | ||
| 120 | + } catch (error) { | ||
| 121 | + // 授权失败,跳转登录 | ||
| 122 | + navigateToAuth() | ||
| 123 | + } | ||
| 124 | +} | ||
| 125 | +``` | ||
| 126 | + | ||
| 127 | +### 2. 处理分享页面 | ||
| 128 | + | ||
| 129 | +```javascript | ||
| 130 | +// 处理从分享链接进入的页面 | ||
| 131 | +async onLoad(options) { | ||
| 132 | + const isAuthorized = await handleSharePageAuth(options, () => { | ||
| 133 | + // 授权成功后的回调 | ||
| 134 | + this.loadData() | ||
| 135 | + }) | ||
| 136 | + | ||
| 137 | + if (isAuthorized) { | ||
| 138 | + // 已经授权,直接加载数据 | ||
| 139 | + this.loadData() | ||
| 140 | + } | ||
| 141 | + // 如果未授权,会自动跳转到登录页 | ||
| 142 | +} | ||
| 143 | +``` | ||
| 144 | + | ||
| 145 | +### 3. 登录页面完成授权后跳转 | ||
| 146 | + | ||
| 147 | +```javascript | ||
| 148 | +// 在登录页面授权成功后 | ||
| 149 | +async handleAuthSuccess() { | ||
| 150 | + // 跳转回原来的页面 | ||
| 151 | + await returnToOriginalPage() | ||
| 152 | +} | ||
| 153 | +``` | ||
| 154 | + | ||
| 155 | +## 不同项目类型的配置示例 | ||
| 156 | + | ||
| 157 | +### 电商类小程序 | ||
| 158 | + | ||
| 159 | +```javascript | ||
| 160 | +const ecommerceConfig = { | ||
| 161 | + authPage: '/pages/login/index', | ||
| 162 | + defaultPage: '/pages/mall/index', | ||
| 163 | + ...TaroAdapters.getAdapters(request), | ||
| 164 | + | ||
| 165 | + // 检查用户是否绑定了手机号 | ||
| 166 | + async checkUserStatus() { | ||
| 167 | + const userInfo = await getUserInfo() | ||
| 168 | + return userInfo && userInfo.mobile | ||
| 169 | + }, | ||
| 170 | + | ||
| 171 | + // 未绑定手机号跳转到绑定页面 | ||
| 172 | + async getRedirectPath(savedPath, defaultPath) { | ||
| 173 | + const hasMobile = await this.checkUserStatus() | ||
| 174 | + return hasMobile ? (savedPath || defaultPath) : '/pages/bind-mobile/index' | ||
| 175 | + } | ||
| 176 | +} | ||
| 177 | +``` | ||
| 178 | + | ||
| 179 | +### 内容类小程序 | ||
| 180 | + | ||
| 181 | +```javascript | ||
| 182 | +const contentConfig = { | ||
| 183 | + authPage: '/pages/auth/index', | ||
| 184 | + defaultPage: '/pages/feed/index', | ||
| 185 | + ...TaroAdapters.getAdapters(request), | ||
| 186 | + | ||
| 187 | + // 检查用户是否选择了兴趣标签 | ||
| 188 | + async checkUserStatus() { | ||
| 189 | + const profile = await getUserProfile() | ||
| 190 | + return profile && profile.interests && profile.interests.length > 0 | ||
| 191 | + }, | ||
| 192 | + | ||
| 193 | + // 未选择兴趣跳转到兴趣选择页面 | ||
| 194 | + async getRedirectPath(savedPath, defaultPath) { | ||
| 195 | + const hasInterests = await this.checkUserStatus() | ||
| 196 | + return hasInterests ? (savedPath || defaultPath) : '/pages/select-interests/index' | ||
| 197 | + } | ||
| 198 | +} | ||
| 199 | +``` | ||
| 200 | + | ||
| 201 | +### 工具类小程序(最简单) | ||
| 202 | + | ||
| 203 | +```javascript | ||
| 204 | +const toolConfig = { | ||
| 205 | + authPage: '/pages/login/index', | ||
| 206 | + defaultPage: '/pages/tools/index', | ||
| 207 | + ...TaroAdapters.getAdapters(request) | ||
| 208 | + // 无需额外的业务逻辑 | ||
| 209 | +} | ||
| 210 | +``` | ||
| 211 | + | ||
| 212 | +## 迁移检查清单 | ||
| 213 | + | ||
| 214 | +- [ ] 复制 `universal-auth-manager.js` 文件 | ||
| 215 | +- [ ] 创建项目配置文件 | ||
| 216 | +- [ ] 更新导入语句 | ||
| 217 | +- [ ] 配置正确的页面路径 | ||
| 218 | +- [ ] 配置HTTP请求适配器 | ||
| 219 | +- [ ] 测试静默授权功能 | ||
| 220 | +- [ ] 测试页面跳转逻辑 | ||
| 221 | +- [ ] 测试分享页面授权 | ||
| 222 | + | ||
| 223 | +## 常见问题 | ||
| 224 | + | ||
| 225 | +### Q: 我的项目使用的不是Taro,可以用吗? | ||
| 226 | +A: 可以,但需要实现对应框架的适配器。参考 `TaroAdapters` 的实现方式。 | ||
| 227 | + | ||
| 228 | +### Q: 我的授权接口地址不同怎么办? | ||
| 229 | +A: 在配置中修改 `authUrl` 参数即可。 | ||
| 230 | + | ||
| 231 | +### Q: 我不需要复杂的业务逻辑,只要基础授权可以吗? | ||
| 232 | +A: 可以,使用极简配置方案,只配置必需的页面路径即可。 | ||
| 233 | + | ||
| 234 | +### Q: 如何调试授权问题? | ||
| 235 | +A: 在配置中添加回调函数来监听授权状态: | ||
| 236 | +```javascript | ||
| 237 | +const config = { | ||
| 238 | + // ... 其他配置 | ||
| 239 | + onAuthSuccess: (result) => console.log('授权成功:', result), | ||
| 240 | + onAuthError: (error) => console.error('授权失败:', error) | ||
| 241 | +} | ||
| 242 | +``` | ||
| 243 | + | ||
| 244 | +## 总结 | ||
| 245 | + | ||
| 246 | +通过这种方式,你可以用最低的成本(约10分钟配置时间)将成熟的静默授权功能迁移到新项目中。核心优势: | ||
| 247 | + | ||
| 248 | +1. **零学习成本** - API保持不变 | ||
| 249 | +2. **最小改动** - 只需修改导入语句 | ||
| 250 | +3. **高度可配置** - 支持各种业务场景 | ||
| 251 | +4. **向后兼容** - 不影响现有功能 | ||
| 252 | +5. **易于维护** - 统一的授权逻辑管理 | ||
| 253 | + | ||
| 254 | +建议先使用极简配置快速验证功能,然后根据实际需求逐步添加业务逻辑。 | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
静默授权功能解耦方案.md
0 → 100644
| 1 | +# 静默授权功能解耦方案 | ||
| 2 | + | ||
| 3 | +## 当前功能分析 | ||
| 4 | + | ||
| 5 | +### 核心功能 | ||
| 6 | +静默授权功能主要包含以下核心能力: | ||
| 7 | +1. **静默授权** - `silentAuth()` 函数,自动获取微信授权并保存会话信息 | ||
| 8 | +2. **授权状态检查** - `needAuth()` 函数,检查是否需要重新授权 | ||
| 9 | +3. **页面路径管理** - 保存和恢复用户访问路径 | ||
| 10 | +4. **授权重定向** - 授权完成后的页面跳转逻辑 | ||
| 11 | +5. **分享页面处理** - 处理从分享链接进入的授权逻辑 | ||
| 12 | + | ||
| 13 | +### 当前耦合点分析 | ||
| 14 | + | ||
| 15 | +#### 1. 项目特定依赖 | ||
| 16 | +- **路由存储**: 依赖 `@/stores/router` (Pinia store) | ||
| 17 | +- **网络请求**: 依赖 `@/utils/request` (axios封装) | ||
| 18 | +- **业务API**: 依赖 `@/api/family` (家庭相关接口) | ||
| 19 | +- **页面路径**: 硬编码了项目特定的页面路径 | ||
| 20 | + - `/pages/Dashboard/index` (首页) | ||
| 21 | + - `/pages/Welcome/index` (欢迎页) | ||
| 22 | + - `/pages/auth/index` (授权页) | ||
| 23 | + | ||
| 24 | +#### 2. 业务逻辑耦合 | ||
| 25 | +- **家庭状态检查**: `checkUserHasFamily()` 函数与业务强耦合 | ||
| 26 | +- **跳转逻辑**: 根据家庭状态决定跳转目标的业务逻辑 | ||
| 27 | +- **测试环境配置**: 硬编码的测试openid列表 | ||
| 28 | + | ||
| 29 | +#### 3. 框架依赖 | ||
| 30 | +- **Taro框架**: 使用Taro的API进行页面跳转和存储操作 | ||
| 31 | +- **微信小程序**: 依赖微信小程序的wx API | ||
| 32 | + | ||
| 33 | +## 解耦方案 | ||
| 34 | + | ||
| 35 | +### 方案一:配置化解耦(推荐) | ||
| 36 | + | ||
| 37 | +创建一个通用的授权管理器,通过配置参数来适配不同项目。 | ||
| 38 | + | ||
| 39 | +#### 1. 核心授权管理器 | ||
| 40 | +```javascript | ||
| 41 | +// auth-manager.js | ||
| 42 | +export class AuthManager { | ||
| 43 | + constructor(config) { | ||
| 44 | + this.config = { | ||
| 45 | + // 授权接口配置 | ||
| 46 | + authUrl: '/srv/?a=openid', | ||
| 47 | + | ||
| 48 | + // 页面路径配置 | ||
| 49 | + authPage: '/pages/auth/index', | ||
| 50 | + defaultPage: '/pages/index/index', | ||
| 51 | + | ||
| 52 | + // 存储配置 | ||
| 53 | + sessionKey: 'sessionid', | ||
| 54 | + routeKey: 'saved_route', | ||
| 55 | + | ||
| 56 | + // 测试环境配置 | ||
| 57 | + testOpenIds: [], | ||
| 58 | + | ||
| 59 | + // 自定义钩子函数 | ||
| 60 | + onAuthSuccess: null, | ||
| 61 | + onAuthError: null, | ||
| 62 | + checkUserStatus: null, // 用户状态检查函数 | ||
| 63 | + getRedirectPath: null, // 获取重定向路径函数 | ||
| 64 | + | ||
| 65 | + // 网络请求实例 | ||
| 66 | + httpClient: null, | ||
| 67 | + | ||
| 68 | + ...config | ||
| 69 | + } | ||
| 70 | + } | ||
| 71 | + | ||
| 72 | + // 静默授权 | ||
| 73 | + async silentAuth(onSuccess, onError) { | ||
| 74 | + // 实现逻辑... | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + // 检查授权状态 | ||
| 78 | + needAuth() { | ||
| 79 | + // 实现逻辑... | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + // 页面跳转管理 | ||
| 83 | + async returnToOriginalPage(defaultPath) { | ||
| 84 | + // 实现逻辑... | ||
| 85 | + } | ||
| 86 | +} | ||
| 87 | +``` | ||
| 88 | + | ||
| 89 | +#### 2. 项目适配配置 | ||
| 90 | +```javascript | ||
| 91 | +// 老来赛项目配置 | ||
| 92 | +import { getMyFamiliesAPI } from '@/api/family' | ||
| 93 | +import request from '@/utils/request' | ||
| 94 | + | ||
| 95 | +const authConfig = { | ||
| 96 | + authPage: '/pages/auth/index', | ||
| 97 | + defaultPage: '/pages/Dashboard/index', | ||
| 98 | + httpClient: request, | ||
| 99 | + testOpenIds: ['h-008', 'h-009', 'h-010'], | ||
| 100 | + | ||
| 101 | + // 用户状态检查 | ||
| 102 | + async checkUserStatus() { | ||
| 103 | + try { | ||
| 104 | + const { code, data } = await getMyFamiliesAPI() | ||
| 105 | + return code && data && data.length > 0 | ||
| 106 | + } catch (error) { | ||
| 107 | + return false | ||
| 108 | + } | ||
| 109 | + }, | ||
| 110 | + | ||
| 111 | + // 获取重定向路径 | ||
| 112 | + async getRedirectPath(savedPath, defaultPath) { | ||
| 113 | + const hasFamily = await this.checkUserStatus() | ||
| 114 | + if (!hasFamily) { | ||
| 115 | + return '/pages/Welcome/index' | ||
| 116 | + } | ||
| 117 | + return savedPath || defaultPath | ||
| 118 | + } | ||
| 119 | +} | ||
| 120 | + | ||
| 121 | +export const authManager = new AuthManager(authConfig) | ||
| 122 | +``` | ||
| 123 | + | ||
| 124 | +### 方案二:插件化解耦 | ||
| 125 | + | ||
| 126 | +将授权功能拆分为多个独立的插件模块。 | ||
| 127 | + | ||
| 128 | +#### 1. 核心授权插件 | ||
| 129 | +```javascript | ||
| 130 | +// plugins/auth-core.js | ||
| 131 | +export class AuthCore { | ||
| 132 | + constructor(storage, http) { | ||
| 133 | + this.storage = storage | ||
| 134 | + this.http = http | ||
| 135 | + } | ||
| 136 | + | ||
| 137 | + async silentAuth(config) { | ||
| 138 | + // 核心授权逻辑 | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + needAuth(sessionKey = 'sessionid') { | ||
| 142 | + // 授权状态检查 | ||
| 143 | + } | ||
| 144 | +} | ||
| 145 | +``` | ||
| 146 | + | ||
| 147 | +#### 2. 路由管理插件 | ||
| 148 | +```javascript | ||
| 149 | +// plugins/route-manager.js | ||
| 150 | +export class RouteManager { | ||
| 151 | + constructor(storage, navigator) { | ||
| 152 | + this.storage = storage | ||
| 153 | + this.navigator = navigator | ||
| 154 | + } | ||
| 155 | + | ||
| 156 | + saveCurrentPath() { | ||
| 157 | + // 保存当前路径 | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + async returnToPath(pathResolver) { | ||
| 161 | + // 返回指定路径 | ||
| 162 | + } | ||
| 163 | +} | ||
| 164 | +``` | ||
| 165 | + | ||
| 166 | +#### 3. 业务适配插件 | ||
| 167 | +```javascript | ||
| 168 | +// plugins/business-adapter.js | ||
| 169 | +export class BusinessAdapter { | ||
| 170 | + constructor(apiClient) { | ||
| 171 | + this.apiClient = apiClient | ||
| 172 | + } | ||
| 173 | + | ||
| 174 | + async checkUserStatus() { | ||
| 175 | + // 业务相关的用户状态检查 | ||
| 176 | + } | ||
| 177 | + | ||
| 178 | + resolveRedirectPath(userStatus, savedPath, defaultPath) { | ||
| 179 | + // 根据业务逻辑解析重定向路径 | ||
| 180 | + } | ||
| 181 | +} | ||
| 182 | +``` | ||
| 183 | + | ||
| 184 | +### 方案三:抽象接口解耦 | ||
| 185 | + | ||
| 186 | +定义标准接口,让不同项目实现自己的适配器。 | ||
| 187 | + | ||
| 188 | +#### 1. 定义抽象接口 | ||
| 189 | +```javascript | ||
| 190 | +// interfaces/auth-interfaces.js | ||
| 191 | +export class IStorageAdapter { | ||
| 192 | + get(key) { throw new Error('Not implemented') } | ||
| 193 | + set(key, value) { throw new Error('Not implemented') } | ||
| 194 | + remove(key) { throw new Error('Not implemented') } | ||
| 195 | +} | ||
| 196 | + | ||
| 197 | +export class IHttpAdapter { | ||
| 198 | + post(url, data) { throw new Error('Not implemented') } | ||
| 199 | +} | ||
| 200 | + | ||
| 201 | +export class INavigatorAdapter { | ||
| 202 | + navigateTo(url) { throw new Error('Not implemented') } | ||
| 203 | + redirectTo(url) { throw new Error('Not implemented') } | ||
| 204 | + reLaunch(url) { throw new Error('Not implemented') } | ||
| 205 | +} | ||
| 206 | + | ||
| 207 | +export class IBusinessAdapter { | ||
| 208 | + async checkUserStatus() { throw new Error('Not implemented') } | ||
| 209 | + resolveRedirectPath(savedPath, defaultPath) { throw new Error('Not implemented') } | ||
| 210 | +} | ||
| 211 | +``` | ||
| 212 | + | ||
| 213 | +#### 2. Taro适配器实现 | ||
| 214 | +```javascript | ||
| 215 | +// adapters/taro-adapters.js | ||
| 216 | +import Taro from '@tarojs/taro' | ||
| 217 | + | ||
| 218 | +export class TaroStorageAdapter extends IStorageAdapter { | ||
| 219 | + get(key) { | ||
| 220 | + return wx.getStorageSync(key) | ||
| 221 | + } | ||
| 222 | + | ||
| 223 | + set(key, value) { | ||
| 224 | + wx.setStorageSync(key, value) | ||
| 225 | + } | ||
| 226 | + | ||
| 227 | + remove(key) { | ||
| 228 | + wx.removeStorageSync(key) | ||
| 229 | + } | ||
| 230 | +} | ||
| 231 | + | ||
| 232 | +export class TaroNavigatorAdapter extends INavigatorAdapter { | ||
| 233 | + navigateTo(url) { | ||
| 234 | + return Taro.navigateTo({ url }) | ||
| 235 | + } | ||
| 236 | + | ||
| 237 | + redirectTo(url) { | ||
| 238 | + return Taro.redirectTo({ url }) | ||
| 239 | + } | ||
| 240 | + | ||
| 241 | + reLaunch(url) { | ||
| 242 | + return Taro.reLaunch({ url }) | ||
| 243 | + } | ||
| 244 | +} | ||
| 245 | +``` | ||
| 246 | + | ||
| 247 | +## 推荐实施步骤 | ||
| 248 | + | ||
| 249 | +### 第一步:提取核心功能 | ||
| 250 | +1. 将 `silentAuth` 和 `needAuth` 函数提取为独立模块 | ||
| 251 | +2. 移除硬编码的页面路径和业务逻辑 | ||
| 252 | +3. 通过参数传递配置信息 | ||
| 253 | + | ||
| 254 | +### 第二步:创建适配层 | ||
| 255 | +1. 创建存储适配器(支持不同的存储方案) | ||
| 256 | +2. 创建网络请求适配器(支持不同的HTTP客户端) | ||
| 257 | +3. 创建导航适配器(支持不同的路由方案) | ||
| 258 | + | ||
| 259 | +### 第三步:业务逻辑分离 | ||
| 260 | +1. 将用户状态检查逻辑抽象为可配置的函数 | ||
| 261 | +2. 将重定向逻辑抽象为策略模式 | ||
| 262 | +3. 提供默认实现和自定义扩展点 | ||
| 263 | + | ||
| 264 | +### 第四步:打包发布 | ||
| 265 | +1. 创建npm包,支持不同框架的适配器 | ||
| 266 | +2. 提供详细的文档和示例 | ||
| 267 | +3. 支持TypeScript类型定义 | ||
| 268 | + | ||
| 269 | +## 使用示例 | ||
| 270 | + | ||
| 271 | +### 在新项目中使用 | ||
| 272 | +```javascript | ||
| 273 | +// 安装 | ||
| 274 | +npm install @your-org/universal-auth | ||
| 275 | + | ||
| 276 | +// 配置 | ||
| 277 | +import { AuthManager } from '@your-org/universal-auth' | ||
| 278 | +import { TaroAdapters } from '@your-org/universal-auth/adapters/taro' | ||
| 279 | + | ||
| 280 | +const authManager = new AuthManager({ | ||
| 281 | + ...TaroAdapters, | ||
| 282 | + authUrl: '/api/auth', | ||
| 283 | + authPage: '/pages/login/index', | ||
| 284 | + defaultPage: '/pages/home/index', | ||
| 285 | + | ||
| 286 | + async checkUserStatus() { | ||
| 287 | + // 项目特定的用户状态检查逻辑 | ||
| 288 | + const userInfo = await getUserInfo() | ||
| 289 | + return userInfo.isActive | ||
| 290 | + }, | ||
| 291 | + | ||
| 292 | + async getRedirectPath(savedPath, defaultPath) { | ||
| 293 | + // 项目特定的重定向逻辑 | ||
| 294 | + const userStatus = await this.checkUserStatus() | ||
| 295 | + return userStatus ? (savedPath || defaultPath) : '/pages/onboarding/index' | ||
| 296 | + } | ||
| 297 | +}) | ||
| 298 | + | ||
| 299 | +// 使用 | ||
| 300 | +await authManager.silentAuth() | ||
| 301 | +``` | ||
| 302 | + | ||
| 303 | +## 总结 | ||
| 304 | + | ||
| 305 | +推荐使用**方案一:配置化解耦**,因为它: | ||
| 306 | +1. **实施成本最低** - 只需要重构现有代码,不需要重新设计架构 | ||
| 307 | +2. **兼容性最好** - 保持现有功能不变,只是增加了配置能力 | ||
| 308 | +3. **维护成本最低** - 一套代码支持多个项目,统一维护和升级 | ||
| 309 | +4. **学习成本最低** - API保持基本不变,只需要了解配置选项 | ||
| 310 | + | ||
| 311 | +通过这种方式,你可以将静默授权功能打包成一个通用的npm包,在其他项目中只需要提供相应的配置参数即可快速集成。 | ||
| ... | \ No newline at end of file | ... | \ No newline at end of file |
-
Please register or login to post a comment