修复拼图生成前订阅授权

新增小程序原生订阅消息授权页,在用户点击后请求生成结果通知授权。

拼图 compile_puzzle_draft 前等待授权页返回或跳过后再发起生成 action。

移除 web-view message 订阅授权路径,改用 storage/hash 回写订阅结果。

补充订阅授权测试、文档和团队踩坑记录。
This commit is contained in:
kdletters
2026-06-08 13:06:07 +08:00
parent 38d9c292ae
commit 3a918687c5
17 changed files with 708 additions and 71 deletions

View File

@@ -0,0 +1,10 @@
/* global Page */
const { GENERATION_RESULT_SUBSCRIBE_TEMPLATE_ID } = require('../../config');
const { createSubscribeMessagePage } = require('./index.shared');
Page(
createSubscribeMessagePage(null, {
templateId: GENERATION_RESULT_SUBSCRIBE_TEMPLATE_ID,
}),
);

View File

@@ -0,0 +1,3 @@
{
"navigationBarTitleText": "生成通知"
}

View File

@@ -0,0 +1,135 @@
/* global wx, getCurrentPages */
const SUBSCRIBE_RESULT_STORAGE_KEY = 'genarrative:wechat-subscribe-result';
function appendSubscribeResult(url, result) {
const hashIndex = String(url || '').indexOf('#');
const baseUrl =
hashIndex >= 0 ? String(url).slice(0, hashIndex) : String(url || '');
const rawHash = hashIndex >= 0 ? String(url).slice(hashIndex + 1) : '';
const nextHash = rawHash
.split('&')
.filter((part) => part && !part.startsWith('wx_subscribe_result='))
.concat(`wx_subscribe_result=${encodeURIComponent(result)}`)
.join('&');
return `${baseUrl}#${nextHash}`;
}
function buildSubscribeResultValue(requestId, status, reason) {
const segments = [requestId, status];
if (reason) {
segments.push(encodeURIComponent(reason));
}
return segments.join(':');
}
function notifyPreviousWebView(requestId, status, reason) {
const result = buildSubscribeResultValue(requestId, status, reason);
wx.setStorageSync(SUBSCRIBE_RESULT_STORAGE_KEY, result);
const pages = getCurrentPages();
const previousPage = pages.length >= 2 ? pages[pages.length - 2] : null;
if (previousPage && typeof previousPage.setData === 'function') {
previousPage.setData({
webViewUrl: appendSubscribeResult(previousPage.data.webViewUrl, result),
});
}
}
function resolveSubscribeStatus(result, templateId) {
return result && result[templateId] === 'accept'
? 'success'
: 'skip';
}
function createSubscribeMessagePage(pageContext, options = {}) {
const templateId = String(options.templateId || '').trim();
const notifyPageResult = (methodThis, status, reason) => {
const page = pageContext ?? methodThis;
const requestId = page.requestId || '';
if (!requestId || page.hasNotifiedSubscribeResult) {
return;
}
page.hasNotifiedSubscribeResult = true;
notifyPreviousWebView(requestId, status, reason);
};
return {
data: {
title: '接收生成结果通知',
errorMessage: '',
requesting: false,
},
onLoad(query) {
const page = pageContext ?? this;
page.requestId = String(query.requestId || '');
page.hasNotifiedSubscribeResult = false;
},
notifyResult(status, reason) {
notifyPageResult(this, status, reason);
},
requestSubscribe() {
const page = pageContext ?? this;
const requestId = page.requestId || '';
if (!requestId) {
page.setData({
errorMessage: '缺少订阅请求参数。',
});
return;
}
if (!templateId) {
notifyPageResult(this, 'skip', 'missing_template_id');
wx.navigateBack();
return;
}
if (typeof wx.requestSubscribeMessage !== 'function') {
notifyPageResult(this, 'skip', 'unsupported');
wx.navigateBack();
return;
}
page.setData({
requesting: true,
errorMessage: '',
});
wx.requestSubscribeMessage({
tmplIds: [templateId],
success(result) {
notifyPageResult(
page,
resolveSubscribeStatus(result, templateId),
'',
);
wx.navigateBack();
},
fail(error) {
notifyPageResult(
page,
'skip',
error && error.errMsg ? error.errMsg : 'failed',
);
wx.navigateBack();
},
});
},
handleSkip() {
notifyPageResult(this, 'skip', 'user_skip');
wx.navigateBack();
},
onUnload() {
notifyPageResult(this, 'skip', 'page_unload');
},
};
}
module.exports = {
SUBSCRIBE_RESULT_STORAGE_KEY,
appendSubscribeResult,
buildSubscribeResultValue,
createSubscribeMessagePage,
resolveSubscribeStatus,
};

View File

@@ -0,0 +1,99 @@
import { beforeEach, describe, expect, test, vi } from 'vitest';
import subscribeMessageBridge from './index.shared.js';
const TEST_TEMPLATE_ID = 'm5z7BkkBhJGbcH0cdDeHaeRU2tViDEguP38XdrRRCdU';
const {
SUBSCRIBE_RESULT_STORAGE_KEY,
appendSubscribeResult,
buildSubscribeResultValue,
createSubscribeMessagePage,
} = subscribeMessageBridge;
describe('subscribe-message mini program bridge', () => {
beforeEach(() => {
globalThis.wx = {
requestSubscribeMessage: vi.fn(),
setStorageSync: vi.fn(),
navigateBack: vi.fn(),
};
globalThis.getCurrentPages = vi.fn(() => []);
});
test('requests subscribe message and notifies previous web-view before returning', () => {
const previousPage = {
data: { webViewUrl: 'https://web.test/#tab=create' },
setData: vi.fn(),
};
globalThis.getCurrentPages = vi.fn(() => [previousPage, {}]);
globalThis.wx.requestSubscribeMessage.mockImplementationOnce((options) => {
options.success?.({
m5z7BkkBhJGbcH0cdDeHaeRU2tViDEguP38XdrRRCdU: 'accept',
});
});
const page = createSubscribeMessagePage(
{
setData: vi.fn(),
},
{ templateId: TEST_TEMPLATE_ID },
);
page.onLoad({ requestId: 'request-1' });
page.requestSubscribe();
expect(globalThis.wx.requestSubscribeMessage).toHaveBeenCalledWith({
tmplIds: [TEST_TEMPLATE_ID],
success: expect.any(Function),
fail: expect.any(Function),
});
expect(globalThis.wx.setStorageSync).toHaveBeenCalledWith(
SUBSCRIBE_RESULT_STORAGE_KEY,
'request-1:success',
);
expect(previousPage.setData).toHaveBeenCalledWith({
webViewUrl:
'https://web.test/#tab=create&wx_subscribe_result=request-1%3Asuccess',
});
expect(globalThis.wx.navigateBack).toHaveBeenCalled();
});
test('skip action notifies previous web-view', () => {
const previousPage = {
data: { webViewUrl: 'https://web.test/' },
setData: vi.fn(),
};
globalThis.getCurrentPages = vi.fn(() => [previousPage, {}]);
const page = createSubscribeMessagePage(
{
setData: vi.fn(),
},
{ templateId: TEST_TEMPLATE_ID },
);
page.onLoad({ requestId: 'request-skip' });
page.handleSkip();
expect(globalThis.wx.setStorageSync).toHaveBeenCalledWith(
SUBSCRIBE_RESULT_STORAGE_KEY,
'request-skip:skip:user_skip',
);
expect(previousPage.setData).toHaveBeenCalledWith({
webViewUrl:
'https://web.test/#wx_subscribe_result=request-skip%3Askip%3Auser_skip',
});
expect(globalThis.wx.navigateBack).toHaveBeenCalled();
});
test('appendSubscribeResult replaces stale subscribe hash', () => {
expect(
appendSubscribeResult(
'https://web.test/#old=1&wx_subscribe_result=old',
'req:skip',
),
).toBe('https://web.test/#old=1&wx_subscribe_result=req%3Askip');
expect(buildSubscribeResultValue('req-1', 'skip', 'user_cancel')).toBe(
'req-1:skip:user_cancel',
);
});
});

View File

@@ -0,0 +1,19 @@
<view class="subscribe-screen">
<view class="subscribe-card">
<view class="subscribe-title">{{title}}</view>
<view wx:if="{{errorMessage}}" class="subscribe-text subscribe-text--danger">
{{errorMessage}}
</view>
<button
class="primary-button"
loading="{{requesting}}"
disabled="{{requesting}}"
bindtap="requestSubscribe"
>
继续并接收通知
</button>
<button class="ghost-button" disabled="{{requesting}}" bindtap="handleSkip">
仅继续生成
</button>
</view>
</view>

View File

@@ -0,0 +1,58 @@
.subscribe-screen {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 48rpx;
background: #0b0f14;
box-sizing: border-box;
}
.subscribe-card {
width: 100%;
max-width: 560rpx;
padding: 36rpx;
border: 1rpx solid rgba(255, 255, 255, 0.14);
border-radius: 12rpx;
background: rgba(255, 255, 255, 0.06);
box-sizing: border-box;
}
.subscribe-title {
font-size: 34rpx;
font-weight: 600;
line-height: 1.35;
color: #f5f7fb;
}
.subscribe-text {
margin-top: 16rpx;
font-size: 26rpx;
line-height: 1.55;
color: rgba(245, 247, 251, 0.72);
}
.subscribe-text--danger {
color: #ffb4a9;
}
.primary-button,
.ghost-button {
margin-top: 28rpx;
width: 100%;
border-radius: 8rpx;
font-size: 26rpx;
line-height: 2.6;
}
.primary-button {
background: #f5f7fb;
color: #0b0f14;
}
.ghost-button {
margin-top: 20rpx;
border: 1rpx solid rgba(255, 255, 255, 0.24);
background: transparent;
color: rgba(245, 247, 251, 0.86);
}