refactor(config): 七牛/微信密钥外置——AK/SK+微信AppID 入本地 app_config, domain/bucket 走远程配置

- app_config.json + AppConfig: qiniu_access_key/qiniu_secret_key/wechat_app_id/wechat_app_secret(扁平snake_case)
- RemoteConfig.VersionFields + audio_domain/audio_bucket(4层可携带), VersionDecision + audioDomain/audioBucket
- VersionResolver.resolve 复用 pickStr 解析 audioDomain/audioBucket(4层逐级回退)
- QiniuToken.uploadToken(accessKey,secretKey,bucket) 改传参,去掉硬编码常量
- AudioProvider.doUpload: AK/SK取本地配置, domain/bucket取远程配置(cfg.resolve), URL用远程domain
- WeChatApi.configure(appId) 注入AppID(去WX_APP_ID常量), EntryAbility.onCreate 从配置注入
- 更新 QiniuToken 单测传参

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
lanterngamescn
2026-06-27 19:12:43 +08:00
co-authored by Claude Opus 4.8
parent e5d8eeb3b1
commit c39d8eb400
10 changed files with 85 additions and 28 deletions
@@ -29,6 +29,14 @@ export interface AppConfig {
other: string; other: string;
/** 推广/邀请码(写入 app_data.js 的 app_invitationcode)。 */ /** 推广/邀请码(写入 app_data.js 的 app_invitationcode)。 */
tuiguang: string; tuiguang: string;
/** 七牛 AccessKey(录音上传 token 签名;随包内置,不写死在源码)。 */
qiniuAccessKey: string;
/** 七牛 SecretKey(录音上传 token 签名)。 */
qiniuSecretKey: string;
/** 微信开放平台 AppIDWeChatApi 创建 WXApi)。 */
wechatAppId: string;
/** 微信开放平台 AppSecret(当前端侧不消费,登录走 code+服务端;留位避免硬编码)。 */
wechatAppSecret: string;
} }
/** 解析中间体(字段全可选,用于从 JSON 归一化)。 */ /** 解析中间体(字段全可选,用于从 JSON 归一化)。 */
@@ -44,6 +52,10 @@ export interface AppConfigRaw {
gameconfig?: string; gameconfig?: string;
other?: string; other?: string;
tuiguang?: string; tuiguang?: string;
qiniu_access_key?: string;
qiniu_secret_key?: string;
wechat_app_id?: string;
wechat_app_secret?: string;
} }
/** 把可选解析体归一化为完整 AppConfig(缺失键 → 空串)。 */ /** 把可选解析体归一化为完整 AppConfig(缺失键 → 空串)。 */
@@ -60,5 +72,9 @@ export function normalizeConfig(raw: AppConfigRaw): AppConfig {
gameconfig: raw.gameconfig ?? '', gameconfig: raw.gameconfig ?? '',
other: raw.other ?? '', other: raw.other ?? '',
tuiguang: raw.tuiguang ?? '', tuiguang: raw.tuiguang ?? '',
qiniuAccessKey: raw.qiniu_access_key ?? '',
qiniuSecretKey: raw.qiniu_secret_key ?? '',
wechatAppId: raw.wechat_app_id ?? '',
wechatAppSecret: raw.wechat_app_secret ?? '',
}; };
} }
@@ -34,6 +34,10 @@ export interface VersionFields {
game_zip?: string; game_zip?: string;
game_size?: string; game_size?: string;
showmessage?: string; showmessage?: string;
/** 录音上传七牛回放域名(4 层可携带,逐级回退)。 */
audio_domain?: string;
/** 录音上传七牛 bucket(4 层可携带,逐级回退)。 */
audio_bucket?: string;
} }
/** 市场层(marketid 数字命中;携带 app 升级 + 可选资源覆盖)。 */ /** 市场层(marketid 数字命中;携带 app 升级 + 可选资源覆盖)。 */
@@ -87,4 +91,8 @@ export interface VersionDecision {
gameVersion: number; gameVersion: number;
gameDownload: string; gameDownload: string;
gameSize: string; gameSize: string;
/** 录音上传七牛回放域名(来源 audio_domain4 层回退)。 */
audioDomain: string;
/** 录音上传七牛 bucket(来源 audio_bucket4 层回退)。 */
audioBucket: string;
} }
@@ -94,6 +94,8 @@ export class VersionResolver {
gameVersion: VersionResolver.pickVer(layers, (n: VersionFields) => n.game_version), gameVersion: VersionResolver.pickVer(layers, (n: VersionFields) => n.game_version),
gameDownload: VersionResolver.pickStr(layers, (n: VersionFields) => n.game_zip), gameDownload: VersionResolver.pickStr(layers, (n: VersionFields) => n.game_zip),
gameSize: VersionResolver.pickStr(layers, (n: VersionFields) => n.game_size), gameSize: VersionResolver.pickStr(layers, (n: VersionFields) => n.game_size),
audioDomain: VersionResolver.pickStr(layers, (n: VersionFields) => n.audio_domain),
audioBucket: VersionResolver.pickStr(layers, (n: VersionFields) => n.audio_bucket),
}; };
} }
} }
@@ -4,6 +4,7 @@ import { window } from '@kit.ArkUI';
import { BusinessError } from '@kit.BasicServicesKit'; import { BusinessError } from '@kit.BasicServicesKit';
import { EventBus } from 'common'; import { EventBus } from 'common';
import { WeChatApi } from 'feature_capabilities'; import { WeChatApi } from 'feature_capabilities';
import { ConfigManager } from 'domain_resource';
import { AppEvents } from '../routes/AppRoutes'; import { AppEvents } from '../routes/AppRoutes';
import { installErrorSink } from '../di/ErrorReporter'; import { installErrorSink } from '../di/ErrorReporter';
@@ -19,7 +20,12 @@ export default class EntryAbility extends UIAbility {
hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate');
// 可观测(T-M5-06):安装错误上报 sink,打通 ErrorCenter → APM/Crash 通道 // 可观测(T-M5-06):安装错误上报 sink,打通 ErrorCenter → APM/Crash 通道
installErrorSink(); installErrorSink();
// 微信回调(冷启动经 want 拉回):交 SDK 解析 → 路由到 Share/Login 回调 // 微信回调(冷启动经 want 拉回):先从本地 app_config 注入 AppID(不再硬编码),再交 SDK 解析
try {
WeChatApi.configure(ConfigManager.load(this.context).getLocal().wechatAppId);
} catch (e) {
hilog.warn(DOMAIN, 'testTag', 'configure WeChat appId failed: %{public}s', JSON.stringify(e));
}
WeChatApi.getInstance().handleWant(want); WeChatApi.getInstance().handleWant(want);
} }
@@ -9,5 +9,9 @@
"weburl": "", "weburl": "",
"gameconfig": "tsgames.daoqi88.cn-config_test-update_jsonv2_test", "gameconfig": "tsgames.daoqi88.cn-config_test-update_jsonv2_test",
"other": "", "other": "",
"tuiguang": "" "tuiguang": "",
"qiniu_access_key": "ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf",
"qiniu_secret_key": "6VXav9eqUCORJTYvTFOeTVRonAyk4Hrs-PIZ8jmZ",
"wechat_app_id": "wxd2bd650e06bdfe58",
"wechat_app_secret": ""
} }
+4 -3
View File
@@ -5,16 +5,17 @@ import { util } from '@kit.ArkTS';
export default function QiniuTokenTest() { export default function QiniuTokenTest() {
describe('QiniuTokenTest', () => { describe('QiniuTokenTest', () => {
it('token_has_three_parts_and_valid_policy', 0, async () => { it('token_has_three_parts_and_valid_policy', 0, async () => {
const token: string = await QiniuToken.uploadToken(); // AK/SK/bucket 现由调用方注入(本地 app_config + 远程 audio_bucket),单测用固定测试值
const token: string = await QiniuToken.uploadToken('AK_TEST', 'SK_TEST', 'bk_test');
const parts: string[] = token.split(':'); const parts: string[] = token.split(':');
expect(parts.length).assertEqual(3); expect(parts.length).assertEqual(3);
expect(parts[0]).assertEqual('ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf'); expect(parts[0]).assertEqual('AK_TEST');
expect(parts[1].length).assertEqual(28); expect(parts[1].length).assertEqual(28);
const helper = new util.Base64Helper(); const helper = new util.Base64Helper();
const std: string = parts[2].replace(/-/g, '+').replace(/_/g, '/'); const std: string = parts[2].replace(/-/g, '+').replace(/_/g, '/');
const decoded: Uint8Array = helper.decodeSync(std); const decoded: Uint8Array = helper.decodeSync(std);
const policy: string = String.fromCharCode.apply(null, Array.from(decoded)); const policy: string = String.fromCharCode.apply(null, Array.from(decoded));
expect(policy.indexOf('"scope":"gameaudio"') >= 0).assertTrue(); expect(policy.indexOf('"scope":"bk_test"') >= 0).assertTrue();
}); });
}); });
} }
+1 -1
View File
@@ -23,7 +23,7 @@ export { AmrRecorder } from './src/main/ets/providers/AmrRecorder';
export { NavProvider } from './src/main/ets/providers/NavProvider'; export { NavProvider } from './src/main/ets/providers/NavProvider';
// 微信 SDK 封装(EntryAbility 处理回调 want 需用) // 微信 SDK 封装(EntryAbility 处理回调 want 需用)
export { WeChatApi, WX_APP_ID } from './src/main/ets/wx/WeChatApi'; export { WeChatApi } from './src/main/ets/wx/WeChatApi';
// 分享文本拼接纯函数(供单测) // 分享文本拼接纯函数(供单测)
export { buildShareText } from './src/main/ets/share/ShareText'; export { buildShareText } from './src/main/ets/share/ShareText';
@@ -23,7 +23,7 @@ import { common, abilityAccessCtrl } from '@kit.AbilityKit';
import { fileIo } from '@kit.CoreFileKit'; import { fileIo } from '@kit.CoreFileKit';
import { BridgeController } from 'feature_bridge'; import { BridgeController } from 'feature_bridge';
import { InboundHandlers, OutboundHandlers, MediaTypeAudioReq, SrcIsLoopReq, GetAudioUrlResp } from 'contracts'; import { InboundHandlers, OutboundHandlers, MediaTypeAudioReq, SrcIsLoopReq, GetAudioUrlResp } from 'contracts';
import { ConfigManager, ResourceManager } from 'domain_resource'; import { ConfigManager, ResourceManager, VersionDecision, AppConfig } from 'domain_resource';
import { FileSystem, QiniuUploader, QiniuToken } from 'platform'; import { FileSystem, QiniuUploader, QiniuToken } from 'platform';
import { Logger, EventBus, EventPayload, AudioEvents, ShowRecordingRequest, RecordResult } from 'common'; import { Logger, EventBus, EventPayload, AudioEvents, ShowRecordingRequest, RecordResult } from 'common';
import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider'; import { CapabilityContext, CapabilityProvider } from '../core/CapabilityProvider';
@@ -406,11 +406,23 @@ export class AudioProvider implements CapabilityProvider {
private async doUpload(filePath: string, key: string, timeSec: number): Promise<void> { private async doUpload(filePath: string, key: string, timeSec: number): Promise<void> {
const ctx: common.UIAbilityContext | undefined = this.context; const ctx: common.UIAbilityContext | undefined = this.context;
if (ctx === undefined || filePath === '') { const cfg: ConfigManager | undefined = this.config;
if (ctx === undefined || cfg === undefined || filePath === '') {
return; // 已发过空回投(reset 语义);上传不了则保持"无语音" return; // 已发过空回投(reset 语义);上传不了则保持"无语音"
} }
try { try {
const token: string = await QiniuToken.uploadToken(); // AK/SK 取本地 app_configdomain/bucket 取远程配置(audio_domain/audio_bucket4 层回退)。
const local: AppConfig = cfg.getLocal();
const ak: string = local.qiniuAccessKey;
const sk: string = local.qiniuSecretKey;
const decision: VersionDecision = await cfg.resolve();
const bucket: string = decision.audioBucket;
const domain: string = decision.audioDomain;
if (ak === '' || sk === '' || bucket === '' || domain === '') {
this.log.w(`qiniu config missing: ak/sk(local) ok=${ak !== '' && sk !== ''}, bucket='${bucket}' domain='${domain}'(remote)`);
return;
}
const token: string = await QiniuToken.uploadToken(ak, sk, bucket);
const ok: boolean = await QiniuUploader.upload(ctx, filePath, key, () => Promise.resolve(token)); const ok: boolean = await QiniuUploader.upload(ctx, filePath, key, () => Promise.resolve(token));
try { try {
fileIo.unlinkSync(filePath); fileIo.unlinkSync(filePath);
@@ -418,7 +430,7 @@ export class AudioProvider implements CapabilityProvider {
this.log.w(`cleanup uploaded file failed: ${(e as Error).message}`); this.log.w(`cleanup uploaded file failed: ${(e as Error).message}`);
} }
if (ok) { if (ok) {
this.emitAudioUrl(`http://gameaudio.daoqi88.cn/${key}`, timeSec); this.emitAudioUrl(`http://${domain}/${key}`, timeSec);
} else { } else {
this.log.w(`qiniu upload failed key=${key}`); // 已发空回投,不再重复 this.log.w(`qiniu upload failed key=${key}`); // 已发空回投,不再重复
} }
@@ -10,26 +10,35 @@ import {
} from '@tencent/wechat_open_sdk'; } from '@tencent/wechat_open_sdk';
import { Logger } from 'common'; import { Logger } from 'common';
/** 微信 AppID(公开标识,契约 §13)。 */
export const WX_APP_ID: string = 'wxd2bd650e06bdfe58';
export type AuthRespCallback = (resp: SendAuthResp) => void; export type AuthRespCallback = (resp: SendAuthResp) => void;
export class WeChatApi { export class WeChatApi {
private static readonly log: Logger = Logger.tag('WeChatApi'); private static readonly log: Logger = Logger.tag('WeChatApi');
private static inst: WeChatApi | undefined = undefined; private static inst: WeChatApi | undefined = undefined;
/** 微信 AppID(从本地 app_config.wechat_app_id 注入;须在首次 getInstance 前 configure)。 */
private static appId: string = '';
private readonly api: WXApi; private readonly api: WXApi;
private readonly handler: WXApiEventHandler; private readonly handler: WXApiEventHandler;
private authCb: AuthRespCallback | undefined = undefined; private authCb: AuthRespCallback | undefined = undefined;
private constructor() { private constructor() {
this.api = WXAPIFactory.createWXAPI(WX_APP_ID); this.api = WXAPIFactory.createWXAPI(WeChatApi.appId);
this.handler = { this.handler = {
onReq: (_req: BaseReq) => { }, onReq: (_req: BaseReq) => { },
onResp: (resp: BaseResp) => this.routeResp(resp), onResp: (resp: BaseResp) => this.routeResp(resp),
}; };
} }
/**
* 注入微信 AppID(来自本地 app_config,不再硬编码)。须在首次 getInstance() 之前调用,
* 通常在 EntryAbility.onCreate 处理微信回调 want 之前。空串/重复 configure 安全。
*/
static configure(appId: string): void {
if (appId !== '') {
WeChatApi.appId = appId;
}
}
static getInstance(): WeChatApi { static getInstance(): WeChatApi {
if (WeChatApi.inst === undefined) { if (WeChatApi.inst === undefined) {
WeChatApi.inst = new WeChatApi(); WeChatApi.inst = new WeChatApi();
+13 -14
View File
@@ -1,27 +1,26 @@
/** /**
* 七牛 uploadToken 端侧生成(设计 §7)。 * 七牛 uploadToken 端侧生成(设计 §7)。token 算法见七牛官方:AK:urlsafeB64(HMAC_SHA1(SK,policy)):policy。
* ⚠️ 刻意对齐原 Android 工程:AK/SK 端侧硬编码、不走服务端——违背 CLAUDE.md 附录B 加固红线 * ⚠️ 不走服务端(对齐原工程AK/SK 由调用方从本地 app_config 注入、bucket 从远程配置注入
* 已与用户明确确认(对齐原工程)。token 算法见七牛官方:AK:urlsafeB64(HMAC_SHA1(SK,policy)):policy * 本类不再持有任何密钥常量。AK/SK 入端仍违背 CLAUDE.md 附录B 加固红线,已与用户确认
*/ */
import { cryptoFramework } from '@kit.CryptoArchitectureKit'; import { cryptoFramework } from '@kit.CryptoArchitectureKit';
import { buffer, util } from '@kit.ArkTS'; import { buffer, util } from '@kit.ArkTS';
export class QiniuToken { export class QiniuToken {
private static readonly ACCESS_KEY: string = 'ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf'; /**
private static readonly SECRET_KEY: string = '6VXav9eqUCORJTYvTFOeTVRonAyk4Hrs-PIZ8jmZ'; * 生成 uploadTokendeadline=now+1h)。
// bucket 取 'gameaudio'(有 d,与回放域名 gameaudio.daoqi88.cn / Android NewwebviewActivity 一致) * @param accessKey 七牛 AccessKey(本地 app_config.qiniu_access_key
// 原 webviewActivity 里的 'gameauio'(无 d) 是拼写错误,七牛上无该 bucket(区域查询 no such bucket)。 * @param secretKey 七牛 SecretKey(本地 app_config.qiniu_secret_key
private static readonly BUCKET: string = 'gameaudio'; * @param bucket 存储空间名(远程配置 audio_bucket
*/
/** 生成 uploadTokendeadline=now+1h)。 */ static async uploadToken(accessKey: string, secretKey: string, bucket: string): Promise<string> {
static async uploadToken(): Promise<string> {
const deadline: number = Math.floor(Date.now() / 1000) + 3600; const deadline: number = Math.floor(Date.now() / 1000) + 3600;
const policy: string = `{"scope":"${QiniuToken.BUCKET}","deadline":${deadline}}`; const policy: string = `{"scope":"${bucket}","deadline":${deadline}}`;
const encodedPolicy: string = const encodedPolicy: string =
QiniuToken.urlsafeBase64(new Uint8Array(buffer.from(policy, 'utf-8').buffer)); QiniuToken.urlsafeBase64(new Uint8Array(buffer.from(policy, 'utf-8').buffer));
const sign: Uint8Array = await QiniuToken.hmacSha1(QiniuToken.SECRET_KEY, encodedPolicy); const sign: Uint8Array = await QiniuToken.hmacSha1(secretKey, encodedPolicy);
const encodedSign: string = QiniuToken.urlsafeBase64(sign); const encodedSign: string = QiniuToken.urlsafeBase64(sign);
return `${QiniuToken.ACCESS_KEY}:${encodedSign}:${encodedPolicy}`; return `${accessKey}:${encodedSign}:${encodedPolicy}`;
} }
private static async hmacSha1(secret: string, message: string): Promise<Uint8Array> { private static async hmacSha1(secret: string, message: string): Promise<Uint8Array> {