优化
This commit is contained in:
@@ -5,7 +5,7 @@ import { exchangeImpersonationCode, loginMerchant, logoutMerchant } from '@/modu
|
||||
import { ApiError } from '@/api/http'
|
||||
import {
|
||||
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
|
||||
TENANT_PERMISSION_GROUPS,
|
||||
hasMappedMerchantPermission,
|
||||
getCurrentMerchantPermissionCodes,
|
||||
getMerchantPersonalProfile,
|
||||
getTenantProfile,
|
||||
@@ -14,16 +14,6 @@ import {
|
||||
import { clearSessionTokens, getAccessToken, getRefreshToken, persistTemporaryAccessToken, persistTokenPair } from '@/api/session'
|
||||
import { decodeMerchantToken, type MerchantJwtPayload } from '@/modules/auth/utils/jwt'
|
||||
|
||||
const frontendPermissionCodes = new Set(TENANT_PERMISSION_GROUPS.flatMap((group) => group.items.map((item) => item.code)))
|
||||
const backendPermissionMapping = Object.entries(FRONTEND_TO_MERCHANT_PERMISSION_CODES)
|
||||
.reduce<Record<string, string[]>>((mapping, [frontendCode, backendCodes]) => {
|
||||
for (const backendCode of backendCodes) {
|
||||
if (!mapping[backendCode]) mapping[backendCode] = []
|
||||
mapping[backendCode].push(frontendCode)
|
||||
}
|
||||
return mapping
|
||||
}, {})
|
||||
|
||||
export interface ClientAccountProfile {
|
||||
username: string
|
||||
realName: string
|
||||
@@ -43,9 +33,8 @@ const emptyProfile: ClientAccountProfile = {
|
||||
function mapPermissions(backendPermissionCodes: string[] = []) {
|
||||
const backendPermissions = new Set(backendPermissionCodes)
|
||||
const result = new Set<string>(['dashboard:view', 'account:profile:view'])
|
||||
for (const permission of backendPermissions) {
|
||||
if (frontendPermissionCodes.has(permission)) result.add(permission)
|
||||
for (const mapped of backendPermissionMapping[permission] ?? []) result.add(mapped)
|
||||
for (const [frontendCode] of Object.entries(FRONTEND_TO_MERCHANT_PERMISSION_CODES)) {
|
||||
if (hasMappedMerchantPermission(frontendCode, backendPermissions)) result.add(frontendCode)
|
||||
}
|
||||
return [...result]
|
||||
}
|
||||
|
||||
@@ -74,7 +74,9 @@ function mapBillLine(line: FinanceBillLineWire): FinanceBillLine {
|
||||
}
|
||||
|
||||
export function mapFinanceBills(bills: FinanceBillWire[]) {
|
||||
return bills.map(mapBillSummary)
|
||||
return bills
|
||||
.filter((bill) => bill.status === 'ISSUED' || bill.status === 'PARTIALLY_PAID' || bill.status === 'PAID')
|
||||
.map(mapBillSummary)
|
||||
}
|
||||
|
||||
export function mapFinanceBillDetail(bill: FinanceBillWire): FinanceBillDetail {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
import ts from 'typescript'
|
||||
|
||||
const routesSource = readFileSync(new URL('../routes.ts', import.meta.url), 'utf8')
|
||||
const viewSource = readFileSync(new URL('./FinanceBillsView.vue', import.meta.url), 'utf8')
|
||||
@@ -8,6 +9,17 @@ const apiSource = readFileSync(new URL('../api/finance.ts', import.meta.url), 'u
|
||||
const adapterSource = readFileSync(new URL('../api/adapters.ts', import.meta.url), 'utf8')
|
||||
const permissionSource = readFileSync(new URL('../../tenant-management/contracts.ts', import.meta.url), 'utf8')
|
||||
|
||||
async function importTypeScriptModule(url) {
|
||||
const source = readFileSync(url, 'utf8')
|
||||
const compiled = ts.transpileModule(source, {
|
||||
compilerOptions: { module: ts.ModuleKind.ESNext, target: ts.ScriptTarget.ES2022 },
|
||||
}).outputText
|
||||
return import(`data:text/javascript;base64,${Buffer.from(compiled).toString('base64')}`)
|
||||
}
|
||||
|
||||
const financeAdapters = await importTypeScriptModule(new URL('../api/adapters.ts', import.meta.url))
|
||||
const permissionContracts = await importTypeScriptModule(new URL('../../tenant-management/contracts.ts', import.meta.url))
|
||||
|
||||
test('activates the customer finance route with a read-only permission', () => {
|
||||
assert.match(routesSource, /path: 'finance\/bills'/)
|
||||
assert.match(routesSource, /FinanceBillsView\.vue/)
|
||||
@@ -35,6 +47,37 @@ test('adapts the finance snake-case wire contract and filters receipts locally b
|
||||
assert.doesNotMatch(viewSource, /loadDetail\(row\.billNo\)/)
|
||||
})
|
||||
|
||||
test('does not expose draft or voided bills returned by the server', () => {
|
||||
const bill = (id, status) => ({
|
||||
id,
|
||||
bill_no: `B-${id}`,
|
||||
customer_name_snapshot: '测试客户',
|
||||
currency_code: 'CNY',
|
||||
bill_date: '2026-08-11',
|
||||
due_date: '2026-09-10',
|
||||
status,
|
||||
total_amount: '100.00',
|
||||
received_amount: '0.00',
|
||||
outstanding_amount: '100.00',
|
||||
})
|
||||
const visible = financeAdapters.mapFinanceBills([
|
||||
bill(1, 'DRAFT'),
|
||||
bill(2, 'ISSUED'),
|
||||
bill(3, 'PARTIALLY_PAID'),
|
||||
bill(4, 'PAID'),
|
||||
bill(5, 'VOIDED'),
|
||||
])
|
||||
assert.deepEqual(visible.map((item) => item.status), ['ISSUED', 'PARTIALLY_PAID', 'PAID'])
|
||||
})
|
||||
|
||||
test('requires both finance statement authorities for the page permission', () => {
|
||||
const allowed = (codes) => permissionContracts.hasMappedMerchantPermission('finance:bill:view', new Set(codes))
|
||||
assert.equal(allowed([]), false)
|
||||
assert.equal(allowed(['merchant:finance:statement:list']), false)
|
||||
assert.equal(allowed(['merchant:finance:statement:view']), false)
|
||||
assert.equal(allowed(['merchant:finance:statement:list', 'merchant:finance:statement:view']), true)
|
||||
})
|
||||
|
||||
test('keeps actions first and exposes list, balance, detail, receipt, empty and error states', () => {
|
||||
const actionIndex = viewSource.indexOf('label="操作"')
|
||||
const billNumberIndex = viewSource.indexOf('label="账单编号"')
|
||||
@@ -52,4 +95,5 @@ test('catalogs customer finance as an assignable view-only role permission', ()
|
||||
assert.match(permissionSource, /不包含任何财务写操作/)
|
||||
assert.match(permissionSource, /merchant:finance:statement:list/)
|
||||
assert.match(permissionSource, /merchant:finance:statement:view/)
|
||||
assert.match(permissionSource, /FRONTEND_PERMISSIONS_REQUIRING_ALL_BACKEND_CODES/)
|
||||
})
|
||||
|
||||
@@ -89,3 +89,15 @@ export const FRONTEND_TO_MERCHANT_PERMISSION_CODES: Readonly<Record<string, read
|
||||
'company:permission:assign': ['merchant:role:grant'],
|
||||
'finance:bill:view': ['merchant:finance:statement:list', 'merchant:finance:statement:view'],
|
||||
}
|
||||
|
||||
export const FRONTEND_PERMISSIONS_REQUIRING_ALL_BACKEND_CODES = new Set<string>([
|
||||
'finance:bill:view',
|
||||
])
|
||||
|
||||
export function hasMappedMerchantPermission(frontendCode: string, grantedBackendCodes: ReadonlySet<string>) {
|
||||
const backendCodes = FRONTEND_TO_MERCHANT_PERMISSION_CODES[frontendCode]
|
||||
if (!backendCodes?.length) return grantedBackendCodes.has(frontendCode)
|
||||
return FRONTEND_PERMISSIONS_REQUIRING_ALL_BACKEND_CODES.has(frontendCode)
|
||||
? backendCodes.every((code) => grantedBackendCodes.has(code))
|
||||
: backendCodes.some((code) => grantedBackendCodes.has(code))
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export {
|
||||
export {
|
||||
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
|
||||
TENANT_PERMISSION_GROUPS,
|
||||
hasMappedMerchantPermission,
|
||||
type MerchantTenantStatus,
|
||||
} from './contracts'
|
||||
export { useTenantAdministrationStore } from './store/tenantAdministration'
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
FRONTEND_TO_MERCHANT_PERMISSION_CODES,
|
||||
REQUIRED_TENANT_PERMISSION_CODES,
|
||||
TENANT_PERMISSION_GROUPS,
|
||||
hasMappedMerchantPermission,
|
||||
type MerchantTenantStatus,
|
||||
} from '@/modules/tenant-management/contracts'
|
||||
import { normalizeQualificationStatus, type TenantQualificationStatus } from '@/modules/tenant-management/utils/qualificationStatus'
|
||||
@@ -130,7 +131,7 @@ export const useTenantAdministrationStore = defineStore('tenant-administration',
|
||||
.filter((resource) => grantedIds.includes(String(resource.id)))
|
||||
.map((resource) => resource.permissionCode))
|
||||
const permissionCodes = Object.entries(FRONTEND_TO_MERCHANT_PERMISSION_CODES)
|
||||
.filter(([, backendCodes]) => backendCodes.some((code) => grantedBackendCodes.has(code)))
|
||||
.filter(([frontendCode]) => hasMappedMerchantPermission(frontendCode, grantedBackendCodes))
|
||||
.map(([frontendCode]) => frontendCode)
|
||||
const effectivePermissionCodes = [...new Set([...REQUIRED_TENANT_PERMISSION_CODES, ...permissionCodes])]
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user