refactor(platform): remove safety management module
This commit is contained in:
@@ -73,15 +73,9 @@ export function auditPlatform({ manifest, resources, readOnlyPage, routeSources,
|
||||
}
|
||||
if (!/^[\u4e00-\u9fff]/.test(resource.title) || resource.fields.length === 0 || resource.fields.some((field) => field.key === 'id' || field.key.endsWith('_id')) || resource.fields.some((field) => !/^[\u4e00-\u9fff]/.test(field.label))) failures.push(`${label}: invalid frontend allowlist`);
|
||||
for (const expected of requiredBackendRoutes(contract)) if (!manifest.routes.some((route) => route.method === expected.method && route.path === expected.path)) failures.push(`${label}: missing backend ${expected.method}`);
|
||||
if (contract.mode === 'append_only') {
|
||||
const event = resources.find((item) => item.name === 'safe_event');
|
||||
if (!event?.detailActions?.some((action) => action.name === contract.name && action.resource === contract.path)) failures.push(`${label}: missing safe_event detail action`);
|
||||
if ([...viewSources.keys()].some((file) => file.includes(`/${contract.name}/`))) failures.push(`${label}: independent page exposed`);
|
||||
} else {
|
||||
const coverage = routeCoverage(contract, routeSources, viewSources);
|
||||
if (!coverage.hasPage) failures.push(`${label}: missing page`);
|
||||
if (!coverage.hasMenu) failures.push(`${label}: missing menu route`);
|
||||
}
|
||||
const coverage = routeCoverage(contract, routeSources, viewSources);
|
||||
if (!coverage.hasPage) failures.push(`${label}: missing page`);
|
||||
if (!coverage.hasMenu) failures.push(`${label}: missing menu route`);
|
||||
if (contract.mode === 'readonly') {
|
||||
const statusCall = new RegExp(`resourceApi\\.updateStatus\\(\\s*['\"]${escapeRegExp(contract.path)}['\"]`);
|
||||
for (const [file, source] of [...sourceEntries(routeSources, 'src/router'), ...sourceEntries(apiSources, 'src/api'), ...sourceEntries(viewSources, 'src/views')]) {
|
||||
|
||||
@@ -7,11 +7,8 @@ const resourcesSource = readFileSync('src/api/resources.ts', 'utf8');
|
||||
const legacySafetyPrefix = ['sa', 'f_'].join('');
|
||||
const legacyAuditPrefix = ['au', 'd_'].join('');
|
||||
|
||||
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'/);
|
||||
test('安全模块资源定义已完全删除', () => {
|
||||
assert.doesNotMatch(resourcesSource, /safe_(?:rule|event|inspection|event_disposal)|\/safety\//);
|
||||
|
||||
assert.doesNotMatch(resourcesSource, new RegExp(`define\\('${legacySafetyPrefix}(?:rule|event|inspection|event_disposal)',`));
|
||||
assert.doesNotMatch(resourcesSource, new RegExp(`action\\('${legacySafetyPrefix}event_disposal',`));
|
||||
@@ -76,25 +73,6 @@ test('每个资源必须由带菜单元数据的路由实际加载对应页面',
|
||||
assert.deepEqual(failures, ['gas/gas_basic: missing menu route']);
|
||||
});
|
||||
|
||||
test('仅追加处置必须挂在安全事件详情动作且不得有独立页面', () => {
|
||||
const failures = auditPlatform({
|
||||
manifest: { resources: [{ domain: 'safety', name: 'safe_event_disposal', path: '/safety/safe_event/:identity/disposals', mode: 'append_only', pageKind: 'list' }], routes: [
|
||||
{ method: 'GET', path: '/safety/safe_event/:identity/disposals' },
|
||||
{ method: 'POST', path: '/safety/safe_event/:identity/disposals' },
|
||||
] },
|
||||
resources: [{ name: 'safe_event_disposal', resource: '/safety/safe_event/:identity/disposals', mode: 'append_only', pageKind: 'list', title: '事件处置', fields: [{ key: 'action', label: '处置动作' }] }],
|
||||
readOnlyPage: '',
|
||||
routeSources: [],
|
||||
viewSources: new Map([['src/views/safety/safe_event_disposal/ListPage.vue', '<template />']]),
|
||||
apiSources: new Map(),
|
||||
});
|
||||
|
||||
assert.deepEqual(failures, [
|
||||
'safety/safe_event_disposal: missing safe_event detail action',
|
||||
'safety/safe_event_disposal: independent page exposed',
|
||||
]);
|
||||
});
|
||||
|
||||
test('只读资源拒绝 API 或路由中的状态写入', () => {
|
||||
const failures = auditPlatform({
|
||||
manifest: { resources: [{ domain: 'wallet', name: 'wallet', path: '/wallet/wallet', mode: 'readonly', pageKind: 'list' }], routes: [
|
||||
|
||||
@@ -56,25 +56,11 @@ test('表单载荷构造器省略空的可选关系并转换字段类型', async
|
||||
);
|
||||
});
|
||||
|
||||
test('安全规则与订单明细的文本字段允许任意文本提交', () => {
|
||||
test('订单明细文本字段允许任意文本提交', () => {
|
||||
const resources = loadResources();
|
||||
const { buildResourcePayload } = loadResourceForm();
|
||||
const fields = (name) => resources.find((item) => item.name === name).fields;
|
||||
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(buildResourcePayload(fields('safe_rule'), {
|
||||
rule_code: 'pressure-limit',
|
||||
threshold: '{invalid}',
|
||||
action: 'close-valve',
|
||||
gray_scope: 'north region only',
|
||||
}))),
|
||||
{
|
||||
rule_code: 'pressure-limit',
|
||||
threshold: '{invalid}',
|
||||
action: 'close-valve',
|
||||
gray_scope: 'north region only',
|
||||
},
|
||||
);
|
||||
assert.deepEqual(
|
||||
JSON.parse(JSON.stringify(buildResourcePayload(fields('ec_order_item'), {
|
||||
ec_order_identity: 'order-a',
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import { platformAPI, type DashboardOverview, type OrgDeliveryPoint, type OrgGasStation, type OrgServicePerson, type PlatfromAccount, type Profile, type SafeEvent } from './api/platform';
|
||||
import { platformAPI, type DashboardOverview, type OrgDeliveryPoint, type OrgGasStation, type OrgServicePerson, type PlatfromAccount, type Profile } from './api/platform';
|
||||
|
||||
type Tab = 'dashboard' | 'station' | 'delivery' | 'person' | 'user' | 'safety' | 'trade' | 'finance' | 'content' | 'track' | 'operation' | 'audit';
|
||||
type Tab = 'dashboard' | 'station' | 'delivery' | 'person' | 'user' | 'trade' | 'finance' | 'content' | 'track' | 'operation' | 'audit';
|
||||
type CatalogItem = { title: string; description: string; operations: string[] };
|
||||
|
||||
const currentTab = ref<Tab>('dashboard');
|
||||
@@ -14,14 +14,13 @@ const stations = ref<OrgGasStation[]>([]);
|
||||
const deliveryPoints = ref<OrgDeliveryPoint[]>([]);
|
||||
const servicePeople = ref<OrgServicePerson[]>([]);
|
||||
const users = ref<PlatfromAccount[]>([]);
|
||||
const safetyEvents = ref<SafeEvent[]>([]);
|
||||
const profile = ref<Profile>();
|
||||
const loginForm = reactive({ username: 'root', password: '' });
|
||||
const stationForm = reactive({ stationCode: '', name: '', principal: '', serviceArea: '' });
|
||||
|
||||
const tabs: Array<{ key: Tab; label: string }> = [
|
||||
{ key: 'dashboard', label: '运营总览' }, { key: 'station', label: '气站管理' }, { key: 'delivery', label: '配送点管理' },
|
||||
{ key: 'person', label: '服务人员' }, { key: 'user', label: '用户管理' }, { key: 'safety', label: '安全运营' },
|
||||
{ key: 'person', label: '服务人员' }, { key: 'user', label: '用户管理' },
|
||||
{ key: 'trade', label: '商品交易' }, { key: 'finance', label: '资金结算' }, { key: 'content', label: '内容客服' },
|
||||
{ key: 'track', label: '邀请与轨迹' }, { key: 'operation', label: '全局运营' }, { key: 'audit', label: '审计合规' },
|
||||
];
|
||||
@@ -37,7 +36,7 @@ const catalog: Partial<Record<Tab, CatalogItem[]>> = {
|
||||
|
||||
const overviewCards = computed(() => [
|
||||
['启用气站', overview.value.gasStationCount ?? 0], ['启用配送点', overview.value.deliveryPointCount ?? 0],
|
||||
['在岗服务人员', overview.value.servicePersonCount ?? 0], ['服务用户', overview.value.userCount ?? 0], ['待处理安全事件', overview.value.pendingSafetyCount ?? 0],
|
||||
['在岗服务人员', overview.value.servicePersonCount ?? 0], ['服务用户', overview.value.userCount ?? 0],
|
||||
]);
|
||||
const activeLabel = computed(() => tabs.find((tab) => tab.key === currentTab.value)?.label ?? '平台总后台');
|
||||
|
||||
@@ -46,9 +45,9 @@ async function loadData() {
|
||||
loading.value = true;
|
||||
errorMessage.value = '';
|
||||
try {
|
||||
[profile.value, overview.value, stations.value, deliveryPoints.value, servicePeople.value, users.value, safetyEvents.value] = await Promise.all([
|
||||
[profile.value, overview.value, stations.value, deliveryPoints.value, servicePeople.value, users.value] = await Promise.all([
|
||||
platformAPI.getProfile(), platformAPI.getDashboard(), platformAPI.listOrgGasStation(), platformAPI.listOrgDeliveryPoint(),
|
||||
platformAPI.listOrgServicePerson(), platformAPI.listPlatfromAccount(), platformAPI.listSafeEvent(),
|
||||
platformAPI.listOrgServicePerson(), platformAPI.listPlatfromAccount(),
|
||||
]);
|
||||
} catch (error) {
|
||||
errorMessage.value = error instanceof Error ? error.message : '加载数据失败';
|
||||
@@ -103,7 +102,6 @@ onMounted(loadData);
|
||||
<template v-if="currentTab === 'delivery'"><article class="panel"><h2>配送点管理边界</h2><p>配送点归属气站,负责管理服务人员、用户、配送班次、订单交付与配送轨迹;平台可跨组织查看、审核、冻结和迁移。</p></article><DataTable :headers="['编码', '配送点', '归属气站', '服务区域', '状态']" :rows="deliveryPoints.map((item) => [item.deliveryCode, item.name, item.gasStationName, item.serviceArea, item.status])" /></template>
|
||||
<template v-if="currentTab === 'person'"><article class="panel"><h2>服务人员生命周期</h2><p>覆盖安装维修、安检、配送角色、资质、登录设备、接单状态、任务绩效及停用留档。</p></article><DataTable :headers="['姓名', '手机号', '角色', '工作状态', '资质状态']" :rows="servicePeople.map((item) => [item.name, item.phoneMasked, item.roles, item.workStatus, item.credentialStatus])" /></template>
|
||||
<template v-if="currentTab === 'user'"><article class="panel"><h2>平台账户管理</h2><p>统一查看平台账户资料、角色、头像、手机号、状态与操作记录;敏感信息按最小化原则展示。</p></article><DataTable :headers="['账号', '名称', '头像', '手机号', '角色', '状态']" :rows="users.map((item) => [item.username, item.displayName, item.avatar || '未设置', item.phoneMasked, item.roleCode, item.status])" /></template>
|
||||
<template v-if="currentTab === 'safety'"><article class="panel"><h2>安全运营中心</h2><p>对智能瓶阀告警、安检异常、可燃气体风险、资质临期与订单异常进行分级、派发、升级和复盘。</p></article><DataTable :headers="['事件编码', '等级', '事件说明', '状态', '创建时间']" :rows="safetyEvents.map((item) => [item.eventCode, `${item.level} 级`, item.title, item.status, new Date(item.createdAt).toLocaleString()])" /></template>
|
||||
<template v-if="catalog[currentTab]"><div class="catalog"><article v-for="item in catalog[currentTab]" :key="item.title" class="panel"><h2>{{ item.title }}</h2><p>{{ item.description }}</p><div class="tags"><span v-for="operation in item.operations" :key="operation">{{ operation }}</span></div></article></div></template>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -62,17 +62,8 @@ const labels: Record<string, string> = {
|
||||
effective_at: '生效时间',
|
||||
reported_at: '上报时间',
|
||||
payload: '遥测数据',
|
||||
rule_code: '规则编码',
|
||||
version_no: '版本号',
|
||||
threshold: '阈值',
|
||||
action: '处置动作',
|
||||
gray_scope: '灰度范围',
|
||||
event_code: '事件编码',
|
||||
level: '事件等级',
|
||||
title: '标题',
|
||||
sla_at: '处置时限',
|
||||
result: '检查结果',
|
||||
evidence_uri: '凭证地址',
|
||||
reason: '处置原因',
|
||||
sort_no: '排序号',
|
||||
product_code: '商品编码',
|
||||
@@ -161,7 +152,6 @@ const datetimeFields = new Set([
|
||||
'expired_at',
|
||||
'effective_at',
|
||||
'reported_at',
|
||||
'sla_at',
|
||||
'occurred_at',
|
||||
'started_at',
|
||||
'completed_at',
|
||||
@@ -181,8 +171,6 @@ const textareaFields = new Set([
|
||||
'reason',
|
||||
'opinion',
|
||||
'payload',
|
||||
'threshold',
|
||||
'gray_scope',
|
||||
'product_snapshot',
|
||||
]);
|
||||
const fieldType = (key: string): ResourceFieldType => {
|
||||
@@ -223,10 +211,6 @@ const titles: Record<string, string> = {
|
||||
dev_smart_cylinder_valve: '智能钢瓶阀',
|
||||
dev_device_binding: '设备绑定',
|
||||
dev_telemetry: '设备遥测',
|
||||
safe_rule: '安全规则',
|
||||
safe_event: '安全事件',
|
||||
safe_inspection: '安全检查',
|
||||
safe_event_disposal: '事件处置',
|
||||
ec_category: '商品分类',
|
||||
ec_product: '商品管理',
|
||||
ec_product_attribute: '商品属性',
|
||||
@@ -406,45 +390,6 @@ export const resources: ResourceUiDefinition[] = [
|
||||
'reported_at',
|
||||
'payload',
|
||||
]),
|
||||
define('safe_rule', '/safety/safe_rule', 'writable', 'list', [
|
||||
'rule_code!',
|
||||
'version_no',
|
||||
'threshold',
|
||||
'action',
|
||||
'gray_scope',
|
||||
]),
|
||||
define(
|
||||
'safe_event',
|
||||
'/safety/safe_event',
|
||||
'writable',
|
||||
'list',
|
||||
[
|
||||
'event_code!',
|
||||
'level!',
|
||||
'title!',
|
||||
'smart_cylinder_valve_identity!',
|
||||
'sla_at',
|
||||
],
|
||||
[
|
||||
action('safe_event_disposal', '/safety/safe_event/:identity/disposals', [
|
||||
'action!',
|
||||
'reason!',
|
||||
]),
|
||||
],
|
||||
),
|
||||
define('safe_inspection', '/safety/safe_inspection', 'writable', 'list', [
|
||||
'user_account_identity!',
|
||||
'staff_account_identity!',
|
||||
'result!',
|
||||
'evidence_uri',
|
||||
]),
|
||||
define(
|
||||
'safe_event_disposal',
|
||||
'/safety/safe_event/:identity/disposals',
|
||||
'append_only',
|
||||
'list',
|
||||
['action!', 'reason!'],
|
||||
),
|
||||
define('ec_category', '/ec/ec_category', 'writable', 'tree', [
|
||||
'parent_identity',
|
||||
'name!',
|
||||
|
||||
@@ -5,7 +5,6 @@ import deliveryRoutes from './delivery';
|
||||
import staffRoutes from './staff';
|
||||
import userRoutes from './user';
|
||||
import deviceRoutes from './device';
|
||||
import safetyRoutes from './safety';
|
||||
import ecRoutes from './ec';
|
||||
import financeRoutes from './finance';
|
||||
import contentRoutes from './content';
|
||||
@@ -19,4 +18,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, ...customerServiceRoutes, ...walletRoutes];
|
||||
export default [...platformRoutes, ...gasRoutes, ...deliveryRoutes, ...staffRoutes, ...userRoutes, ...deviceRoutes, ...ecRoutes, ...financeRoutes, ...contentRoutes, ...customerServiceRoutes, ...walletRoutes];
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { DEFAULT_LAYOUT } from '../base';
|
||||
import type { AppRouteRecordRaw } from '../types';
|
||||
const routes: AppRouteRecordRaw[] = [{ path: '/safety', name: 'safety', component: DEFAULT_LAYOUT, meta: { locale: 'menu.platform.safety', requiresAuth: true, icon: 'icon-apps', order: 15 }, children: [
|
||||
{ path: 'safe-rule', name: 'safety-safe-rule', component: () => import('@/views/safety/safe_rule/ListPage.vue'), meta: { locale: 'menu.platform.safety.safe_rule', requiresAuth: true, menuCode: 'safety' } },
|
||||
{ path: 'safe-event', name: 'safety-safe-event', component: () => import('@/views/safety/safe_event/ListPage.vue'), meta: { locale: 'menu.platform.safety.safe_event', requiresAuth: true, menuCode: 'safety' } },
|
||||
{ path: 'safe-inspection', name: 'safety-safe-inspection', component: () => import('@/views/safety/safe_inspection/ListPage.vue'), meta: { locale: 'menu.platform.safety.safe_inspection', requiresAuth: true, menuCode: 'safety' } }
|
||||
] }];
|
||||
export default routes;
|
||||
@@ -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('/safety/safe_event');
|
||||
</script>
|
||||
@@ -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('/safety/safe_inspection');
|
||||
</script>
|
||||
@@ -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('/safety/safe_rule');
|
||||
</script>
|
||||
Reference in New Issue
Block a user