fix platform authorization and workflow integrity

This commit is contained in:
david
2026-07-29 15:54:20 +08:00
parent a7d318a013
commit 969d7271f9
26 changed files with 566 additions and 161 deletions

View File

@@ -33,6 +33,7 @@ export type DetailAction = {
method?: 'POST' | 'PUT' | 'PATCH';
danger?: boolean;
fields?: ResourceField[];
visibleFor?: { field: string; values: Array<string | number> };
};
export type ResourceUiDefinition = {
@@ -303,24 +304,24 @@ export const resources: ResourceUiDefinition[] = [
define('product_owner', '智能气阀归属记录', 'readonly', []),
define('gasorder_contract', '配送合同', 'managed', [f('contract_no', { required: true }), relation('user_account_identity', '/user_account', true), relation('gas_basic_identity', '/gas_basic', true), relation('delivery_basic_identity', '/delivery_basic'), f('title', { required: true }), f('terms'), f('file_uri'), f('default_delivery_fee'), f('signed_at', { required: true }), f('effective_at', { required: true }), f('expired_at')], 'list', [
{ name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason },
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason] },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason },
{ name: '启用合同', resource: '/gasorder_contract/:identity/activate', fields: reason, visibleFor: { field: 'contract_status', values: [10] } },
{ name: '续签合同', resource: '/gasorder_contract/:identity/renew', fields: [f('effective_at', { required: true }), f('expired_at'), ...reason], visibleFor: { field: 'contract_status', values: [11, 12] } },
{ name: '终止合同', resource: '/gasorder_contract/:identity/terminate', danger: true, fields: reason, visibleFor: { field: 'contract_status', values: [11] } },
], { canCreate: true, canEdit: true }),
define('gasorder_contract_product', '合同气瓶', 'append_only', [relation('gasorder_contract_identity', '/gasorder_contract', true), relation('product_info_identity', '/product_info', true), f('unit_price')], 'list', [
{ name: '解绑气瓶', resource: '/gasorder_contract_product/:identity/unbind', danger: true, fields: reason },
]),
define('gasorder_contract_revision', '合同修订记录', 'readonly', []),
define('gasorder_basic', '气体配送订单', 'append_only', [f('request_no', { required: true }), relation('gasorder_contract_identity', '/gasorder_contract', true), f('creator_type', { required: true, type: 'select', options: [{ label: '用户', value: 'user' }, { label: '工作人员', value: 'staff' }, { label: '配送站', value: 'delivery' }, { label: '气站', value: 'gas' }] }), f('creator_identity', { required: true }), relation('user_address_identity', '/user_address', true), f('gasorder_contract_product_identities', { required: true, type: 'identity-list', relation: '/gasorder_contract_product' }), f('contact_name', { required: true }), f('contact_phone', { required: true }), f('discount_amount'), f('remark')], 'list', [
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true), relation('staff_account_identity', '/staff_account', true), ...reason] },
{ name: '开始罐装', resource: '/gasorder_basic/:identity/filling', fields: reason },
{ name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason },
{ name: '开始配送', resource: '/gasorder_basic/:identity/delivering', fields: reason },
{ name: '等待签收', resource: '/gasorder_basic/:identity/awaiting-confirmation', fields: reason },
{ name: '完成订单', resource: '/gasorder_basic/:identity/complete', fields: [...reason, f('confirm_type', { required: true }), f('recipient_name', { required: true }), f('recipient_phone'), f('proof_uri'), f('remark')] },
{ name: '标记异常', resource: '/gasorder_basic/:identity/exception', fields: reason },
{ name: '恢复订单', resource: '/gasorder_basic/:identity/recover', fields: reason },
{ name: '取消订单', resource: '/gasorder_basic/:identity/cancel', danger: true, fields: reason },
{ name: '分配订单', resource: '/gasorder_basic/:identity/assign', fields: [relation('delivery_basic_identity', '/delivery_basic', true), relation('staff_account_identity', '/staff_account', true), ...reason], visibleFor: { field: 'order_status', values: [16, 18] } },
{ name: '开始罐装', resource: '/gasorder_basic/:identity/filling', fields: reason, visibleFor: { field: 'order_status', values: [18] } },
{ name: '待配送', resource: '/gasorder_basic/:identity/ready', fields: reason, visibleFor: { field: 'order_status', values: [19] } },
{ name: '开始配送', resource: '/gasorder_basic/:identity/delivering', fields: reason, visibleFor: { field: 'order_status', values: [20] } },
{ name: '等待签收', resource: '/gasorder_basic/:identity/awaiting-confirmation', fields: reason, visibleFor: { field: 'order_status', values: [33] } },
{ name: '完成订单', resource: '/gasorder_basic/:identity/complete', fields: [...reason, f('confirm_type', { required: true }), f('recipient_name', { required: true }), f('recipient_phone'), f('proof_uri'), f('remark')], visibleFor: { field: 'order_status', values: [34] } },
{ name: '标记异常', resource: '/gasorder_basic/:identity/exception', fields: reason, visibleFor: { field: 'order_status', values: [19, 20, 33, 34] } },
{ name: '恢复订单', resource: '/gasorder_basic/:identity/recover', fields: reason, visibleFor: { field: 'order_status', values: [21] } },
{ name: '取消订单', resource: '/gasorder_basic/:identity/cancel', danger: true, fields: reason, visibleFor: { field: 'order_status', values: [16, 18] } },
]),
define('gasorder_item', '订单明细', 'readonly', []),
define('gasorder_assign', '分配记录', 'readonly', []),
@@ -349,7 +350,7 @@ export const resources: ResourceUiDefinition[] = [
define('wallet_refund', '退款记录', 'readonly', []),
define('wallet_apply_cash', '提现记录', 'readonly', [
f('cash_no'),
relation('wallet_basic_identity', '/wallet_basic'),
f('wallet_basic_identity', { type: 'identity' }),
f('amount'),
f('apply_status', { type: 'select', options: [
{ label: '待处理', value: 10 },
@@ -365,8 +366,9 @@ export const resources: ResourceUiDefinition[] = [
f('trade_no'),
f('remark'),
], 'list', [
{ name: '审核通过', resource: '/wallet_apply_cash/:identity/approve', fields: reason },
{ name: '审核驳回', resource: '/wallet_apply_cash/:identity/reject', danger: true, fields: reason },
{ name: '审核通过', resource: '/wallet_apply_cash/:identity/approve', fields: reason, visibleFor: { field: 'apply_status', values: [10] } },
{ name: '审核驳回', resource: '/wallet_apply_cash/:identity/reject', danger: true, fields: reason, visibleFor: { field: 'apply_status', values: [10] } },
{ name: '标记处理完成', resource: '/wallet_apply_cash/:identity/complete', fields: [f('trade_no', { required: true }), f('callback_msg')], visibleFor: { field: 'apply_status', values: [25] } },
]),
define('fin_payment', '财务支付记录', 'readonly', []),

File diff suppressed because one or more lines are too long

View File

@@ -62,8 +62,8 @@ const routes: AppRouteRecordRaw[] = [
group('organization', 'organization', '机构管理', 'icon-storage', 10, [
child('organization', 'gas-basic', 'gas-basic', '气站管理', '/gas_basic', 'gas_basic'),
child('organization', 'delivery-basic', 'delivery-basic', '配送点管理', '/delivery_basic', 'delivery_basic'),
child('organization', 'gas-account', 'gas-account', '气站账户', '/gas_account', 'gas_basic', true, 'gas-basic'),
child('organization', 'delivery-account', 'delivery-account', '配送点账户', '/delivery_account', 'delivery_basic', true, 'delivery-basic'),
child('organization', 'gas-account', 'gas-account', '气站账户', '/gas_account', 'gas_basic', true, 'organization-gas-basic'),
child('organization', 'delivery-account', 'delivery-account', '配送点账户', '/delivery_account', 'delivery_basic', true, 'organization-delivery-basic'),
]),
group('staff', 'staff', '工作人员管理', 'icon-user-group', 30, [
{ ...child('staff', 'add', 'add', '新增工作人员', '/staff_account', 'staff_add'), meta: { title: '新增工作人员', resource: '/staff_account', requiresAuth: true, menuCode: 'staff_add', createMode: true } },
@@ -97,10 +97,10 @@ const routes: AppRouteRecordRaw[] = [
child('gasorder', 'track-points', 'track-points', '轨迹点', '/gasorder_track_point', 'gasorder_track', true, 'gasorder-tracks'),
child('gasorder', 'confirms', 'confirms', '确认记录', '/gasorder_confirm', 'gasorder_basic', true, 'gasorder-orders'),
child('gasorder', 'payments', 'payments', '订单支付记录', '/gasorder_payment', 'gasorder_basic', true, 'gasorder-orders'),
], 'delivery'),
group('ec', 'ec', '商城管理', 'icon-gift', 70, [
], 'gasorder'),
group('ec', 'ec', '电商平台管理', 'icon-gift', 70, [
child('ec', 'categories', 'categories', '商品分类', '/ec_category', 'ec_category'),
child('ec', 'products', 'products', '商品', '/ec_product', 'ec_product'),
child('ec', 'products', 'products', '商品管理', '/ec_product', 'ec_product'),
child('ec', 'attributes', 'attributes', '商品属性', '/ec_product_attribute', 'ec_product', true, 'ec-products'),
child('ec', 'images', 'images', '商品图片', '/ec_product_image', 'ec_product', true, 'ec-products'),
child('ec', 'carts', 'carts', '购物车', '/ec_cart', 'ec_cart'),

View File

@@ -68,7 +68,7 @@ const cards: { key: CountKey; label: string; hint: string; money?: boolean }[] =
{ key: 'paid_amount', label: '累计实收金额', hint: '支付成功口径', money: true },
];
const actions = [
{ label: '新建气站', route: 'gas-basic', menu: 'gas_basic', icon: IconPlus },
{ label: '新建气站', route: 'organization-gas-basic', menu: 'gas_basic', icon: IconPlus },
{ label: '配送订单', route: 'gasorder-orders', menu: 'gasorder_basic', icon: IconFile },
{ label: '智能气阀', route: 'product-info', menu: 'product_info', icon: IconStorage },
{ label: '用户管理', route: 'user-account', menu: 'user_account', icon: IconUser },

View File

@@ -14,19 +14,6 @@
<a-button type="primary" html-type="submit">查询</a-button>
<a-button @click="resetSearch">重置</a-button>
</a-form>
<a-form v-if="definition.name === 'wallet_basic'" :model="walletOwner" layout="inline" class="wallet-owner" @submit.prevent="getOwnerWallet">
<a-form-item label="归属类型">
<a-select v-model="walletOwner.type" style="width: 140px">
<a-option value="user">用户</a-option>
<a-option value="staff">工作人员</a-option>
<a-option value="delivery">配送站</a-option>
<a-option value="gas">气站</a-option>
<a-option value="platform">平台</a-option>
</a-select>
</a-form-item>
<a-form-item label="归属标识"><a-input v-model="walletOwner.identity" placeholder="请输入 identity" /></a-form-item>
<a-button type="primary" html-type="submit">获取或创建钱包</a-button>
</a-form>
<a-table :data="list" :loading="loading" :pagination="false" row-key="identity">
<template #columns>
<a-table-column title="业务标识" data-index="identity" :width="220" ellipsis tooltip />
@@ -114,7 +101,7 @@
</a-tab-pane>
</a-tabs>
<a-space class="detail-actions">
<a-button v-for="action in definition.detailActions" :key="action.name" type="primary" :status="action.danger ? 'danger' : 'normal'" @click="openDetailAction(action)">{{ action.name }}</a-button>
<a-button v-for="action in visibleDetailActions" :key="action.name" type="primary" :status="action.danger ? 'danger' : 'normal'" @click="openDetailAction(action)">{{ action.name }}</a-button>
</a-space>
</a-drawer>
@@ -173,7 +160,6 @@ const walletByOwner = ref<Record<string, Row>>({});
const filters = reactive({
keyword: typeof route.query.keyword === 'string' ? route.query.keyword : '',
});
const walletOwner = reactive({ type: 'user', identity: '' });
const formVisible = ref(false);
const detailVisible = ref(false);
const editingIdentity = ref('');
@@ -289,6 +275,14 @@ const currentIdentity = computed(() =>
'',
),
);
const visibleDetailActions = computed(() =>
(props.definition.detailActions ?? []).filter((action) => {
if (!action.visibleFor) return true;
return action.visibleFor.values.includes(
detail.value[action.visibleFor.field] as string | number,
);
}),
);
function resetForm(data?: Row) {
for (const field of props.definition.fields) {
@@ -306,31 +300,36 @@ async function load() {
loading.value = true;
try {
if (staffType.value) {
const staff = await loadAllRows(props.definition.resource);
const keyword = filters.keyword.trim().toLowerCase();
const filtered = staff.filter(
(row) =>
String(row.role_code ?? '') === staffType.value &&
(!keyword ||
['username', 'name', 'phone'].some((key) =>
String(row[key] ?? '').toLowerCase().includes(keyword),
)),
const result = await resourceApi.list<Row>(
props.definition.resource,
page.value,
pageSize,
{
role_code: staffType.value,
...(filters.keyword ? { keyword: filters.keyword } : {}),
},
);
list.value = filtered.slice((page.value - 1) * pageSize, page.value * pageSize);
total.value = filtered.length;
list.value = result.list;
total.value = result.total;
} else if (managedOwnerIdentity.value && managedRelationKey.value) {
const accounts = await loadAllRows(props.definition.resource);
const keyword = filters.keyword.trim().toLowerCase();
const filtered = accounts.filter(
(row) =>
String(row[managedRelationKey.value] ?? '') === managedOwnerIdentity.value &&
(!keyword ||
['username', 'display_name', 'role_code'].some((key) =>
String(row[key] ?? '').toLowerCase().includes(keyword),
)),
const serverFilters: Record<string, string> = props.definition.name === 'staff_credential'
? { staff_account_identity: managedOwnerIdentity.value }
: props.definition.name === 'gas_account'
? { gas_basic_identities: managedOwnerIdentity.value }
: props.definition.name === 'delivery_account'
? { delivery_basic_identities: managedOwnerIdentity.value }
: {};
const result = await resourceApi.list<Row>(
props.definition.resource,
page.value,
pageSize,
{
...serverFilters,
...(filters.keyword ? { keyword: filters.keyword } : {}),
},
);
list.value = filtered.slice((page.value - 1) * pageSize, page.value * pageSize);
total.value = filtered.length;
list.value = result.list;
total.value = result.total;
} else {
const result = await resourceApi.list<Row>(
props.definition.resource,
@@ -356,7 +355,17 @@ async function loadWallets() {
walletByOwner.value = {};
return;
}
const wallets = await loadAllRows('/wallet_basic');
const ownerIdentities = list.value.map((row) => String(row.identity ?? '')).filter(Boolean);
if (!ownerIdentities.length) {
walletByOwner.value = {};
return;
}
const wallets = (
await resourceApi.list<Row>('/wallet_basic', 1, 100, {
owner_type: ownerType,
owner_identities: ownerIdentities.join(','),
})
).list;
walletByOwner.value = wallets.reduce<Record<string, Row>>((result, wallet) => {
if (wallet.owner_type === ownerType) {
result[String(wallet.owner_identity ?? '')] = wallet;
@@ -365,10 +374,10 @@ async function loadWallets() {
}, {});
}
async function loadAllRows(resource: string) {
async function loadAllRows(resource: string, filters: Record<string, string> = {}) {
const rows: Row[] = [];
for (let currentPage = 1; currentPage <= 100; currentPage += 1) {
const result = await resourceApi.list<Row>(resource, currentPage, 100);
const result = await resourceApi.list<Row>(resource, currentPage, 100, filters);
rows.push(...result.list);
if (rows.length >= result.total || result.list.length < 100) break;
}
@@ -381,7 +390,17 @@ async function loadAccountCounts() {
accountCounts.value = {};
return;
}
const accounts = await loadAllRows(management.resource);
const ownerIdentities = list.value.map((row) => String(row.identity ?? '')).filter(Boolean);
if (!ownerIdentities.length) {
accountCounts.value = {};
return;
}
const filterKey = management.relationKey === 'gas_basic_identity'
? 'gas_basic_identities'
: 'delivery_basic_identities';
const accounts = await loadAllRows(management.resource, {
[filterKey]: ownerIdentities.join(','),
});
accountCounts.value = accounts.reduce<Record<string, number>>((counts, account) => {
const identity = String(account[management.relationKey] ?? '');
if (identity) counts[identity] = (counts[identity] ?? 0) + 1;
@@ -619,23 +638,6 @@ async function changePage(next: number) {
await load();
}
async function getOwnerWallet() {
if (!walletOwner.identity.trim()) {
Message.warning('请输入归属标识');
return;
}
try {
detail.value = await resourceApi.detail<Row>(
'/wallet_basic/owner',
`${walletOwner.type}/${walletOwner.identity.trim()}`,
);
detailVisible.value = true;
await load();
} catch (error) {
Message.error((error as Error).message);
}
}
function formatValue(value: unknown) {
return JSON.stringify(value, null, 2);
}
@@ -718,12 +720,14 @@ function displayValue(key: string, value: unknown) {
async function loadRelation(resource: string, keyword = '') {
relationLoading[resource] = true;
try {
const extraFilters: Record<string, string> = keyword ? { keyword } : {};
if (resource === '/staff_account') extraFilters.role_code = 'delivery';
relationOptions[resource] = (
await resourceApi.list<Row>(
resource,
1,
100,
keyword ? { keyword } : {},
extraFilters,
)
).list;
} catch (error) {
@@ -745,7 +749,9 @@ function searchRelation(resource: string | undefined, keyword: string) {
onMounted(async () => {
if (!route.meta.createMode) await load();
const actionFields = props.definition.detailActions?.flatMap((item) => item.fields ?? []) ?? [];
const actionFields = route.meta.createMode
? []
: props.definition.detailActions?.flatMap((item) => item.fields ?? []) ?? [];
const relationPaths = new Set(
[...props.definition.fields, ...actionFields]
.map((field) => field.relation)
@@ -789,11 +795,6 @@ function optionLabel(option: Row) {
.detail-actions {
margin-top: 16px;
}
.wallet-owner {
margin-bottom: 16px;
padding: 12px;
background: var(--color-fill-1);
}
.muted-text {
color: var(--color-text-3);
}