From 6dedd9f85226656af37d514b40d0df8b5039d0a4 Mon Sep 17 00:00:00 2001 From: luo Date: Tue, 20 Oct 2026 17:21:48 +0800 Subject: [PATCH] fix:some --- scripts/tests/carrierRealnameControl.test.mjs | 245 ++++++++++++++++++ src/config/constants/carrierTypes.ts | 10 +- src/types/api/carrier.ts | 4 + .../system/carrier-management/detail.vue | 10 +- src/views/system/carrier-management/index.vue | 46 +++- 5 files changed, 310 insertions(+), 5 deletions(-) create mode 100644 scripts/tests/carrierRealnameControl.test.mjs diff --git a/scripts/tests/carrierRealnameControl.test.mjs b/scripts/tests/carrierRealnameControl.test.mjs new file mode 100644 index 0000000..7f3c296 --- /dev/null +++ b/scripts/tests/carrierRealnameControl.test.mjs @@ -0,0 +1,245 @@ +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 { compileScript, parse } from '@vue/compiler-sfc' + +function evaluate(source, imports = {}, globals = {}) { + const compiled = ts.transpileModule(source, { + compilerOptions: { target: ts.ScriptTarget.ES2022, module: ts.ModuleKind.CommonJS } + }).outputText + const module = { exports: {} } + new Function('require', 'module', 'exports', ...Object.keys(globals), compiled)( + (name) => { + assert.ok(imports[name], `Unexpected dependency: ${name}`) + return imports[name] + }, + module, + module.exports, + ...Object.values(globals) + ) + return module.exports +} + +const read = (path) => readFileSync(path, 'utf8') +const carrierTypes = evaluate(read('src/types/api/carrier.ts')) +const carrierConfig = evaluate(read('src/config/constants/carrierTypes.ts'), { + '@/types/api': carrierTypes +}) +const checkedColumns = evaluate(read('src/composables/useCheckedColumns.ts'), { vue }) +const components = Object.fromEntries( + ['index', 'detail'].map((name) => { + const { descriptor } = parse(read(`src/views/system/carrier-management/${name}.vue`)) + return [ + name, + { descriptor, script: compileScript(descriptor, { id: `carrier-${name}` }).content } + ] + }) +) + +const carrier = (enabled) => ({ + id: 7, + carrier_code: 'carrier-test', + carrier_name: '测试运营商', + carrier_type: 'CMCC', + realname_control_enabled: enabled, + status: 1, + created_at: '', + updated_at: '' +}) + +function harness(userType, responseCarrier = carrier(1)) { + const writes = [] + const user = vue.reactive({ info: { user_type: userType }, permissions: [], buttons: [] }) + const permission = evaluate(read('src/composables/usePermission.ts'), { + vue, + '@/store/modules/user': { useUserStore: () => user } + }) + const service = { + getCarriers: async () => ({ code: 0, data: { items: [responseCarrier], total: 1 } }), + getCarrierDetail: async () => ({ code: 0, data: responseCarrier }), + createCarrier: async (data) => writes.push({ method: 'create', data: { ...data } }), + updateCarrier: async (id, data) => writes.push({ method: 'update', id, data: { ...data } }) + } + const runtimeVue = { ...vue, onMounted: () => {} } + const imports = { + vue: runtimeVue, + 'vue-router': { useRouter: () => ({ push() {} }), useRoute: () => ({ params: { id: '7' } }) }, + '@vueuse/core': { useWindowSize: () => ({ width: vue.ref(1200) }) }, + '@/api/modules': { CarrierService: service }, + 'element-plus': { + ElMessage: { success() {}, warning() {} }, + ElMessageBox: {}, + ElTag: {}, + ElSwitch: {}, + ElButton: {} + }, + '@/composables/useCheckedColumns': checkedColumns, + '@/composables/useAuth': { useAuth: () => ({ hasAuth: () => true }) }, + '@/composables/usePermission': permission, + '@/router/routesAlias': { + RoutesAlias: { CarrierDetail: '/carrier', CarrierManagement: '/carrier' } + }, + '@/utils/codeGenerator': { generateCarrierCode: () => 'carrier-test' }, + '@/utils/business/format': { formatDateTime: (value) => value }, + '@/config/constants': { + ...carrierConfig, + CommonStatus: { ENABLED: 1, DISABLED: 0 }, + getStatusText: (value) => String(value) + } + } + const setup = (name) => + evaluate(components[name].script, imports, runtimeVue).default.setup({}, { expose() {} }) + return { list: setup('index'), detail: setup('detail'), writes } +} + +const validForm = { + validate: async (callback) => callback(true), + resetFields() {}, + clearValidate() {} +} + +for (const userType of [1, 2, '1', '2', 3, 4, 0, undefined]) { + const allowed = [1, 2].includes(Number(userType)) + test(`user_type=${userType}: list visibility, detail visibility and create/update field access`, async () => { + const { list, detail, writes } = harness(userType) + assert.equal(list.canConfigureRealnameControl.value, allowed) + assert.equal(detail.canConfigureRealnameControl.value, allowed) + assert.equal( + list.columnOptions.some((column) => column.prop === 'realname_control_enabled'), + allowed + ) + assert.equal( + list.columns.value.some((column) => column.prop === 'realname_control_enabled'), + allowed + ) + assert.equal( + list.columnChecks.value.some((column) => column.prop === 'realname_control_enabled'), + allowed + ) + + await list.getTableData() + await detail.loadCarrier() + assert.equal(list.carrierList.value[0].realname_control_enabled, 1) + assert.equal(detail.carrier.value.realname_control_enabled, 1) + + list.showDialog('add') + assert.equal(list.form.realname_control_enabled, 0) + list.form.realname_control_enabled = 1 + await list.handleSubmit(validForm) + list.showDialog('edit', carrier(1)) + assert.equal(list.form.realname_control_enabled, 1) + list.form.realname_control_enabled = 0 + await list.handleSubmit(validForm) + + assert.equal(writes.length, 2) + assert.equal(writes[0].method, 'create') + assert.equal(writes[1].method, 'update') + assert.equal(writes[1].id, 7) + for (const [index, write] of writes.entries()) { + assert.equal(Object.hasOwn(write.data, 'realname_control_enabled'), allowed) + if (allowed) assert.equal(write.data.realname_control_enabled, index === 0 ? 1 : 0) + } + }) +} + +test('labels preserve numeric zero and handle missing fields without inventing a state', () => { + const { getCarrierRealnameControlLabel: label } = carrierConfig + assert.equal(label(0), '不管控') + assert.equal(label(1), '管控') + assert.equal(label(undefined), '-') + assert.equal(label(null), '-') + const { list } = harness(1) + const column = list.columns.value.find((item) => item.prop === 'realname_control_enabled') + assert.equal(column.formatter(carrier(0)), '不管控') + assert.equal(column.formatter(carrier(1)), '管控') + assert.equal(column.formatter(carrier(undefined)), '-') +}) + +test('editing then creating resets control state; default creation sends numeric zero', async () => { + const { list, writes } = harness(2) + list.showDialog('edit', carrier(1)) + list.showDialog('add') + assert.equal(list.form.realname_control_enabled, 0) + await list.handleSubmit(validForm) + assert.equal(writes[0].data.realname_control_enabled, 0) + list.showDialog('edit', carrier(0)) + assert.equal(list.form.realname_control_enabled, 0) + list.showDialog('edit', carrier(undefined)) + assert.equal(list.form.realname_control_enabled, 0) +}) + +function findElement(node, predicate) { + if (node.type === 1 && predicate(node)) return node + for (const child of node.children || []) { + const match = findElement(child, predicate) + if (match) return match + } +} + +test('form and detail guard the field, and the switch uses numeric 1/0', () => { + for (const [name, tag] of [ + ['index', 'ElFormItem'], + ['detail', 'ElDescriptionsItem'] + ]) { + const field = findElement( + components[name].descriptor.template.ast, + (node) => + node.tag === tag && + node.props.some((prop) => prop.name === 'label' && prop.value?.content === '通道实名管控') + ) + assert.ok(field) + assert.equal( + field.props.find((prop) => prop.type === 7 && prop.name === 'if')?.exp?.content, + 'canConfigureRealnameControl' + ) + if (name === 'index') { + const toggle = findElement(field, (node) => node.tag === 'ElSwitch') + for (const [arg, value] of [ + ['active-value', '1'], + ['inactive-value', '0'] + ]) { + assert.equal( + toggle.props.find((prop) => prop.type === 7 && prop.arg?.content === arg)?.exp?.content, + value + ) + } + } + } +}) + +test('carrier endpoints preserve the field in list/detail responses and create/update requests', async () => { + const calls = [] + class BaseService { + static getPage(url) { + calls.push({ method: 'GET', url }) + return Promise.resolve({ code: 0, data: { items: [carrier(1)] } }) + } + static getOne(url) { + calls.push({ method: 'GET', url }) + return Promise.resolve({ code: 0, data: carrier(0) }) + } + static create(url, data) { + calls.push({ method: 'POST', url, data }) + return Promise.resolve({ code: 0, data }) + } + static update(url, data) { + calls.push({ method: 'PUT', url, data }) + return Promise.resolve({ code: 0, data }) + } + } + const { CarrierService } = evaluate(read('src/api/modules/carrier.ts'), { + '../BaseService': { BaseService } + }) + assert.equal((await CarrierService.getCarriers()).data.items[0].realname_control_enabled, 1) + assert.equal((await CarrierService.getCarrierDetail(7)).data.realname_control_enabled, 0) + await CarrierService.createCarrier({ ...carrier(1) }) + await CarrierService.updateCarrier(7, { realname_control_enabled: 0 }) + assert.deepEqual(calls, [ + { method: 'GET', url: '/api/admin/carriers' }, + { method: 'GET', url: '/api/admin/carriers/7' }, + { method: 'POST', url: '/api/admin/carriers', data: carrier(1) }, + { method: 'PUT', url: '/api/admin/carriers/7', data: { realname_control_enabled: 0 } } + ]) +}) diff --git a/src/config/constants/carrierTypes.ts b/src/config/constants/carrierTypes.ts index 4460fde..45dda4a 100644 --- a/src/config/constants/carrierTypes.ts +++ b/src/config/constants/carrierTypes.ts @@ -2,7 +2,7 @@ * 运营商类型配置 */ -import { CarrierType } from '@/types/api' +import { CarrierType, type CarrierRealnameControlEnabled } from '@/types/api' // 运营商类型选项 export const CARRIER_TYPE_OPTIONS = [ @@ -30,3 +30,11 @@ export function getCarrierTypeLabel(type: CarrierType): string { export function getCarrierTypeColor(type: CarrierType): string { return CARRIER_TYPE_MAP[type]?.color || '#666' } + +export function getCarrierRealnameControlLabel( + enabled?: CarrierRealnameControlEnabled | null +): string { + if (enabled === 1) return '管控' + if (enabled === 0) return '不管控' + return '-' +} diff --git a/src/types/api/carrier.ts b/src/types/api/carrier.ts index 7cb208f..a278c2f 100644 --- a/src/types/api/carrier.ts +++ b/src/types/api/carrier.ts @@ -14,6 +14,7 @@ export enum CarrierType { export type CarrierTrafficThresholdEnabled = 0 | 1 export type CarrierTrafficThresholdUnit = 'MB' | 'GB' +export type CarrierRealnameControlEnabled = 0 | 1 // 运营商响应数据 export interface Carrier { @@ -23,6 +24,7 @@ export interface Carrier { carrier_type: CarrierType description?: string | null data_reset_day?: number | null // 上游流量重置日(1-28) + realname_control_enabled?: CarrierRealnameControlEnabled // 通道实名管控,仅超级管理员与平台账号可见 realname_link_type?: string | null // 实名链接类型 realname_link_template?: string | null // 实名链接模板 traffic_threshold_enabled?: CarrierTrafficThresholdEnabled | null // 通道流量阈值开关 @@ -47,6 +49,7 @@ export interface CreateCarrierParams { carrier_type: CarrierType description?: string | null data_reset_day?: number | null // 上游流量重置日(1-28) + realname_control_enabled?: CarrierRealnameControlEnabled // 通道实名管控 (0:不管控, 1:管控) realname_link_type?: string | null // 实名链接类型 realname_link_template?: string | null // 实名链接模板 traffic_threshold_enabled?: CarrierTrafficThresholdEnabled | null // 通道流量阈值开关 @@ -59,6 +62,7 @@ export interface UpdateCarrierParams { carrier_name?: string description?: string | null data_reset_day?: number | null // 上游流量重置日(1-28) + realname_control_enabled?: CarrierRealnameControlEnabled // 通道实名管控 (0:不管控, 1:管控) realname_link_type?: string | null // 实名链接类型 realname_link_template?: string | null // 实名链接模板 traffic_threshold_enabled?: CarrierTrafficThresholdEnabled | null // 通道流量阈值开关 diff --git a/src/views/system/carrier-management/detail.vue b/src/views/system/carrier-management/detail.vue index 5a04e5d..c3ef5e9 100644 --- a/src/views/system/carrier-management/detail.vue +++ b/src/views/system/carrier-management/detail.vue @@ -34,6 +34,9 @@ {{ carrier.data_reset_day ?? '未配置' }} + + {{ getCarrierRealnameControlLabel(carrier.realname_control_enabled) }} + {{ carrier.realname_link_type || '未配置' }} @@ -71,7 +74,11 @@ import { usePermission } from '@/composables/usePermission' import { RoutesAlias } from '@/router/routesAlias' import { formatDateTime } from '@/utils/business/format' - import { getStatusText, getCarrierTypeLabel } from '@/config/constants' + import { + getStatusText, + getCarrierTypeLabel, + getCarrierRealnameControlLabel + } from '@/config/constants' import type { Carrier } from '@/types/api' defineOptions({ name: 'CarrierDetail' }) @@ -80,6 +87,7 @@ const router = useRouter() const { isPlatformAccount } = usePermission() const canConfigureTrafficThreshold = computed(() => isPlatformAccount.value) + const canConfigureRealnameControl = computed(() => isPlatformAccount.value) const { width } = useWindowSize() const descriptionColumns = computed(() => (width.value < 768 ? 1 : 2)) const loading = ref(false) diff --git a/src/views/system/carrier-management/index.vue b/src/views/system/carrier-management/index.vue index 3ce6f07..63a1d2b 100644 --- a/src/views/system/carrier-management/index.vue +++ b/src/views/system/carrier-management/index.vue @@ -144,6 +144,20 @@ /> + + + isPlatformAccount.value) + const canConfigureRealnameControl = computed(() => isPlatformAccount.value) const canUpdateCarrierStatus = hasAuth('carrier:update_status') const dialogVisible = ref(false) const loading = ref(false) @@ -284,6 +300,9 @@ { label: '运营商名称', prop: 'carrier_name' }, { label: '运营商类型', prop: 'carrier_type' }, { label: '运营商描述', prop: 'description' }, + ...(canConfigureRealnameControl.value + ? [{ label: '通道实名管控', prop: 'realname_control_enabled' }] + : []), ...(canConfigureTrafficThreshold.value ? [ { label: '通道阈值状态', prop: 'traffic_threshold_enabled' }, @@ -352,6 +371,7 @@ carrier_type: null, description: '', data_reset_day: 1, + realname_control_enabled: 0, traffic_threshold_mode: 'unconfigured', traffic_threshold_unit: null, traffic_threshold_value: null, @@ -405,6 +425,17 @@ minWidth: 200, showOverflowTooltip: true }, + ...(canConfigureRealnameControl.value + ? [ + { + prop: 'realname_control_enabled', + label: '通道实名管控', + width: 140, + formatter: (row: Carrier) => + getCarrierRealnameControlLabel(row.realname_control_enabled) + } + ] + : []), ...(canConfigureTrafficThreshold.value ? [ { @@ -574,6 +605,7 @@ form.carrier_type = row.carrier_type form.description = row.description form.data_reset_day = row.data_reset_day || 1 + form.realname_control_enabled = row.realname_control_enabled ?? 0 form.traffic_threshold_mode = getThresholdMode(row.traffic_threshold_enabled) form.traffic_threshold_unit = row.traffic_threshold_unit ?? null form.traffic_threshold_value = row.traffic_threshold_value ?? null @@ -586,6 +618,7 @@ form.carrier_type = null form.description = '' form.data_reset_day = 1 + form.realname_control_enabled = 0 form.traffic_threshold_mode = 'unconfigured' form.traffic_threshold_unit = null form.traffic_threshold_value = null @@ -621,6 +654,11 @@ const formatThresholdValue = (value?: number | null) => value === null || value === undefined ? '未配置' : String(value) + const getRealnameControlPayload = (): Pick => + canConfigureRealnameControl.value + ? { realname_control_enabled: form.realname_control_enabled } + : {} + const getTrafficThresholdPayload = () => { if (!canConfigureTrafficThreshold.value) return {} if (form.traffic_threshold_mode === 'unconfigured') { @@ -701,7 +739,8 @@ realname_link_type: form.realname_link_type, realname_link_template: form.realname_link_type === 'template' ? form.realname_link_template : undefined, - ...getTrafficThresholdPayload() + ...getTrafficThresholdPayload(), + ...getRealnameControlPayload() } if (dialogType.value === 'add') { @@ -715,7 +754,8 @@ realname_link_type: form.realname_link_type, realname_link_template: form.realname_link_type === 'template' ? form.realname_link_template : undefined, - ...getTrafficThresholdPayload() + ...getTrafficThresholdPayload(), + ...getRealnameControlPayload() } await CarrierService.updateCarrier(form.id, updateData) ElMessage.success('修改成功')