diff --git a/docs/【项目基线】当前产品与工程约束-2026-05-15.md b/docs/【项目基线】当前产品与工程约束-2026-05-15.md index 68c05a44..f5d50db7 100644 --- a/docs/【项目基线】当前产品与工程约束-2026-05-15.md +++ b/docs/【项目基线】当前产品与工程约束-2026-05-15.md @@ -46,6 +46,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当 3. 登录弹窗继续复用现有独立 modal 和页签结构,不在页面中新增功能说明类文案,也不把邀请码输入放回登录面板。 4. 微信小程序 `web-view` 外壳默认不预登录,首次进入直接打开 H5,并保持与 Web 端一致的未登录状态;只有 H5 触发 `openLoginModal` / `requireAuth` 等受保护入口时,才跳转小程序原生授权态。 5. 小程序内需要登录时不展示 H5 登录弹窗,也不走手输手机号 / 短信验证码流程;统一通过原生 `button open-type="getPhoneNumber"` 获取微信手机号授权,再调用 `/api/auth/wechat/miniprogram-login` 与 `/api/auth/wechat/bind-phone` 换取系统登录态。 +6. 小程序 `web-view` 页必须启用好友分享与朋友圈分享,分享目标固定回到 `pages/web-view/index`,不把 H5 当前 URL 作为不受控启动参数传回小程序页。 ## 账户与充值 diff --git a/miniprogram/pages/web-view/index.js b/miniprogram/pages/web-view/index.js index 1f33e2cb..0c2677f4 100644 --- a/miniprogram/pages/web-view/index.js +++ b/miniprogram/pages/web-view/index.js @@ -16,6 +16,33 @@ const PAY_RESULT_STORAGE_KEY = 'genarrative:wechat-pay-result'; const AUTH_RESULT_STORAGE_KEY = 'genarrative:mini-program-auth-result'; const AUTH_ACTION_LOGIN = 'login'; const PAY_RESULT_RECHECK_DELAY_MS = 120; +const WEB_VIEW_SHARE_TITLE = '陶泥儿'; +const WEB_VIEW_SHARE_PATH = '/pages/web-view/index'; + +function showWebViewShareMenu() { + if (typeof wx.showShareMenu !== 'function') { + return; + } + + wx.showShareMenu({ + withShareTicket: true, + menus: ['shareAppMessage', 'shareTimeline'], + }); +} + +function buildWebViewShareAppMessage() { + return { + title: WEB_VIEW_SHARE_TITLE, + path: WEB_VIEW_SHARE_PATH, + }; +} + +function buildWebViewShareTimeline() { + return { + title: WEB_VIEW_SHARE_TITLE, + query: '', + }; +} function isConfiguredEntryUrl(value) { const trimmed = String(value || '').trim(); @@ -271,18 +298,6 @@ async function resolveAuthResult() { }; } -async function refreshMiniProgramSessionSilently() { - if (!isConfiguredApiBaseUrl(API_BASE_URL)) { - return null; - } - try { - return await resolveAuthResult(); - } catch (error) { - console.warn('[web-view] silent mini program login refresh failed', error); - return null; - } -} - Page({ data: { authResult: null, @@ -296,6 +311,7 @@ Page({ async onLoad(query = {}) { this._lastLaunchQuery = query; + showWebViewShareMenu(); // 中文注释:web-view 只能打开已配置业务域名;未配置时展示本地提示,避免空白页误判。 if (!isConfiguredEntryUrl(WEB_VIEW_ENTRY_URL)) { this.setData({ @@ -309,14 +325,13 @@ Page({ const forcedPhoneBinding = parseBooleanQueryFlag(query.phoneBindingRequired); const returnToPreviousPage = shouldReturnToPreviousPage(query); if (!shouldStartAuthFromQuery(query) && !forcedPhoneBinding) { - const authResult = await refreshMiniProgramSessionSilently(); this.setData({ - authResult, + authResult: null, errorMessage: '', loading: false, phoneBindingRequired: false, returnToPreviousPage: false, - webViewUrl: resolveWebViewUrl(authResult), + webViewUrl: resolveWebViewUrl(null), }); return; } @@ -490,4 +505,12 @@ Page({ // 中文注释:支付由独立 native 页面承接,web-view 消息只保留调试输出。 console.info('[web-view] message', event.detail); }, + + onShareAppMessage() { + return buildWebViewShareAppMessage(); + }, + + onShareTimeline() { + return buildWebViewShareTimeline(); + }, }); diff --git a/scripts/miniprogram-web-view-auth.test.ts b/scripts/miniprogram-web-view-auth.test.ts index 388677e0..fdac9e72 100644 --- a/scripts/miniprogram-web-view-auth.test.ts +++ b/scripts/miniprogram-web-view-auth.test.ts @@ -17,6 +17,8 @@ type MiniProgramPage = { data: Record; setData: (patch: Record) => void; onLoad: (query?: Record) => Promise; + onShareAppMessage: () => Record; + onShareTimeline: () => Record; onShow: () => void; consumePayResult: () => void; }; @@ -29,6 +31,7 @@ function createWxMock() { navigateBack: vi.fn(), removeStorageSync: vi.fn(), request: vi.fn(), + showShareMenu: vi.fn(), setStorageSync: vi.fn(), }; } @@ -91,7 +94,7 @@ describe('mini-program web-view auth page', () => { vi.clearAllMocks(); }); - test('默认进入时刷新微信小程序登录态后打开 web-view', async () => { + test('默认进入时不预登录,直接打开未登录 web-view', async () => { const wxMock = createWxMock(); wxMock.login.mockImplementation(({ success }) => { success({ code: 'wx-login-code' }); @@ -109,19 +112,58 @@ describe('mini-program web-view auth page', () => { await page.onLoad({}); - expect(wxMock.login).toHaveBeenCalledTimes(1); - expect(wxMock.request).toHaveBeenCalledWith( - expect.objectContaining({ - url: 'https://www.genarrative.world/api/auth/wechat/miniprogram-login', - method: 'POST', - data: { code: 'wx-login-code' }, - }), - ); + expect(wxMock.login).not.toHaveBeenCalled(); + expect(wxMock.request).not.toHaveBeenCalled(); expect(page.data.loading).toBe(false); expect(page.data.phoneBindingRequired).toBe(false); expect(page.data.webViewUrl).toBe( - 'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program#auth_provider=wechat&auth_token=jwt-active-wechat&auth_binding_status=active', + 'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program', ); + expect(wxMock.showShareMenu).toHaveBeenCalledWith({ + withShareTicket: true, + menus: ['shareAppMessage', 'shareTimeline'], + }); + }); + + test('默认进入时即便微信新身份待绑手机号,也不弹出绑定手机号页', async () => { + const wxMock = createWxMock(); + wxMock.login.mockImplementation(({ success }) => { + success({ code: 'wx-login-code' }); + }); + wxMock.request.mockImplementation(({ success }) => { + success({ + statusCode: 200, + data: { + token: 'jwt-pending-wechat', + bindingStatus: 'pending_bind_phone', + }, + }); + }); + const page = loadWebViewPage(wxMock); + + await page.onLoad({}); + + expect(wxMock.login).not.toHaveBeenCalled(); + expect(wxMock.request).not.toHaveBeenCalled(); + expect(page.data.loading).toBe(false); + expect(page.data.phoneBindingRequired).toBe(false); + expect(page.data.webViewUrl).toBe( + 'https://www.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program', + ); + }); + + test('web-view 页面分享好友和朋友圈都回到小程序 web-view 入口', () => { + const wxMock = createWxMock(); + const page = loadWebViewPage(wxMock); + + expect(page.onShareAppMessage()).toEqual({ + title: '陶泥儿', + path: '/pages/web-view/index', + }); + expect(page.onShareTimeline()).toEqual({ + title: '陶泥儿', + query: '', + }); }); test('默认匿名进入 web-view 仍不依赖 API_BASE_URL 配置', async () => {