fix: some
All checks were successful
构建并部署前端到测试环境 / deploy (push) Successful in 54s

This commit is contained in:
luo
2026-10-28 15:18:44 +08:00
parent 06684e1fc0
commit f2614d8efb
99 changed files with 11511 additions and 132 deletions

View File

@@ -0,0 +1,517 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import ts from 'typescript'
import * as vue from 'vue'
import { parse, compileScript, compileTemplate } from '@vue/compiler-sfc'
const read = (path) => readFileSync(path, 'utf8')
function evaluate(source, imports = {}, globals = {}) {
const code = ts.transpileModule(source, {
compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS }
}).outputText
const m = { exports: {} }
new Function('require', 'module', 'exports', ...Object.keys(globals), code)(
(name) => {
if (name.endsWith('.vue')) return { default: {} }
assert.ok(imports[name], `Unexpected dependency: ${name}`)
return imports[name]
},
m,
m.exports,
...Object.values(globals)
)
return m.exports
}
const helpers = evaluate(read('src/utils/business/packageSilence.ts'))
const constants = evaluate(read('src/config/constants/packageSilence.ts'))
const format = evaluate(read('src/utils/business/format.ts'))
const gateway = evaluate(read('src/utils/business/gateway.ts'), { './format': format })
const polling = evaluate(read('src/utils/business/pollingScheduler.ts'), { './gateway': gateway })
const exportConfig = evaluate(read('src/config/constants/exportTask.ts'))
test('silence business errors never count as successful orders, package saves or refunds', () => {
for (const code of [1260, 1261, 1262, 1268, 1275])
assert.throws(() => helpers.assertSilenceResponse({ code, data: null }), {
message: helpers.SILENCE_ERROR_MESSAGES[code]
})
assert.throws(
() => helpers.assertSilenceResponse({ code: 1266, msg: 'min_days=30,max_days=90' }),
{ message: 'min_days=30,max_days=90' }
)
const response = { code: 0, data: { warnings: ['saved'] } }
assert.equal(helpers.assertSilenceResponse(response), response)
for (const path of [
'src/views/order-management/order-list/index.vue',
'src/views/package-management/package-list/index.vue',
'src/views/asset-management/asset-information/components/dialogs/PackageRechargeDialog.vue',
'src/components/business/CreateRefundDialog.vue'
])
assert.ok(read(path).includes('assertSilenceResponse'))
})
function setup(path, { type = 2, props = {}, permissions = [], overrides = {} } = {}) {
const calls = [],
unmount = []
const user = vue.reactive({
info: { id: 1, user_type: type },
get isSuperAdmin() {
return Number(this.info.user_type) === 1
},
get getUserInfo() {
return this.info
}
})
const services = new Proxy(
{},
{
get:
(_, name) =>
async (...args) => {
calls.push({ name, args })
return overrides[name]
? overrides[name](...args)
: { code: 0, data: { items: [], total: 0 } }
}
}
)
const runtime = {
...vue,
onMounted() {},
onActivated() {},
onDeactivated() {},
onBeforeUnmount: (fn) => unmount.push(fn)
}
const store = { useUserStore: () => user }
const auth = {
useAuth: () => ({ hasAuth: (code) => user.isSuperAdmin || permissions.includes(code) })
}
const scheduler = evaluate(read('src/composables/useSchedulerRequest.ts'), {
vue: runtime,
'@/store/modules/user': store,
'@/utils/business/gateway': gateway,
'@/utils/business/pollingScheduler': polling
})
const checked = evaluate(read('src/composables/useCheckedColumns.ts'), { vue: runtime })
const imports = {
vue: runtime,
pinia: { storeToRefs: vue.toRefs },
'vue-router': { useRouter: () => ({ push() {}, back() {} }) },
'element-plus': {
ElMessage: { success() {}, warning() {}, error() {} },
ElMessageBox: { confirm: async () => {} },
ElTag: {}
},
'@/api/modules': {
OrderService: services,
ExportTaskService: services,
CardService: services,
DeviceService: services,
PackageManageService: services,
ShopService: services,
AssetService: services
},
'@/api/modules/packageSilence': { PackageSilenceService: services },
'@/store/modules/user': store,
'@/composables/useAuth': auth,
'@/composables/useCheckedColumns': checked,
'@/composables/useSchedulerRequest': scheduler,
'@/utils/business/format': format,
'@/utils/business/packageSilence': helpers,
'@/utils/business/pollingScheduler': polling,
'@/utils/business/gateway': gateway,
'@/utils/business/orderPayment': {},
'@/utils/business': { hasVoucherKeys: (v) => !!v?.length, toVoucherKeyList: (v) => v },
'@/router/routesAlias': { RoutesAlias: {} },
'@/config/constants/audit': { AUDIT_PERMISSIONS: {} },
'@/utils/business/auditNavigation': {},
'@/components/business/audit/investigationController': {},
'@/config/constants/bulkPurchase': { BULK_PURCHASE_PERMISSIONS: {} },
'@/config/constants/packageSilence': constants,
'@/config/constants': exportConfig,
'@/config/constants/exportTask': exportConfig
}
const { descriptor } = parse(read(path)),
scope = vue.effectScope()
const component = evaluate(
compileScript(descriptor, { id: 'silence-integration' }).content,
imports,
runtime
).default
const state = scope.run(() => component.setup(props, { expose() {}, emit() {} }))
return {
state,
calls,
user,
dispose() {
unmount.forEach((fn) => fn())
scope.stop()
}
}
}
test('order category role matrix and export scene boundaries match the document', () => {
for (const type of [1, 2, 3, 4, 0, undefined])
for (const category of ['all', 'platform', 'agent', 'online']) {
const allowed = (category === 'online' ? [1, 2] : [1, 2, 3]).includes(type)
assert.equal(helpers.canAccessOrderCategory(type, category), allowed)
assert.equal(
exportConfig.canAccessExportTaskScene(
constants.ORDER_CATEGORY_CONFIG[category].scene,
type
),
allowed
)
}
assert.equal(helpers.canEditRestrictedAsset(4), false)
assert.equal(helpers.canEditRestrictedAsset(3), true)
})
test('three category endpoints preserve query and shared legacy detail contract', async () => {
const calls = []
class BaseService {}
for (const name of ['get', 'getOne'])
BaseService[name] = (url, params) => {
calls.push({ url, params })
return Promise.resolve({ code: 0 })
}
const { OrderService: S } = evaluate(read('src/api/modules/order.ts'), {
'../BaseService': { BaseService }
})
const query = { page: 1, page_size: 20, is_expired: false, seller_shop_id: 8 }
await S.getPlatformOrders(query)
await S.getAgentOrders(query)
await S.getOnlineOrders(query)
await S.getOrders(query)
await S.getOrderById(1)
assert.deepEqual(
calls.map((x) => x.url),
[
'/api/admin/orders/platform',
'/api/admin/orders/agent',
'/api/admin/orders/online',
'/api/admin/orders',
'/api/admin/orders/1'
]
)
for (const call of calls.slice(0, 4)) {
assert.deepEqual(call.params, query)
assert.equal('category' in call.params, false)
}
})
for (const [category, method] of [
['platform', 'getPlatformOrders'],
['agent', 'getAgentOrders'],
['online', 'getOnlineOrders'],
['all', 'getOrders']
])
test(`order view ${category} uses exact service and scene, blocks disallowed identities`, async () => {
for (const type of [1, 2, 3, 4]) {
const h = setup('src/views/order-management/order-list/index.vue', {
type,
props: { category }
})
h.state.searchForm.is_expired = 0
await h.state.getTableData()
const allowed = helpers.canAccessOrderCategory(type, category)
assert.equal(h.calls.length, allowed ? 1 : 0)
if (allowed) {
assert.equal(h.calls[0].name, method)
assert.equal(h.calls[0].args[0].is_expired, false)
assert.equal('category' in h.calls[0].args[0], false)
}
assert.equal(
h.state.categoryConfig.value.scene,
constants.ORDER_CATEGORY_CONFIG[category].scene
)
h.dispose()
}
})
test('package defaults enforce 1..90, nullable purchase defaults, addon omission and immutable activation base', () => {
for (const days of [0, 91, 1.5, NaN])
assert.throws(() =>
helpers.packageSilenceFields({
package_type: 'formal',
expiry_base: 'from_activation',
silence_period_days: days
})
)
for (const days of [1, 30, 90])
assert.equal(
helpers.packageSilenceFields({
package_type: 'formal',
expiry_base: 'from_activation',
silence_period_days: days
}).silence_period_days,
days
)
assert.throws(() =>
helpers.packageSilenceFields({
package_type: 'formal',
expiry_base: 'from_activation',
silence_period_days: null
})
)
assert.equal(
helpers.packageSilenceFields({
package_type: 'formal',
expiry_base: 'from_purchase',
silence_period_days: null,
silence_extension_enabled: false
}).silence_extension_enabled,
false
)
assert.deepEqual(
helpers.packageSilenceFields({ package_type: 'addon', silence_period_days: 999 }),
{}
)
assert.throws(() =>
helpers.packageSilenceFields(
{ package_type: 'formal', expiry_base: 'from_purchase' },
'from_activation'
)
)
})
test('allocation overrides enforce source defaults before single, batch and series writes', async () => {
const packageData = {
package_type: 'formal',
expiry_base: 'from_purchase',
silence_period_days: null
}
let reads = 0
const validation = evaluate(read('src/api/modules/packageSilenceValidation.ts'), {
'./packageManage': {
PackageManageService: {
getPackageDetail: async () => {
reads++
return { code: 0, data: { ...packageData } }
}
}
},
'@/utils/business/packageSilence': helpers
})
const writes = []
class BaseService {}
for (const method of ['post', 'put', 'patch', 'create'])
BaseService[method] = (url, data) => {
writes.push({ url, data })
return Promise.resolve({ code: 0, data })
}
const imports = { '../BaseService': { BaseService }, './packageSilenceValidation': validation }
const S = evaluate(
read('src/api/modules/shopPackageAllocation.ts'),
imports
).ShopPackageAllocationService
const G = evaluate(read('src/api/modules/shopSeriesGrant.ts'), imports).ShopSeriesGrantService
const calls = [
() => S.createShopPackageAllocation({ package_id: 8, expiry_base_override: 'from_activation' }),
() =>
S.createShopPackageBatchAllocations({
package_ids: [8, 9],
expiry_base_override: 'from_activation'
}),
() =>
S.updateShopPackageAllocationExpiryBase(1, { expiry_base_override: 'from_activation' }, 8),
() =>
G.createShopSeriesGrant({
packages: [{ package_id: 8 }],
expiry_base_override: 'from_activation'
}),
() =>
G.manageGrantPackages(1, {
packages: [{ package_id: 8 }],
expiry_base_override: 'from_activation'
})
]
for (const call of calls) await assert.rejects(call, /先配置默认沉默期/)
assert.equal(writes.length, 0)
packageData.silence_period_days = 30
for (const call of calls) await call()
assert.equal(writes.length, 5)
packageData.expiry_base = 'from_activation'
await assert.rejects(
() => S.updateShopPackageAllocationExpiryBase(1, { expiry_base_override: 'from_purchase' }, 8),
/不允许覆盖/
)
const previousReads = reads
await S.createShopPackageAllocation({ package_id: 8, expiry_base_override: null })
await G.manageGrantPackages(1, {
packages: [{ package_id: 8, remove: true }],
expiry_base_override: 'from_purchase'
})
assert.equal(reads, previousReads)
})
test('purchase overrides do not impose a 90-day maximum and never leak unauthorized fields', () => {
assert.deepEqual(helpers.silencePurchaseFields(false, 91, 'why'), {})
assert.deepEqual(helpers.silencePurchaseFields(true, null, 'why'), {})
assert.deepEqual(helpers.silencePurchaseFields(true, 365, ' why '), {
silence_period_days: 365,
silence_reason: 'why'
})
for (const days of [0, -1, 1.5, NaN])
assert.throws(() => helpers.silencePurchaseFields(true, days, 'why'))
assert.throws(() => helpers.silencePurchaseFields(true, 30, ' '))
})
test('purchase override remains null until explicitly enabled and disabling clears the reason', async () => {
const path = 'src/components/business/package-silence/SilencePurchaseFields.vue'
const { descriptor } = parse(read(path))
const definition = evaluate(compileScript(descriptor, { id: 'optional-override' }).content, {
vue
}).default
let state
const days = vue.ref(null),
reason = vue.ref('')
const component = {
...definition,
setup(props, context) {
state = definition.setup(props, context)
return () => null
}
}
const renderer = vue.createRenderer({
createComment: () => ({}),
insert() {},
remove() {},
parentNode: () => null,
nextSibling: () => null
})
const app = renderer.createApp({
render: () =>
vue.h(component, {
days: days.value,
reason: reason.value,
'onUpdate:days': (v) => (days.value = v),
'onUpdate:reason': (v) => (reason.value = v)
})
})
app.mount({})
assert.equal(state.overrideEnabled.value, false)
assert.equal(days.value, null)
state.overrideEnabled.value = true
await vue.nextTick()
assert.equal(days.value, 1)
days.value = 365
reason.value = 'why'
await vue.nextTick()
assert.equal(state.overrideEnabled.value, true)
state.overrideEnabled.value = false
await vue.nextTick()
assert.equal(days.value, null)
assert.equal(reason.value, '')
assert.match(descriptor.template.content, /<ElInputNumber\s+v-if="overrideEnabled"/)
app.unmount()
})
test('first purchase range is shared RFC3339 and rejects invalid or reversed input', () => {
const range = ['2026-10-01T00:00:00+08:00', '2026-10-28T23:59:59+08:00']
assert.deepEqual(helpers.firstPurchaseRange(range), {
first_purchase_start: '2026-09-30T16:00:00Z',
first_purchase_end: '2026-10-28T15:59:59Z'
})
assert.deepEqual(helpers.firstPurchaseRange([]), {})
for (const value of [['bad', 'date'], range.toReversed(), [range[0]]])
assert.throws(() => helpers.firstPurchaseRange(value))
})
test('new order exports keep query.filters as an object, preserving zero/false and legacy serialization', () => {
for (const scene of ['order_platform', 'order_agent', 'order_online', 'order']) {
const filters = {
payment_status: 2,
is_expired: false,
seller_shop_id: 0,
start_time: '2026-10-01T00:00:00+08:00'
}
const h = setup('src/components/business/ExportTaskCreateDialog.vue', {
props: { scene, query: { filters }, modelValue: true, defaultFormat: 'xlsx' }
})
const query = h.state.getNormalizedQuery()
assert.deepEqual(query.filters, scene === 'order' ? JSON.stringify(filters) : filters)
h.dispose()
}
})
test('silence switch uses dedicated endpoints and boolean state, never generic system config', async () => {
const calls = []
class BaseService {}
for (const method of ['get', 'put'])
BaseService[method] = (url, data) => {
calls.push({ method, url, data })
return Promise.resolve({ code: 0 })
}
const { PackageSilenceService: S } = evaluate(read('src/api/modules/packageSilence.ts'), {
'../BaseService': { BaseService }
})
await S.getSwitch()
await S.setSwitch(false)
assert.deepEqual(calls, [
{ method: 'get', url: '/api/admin/package-silence/switch', data: undefined },
{ method: 'put', url: '/api/admin/package-silence/switch', data: { enabled: false } }
])
})
test('silence switch refuses ordinary accounts and exposes failed prechecks from business errors', async () => {
for (const type of [2, 3, 4]) {
const h = setup('src/views/settings/package-silence-switch/index.vue', { type })
await h.state.load()
await h.state.toggle()
assert.equal(h.calls.length, 0)
h.dispose()
}
const failed = [{ code: 'unconfigured', name: 'missing config', passed: false, count: 3 }]
const h = setup('src/views/settings/package-silence-switch/index.vue', {
type: 1,
overrides: {
getSwitch: () => ({ code: 0, data: { enabled: false, precheck: failed } }),
setSwitch: () => ({ code: 1275, msg: 'check failed', data: { failed_items: failed } })
}
})
await h.state.load()
await h.state.toggle()
assert.equal(h.state.data.value.enabled, false)
assert.deepEqual(h.state.failed.value, failed)
assert.equal(h.state.error.value, 'check failed')
await h.state.load()
assert.deepEqual(h.state.failed.value, [])
assert.equal(h.state.error.value, '')
h.dispose()
})
test('all new and changed business templates compile, including conditional root order view', () => {
const paths = [
'src/views/order-management/order-list/index.vue',
'src/views/package-management/package-list/index.vue',
'src/views/settings/package-silence-switch/index.vue',
'src/views/asset-management/asset-information/components/PackageListCard.vue',
'src/views/asset-management/asset-information/components/CurrentPackageCard.vue',
'src/components/business/package-silence/SilenceSummary.vue',
'src/components/business/package-silence/SilencePurchaseFields.vue',
'src/components/business/package-silence/PackageSilenceFields.vue'
]
for (const path of paths) {
const { descriptor, errors } = parse(read(path))
assert.deepEqual(errors, [], path)
const result = compileTemplate({
source: descriptor.template.content,
filename: path,
id: path,
compilerOptions: { expressionPlugins: ['typescript'] }
})
assert.deepEqual(result.errors, [], path)
}
})
test('198 base regressions remain compatible while metadata and zero refunds follow the new contract', () => {
assert.equal(Object.keys(helpers.SILENCE_ERROR_MESSAGES).length, 16)
for (const type of ['override', 'extend', 'extend_batch', 'offline', 'exception', 'policy'])
assert.ok(
Object.values(constants.PACKAGE_SILENCE_PERMISSIONS).includes(`package_silence:${type}`)
)
assert.match(read('src/types/api/wecom.ts'), /package_silence_approval/)
assert.match(read('src/views/settings/wecom/scenes/index.vue'), /沉默期审批/)
assert.match(read('src/views/finance/refund/index.vue'), /requested_refund_amount"\s+:min="0"/)
assert.match(
read('src/views/finance/refund/index.vue'),
/row.requested_refund_amount == null \? undefined : fenToYuan/
)
for (const path of [
'src/views/asset-management/iot-card-management/index.vue',
'src/views/asset-management/device-list/index.vue'
])
assert.ok(read(path).includes('firstPurchaseRange'))
})