fix: switch miniprogram channel by env version

This commit is contained in:
kdletters
2026-06-04 23:52:59 +08:00
parent dd5861d5f5
commit b19a3e4231
4 changed files with 132 additions and 9 deletions

View File

@@ -47,6 +47,7 @@ Genarrative / 陶泥儿是一个 AI 原生互动内容与小游戏平台。当
4. 微信小程序 `web-view` 外壳默认不预登录,首次进入直接打开 H5并保持与 Web 端一致的未登录状态;只有 H5 触发 `openLoginModal` / `requireAuth` 等受保护入口时,才跳转小程序原生授权态。 4. 微信小程序 `web-view` 外壳默认不预登录,首次进入直接打开 H5并保持与 Web 端一致的未登录状态;只有 H5 触发 `openLoginModal` / `requireAuth` 等受保护入口时,才跳转小程序原生授权态。
5. 小程序内需要登录时不展示 H5 登录弹窗,也不走手输手机号 / 短信验证码流程;统一通过原生 `button open-type="getPhoneNumber"` 获取微信手机号授权,再调用 `/api/auth/wechat/miniprogram-login``/api/auth/wechat/bind-phone` 换取系统登录态。 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 作为不受控启动参数传回小程序页。 6. 小程序 `web-view` 页必须启用好友分享与朋友圈分享,分享目标固定回到 `pages/web-view/index`,不把 H5 当前 URL 作为不受控启动参数传回小程序页。
7. 小程序 `web-view` 外壳运行时通过 `wx.getAccountInfoSync().miniProgram.envVersion` 自动识别版本:线上版 `release` 使用 `www.genarrative.world`,体验版 `trial` 与开发版 `develop` 使用 `dev.genarrative.world`;传给后端的 `x-mini-program-env` 分别为 `release``trial``dev`
## 账户与充值 ## 账户与充值

View File

@@ -2,15 +2,17 @@
// 示例https://game.example.com/ // 示例https://game.example.com/
// 注意:必须是 https 域名,不能是 localhost、IP 地址或未备案域名。 // 注意:必须是 https 域名,不能是 localhost、IP 地址或未备案域名。
const WEB_VIEW_ENTRY_URL = 'https://www.genarrative.world'; const WEB_VIEW_ENTRY_URL = 'https://www.genarrative.world';
const DEV_WEB_VIEW_ENTRY_URL = 'https://dev.genarrative.world';
// 中文注释:这里填写 Rust api-server 的公网 HTTPS 域名,必须在“小程序后台-开发设置-request 合法域名”中配置。 // 中文注释:这里填写 Rust api-server 的公网 HTTPS 域名,必须在“小程序后台-开发设置-request 合法域名”中配置。
// 如果 H5 和 API 同域,可保持和 WEB_VIEW_ENTRY_URL 同一个域名;请求路径会固定走 /api/auth/wechat/miniprogram-login。 // 如果 H5 和 API 同域,可保持和 WEB_VIEW_ENTRY_URL 同一个域名;请求路径会固定走 /api/auth/wechat/miniprogram-login。
const API_BASE_URL = 'https://www.genarrative.world'; const API_BASE_URL = 'https://www.genarrative.world';
const DEV_API_BASE_URL = 'https://dev.genarrative.world';
// 中文注释:这里填写微信小程序 AppID用于后端记录会话来源project.config.json 里的 appid 也要保持一致。 // 中文注释:这里填写微信小程序 AppID用于后端记录会话来源project.config.json 里的 appid 也要保持一致。
const MINI_PROGRAM_APP_ID = 'wx3da23ea14ca66b65'; const MINI_PROGRAM_APP_ID = 'wx3da23ea14ca66b65';
// 中文注释:按当前上传版本填写 develop / trial / release后端会写入会话来源快照 // 中文注释:仅作为运行时环境识别失败时的兜底;正常情况下由 wx.getAccountInfoSync 自动判断
const MINI_PROGRAM_ENV = 'release'; const MINI_PROGRAM_ENV = 'release';
// 中文注释:给 H5 加一个来源标记,便于后续前端或后端识别这是微信小程序 web-view 宿主。 // 中文注释:给 H5 加一个来源标记,便于后续前端或后端识别这是微信小程序 web-view 宿主。
@@ -21,6 +23,8 @@ const WEB_VIEW_SOURCE_QUERY = {
module.exports = { module.exports = {
API_BASE_URL, API_BASE_URL,
DEV_API_BASE_URL,
DEV_WEB_VIEW_ENTRY_URL,
MINI_PROGRAM_APP_ID, MINI_PROGRAM_APP_ID,
MINI_PROGRAM_ENV, MINI_PROGRAM_ENV,
WEB_VIEW_ENTRY_URL, WEB_VIEW_ENTRY_URL,

View File

@@ -3,6 +3,8 @@
const { const {
API_BASE_URL, API_BASE_URL,
DEV_API_BASE_URL,
DEV_WEB_VIEW_ENTRY_URL,
MINI_PROGRAM_APP_ID, MINI_PROGRAM_APP_ID,
MINI_PROGRAM_ENV, MINI_PROGRAM_ENV,
WEB_VIEW_ENTRY_URL, WEB_VIEW_ENTRY_URL,
@@ -105,6 +107,68 @@ function parseBooleanQueryFlag(value) {
return value === true || value === '1' || value === 'true' || value === 'yes'; return value === true || value === '1' || value === 'true' || value === 'yes';
} }
function normalizeMiniProgramEnv(value) {
const normalized = String(value || '').trim().toLowerCase();
if (normalized === 'release') {
return 'release';
}
if (normalized === 'trial') {
return 'trial';
}
if (
normalized === 'develop' ||
normalized === 'development' ||
normalized === 'dev'
) {
return 'dev';
}
return '';
}
function readMiniProgramEnvVersion() {
if (typeof wx.getAccountInfoSync !== 'function') {
return '';
}
try {
const accountInfo = wx.getAccountInfoSync();
return (
accountInfo &&
accountInfo.miniProgram &&
accountInfo.miniProgram.envVersion
);
} catch (error) {
console.warn('[web-view] read mini program env failed', error);
return '';
}
}
function resolveMiniProgramRuntimeConfig() {
const miniProgramEnv =
normalizeMiniProgramEnv(readMiniProgramEnvVersion()) ||
normalizeMiniProgramEnv(MINI_PROGRAM_ENV) ||
'release';
const useReleaseChannel = miniProgramEnv === 'release';
const webViewEntryUrl = useReleaseChannel
? WEB_VIEW_ENTRY_URL
: DEV_WEB_VIEW_ENTRY_URL || WEB_VIEW_ENTRY_URL;
const apiBaseUrl = useReleaseChannel
? API_BASE_URL
: DEV_API_BASE_URL || API_BASE_URL;
const sourceQuery = {
...WEB_VIEW_SOURCE_QUERY,
};
if (!useReleaseChannel) {
sourceQuery.miniProgramEnv = miniProgramEnv;
}
return {
apiBaseUrl,
miniProgramEnv,
sourceQuery,
webViewEntryUrl,
};
}
function shouldStartAuthFromQuery(query) { function shouldStartAuthFromQuery(query) {
return String((query && query.authAction) || '').trim() === AUTH_ACTION_LOGIN; return String((query && query.authAction) || '').trim() === AUTH_ACTION_LOGIN;
} }
@@ -114,12 +178,13 @@ function shouldReturnToPreviousPage(query) {
} }
function resolveWebViewUrl(authResult) { function resolveWebViewUrl(authResult) {
const entryUrl = String(WEB_VIEW_ENTRY_URL || '').trim(); const runtimeConfig = resolveMiniProgramRuntimeConfig();
const entryUrl = String(runtimeConfig.webViewEntryUrl || '').trim();
if (!isConfiguredEntryUrl(entryUrl)) { if (!isConfiguredEntryUrl(entryUrl)) {
return ''; return '';
} }
const sourcedUrl = appendQuery(entryUrl, WEB_VIEW_SOURCE_QUERY); const sourcedUrl = appendQuery(entryUrl, runtimeConfig.sourceQuery);
if (!authResult || !authResult.token) { if (!authResult || !authResult.token) {
return sourcedUrl; return sourcedUrl;
} }
@@ -205,7 +270,8 @@ function wxLogin() {
function requestMiniProgramLogin(code) { function requestMiniProgramLogin(code) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const apiBaseUrl = trimTrailingSlash(API_BASE_URL); const runtimeConfig = resolveMiniProgramRuntimeConfig();
const apiBaseUrl = trimTrailingSlash(runtimeConfig.apiBaseUrl);
if (!isConfiguredApiBaseUrl(apiBaseUrl)) { if (!isConfiguredApiBaseUrl(apiBaseUrl)) {
reject(new Error('请先配置 API_BASE_URL')); reject(new Error('请先配置 API_BASE_URL'));
return; return;
@@ -222,7 +288,7 @@ function requestMiniProgramLogin(code) {
'x-client-platform': resolveClientPlatform(), 'x-client-platform': resolveClientPlatform(),
'x-client-instance-id': getClientInstanceId(), 'x-client-instance-id': getClientInstanceId(),
'x-mini-program-app-id': MINI_PROGRAM_APP_ID, 'x-mini-program-app-id': MINI_PROGRAM_APP_ID,
'x-mini-program-env': MINI_PROGRAM_ENV, 'x-mini-program-env': runtimeConfig.miniProgramEnv,
}, },
success(response) { success(response) {
if (response.statusCode >= 200 && response.statusCode < 300) { if (response.statusCode >= 200 && response.statusCode < 300) {
@@ -246,7 +312,8 @@ function requestMiniProgramLogin(code) {
function requestMiniProgramBindPhone(authToken, wechatPhoneCode) { function requestMiniProgramBindPhone(authToken, wechatPhoneCode) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const apiBaseUrl = trimTrailingSlash(API_BASE_URL); const runtimeConfig = resolveMiniProgramRuntimeConfig();
const apiBaseUrl = trimTrailingSlash(runtimeConfig.apiBaseUrl);
if (!isConfiguredApiBaseUrl(apiBaseUrl)) { if (!isConfiguredApiBaseUrl(apiBaseUrl)) {
reject(new Error('请先配置 API_BASE_URL')); reject(new Error('请先配置 API_BASE_URL'));
return; return;
@@ -264,7 +331,7 @@ function requestMiniProgramBindPhone(authToken, wechatPhoneCode) {
'x-client-platform': resolveClientPlatform(), 'x-client-platform': resolveClientPlatform(),
'x-client-instance-id': getClientInstanceId(), 'x-client-instance-id': getClientInstanceId(),
'x-mini-program-app-id': MINI_PROGRAM_APP_ID, 'x-mini-program-app-id': MINI_PROGRAM_APP_ID,
'x-mini-program-env': MINI_PROGRAM_ENV, 'x-mini-program-env': runtimeConfig.miniProgramEnv,
}, },
success(response) { success(response) {
if (response.statusCode >= 200 && response.statusCode < 300) { if (response.statusCode >= 200 && response.statusCode < 300) {
@@ -312,8 +379,9 @@ Page({
async onLoad(query = {}) { async onLoad(query = {}) {
this._lastLaunchQuery = query; this._lastLaunchQuery = query;
showWebViewShareMenu(); showWebViewShareMenu();
const runtimeConfig = resolveMiniProgramRuntimeConfig();
// 中文注释web-view 只能打开已配置业务域名;未配置时展示本地提示,避免空白页误判。 // 中文注释web-view 只能打开已配置业务域名;未配置时展示本地提示,避免空白页误判。
if (!isConfiguredEntryUrl(WEB_VIEW_ENTRY_URL)) { if (!isConfiguredEntryUrl(runtimeConfig.webViewEntryUrl)) {
this.setData({ this.setData({
errorMessage: '请先在 miniprogram/config.js 填写 WEB_VIEW_ENTRY_URL。', errorMessage: '请先在 miniprogram/config.js 填写 WEB_VIEW_ENTRY_URL。',
loading: false, loading: false,
@@ -336,7 +404,7 @@ Page({
return; return;
} }
if (!isConfiguredApiBaseUrl(API_BASE_URL)) { if (!isConfiguredApiBaseUrl(runtimeConfig.apiBaseUrl)) {
this.setData({ this.setData({
errorMessage: '请先在 miniprogram/config.js 填写 API_BASE_URL。', errorMessage: '请先在 miniprogram/config.js 填写 API_BASE_URL。',
loading: false, loading: false,

View File

@@ -25,6 +25,9 @@ type MiniProgramPage = {
function createWxMock() { function createWxMock() {
return { return {
getAccountInfoSync: vi.fn(() => ({
miniProgram: { envVersion: 'release' },
})),
getStorageSync: vi.fn(() => ''), getStorageSync: vi.fn(() => ''),
getSystemInfoSync: vi.fn(() => ({ platform: 'ios' })), getSystemInfoSync: vi.fn(() => ({ platform: 'ios' })),
login: vi.fn(), login: vi.fn(),
@@ -57,6 +60,8 @@ function loadWebViewPage(
if (requestPath === '../../config') { if (requestPath === '../../config') {
return { return {
API_BASE_URL: 'https://www.genarrative.world/', API_BASE_URL: 'https://www.genarrative.world/',
DEV_API_BASE_URL: 'https://dev.genarrative.world/',
DEV_WEB_VIEW_ENTRY_URL: 'https://dev.genarrative.world/',
MINI_PROGRAM_APP_ID: 'wx-test-app', MINI_PROGRAM_APP_ID: 'wx-test-app',
MINI_PROGRAM_ENV: 'release', MINI_PROGRAM_ENV: 'release',
WEB_VIEW_ENTRY_URL: 'https://www.genarrative.world/', WEB_VIEW_ENTRY_URL: 'https://www.genarrative.world/',
@@ -182,6 +187,51 @@ describe('mini-program web-view auth page', () => {
); );
}); });
test('体验版自动切到 dev 子域名并透传 trial 环境', async () => {
const wxMock = createWxMock();
wxMock.getAccountInfoSync.mockReturnValue({
miniProgram: { envVersion: 'trial' },
});
const page = loadWebViewPage(wxMock);
await page.onLoad({});
expect(page.data.webViewUrl).toBe(
'https://dev.genarrative.world/?clientType=mini_program&clientRuntime=wechat_mini_program&miniProgramEnv=trial',
);
});
test('开发版自动切到 dev 子域名并把 develop 规整为 dev', async () => {
const wxMock = createWxMock();
wxMock.getAccountInfoSync.mockReturnValue({
miniProgram: { envVersion: 'develop' },
});
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({ authAction: 'login', returnTo: 'previous' });
expect(wxMock.request).toHaveBeenCalledWith(
expect.objectContaining({
url: 'https://dev.genarrative.world/api/auth/wechat/miniprogram-login',
header: expect.objectContaining({
'x-mini-program-env': 'dev',
}),
}),
);
});
test('onShow 二次检查支付结果并写回 web-view hash', () => { test('onShow 二次检查支付结果并写回 web-view hash', () => {
const wxMock = createWxMock(); const wxMock = createWxMock();
wxMock.getStorageSync.mockImplementation((key) => wxMock.getStorageSync.mockImplementation((key) =>