diff --git a/.workbuddy/memory/MEMORY.md b/.workbuddy/memory/MEMORY.md index c50d4c2..ebf3e4c 100644 --- a/.workbuddy/memory/MEMORY.md +++ b/.workbuddy/memory/MEMORY.md @@ -48,22 +48,25 @@ - 新订单检测 Hook:`src/hooks/useNewOrderDetector.ts`(30秒轮询,页面隐藏暂停) ## 微信隐私协议(基础库 3.16.1+ 强制) -- 报错特征:`chooseImage:fail api scope is not declared in the privacy agreement` + `errno:112` -- 两处代码修复: - 1. 调用 `Taro.chooseImage` 前必须先 `getPrivacySetting` → `requirePrivacyAuthorize` 预检(封装在 `src/api/system/file/index.ts` 的 `ensurePrivacyAuthorized()` 里) - 2. `src/app.tsx` 的 `useLaunch` 中注册 `Taro.onNeedPrivacyAuthorization` 回调(用 `showModal` 自定义弹窗) -- 一处手动配置:**小程序管理后台** → 设置 → 第三方设置 → 用户隐私保护指引 → 添加「开发者收集你的相册/摄像头」声明 -- ⚠️ **`requiredPrivateInfos` 字段只接受位置类 API 白名单**(chooseAddress/chooseLocation/choosePoi/getFuzzyLocation/getLocation/onLocationChange/startLocationUpdate/startLocationUpdateBackground),不要加 `chooseImage`/`chooseMedia` 等非位置类,会导致 app.json 解析失败 +- 报错特征:`chooseImage:fail api scope is not declared in the privacy agreement` / `getPhoneNumber:fail ... privacy ...` + `errno:112` +- 代码修复: + 1. 调用 `Taro.chooseImage` / `getPhoneNumber` 等敏感 API 前,先 `getPrivacySetting` → `requirePrivacyAuthorize` 预检(封装在 `src/api/system/file/index.ts` 的 `ensurePrivacyAuthorized()` 里,已 export) + 2. `src/app.tsx` 的 `useLaunch` 中注册 `Taro.onNeedPrivacyAuthorization` 回调,通过 `Taro.showModal` 展示隐私协议授权弹窗,用户点击「同意」后再 `resolve({ event: 'agree', button: 'agree' })` + 3. 登录/注册页在 `useDidShow` 中主动预检隐私协议,避免点击授权按钮时因未授权隐私协议而失败 +- 手动配置:**小程序管理后台** → 设置 → 第三方设置 → 用户隐私保护指引 → 添加「开发者收集你的相册/摄像头」和「手机号」声明 +- ⚠️ **`requiredPrivateInfos` 字段只接受位置类 API 白名单**(chooseAddress/chooseLocation/choosePoi/getFuzzyLocation/getLocation/onLocationChange/startLocationUpdate/startLocationUpdateBackground),不要加 `chooseImage`/`chooseMedia`/`getPhoneNumber` 等非位置类,会导致 app.json 解析失败 - 直接调 `Taro.chooseImage` 的页面:`pages/order/evaluate/index.tsx`、`pages/after-sale/apply/index.tsx`、`pages/store/orders/index.tsx` —— 这些页面绕过了 `uploadFile`,需要单独加 `ensurePrivacyAuthorized` 预检 +- 直接调 `getPhoneNumber` 的页面:`passport/login.tsx`、`passport/register.tsx` —— 已增加隐私协议预检 + 降级短信登录 -## 手机号授权登录降级方案(2026-07-15) +## 手机号授权登录降级方案(2026-07-15 ~ 07-16 修复) - **业务现实**:`getPhoneNumber` 按钮被用户拒绝后,微信会短期"记住"用户的选择,再次点击会直接 fail(`errMsg: getPhoneNumber:fail user deny`)。一旦进入这个状态,前端必须提供降级入口 - **统一模式**:`passport/login.tsx` 和 `passport/register.tsx` 都有 `getPhoneNumber` 回调,**不允许**只 toast 一句"未授权手机号"就完事 - 必做:失败时调 `showPhoneAuthFailedModal(errMsg)`,按 errMsg 区分场景弹窗(user deny / privacy / no permission / 通用) - 弹窗带"短信登录"按钮自动跳 `/passport/sms-login`(透传 redirect + 邀请参数 inviter/source/t) - 必做:主登录按钮**长期可见**地提供"使用短信验证码登录"链接,不要依赖弹窗 +- **接口地址统一**:login/register 的手机号登录接口统一使用 `SERVER_API_URL + '/wx-login/loginByMpWxPhone'`(register 之前误写 `https://shop-api.websoft.top`) - **短信登录页**:`/passport/sms-login`(路由表里已有),调 `loginBySms` + `sendSmsCaptcha` 接口 -- **已落地页面**:`passport/login.tsx`(新增 goSmsLogin + showPhoneAuthFailedModal + alt 链接)、`passport/register.tsx`(新增 showPhoneAuthFailedModal + weapp 下也显示短信按钮) +- **已落地页面**:`passport/login.tsx`、`passport/register.tsx`(隐私协议预检 + 统一 request + 降级弹窗) ## 分享 / 朋友圈能力(2026-07-13 接入) - 统一封装:`src/hooks/useShare.ts`(`useShare({title, path?, query?, imageUrl?, enableTimeline?, enableCopyUrl?})`,一次注册 `useShareAppMessage` + `useShareTimeline` + 可选 `onCopyUrl` 复制链接;自动在 path/query 追加 `inviter=${当前用户id}` 做裂变归因;`isTimelineSinglePage()` 判断朋友圈单页模式 scene===1154) diff --git a/src/passport/pay/index.tsx b/src/passport/pay/index.tsx index 4aff7f2..a74307a 100644 --- a/src/passport/pay/index.tsx +++ b/src/passport/pay/index.tsx @@ -4,7 +4,7 @@ import { Button, Cell, Price, Divider } from '@nutui/nutui-react-taro' import { Check, Close, Tips, Clock } from '@nutui/icons-react-taro' import Taro from '@tarojs/taro' import { request } from '@/utils/request' -import { getOpenId, loginByOpenId } from '@/api/passport/wx-login' +import { getWxOpenId, loginByOpenId } from '@/api/layout' import { saveStorageByLoginUser } from '@/utils/server' import { TenantId } from '@/config/app' @@ -157,14 +157,13 @@ const PayPage: React.FC = () => { console.log('[PayPage] Taro.login code:', loginRes.code) // 2. 用 code 换取 openid(关键步骤,之前遗漏了) - const openIdRes = await getOpenId(loginRes.code) - console.log('[PayPage] getOpenId 结果:', openIdRes) + const openIdRes: any = await getWxOpenId({ code: loginRes.code }) + console.log('[PayPage] getWxOpenId 结果:', openIdRes) - if (!openIdRes.success || !openIdRes.openid) { - throw new Error(openIdRes.message || '获取 openid 失败,请重试') + const openid = openIdRes?.openid || openIdRes + if (!openid) { + throw new Error(openIdRes?.message || '获取 openid 失败,请重试') } - - const openid = openIdRes.openid // 缓存 openid,后续支付确认等接口可用 Taro.setStorageSync('openid', openid) console.log('[PayPage] 获取到 openid:', openid) @@ -173,37 +172,36 @@ const PayPage: React.FC = () => { // 如果用户已在小程序注册过,loginByOpenId 会返回 token // 如果用户未注册,不影响 openid 已获取,但 mp-prepay 可能需要特殊处理 try { - const loginResult = await loginByOpenId({ + const loginResult: any = await loginByOpenId({ code: loginRes.code, tenantId: TenantId }) console.log('[PayPage] loginByOpenId 结果:', loginResult) - if (loginResult.success && loginResult.data) { + if (loginResult) { // 登录成功,保存 token - saveStorageByLoginUser(loginResult.data.access_token || '', loginResult.data.user) + saveStorageByLoginUser(loginResult.access_token || '', loginResult.user) console.log('[PayPage] 自动登录成功,已保存 token') } else { - console.warn('[PayPage] 自动登录失败:', loginResult.message) - // 未注册用户:openid 已经有了,但 mp-prepay 需要 userId - // 提示用户先注册/登录 - if (loginResult.message?.includes('未注册') || loginResult.message?.includes('不存在')) { - Taro.showModal({ - title: '需要先登录', - content: '支付前需要先注册账号,是否前往登录?', - confirmText: '去登录', - cancelText: '取消' - }).then(modalRes => { - if (modalRes.confirm) { - Taro.navigateTo({ url: '/passport/login' }) - } - }) - return // 不继续支付流程 - } + console.warn('[PayPage] 自动登录未返回数据') } - } catch (loginErr) { + } catch (loginErr: any) { console.warn('[PayPage] 自动登录异常:', loginErr) // 登录异常但不阻断支付,openid 已拿到,尝试继续 + // 如果后端提示未注册,引导用户去登录 + if (loginErr?.message?.includes('未注册') || loginErr?.message?.includes('不存在')) { + Taro.showModal({ + title: '需要先登录', + content: '支付前需要先注册账号,是否前往登录?', + confirmText: '去登录', + cancelText: '取消' + }).then(modalRes => { + if (modalRes.confirm) { + Taro.navigateTo({ url: '/passport/login' }) + } + }) + return // 不继续支付流程 + } } // 4. 请求后端创建 JSAPI 预支付订单