diff --git a/domain_resource/src/main/ets/config/AppConfig.ets b/domain_resource/src/main/ets/config/AppConfig.ets index 5e59f37..4dc88ac 100644 --- a/domain_resource/src/main/ets/config/AppConfig.ets +++ b/domain_resource/src/main/ets/config/AppConfig.ets @@ -29,6 +29,14 @@ export interface AppConfig { other: string; /** 推广/邀请码(写入 app_data.js 的 app_invitationcode)。 */ tuiguang: string; + /** 七牛 AccessKey(录音上传 token 签名;随包内置,不写死在源码)。 */ + qiniuAccessKey: string; + /** 七牛 SecretKey(录音上传 token 签名)。 */ + qiniuSecretKey: string; + /** 微信开放平台 AppID(WeChatApi 创建 WXApi)。 */ + wechatAppId: string; + /** 微信开放平台 AppSecret(当前端侧不消费,登录走 code+服务端;留位避免硬编码)。 */ + wechatAppSecret: string; } /** 解析中间体(字段全可选,用于从 JSON 归一化)。 */ @@ -44,6 +52,10 @@ export interface AppConfigRaw { gameconfig?: string; other?: string; tuiguang?: string; + qiniu_access_key?: string; + qiniu_secret_key?: string; + wechat_app_id?: string; + wechat_app_secret?: string; } /** 把可选解析体归一化为完整 AppConfig(缺失键 → 空串)。 */ @@ -60,5 +72,9 @@ export function normalizeConfig(raw: AppConfigRaw): AppConfig { gameconfig: raw.gameconfig ?? '', other: raw.other ?? '', tuiguang: raw.tuiguang ?? '', + qiniuAccessKey: raw.qiniu_access_key ?? '', + qiniuSecretKey: raw.qiniu_secret_key ?? '', + wechatAppId: raw.wechat_app_id ?? '', + wechatAppSecret: raw.wechat_app_secret ?? '', }; } diff --git a/domain_resource/src/main/ets/config/RemoteConfig.ets b/domain_resource/src/main/ets/config/RemoteConfig.ets index 86d009a..cfcf6d7 100644 --- a/domain_resource/src/main/ets/config/RemoteConfig.ets +++ b/domain_resource/src/main/ets/config/RemoteConfig.ets @@ -34,6 +34,10 @@ export interface VersionFields { game_zip?: string; game_size?: string; showmessage?: string; + /** 录音上传七牛回放域名(4 层可携带,逐级回退)。 */ + audio_domain?: string; + /** 录音上传七牛 bucket(4 层可携带,逐级回退)。 */ + audio_bucket?: string; } /** 市场层(marketid 数字命中;携带 app 升级 + 可选资源覆盖)。 */ @@ -87,4 +91,8 @@ export interface VersionDecision { gameVersion: number; gameDownload: string; gameSize: string; + /** 录音上传七牛回放域名(来源 audio_domain,4 层回退)。 */ + audioDomain: string; + /** 录音上传七牛 bucket(来源 audio_bucket,4 层回退)。 */ + audioBucket: string; } diff --git a/domain_resource/src/main/ets/version/VersionResolver.ets b/domain_resource/src/main/ets/version/VersionResolver.ets index ec7b2ed..7e5c9e7 100644 --- a/domain_resource/src/main/ets/version/VersionResolver.ets +++ b/domain_resource/src/main/ets/version/VersionResolver.ets @@ -94,6 +94,8 @@ export class VersionResolver { gameVersion: VersionResolver.pickVer(layers, (n: VersionFields) => n.game_version), gameDownload: VersionResolver.pickStr(layers, (n: VersionFields) => n.game_zip), 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), }; } } diff --git a/entry/src/main/ets/entryability/EntryAbility.ets b/entry/src/main/ets/entryability/EntryAbility.ets index 351c1b1..dad85c1 100644 --- a/entry/src/main/ets/entryability/EntryAbility.ets +++ b/entry/src/main/ets/entryability/EntryAbility.ets @@ -4,6 +4,7 @@ import { window } from '@kit.ArkUI'; import { BusinessError } from '@kit.BasicServicesKit'; import { EventBus } from 'common'; import { WeChatApi } from 'feature_capabilities'; +import { ConfigManager } from 'domain_resource'; import { AppEvents } from '../routes/AppRoutes'; import { installErrorSink } from '../di/ErrorReporter'; @@ -19,7 +20,12 @@ export default class EntryAbility extends UIAbility { hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onCreate'); // 可观测(T-M5-06):安装错误上报 sink,打通 ErrorCenter → APM/Crash 通道 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); } diff --git a/entry/src/main/resources/rawfile/app_config.json b/entry/src/main/resources/rawfile/app_config.json index 075bca2..3c5215b 100644 --- a/entry/src/main/resources/rawfile/app_config.json +++ b/entry/src/main/resources/rawfile/app_config.json @@ -9,5 +9,9 @@ "weburl": "", "gameconfig": "tsgames.daoqi88.cn-config_test-update_jsonv2_test", "other": "", - "tuiguang": "" + "tuiguang": "", + "qiniu_access_key": "ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf", + "qiniu_secret_key": "6VXav9eqUCORJTYvTFOeTVRonAyk4Hrs-PIZ8jmZ", + "wechat_app_id": "wxd2bd650e06bdfe58", + "wechat_app_secret": "" } diff --git a/entry/src/test/QiniuToken.test.ets b/entry/src/test/QiniuToken.test.ets index 1e5b74f..f4435e4 100644 --- a/entry/src/test/QiniuToken.test.ets +++ b/entry/src/test/QiniuToken.test.ets @@ -5,16 +5,17 @@ import { util } from '@kit.ArkTS'; export default function QiniuTokenTest() { describe('QiniuTokenTest', () => { 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(':'); expect(parts.length).assertEqual(3); - expect(parts[0]).assertEqual('ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf'); + expect(parts[0]).assertEqual('AK_TEST'); expect(parts[1].length).assertEqual(28); const helper = new util.Base64Helper(); const std: string = parts[2].replace(/-/g, '+').replace(/_/g, '/'); const decoded: Uint8Array = helper.decodeSync(std); 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(); }); }); } diff --git a/feature_capabilities/Index.ets b/feature_capabilities/Index.ets index 79d8f9e..9ae95c4 100644 --- a/feature_capabilities/Index.ets +++ b/feature_capabilities/Index.ets @@ -23,7 +23,7 @@ export { AmrRecorder } from './src/main/ets/providers/AmrRecorder'; export { NavProvider } from './src/main/ets/providers/NavProvider'; // 微信 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'; diff --git a/feature_capabilities/src/main/ets/providers/AudioProvider.ets b/feature_capabilities/src/main/ets/providers/AudioProvider.ets index c0a6186..a34ab91 100644 --- a/feature_capabilities/src/main/ets/providers/AudioProvider.ets +++ b/feature_capabilities/src/main/ets/providers/AudioProvider.ets @@ -23,7 +23,7 @@ import { common, abilityAccessCtrl } from '@kit.AbilityKit'; import { fileIo } from '@kit.CoreFileKit'; import { BridgeController } from 'feature_bridge'; 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 { Logger, EventBus, EventPayload, AudioEvents, ShowRecordingRequest, RecordResult } from 'common'; 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 { 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 语义);上传不了则保持"无语音" } try { - const token: string = await QiniuToken.uploadToken(); + // AK/SK 取本地 app_config;domain/bucket 取远程配置(audio_domain/audio_bucket,4 层回退)。 + 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)); try { fileIo.unlinkSync(filePath); @@ -418,7 +430,7 @@ export class AudioProvider implements CapabilityProvider { this.log.w(`cleanup uploaded file failed: ${(e as Error).message}`); } if (ok) { - this.emitAudioUrl(`http://gameaudio.daoqi88.cn/${key}`, timeSec); + this.emitAudioUrl(`http://${domain}/${key}`, timeSec); } else { this.log.w(`qiniu upload failed key=${key}`); // 已发空回投,不再重复 } diff --git a/feature_capabilities/src/main/ets/wx/WeChatApi.ets b/feature_capabilities/src/main/ets/wx/WeChatApi.ets index e33f195..4e03779 100644 --- a/feature_capabilities/src/main/ets/wx/WeChatApi.ets +++ b/feature_capabilities/src/main/ets/wx/WeChatApi.ets @@ -10,26 +10,35 @@ import { } from '@tencent/wechat_open_sdk'; import { Logger } from 'common'; -/** 微信 AppID(公开标识,契约 §13)。 */ -export const WX_APP_ID: string = 'wxd2bd650e06bdfe58'; - export type AuthRespCallback = (resp: SendAuthResp) => void; export class WeChatApi { private static readonly log: Logger = Logger.tag('WeChatApi'); private static inst: WeChatApi | undefined = undefined; + /** 微信 AppID(从本地 app_config.wechat_app_id 注入;须在首次 getInstance 前 configure)。 */ + private static appId: string = ''; private readonly api: WXApi; private readonly handler: WXApiEventHandler; private authCb: AuthRespCallback | undefined = undefined; private constructor() { - this.api = WXAPIFactory.createWXAPI(WX_APP_ID); + this.api = WXAPIFactory.createWXAPI(WeChatApi.appId); this.handler = { onReq: (_req: BaseReq) => { }, 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 { if (WeChatApi.inst === undefined) { WeChatApi.inst = new WeChatApi(); diff --git a/platform/src/main/ets/upload/QiniuToken.ets b/platform/src/main/ets/upload/QiniuToken.ets index 40b8a75..060baf5 100644 --- a/platform/src/main/ets/upload/QiniuToken.ets +++ b/platform/src/main/ets/upload/QiniuToken.ets @@ -1,27 +1,26 @@ /** - * 七牛 uploadToken 端侧生成(设计 §7)。 - * ⚠️ 刻意对齐原 Android 工程:AK/SK 端侧硬编码、不走服务端——违背 CLAUDE.md 附录B 加固红线, - * 已与用户明确确认(对齐原工程)。token 算法见七牛官方:AK:urlsafeB64(HMAC_SHA1(SK,policy)):policy。 + * 七牛 uploadToken 端侧生成(设计 §7)。token 算法见七牛官方:AK:urlsafeB64(HMAC_SHA1(SK,policy)):policy。 + * ⚠️ 不走服务端(对齐原工程):AK/SK 由调用方从本地 app_config 注入、bucket 从远程配置注入, + * 本类不再持有任何密钥常量。AK/SK 入端仍违背 CLAUDE.md 附录B 加固红线,已与用户确认。 */ import { cryptoFramework } from '@kit.CryptoArchitectureKit'; import { buffer, util } from '@kit.ArkTS'; export class QiniuToken { - private static readonly ACCESS_KEY: string = 'ngN3rFW1j8dn7ZGpATKl7mreaNmp2Ei_l9AIhkIf'; - private static readonly SECRET_KEY: string = '6VXav9eqUCORJTYvTFOeTVRonAyk4Hrs-PIZ8jmZ'; - // bucket 取 'gameaudio'(有 d,与回放域名 gameaudio.daoqi88.cn / Android NewwebviewActivity 一致); - // 原 webviewActivity 里的 'gameauio'(无 d) 是拼写错误,七牛上无该 bucket(区域查询 no such bucket)。 - private static readonly BUCKET: string = 'gameaudio'; - - /** 生成 uploadToken(deadline=now+1h)。 */ - static async uploadToken(): Promise { + /** + * 生成 uploadToken(deadline=now+1h)。 + * @param accessKey 七牛 AccessKey(本地 app_config.qiniu_access_key) + * @param secretKey 七牛 SecretKey(本地 app_config.qiniu_secret_key) + * @param bucket 存储空间名(远程配置 audio_bucket) + */ + static async uploadToken(accessKey: string, secretKey: string, bucket: string): Promise { 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 = 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); - return `${QiniuToken.ACCESS_KEY}:${encodedSign}:${encodedPolicy}`; + return `${accessKey}:${encodedSign}:${encodedPolicy}`; } private static async hmacSha1(secret: string, message: string): Promise {