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:
co-authored by
Claude Opus 4.8
parent
e5d8eeb3b1
commit
c39d8eb400
@@ -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;
|
||||||
|
/** 微信开放平台 AppID(WeChatApi 创建 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_domain,4 层回退)。 */
|
||||||
|
audioDomain: string;
|
||||||
|
/** 录音上传七牛 bucket(来源 audio_bucket,4 层回退)。 */
|
||||||
|
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": ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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_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));
|
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();
|
||||||
|
|||||||
@@ -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';
|
* 生成 uploadToken(deadline=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)
|
||||||
|
*/
|
||||||
/** 生成 uploadToken(deadline=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> {
|
||||||
|
|||||||
Reference in New Issue
Block a user