refactor(platform): remove deprecated reporting and audit modules

This commit is contained in:
2026-07-27 15:31:22 +08:00
parent e88734c0fd
commit a98fdd0fca
42 changed files with 86 additions and 674 deletions

View File

@@ -7,19 +7,16 @@ const resourcesSource = readFileSync('src/api/resources.ts', 'utf8');
const legacySafetyPrefix = ['sa', 'f_'].join('');
const legacyAuditPrefix = ['au', 'd_'].join('');
test('资源定义使用 safe 和 audit 前缀', () => {
test('资源定义使用 safe 前缀且不保留已删除审计资源', () => {
assert.match(resourcesSource, /define\(\s*'safe_rule',\s*'\/safety\/safe_rule'/);
assert.match(resourcesSource, /define\(\s*'safe_event',\s*'\/safety\/safe_event'/);
assert.match(resourcesSource, /define\(\s*'safe_inspection',\s*'\/safety\/safe_inspection'/);
assert.match(resourcesSource, /define\(\s*'safe_event_disposal',\s*'\/safety\/safe_event\/:identity\/disposals'/);
assert.match(resourcesSource, /define\(\s*'audit_operation_log',\s*'\/audit\/audit_operation_log'/);
assert.match(resourcesSource, /define\(\s*'audit_export_log',\s*'\/audit\/audit_export_log'/);
assert.match(resourcesSource, /define\(\s*'audit_approval',\s*'\/audit\/audit_approval'/);
assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacySafetyPrefix}(?:rule|event|inspection|event_disposal)',`));
assert.doesNotMatch(resourcesSource, new RegExp(`action\\('${legacySafetyPrefix}event_disposal',`));
assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacyAuditPrefix}(?:operation_log|export_log|approval)',`));
assert.doesNotMatch(resourcesSource, new RegExp(`/audit/${legacyAuditPrefix}approval/:identity/approve`));
assert.doesNotMatch(resourcesSource, /audit_(?:operation_log|export_log|approval)/);
});
test('只读页面将状态变更视为违规写操作', () => {

View File

@@ -28,13 +28,13 @@ function loadResourceForm() {
return resourceModule.exports;
}
test('资源字段声明保留数字、布尔、时间与 JSON 类型', () => {
test('资源字段声明保留数字、布尔、时间与文本类型', () => {
const resources = loadResources();
const field = (resource, key) => resources.find((item) => item.name === resource).fields.find((item) => item.key === key);
assert.equal(field('ec_product', 'price_amount').type, 'number');
assert.equal(field('ec_product_image', 'is_cover').type, 'boolean');
assert.equal(field('delivery_track', 'started_at').type, 'datetime');
assert.equal(field('dev_telemetry', 'payload').type, 'json');
assert.equal(field('dev_telemetry', 'payload').type, 'textarea');
});
test('表单载荷构造器省略空的可选关系并转换字段类型', async () => {
@@ -56,7 +56,7 @@ test('表单载荷构造器省略空的可选关系并转换字段类型', async
);
});
test('安全规则与订单明细将 JSON 字段作为有效 JSON 字符串提交', () => {
test('安全规则与订单明细的文本字段允许任意文本提交', () => {
const resources = loadResources();
const { buildResourcePayload } = loadResourceForm();
const fields = (name) => resources.find((item) => item.name === name).fields;
@@ -64,29 +64,29 @@ test('安全规则与订单明细将 JSON 字段作为有效 JSON 字符串提
assert.deepEqual(
JSON.parse(JSON.stringify(buildResourcePayload(fields('safe_rule'), {
rule_code: 'pressure-limit',
threshold: '{"max":10}',
threshold: '{invalid}',
action: 'close-valve',
gray_scope: '["north"]',
gray_scope: 'north region only',
}))),
{
rule_code: 'pressure-limit',
threshold: '{"max":10}',
threshold: '{invalid}',
action: 'close-valve',
gray_scope: '["north"]',
gray_scope: 'north region only',
},
);
assert.deepEqual(
JSON.parse(JSON.stringify(buildResourcePayload(fields('ec_order_item'), {
ec_order_identity: 'order-a',
ec_product_identity: 'product-a',
product_snapshot: '{"name":"液化气"}',
product_snapshot: 'arbitrary product snapshot text',
quantity: 2,
sale_amount: 500,
}))),
{
ec_order_identity: 'order-a',
ec_product_identity: 'product-a',
product_snapshot: '{"name":"液化气"}',
product_snapshot: 'arbitrary product snapshot text',
quantity: 2,
sale_amount: 500,
},
@@ -131,11 +131,11 @@ test('日期按 RFC3339 提交,密码只在创建时必填并提交', () => {
);
});
test('审批只读页提供同意和驳回操作', () => {
test('已删除的审批模块不再保留前端资源或页面', () => {
const source = fs.readFileSync(fromProjectRoot('src/views/shared/ReadOnlyListPage.vue'), 'utf8');
const resources = fs.readFileSync(fromProjectRoot('src/api/resources.ts'), 'utf8');
assert.match(resources, /audit_approval[\s\S]*\/audit\/audit_approval\/:identity\/approve/);
assert.match(source, /submitDetailAction/);
assert.doesNotMatch(resources, /audit_approval|audit_operation_log|audit_export_log/);
assert.equal(fs.existsSync(fromProjectRoot('src/views/audit')), false);
});
test('树页面通过资源归档接口归档节点', () => {

View File

@@ -43,12 +43,6 @@ export function buildResourcePayload(
case 'boolean':
payload[field.key] = value === true || value === 'true';
break;
case 'json':
if (typeof value !== 'string')
throw new Error(`${field.label}必须是有效 JSON`);
JSON.parse(value);
payload[field.key] = value;
break;
case 'date':
case 'datetime': {
const date = new Date(String(value));

View File

@@ -10,7 +10,6 @@ export type ResourceFieldType =
| 'boolean'
| 'date'
| 'datetime'
| 'json'
| 'textarea';
export type ResourceField = {
key: string;
@@ -175,14 +174,17 @@ const datetimeFields = new Set([
'handled_at',
'created_at',
]);
const jsonFields = new Set([
// 大文本字段按普通文本处理,不再要求或解析 JSON。
const textareaFields = new Set([
'body',
'content',
'reason',
'opinion',
'payload',
'threshold',
'gray_scope',
'product_snapshot',
'field_scope',
]);
const textareaFields = new Set(['body', 'content', 'reason', 'opinion']);
const fieldType = (key: string): ResourceFieldType => {
if (key === 'platform_role_code') return 'role-code';
if (key === 'menu_identities') return 'menu-identities';
@@ -192,7 +194,6 @@ const fieldType = (key: string): ResourceFieldType => {
if (booleanFields.has(key)) return 'boolean';
if (dateFields.has(key)) return 'date';
if (datetimeFields.has(key)) return 'datetime';
if (jsonFields.has(key)) return 'json';
if (textareaFields.has(key)) return 'textarea';
return 'text';
};
@@ -237,8 +238,7 @@ const titles: Record<string, string> = {
fin_payment: '支付记录',
fin_settlement: '财务结算',
fin_reconciliation: '财务对账',
cnt_content: '内容管理',
ntf_template: '通知模板',
cms_content: '内容管理',
cs_ticket: '客服工单',
platfrom_account: '平台账户',
platform_role: '平台角色',
@@ -247,12 +247,6 @@ const titles: Record<string, string> = {
wallet_ledger: '钱包流水',
wallet_recharge: '钱包充值',
wallet_withdrawal: '钱包提现',
report: '报表',
report_item: '报表项目',
report_metric_snapshot: '指标快照',
audit_operation_log: '操作审计',
audit_export_log: '导出审计',
audit_approval: '审批审计',
};
const define = (
name: string,
@@ -523,18 +517,13 @@ export const resources: ResourceUiDefinition[] = [
'list',
['channel!', 'bill_date!', 'difference_amount!'],
),
define('cnt_content', '/content/cnt_content', 'writable', 'list', [
define('cms_content', '/content/cms_content', 'writable', 'list', [
'content_type!',
'title!',
'body!',
'version_no',
'publish_status',
]),
define('ntf_template', '/notification/ntf_template', 'writable', 'list', [
'template_code!',
'channel!',
'content!',
]),
define('cs_ticket', '/customer_service/cs_ticket', 'writable', 'list', [
'user_account_identity!',
'ticket_no!',
@@ -592,62 +581,6 @@ export const resources: ResourceUiDefinition[] = [
'amount',
'status',
]),
define('report', '/report/report', 'readonly', 'list', [
'report_code',
'report_type',
'stat_period',
'generated_at',
'status',
]),
define('report_item', '/report/report_item', 'readonly', 'list', [
'report_identity',
'dimension',
'metric_code',
'metric_value',
]),
define(
'report_metric_snapshot',
'/report/report_metric_snapshot',
'readonly',
'list',
['metric_code', 'scope_type', 'stat_at', 'metric_value'],
),
define('audit_operation_log', '/audit/audit_operation_log', 'readonly', 'list', [
'operator_identity',
'action',
'object_identity',
'created_at',
]),
define('audit_export_log', '/audit/audit_export_log', 'readonly', 'list', [
'applicant_identity',
'purpose',
'field_scope',
'approved_at',
'file_uri',
]),
define(
'audit_approval',
'/audit/audit_approval',
'readonly',
'list',
[
'business_type',
'business_identity',
'applicant_identity',
'opinion',
'handler_identity',
'status',
'handled_at',
],
[
action('同意', '/audit/audit_approval/:identity/approve', ['opinion'], {
status: 'approved',
}),
action('驳回', '/audit/audit_approval/:identity/approve', ['opinion!'], {
status: 'rejected',
}),
],
),
];
export const resourceByName = Object.fromEntries(

View File

@@ -1,12 +0,0 @@
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const routes: AppRouteRecordRaw[] = [{
path: '/audit', name: 'audit', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.audit', requiresAuth: true, icon: 'icon-apps', order: 22 },
children: [
{ path: 'audit-operation-log', name: 'audit-audit-operation-log', component: () => import('@/views/audit/audit_operation_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.audit_operation_log', requiresAuth: true, menuCode: 'audit' } },
{ path: 'audit-export-log', name: 'audit-audit-export-log', component: () => import('@/views/audit/audit_export_log/ListPage.vue'), meta: { locale: 'menu.platform.audit.audit_export_log', requiresAuth: true, menuCode: 'audit' } },
{ path: 'audit-approval', name: 'audit-audit-approval', component: () => import('@/views/audit/audit_approval/ListPage.vue'), meta: { locale: 'menu.platform.audit.audit_approval', requiresAuth: true, menuCode: 'audit' } }
],
}];
export default routes;

View File

@@ -4,7 +4,7 @@ const routes: AppRouteRecordRaw[] = [{
path: '/content', name: 'content', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.content', requiresAuth: true, icon: 'icon-apps', order: 17 },
children: [
{ path: 'cnt-content', name: 'content-cnt-content', component: () => import('@/views/content/cnt_content/ListPage.vue'), meta: { locale: 'menu.platform.content.cnt_content', requiresAuth: true, menuCode: 'content' } }
{ path: 'cms-content', name: 'content-cms-content', component: () => import('@/views/content/cms_content/ListPage.vue'), meta: { locale: 'menu.platform.content.cms_content', requiresAuth: true, menuCode: 'content' } }
],
}];
export default routes;

View File

@@ -1,10 +0,0 @@
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const routes: AppRouteRecordRaw[] = [{
path: '/notification', name: 'notification', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.notification', requiresAuth: true, icon: 'icon-apps', order: 18 },
children: [
{ path: 'ntf-template', name: 'notification-ntf-template', component: () => import('@/views/notification/ntf_template/ListPage.vue'), meta: { locale: 'menu.platform.notification.ntf_template', requiresAuth: true, menuCode: 'notification' } }
],
}];
export default routes;

View File

@@ -9,11 +9,8 @@ import safetyRoutes from './safety';
import ecRoutes from './ec';
import financeRoutes from './finance';
import contentRoutes from './content';
import notificationRoutes from './notification';
import customerServiceRoutes from './customer_service';
import walletRoutes from './wallet';
import reportRoutes from './report';
import auditRoutes from './audit';
const platformRoutes: AppRouteRecordRaw[] = [
{ path: '/dashboard', name: 'dashboard', component: DEFAULT_LAYOUT, meta: { locale: 'menu.dashboard', requiresAuth: true, icon: 'icon-dashboard', order: 0 }, children: [{ path: 'overview', name: 'DashboardOverview', component: () => import('@/views/dashboard/DashboardPage.vue'), meta: { locale: 'menu.platform.dashboard', requiresAuth: true, menuCode: 'dashboard' } }] },
{ path: '/platform', name: 'platform', component: DEFAULT_LAYOUT, meta: { locale: 'menu.platform.config', requiresAuth: true, icon: 'icon-settings', order: 30 }, children: [
@@ -22,4 +19,4 @@ const platformRoutes: AppRouteRecordRaw[] = [
{ path: 'platform-menu', name: 'platform-platform-menu', component: () => import('@/views/platform/platform_menu/TreePage.vue'), meta: { locale: 'menu.platform.platform.platform_menu', requiresAuth: true, menuCode: 'platform' } },
] },
];
export default [...platformRoutes, ...gasRoutes, ...deliveryRoutes, ...staffRoutes, ...userRoutes, ...deviceRoutes, ...safetyRoutes, ...ecRoutes, ...financeRoutes, ...contentRoutes, ...notificationRoutes, ...customerServiceRoutes, ...walletRoutes, ...reportRoutes, ...auditRoutes];
export default [...platformRoutes, ...gasRoutes, ...deliveryRoutes, ...staffRoutes, ...userRoutes, ...deviceRoutes, ...safetyRoutes, ...ecRoutes, ...financeRoutes, ...contentRoutes, ...customerServiceRoutes, ...walletRoutes];

View File

@@ -1,12 +0,0 @@
import { DEFAULT_LAYOUT } from '../base';
import type { AppRouteRecordRaw } from '../types';
const routes: AppRouteRecordRaw[] = [{
path: '/report', name: 'report', component: DEFAULT_LAYOUT,
meta: { locale: 'menu.platform.report', requiresAuth: true, icon: 'icon-apps', order: 21 },
children: [
{ path: 'report', name: 'report-report', component: () => import('@/views/report/report/ListPage.vue'), meta: { locale: 'menu.platform.report.report', requiresAuth: true, menuCode: 'report' } },
{ path: 'report-item', name: 'report-report-item', component: () => import('@/views/report/report_item/ListPage.vue'), meta: { locale: 'menu.platform.report.report_item', requiresAuth: true, menuCode: 'report' } },
{ path: 'report-metric-snapshot', name: 'report-report-metric-snapshot', component: () => import('@/views/report/report_metric_snapshot/ListPage.vue'), meta: { locale: 'menu.platform.report.report_metric_snapshot', requiresAuth: true, menuCode: 'report' } }
],
}];
export default routes;

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/audit/audit_approval');
</script>

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/audit/audit_export_log');
</script>

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/audit/audit_operation_log');
</script>

View File

@@ -2,5 +2,5 @@
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/content/cnt_content');
const definition = getResource('/content/cms_content');
</script>

View File

@@ -1,6 +0,0 @@
<template><CrudListPage :definition="definition" /></template>
<script setup lang="ts">
import CrudListPage from '@/views/shared/CrudListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/notification/ntf_template');
</script>

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/report/report');
</script>

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/report/report_item');
</script>

View File

@@ -1,6 +0,0 @@
<template><ReadOnlyListPage :definition="definition" /></template>
<script setup lang="ts">
import ReadOnlyListPage from '@/views/shared/ReadOnlyListPage.vue';
import { getResource } from '@/api/resources';
const definition = getResource('/report/report_metric_snapshot');
</script>

View File

@@ -37,7 +37,7 @@
<a-input-number v-else-if="field.type === 'number'" v-model="form[field.key]" />
<a-date-picker v-else-if="field.type === 'date'" v-model="form[field.key]" value-format="YYYY-MM-DD" />
<a-date-picker v-else-if="field.type === 'datetime'" v-model="form[field.key]" show-time value-format="YYYY-MM-DDTHH:mm:ssZ" />
<a-textarea v-else-if="field.type === 'json' || field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
<a-textarea v-else-if="field.type === 'textarea'" v-model="form[field.key]" :auto-size="{ minRows: 3, maxRows: 8 }" />
<a-input-password v-else-if="field.type === 'password'" v-model="form[field.key]" />
<a-select v-else-if="field.type === 'role-code'" v-model="form[field.key]">
<a-option v-for="role in roleOptions" :key="role.role_code" :value="role.role_code">{{ role.name }}</a-option>
@@ -61,7 +61,7 @@
<a-form-item v-for="field in activeAction?.fields" :key="field.key" :label="field.label" :required="field.required">
<a-input-number v-if="field.type === 'number'" v-model="actionForm[field.key]" />
<a-switch v-else-if="field.type === 'boolean'" v-model="actionForm[field.key]" />
<a-textarea v-else-if="field.type === 'json' || field.type === 'textarea'" v-model="actionForm[field.key]" />
<a-textarea v-else-if="field.type === 'textarea'" v-model="actionForm[field.key]" />
<a-select v-else-if="field.type === 'menu-identities'" v-model="actionForm[field.key]" multiple allow-search>
<a-option v-for="menu in menuOptions" :key="menu.identity" :value="menu.identity">{{ menu.name }}</a-option>
</a-select>
@@ -124,12 +124,7 @@ const detailEntries = computed(() =>
function resetForm(data?: Row) {
for (const field of props.definition.fields) {
const value = data?.[field.key];
form[field.key] =
field.type === 'json' && value != null && typeof value !== 'string'
? JSON.stringify(value, null, 2)
: value == null
? undefined
: value;
form[field.key] = value == null ? undefined : value;
}
}

View File

@@ -23,7 +23,7 @@
<a-modal :visible="actionVisible" :title="activeAction?.name" @cancel="actionVisible = false" @ok="submitDetailAction">
<a-form :model="actionForm" layout="vertical">
<a-form-item v-for="field in activeAction?.fields" :key="field.key" :label="field.label" :required="field.required">
<a-textarea v-if="field.type === 'textarea' || field.type === 'json'" v-model="actionForm[field.key]" />
<a-textarea v-if="field.type === 'textarea'" v-model="actionForm[field.key]" />
<a-input v-else v-model="actionForm[field.key]" />
</a-form-item>
</a-form>