refactor(login): move match-route logic to dedicated service and fix import
- 提取登录回跳、产品对比的路由匹配逻辑至独立的 match-route.uts 服务 - 修复登录页面导入对比服务的错误,改为导入新的 match-route 模块 - 保持原有的路由参数校验和跳转逻辑不变,仅重构代码结构
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
// Run after HBuilderX MP-WEIXIN compilation; no network or real account access.
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const repo = path.resolve(__dirname, '..');
|
||||
const built = path.resolve(process.argv[2] || path.join(repo, 'unpackage/dist/dev/mp-weixin'));
|
||||
const app = JSON.parse(fs.readFileSync(path.join(built, 'app.json'), 'utf8'));
|
||||
const roots = app.subPackages.map(item => item.root.replace(/\/$/, '') + '/');
|
||||
const packageOf = file => roots.find(root => file.startsWith(root)) || '';
|
||||
let checks = 0;
|
||||
function test(name, action) { action(); checks++; console.log('PASS ' + name); }
|
||||
|
||||
function jsFiles(dir) {
|
||||
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(item => {
|
||||
const file = path.join(dir, item.name);
|
||||
return item.isDirectory() ? jsFiles(file) : item.name.endsWith('.js') ? [file] : [];
|
||||
});
|
||||
}
|
||||
|
||||
test('all generated static requires stay within main package or their own subpackage', () => {
|
||||
const violations = [];
|
||||
for (const file of jsFiles(built)) {
|
||||
const relative = path.relative(built, file).split(path.sep).join('/');
|
||||
for (const match of fs.readFileSync(file, 'utf8').matchAll(/\brequire\(["']([^"']+)["']\)/g)) {
|
||||
if (!match[1].startsWith('.')) continue;
|
||||
const target = path.posix.normalize(path.posix.join(path.posix.dirname(relative), match[1]));
|
||||
assert.ok(fs.existsSync(path.join(built, target)), relative + ': missing module ' + target);
|
||||
if (packageOf(target) && packageOf(target) !== packageOf(relative)) violations.push(relative + ' -> ' + target);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(violations, [], 'Unavailable subpackage dependencies: ' + violations.join(', '));
|
||||
});
|
||||
|
||||
test('route, selection and login output is freshly compiled', () => {
|
||||
for (const file of ['services/match-route.uts', 'packages/finance/services/compare.uts', 'packages/account/login/login.uvue']) {
|
||||
const output = path.join(built, file.replace(/\.(uts|uvue)$/, '.js'));
|
||||
assert.ok(fs.statSync(output).mtimeMs >= fs.statSync(path.join(repo, file)).mtimeMs, 'Recompile MP-WEIXIN: ' + file);
|
||||
}
|
||||
});
|
||||
|
||||
// Model a cold account-package load: finance/content modules are not registered.
|
||||
const loaded = new Map();
|
||||
let page, onLoad, restoredPath;
|
||||
class JsonObject { constructor(value) { Object.assign(this, value); } }
|
||||
class UtsType { static initProps(options) { return options; } }
|
||||
const vendor = {
|
||||
UTSJSONObject: JsonObject, UTS: { UTSType: UtsType },
|
||||
defineComponent: component => component,
|
||||
ref: value => ({ value }), watch: () => {}, onMounted: () => {}, onUnmounted: () => {},
|
||||
onLoad: callback => { onLoad = callback; },
|
||||
index: {
|
||||
getWindowInfo: () => ({ windowWidth: 390, windowHeight: 844, statusBarHeight: 24 }),
|
||||
getMenuButtonBoundingClientRect: () => ({ width: 0, height: 0, top: 0, left: 0 }),
|
||||
onWindowResize: () => {}, offWindowResize: () => {}
|
||||
}
|
||||
};
|
||||
function load(relative) {
|
||||
assert.ok(!packageOf(relative) || packageOf(relative) === 'packages/account/', 'module is not registered: ' + relative);
|
||||
if (relative === 'common/vendor.js') return vendor;
|
||||
if (loaded.has(relative)) return loaded.get(relative);
|
||||
const exports = {};
|
||||
loaded.set(relative, exports);
|
||||
vm.runInNewContext(fs.readFileSync(path.join(built, relative), 'utf8'), {
|
||||
exports, setInterval, clearInterval,
|
||||
wx: { createPage: component => { page = component; } },
|
||||
require: module => load(path.posix.normalize(path.posix.join(path.posix.dirname(relative), module)))
|
||||
}, { filename: relative });
|
||||
return exports;
|
||||
}
|
||||
|
||||
test('login registers and initializes without loading the finance subpackage', () => {
|
||||
load('packages/account/login/login.js');
|
||||
assert.equal(typeof page.setup, 'function');
|
||||
page.setup({});
|
||||
assert.equal(typeof onLoad, 'function');
|
||||
assert.ok([...loaded.keys()].every(file => !file.startsWith('packages/finance/')));
|
||||
});
|
||||
|
||||
const route = load('services/match-route.js');
|
||||
const realReturnPath = route.matchReturnPath;
|
||||
route.matchReturnPath = (...args) => { restoredPath = realReturnPath(...args); return restoredPath; };
|
||||
test('account login restores match context using the shared route module', () => {
|
||||
onLoad({ resumeMatch: '1', matchPage: 'product', assessmentId: '42', productIds: '3,7', revisionId: '9', productId: '7', tab: 'details', from: 'compare' });
|
||||
assert.equal(restoredPath, '/packages/finance/product/product?assessmentId=42&productIds=3,7&revisionId=9&productId=7&tab=details&from=compare');
|
||||
});
|
||||
|
||||
const cases = [
|
||||
['matches with selected products and revision', ['matches', '42', '3,7', '9'], '/packages/finance/matches/matches?assessmentId=42&productIds=3,7&revisionId=9&group=eligible'],
|
||||
['matches without a selection', ['matches', '42', '', '', '', 'unknown'], '/packages/finance/matches/matches?assessmentId=42&productIds=&group=unknown'],
|
||||
['compare selection order', ['compare', '42', '7,3,8', '9'], '/packages/finance/compare/compare?assessmentId=42&productIds=7,3,8&revisionId=9'],
|
||||
['context product', ['product', '42', '3', '', '3'], '/packages/finance/product/product?assessmentId=42&productIds=3&productId=3&tab=explanation&from='],
|
||||
['public product details', ['product', '', '', '', '3', 'eligible', 'details'], '/packages/finance/product/product?productId=3&tab=details&from='],
|
||||
['public product conditions', ['product', '', '', '', '3', 'eligible', 'conditions', 'archive'], '/packages/finance/product/product?productId=3&tab=conditions&from=archive'],
|
||||
['external return URL', ['https://example.invalid', '42', '', ''], ''],
|
||||
['undeclared page', ['login', '42', '', ''], ''],
|
||||
['missing assessment for matches', ['matches', '', '', ''], ''],
|
||||
['missing assessment for compare', ['compare', '', '3', ''], ''],
|
||||
['invalid assessment', ['compare', '42&x=1', '3', ''], ''],
|
||||
['zero assessment', ['matches', '0', '', ''], ''],
|
||||
['invalid revision', ['matches', '42', '', '0'], ''],
|
||||
['duplicate products', ['compare', '42', '3,3', ''], ''],
|
||||
['too many products', ['compare', '42', '1,2,3,4', ''], ''],
|
||||
['invalid product list', ['compare', '42', '3,x', ''], ''],
|
||||
['empty list member', ['compare', '42', '3,', ''], ''],
|
||||
['invalid matches group', ['matches', '42', '', '', '', 'other'], ''],
|
||||
['missing product', ['product', '42', '', '', ''], ''],
|
||||
['invalid product', ['product', '42', '', '', '3&x=1'], ''],
|
||||
['invalid product tab', ['product', '42', '', '', '3', 'eligible', 'other'], ''],
|
||||
['invalid product source', ['product', '42', '', '', '3', 'eligible', 'details', '//evil.invalid'], ''],
|
||||
['public product cannot restore an explanation', ['product', '', '', '', '3'], ''],
|
||||
['public product cannot carry a selection', ['product', '', '3', '', '3', 'eligible', 'details'], ''],
|
||||
['public product cannot carry a revision', ['product', '', '', '9', '3', 'eligible', 'details'], '']
|
||||
];
|
||||
for (const [name, args, expected] of cases) test(name, () => assert.equal(route.matchReturnPath(...args), expected));
|
||||
test('selection parser keeps the existing limits', () => {
|
||||
assert.deepEqual(Array.from(route.parseCompareIds('')), []);
|
||||
assert.deepEqual(Array.from(route.parseCompareIds('7,3,8')), ['7', '3', '8']);
|
||||
for (const raw of ['1,2,3,4', '3,3', '0', '03', '-3', '3,', '3, 7', '12345678901', '3&tab=details']) assert.throws(() => route.parseCompareIds(raw));
|
||||
});
|
||||
test('finance selection still uses shared validation and isolates session/assessment/revision', () => {
|
||||
let session = { userId: 'synthetic-user', token: 'synthetic-token' };
|
||||
const exports = {};
|
||||
vm.runInNewContext(fs.readFileSync(path.join(built, 'packages/finance/services/compare.js'), 'utf8'), {
|
||||
exports,
|
||||
require(module) {
|
||||
if (module === '../../../services/match-route.js') return route;
|
||||
if (module === './matches.js') return {};
|
||||
if (module === '../../../services/api.js') return { getSession: () => session && { getString: key => session[key] } };
|
||||
throw Error('Unexpected finance dependency: ' + module);
|
||||
}
|
||||
});
|
||||
exports.saveCompareSelection('42', ['7', '3'], '9');
|
||||
assert.deepEqual(Array.from(exports.compareSelection('42', '9')), ['7', '3']);
|
||||
assert.throws(() => exports.saveCompareSelection('42', ['7', '7'], '9'));
|
||||
assert.deepEqual(Array.from(exports.compareSelection('42', '9')), ['7', '3']);
|
||||
for (const change of [() => { session.token = 'another-token'; }, () => { session.userId = 'another-user'; }]) {
|
||||
change(); assert.deepEqual(Array.from(exports.compareSelection('42', '9')), []);
|
||||
exports.saveCompareSelection('42', ['3'], '9');
|
||||
}
|
||||
assert.deepEqual(Array.from(exports.compareSelection('43', '9')), []);
|
||||
exports.saveCompareSelection('42', ['3'], '9');
|
||||
assert.deepEqual(Array.from(exports.compareSelection('42', '10')), []);
|
||||
exports.saveCompareSelection('42', ['3'], '9');
|
||||
session = null;
|
||||
assert.deepEqual(Array.from(exports.compareSelection('42', '9')), []);
|
||||
});
|
||||
console.log(checks + ' checks passed; no network calls.');
|
||||
@@ -0,0 +1,82 @@
|
||||
// Run after MP-WEIXIN compilation; exercise the generated report adapter without network or application initialization.
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
const root = path.resolve(__dirname, '..');
|
||||
const built = path.join(root, 'unpackage/dist/dev/mp-weixin/packages/finance/services/records.js');
|
||||
assert.ok(fs.statSync(built).mtimeMs >= fs.statSync(path.join(root, 'packages/finance/services/records.uts')).mtimeMs, 'Compile MP-WEIXIN before testing');
|
||||
class JsonObject {
|
||||
constructor(value) { Object.assign(this, value); }
|
||||
getString(key) { return typeof this[key] === 'string' ? this[key] : null; }
|
||||
getNumber(key) { return typeof this[key] === 'number' ? this[key] : null; }
|
||||
getBoolean(key) { return typeof this[key] === 'boolean' ? this[key] : null; }
|
||||
}
|
||||
class UtsType { static initProps(options) { return options; } }
|
||||
function awaiter(thisArg, args, PromiseType, generator) {
|
||||
return new (PromiseType || Promise)((resolve, reject) => {
|
||||
const iterator = generator.apply(thisArg, args || []);
|
||||
const step = result => result.done ? resolve(result.value) : Promise.resolve(result.value).then(value => advance('next', value), error => advance('throw', error));
|
||||
const advance = (method, value) => { try { step(iterator[method](value)); } catch (error) { reject(error); } };
|
||||
advance('next');
|
||||
});
|
||||
}
|
||||
const exported = {};
|
||||
let response;
|
||||
vm.runInNewContext(fs.readFileSync(built, 'utf8'), {
|
||||
exports: exported,
|
||||
require(module) {
|
||||
if (module.endsWith('/common/vendor.js')) return { UTS: { UTSType: UtsType, arrayFind: (items, predicate) => items.find(predicate) ?? null }, UTSJSONObject: JsonObject, __awaiter: awaiter };
|
||||
if (module.endsWith('/services/assessment.js')) return { draftText: (item, key) => typeof item[key] === 'string' ? item[key] : '' };
|
||||
if (module.endsWith('/services/api.js')) return { requestJson: async () => response };
|
||||
if (module.endsWith('/financing-grade.js')) return { financingGradeFrom: () => ({}) };
|
||||
if (['/services/assessment-flow.js', '/services/contribution.js', '/services/money.js'].some(suffix => module.endsWith(suffix))) return {};
|
||||
throw Error('Unexpected dependency: ' + module);
|
||||
}
|
||||
}, { filename: built });
|
||||
const field = (key, label, state = 'missing') => new JsonObject({ fieldKey: key, label, state });
|
||||
const reason = (id, code, keys, issues = []) => new JsonObject({ basisId: id, code, fieldRefs: keys, issueRefs: issues });
|
||||
const advice = (text, refs) => new JsonObject({ text, evidenceRefs: refs });
|
||||
const debt = field('totalDebtBalance', '欠款总余额');
|
||||
const income = field('monthlyIncome', '月收入');
|
||||
const debtReason = reason('missing-totalDebtBalance', 'MISSING_FIELD', ['totalDebtBalance']);
|
||||
let checks = 0;
|
||||
const cases = [];
|
||||
function verify(name, suggestions, fields, basis, expected) {
|
||||
cases.push({ name, suggestions, fields, basis, expected });
|
||||
}
|
||||
async function verifySavedReport({ name, suggestions, fields, basis, expected }) {
|
||||
const snapshot = JSON.stringify({ suggestions, fields, basis });
|
||||
response = new JsonObject({
|
||||
assessmentId: '900001', revisionId: '900002', revisionNo: 1, isPreviousResult: false, currentStatus: 'succeeded',
|
||||
resultVersion: 2, reportType: 'financing_needs_analysis', inputVersion: 3, generatedAt: '2026-10-09T08:40:00Z',
|
||||
facts: [], demand: new JsonObject({}), matching: new JsonObject({ status: 'unavailable' }), summary: '合成报告',
|
||||
completeness: new JsonObject({ status: 'evaluated', percent: 90, providedCount: 10, applicableCount: 11, items: fields }),
|
||||
readiness: new JsonObject({ status: 'evaluated', label: '合成准备状态', basis }),
|
||||
improvements: suggestions, strengths: [], riskWarnings: [], systemWarnings: [], disclaimer: '合成报告', modelCode: 'synthetic', financingGrade: null
|
||||
});
|
||||
const actual = await exported.fetchAnalysisReport('900001', '900002');
|
||||
assert.deepEqual(Array.from(actual.priorityItems), expected, name);
|
||||
assert.equal(JSON.stringify({ suggestions, fields, basis }), snapshot, name + ': saved report inputs remain unchanged');
|
||||
checks += 2;
|
||||
}
|
||||
verify('same missing field: AI advice replaces fallback', [advice('欠款总余额尚未填写,建议补充。', ['readiness:missing-totalDebtBalance'])], [debt], [debtReason], ['欠款总余额尚未填写,建议补充。']);
|
||||
verify('fact and readiness references resolve to the same problem', [advice('请补齐欠款', ['fact:totalDebtBalance']), advice('欠款待补充', ['readiness:missing-totalDebtBalance'])], [debt], [debtReason], ['请补齐欠款']);
|
||||
verify('same readiness problem with different wording appears once', [advice('请补齐欠款', ['readiness:missing-totalDebtBalance']), advice('欠款待补充', ['readiness:missing-totalDebtBalance'])], [debt], [debtReason], ['请补齐欠款']);
|
||||
verify('uncovered missing fields remain visible', [advice('请补齐欠款', ['readiness:missing-totalDebtBalance'])], [debt, income], [debtReason], ['请补齐欠款', '补充月收入']);
|
||||
verify('empty advice does not suppress real missing fields', [advice(' ', ['readiness:missing-totalDebtBalance'])], [debt], [debtReason], ['补充欠款总余额']);
|
||||
verify('provided and inapplicable fields create no fallback', [], [field('company', '单位', 'provided'), field('annualRevenue', '营收', 'not_applicable')], [], []);
|
||||
verify('invalid format uses verification fallback', [], [field('totalDebtBalance', '欠款总余额', 'invalid')], [], ['核实欠款总余额']);
|
||||
verify('invalid readiness advice replaces verification fallback', [advice('请核实欠款格式', ['readiness:invalid-totalDebtBalance'])], [field('totalDebtBalance', '欠款总余额', 'invalid')], [reason('invalid-totalDebtBalance', 'INVALID_FIELD', ['totalDebtBalance'])], ['请核实欠款格式']);
|
||||
verify('different problems on the same field remain separate', [advice('请核实额度格式', ['readiness:invalid-creditCardUsedLimit']), advice('已用额度超过总额度', ['readiness:issue-card-limit'])], [field('creditCardUsedLimit', '已使用总额度', 'invalid')], [reason('invalid-creditCardUsedLimit', 'INVALID_FIELD', ['creditCardUsedLimit']), reason('issue-card-limit', 'CREDIT_CARD_USED_EXCEEDS_LIMIT', ['creditCardUsedLimit', 'creditCardTotalLimit'], ['card-limit'])], ['请核实额度格式', '已用额度超过总额度']);
|
||||
verify('combined advice covers each of its missing fields', [advice('请补齐欠款和收入', ['readiness:missing-totalDebtBalance', 'readiness:missing-monthlyIncome'])], [debt, income], [debtReason, reason('missing-monthlyIncome', 'MISSING_FIELD', ['monthlyIncome'])], ['请补齐欠款和收入']);
|
||||
verify('new problem in overlapping advice is retained', [advice('请补齐欠款', ['readiness:missing-totalDebtBalance']), advice('请补齐欠款和收入', ['readiness:missing-totalDebtBalance', 'readiness:missing-monthlyIncome'])], [debt, income], [debtReason, reason('missing-monthlyIncome', 'MISSING_FIELD', ['monthlyIncome'])], ['请补齐欠款', '请补齐欠款和收入']);
|
||||
verify('unknown evidence cannot hide a missing field', [advice('请核实其他资料', ['readiness:unknown'])], [debt], [], ['请核实其他资料', '补充欠款总余额']);
|
||||
verify('surrounding whitespace and duplicate evidence do not create duplicate advice', [advice(' 请补齐欠款 ', ['readiness:missing-totalDebtBalance', 'readiness:missing-totalDebtBalance']), advice('请补齐欠款', ['readiness:missing-totalDebtBalance'])], [debt], [debtReason], ['请补齐欠款']);
|
||||
verify('ordinary fact evidence is not a problem identity', [advice('保留收入证明材料', ['fact:monthlyIncome']), advice('控制每月还款负担', ['fact:monthlyIncome'])], [field('monthlyIncome', '月收入', 'provided')], [], ['保留收入证明材料', '控制每月还款负担']);
|
||||
verify('same known consistency issue is consolidated', [advice('核实信用卡额度', ['readiness:issue-card-limit']), advice('已用额度超过总额度,请核实', ['readiness:issue-card-limit'])], [], [reason('issue-card-limit', 'CREDIT_CARD_USED_EXCEEDS_LIMIT', ['creditCardUsedLimit', 'creditCardTotalLimit'], ['card-limit'])], ['核实信用卡额度']);
|
||||
verify('unknown reference alone cannot equate different problems', [advice('准备收入证明', ['readiness:unknown']), advice('核实负债资料', ['readiness:unknown'])], [debt], [], ['准备收入证明', '核实负债资料', '补充欠款总余额']);
|
||||
(async () => {
|
||||
for (const entry of cases) await verifySavedReport(entry);
|
||||
console.log('PASS ' + checks + ' report priority checks; no network or database');
|
||||
})().catch(error => { console.error(error); process.exitCode = 1; });
|
||||
Reference in New Issue
Block a user