fix: 修复抖音支付双端UTS编译兼容

This commit is contained in:
Codex
2026-09-10 10:57:53 +08:00
parent 3e2e1829a8
commit e692f3d9b1
6 changed files with 180 additions and 9 deletions
+24
View File
@@ -0,0 +1,24 @@
param(
[Parameter(Mandatory = $true)][string]$HBuilderXHome,
[Parameter(Mandatory = $true)][string]$SdkJar
)
$ErrorActionPreference = 'Stop'
# Compile the generated UTS and the original native bridge against real SDKs.
# First export App resources using HBuilderX. SdkJar is classes.jar extracted
# from the official dy-pay-sdk-tob 1.1.0.9 AAR, not a mock or reflection shim.
$projectDir = Split-Path -Parent $PSScriptRoot
$runtimeDir = Join-Path $HBuilderXHome 'plugins/uniapp-runextension'
$compilerDir = Join-Path $runtimeDir 'kotlinc'
$generated = Join-Path $projectDir 'unpackage/resources/uni_modules/tb-douyin-pay/utssdk/app-android/src/index.kt'
$native = Join-Path $projectDir 'uni_modules/tb-douyin-pay/utssdk/app-android/TbDouyinPayNative.kt'
foreach ($required in @($SdkJar, $generated, $native, (Join-Path $compilerDir 'lib/kotlin-compiler.jar'))) {
if (!(Test-Path -LiteralPath $required)) { throw "Missing dependency or source: $required" }
}
$taskClasspath = ((Get-ChildItem -LiteralPath (Join-Path $runtimeDir 'lib') -Filter '*.jar').FullName + @((Resolve-Path -LiteralPath $SdkJar).Path)) -join ';'
$resultDir = New-Item -ItemType Directory -Path (Join-Path ([IO.Path]::GetTempPath()) ('tb-douyin-kotlin-' + [guid]::NewGuid().ToString('N')))
$resultJar = Join-Path $resultDir.FullName 'tb-douyin-pay.jar'
& java -cp (Join-Path $compilerDir 'lib/*') org.jetbrains.kotlin.cli.jvm.K2JVMCompiler `
-kotlin-home $compilerDir $generated $native -classpath $taskClasspath -d $resultJar
if ($LASTEXITCODE -ne 0) { throw "Kotlin compilation failed: $LASTEXITCODE" }
Write-Output "Kotlin compilation passed: $resultJar"
Write-Output 'This check does not build/sign an APK or execute a payment.'
+108
View File
@@ -0,0 +1,108 @@
// Exercises the actual shared UTS logic as JS, not a duplicate implementation.
// This does not replace Kotlin/Swift native compilation or device tests.
const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const vm = require('node:vm');
const hx = process.env.HBUILDERX_HOME;
assert.ok(hx, 'Set HBUILDERX_HOME to the HBuilderX installation directory');
const ts = require(path.join(hx, 'plugins/uniapp-uts-v1/node_modules/@dcloudio/uni-uts-v1/lib/typescript/lib/typescript.js'));
const source = fs.readFileSync(path.join(__dirname, '../uni_modules/tb-douyin-pay/utssdk/common.uts'), 'utf8');
const compiled = ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2020, module: ts.ModuleKind.CommonJS },
}).outputText;
const info = { appid: 'app', partnerid: 'merchant', prepayid: 'prepay', package: 'Sign=DYPay',
noncestr: 'nonce', timestamp: '1780000000', sign: 'signed+value/==' };
function load(stringify = JSON.stringify) {
const timers = new Map();
let timerId = 0;
const exports = {};
const ctx = { exports, JSON: { stringify, parseObject(raw) {
const value = JSON.parse(raw);
return value == null ? null : { getString: key => typeof value[key] === 'string' ? value[key] : null };
} }, setTimeout(fn) { timers.set(++timerId, fn); return timerId; },
clearTimeout(id) { timers.delete(id); } };
vm.runInNewContext(compiled, ctx);
assert.equal(exports.configure('app'), true);
return { ...exports, timers };
}
test('shared bridge keeps signed fields unchanged and handles default/explicit loading', () => {
for (const showLoading of [undefined, null, true, false]) {
const bridge = load();
const results = [];
bridge.invoke({ payInfo: info, showLoading }, result => results.push(result), (payload, loading, done) => {
assert.deepEqual(JSON.parse(payload), info);
assert.equal(loading, showLoading ?? true);
done('{"resultCode":"0","errorMsg":""}');
});
assert.equal(results.length, 1);
assert.equal(results[0].resultCode, '0');
assert.equal(results[0].needsQuery, true);
assert.equal(bridge.timers.size, 0);
}
});
test('missing/null/empty payment fields never reach native code or take a lock', () => {
const invalid = [null, undefined, {}, { ...info, appid: 'wrong' },
{ ...info, package: 'wrong' }, { ...info, timestamp: '1780000000000' },
{ ...info, noncestr: 'n'.repeat(33) }];
for (const key of Object.keys(info)) {
for (const value of [null, undefined, '', ' ']) invalid.push({ ...info, [key]: value });
}
for (const payInfo of invalid) {
const bridge = load();
const results = [];
bridge.invoke({ payInfo }, r => results.push(r), () => assert.fail('Invalid native call'));
assert.equal(results.length, 1);
assert.equal(results[0].resultCode, '-1');
assert.equal(bridge.timers.size, 0);
assert.equal(bridge.configure('app'), true);
}
});
test('null or failed serialization returns an error before the lock and native call', () => {
for (const stringify of [() => null, () => undefined, () => '', () => { throw Error('serialize'); }]) {
const bridge = load(stringify);
bridge.invoke({ payInfo: info }, r => assert.equal(r.resultCode, '-1'), () => assert.fail('Invalid payload'));
assert.equal(bridge.timers.size, 0);
assert.equal(bridge.configure('app'), true);
}
});
test('reentrant calls, duplicate callbacks and late callbacks after reset are isolated', () => {
const bridge = load();
const results = [];
let oldDone;
bridge.invoke({ payInfo: info }, r => results.push(r), (_, __, done) => { oldDone = done; });
bridge.invoke({ payInfo: info }, r => assert.equal(r.resultCode, '103'), () => assert.fail('Duplicate pay'));
assert.equal(bridge.configure('other'), false);
bridge.resetPending();
let newDone;
bridge.invoke({ payInfo: info }, r => results.push(r), (_, __, done) => { newDone = done; });
oldDone('{"resultCode":"0"}');
assert.equal(results.length, 0);
newDone('{"resultCode":"1"}');
newDone('{"resultCode":"0"}');
assert.equal(results.length, 1);
assert.equal(results[0].resultCode, '1');
for (const timer of bridge.timers.values()) timer();
assert.equal(results.length, 1);
});
test('timeout, malformed result and native exception remain query-required and unlock', () => {
for (const mode of ['timeout', 'malformed', 'throw']) {
const bridge = load();
const results = [];
bridge.invoke({ payInfo: info }, r => results.push(r), (_, __, done) => {
if (mode === 'throw') throw Error('native');
if (mode === 'malformed') done('{');
});
if (mode === 'timeout') for (const timer of bridge.timers.values()) timer();
assert.equal(results.length, 1);
assert.notEqual(results[0].resultCode, '0');
assert.equal(results[0].needsQuery, true);
assert.equal(bridge.configure('app'), true);
}
});