feat: 全局客户名称脱敏(首尾保留+中间三个*)
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

- 2-3字: 首字+*** (徐***)
- 4-6字: 首2字+***+末1字 (嘉兴***司)
- 7字+: 首4字+***+末2字 (嘉兴市乍***公司)
- 覆盖所有接口: monitoring, targets, vehicles, weekly-detail

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
kkfluous
2026-04-02 20:23:24 +08:00
parent e7efe179b7
commit 9c9d7a3805
4 changed files with 30 additions and 11 deletions

View File

@@ -1,5 +1,26 @@
import type { AuthUser } from './types.js'; import type { AuthUser } from './types.js';
/** 客户名称脱敏 */
export function maskCustomerName(name: string | null): string | null {
if (!name) return name;
const len = name.length;
if (len <= 1) return '*';
if (len <= 3) return name[0] + '***';
if (len <= 6) return name.slice(0, 2) + '***' + name.slice(-1);
return name.slice(0, 4) + '***' + name.slice(-2);
}
/** 对数据列表中的客户名称进行脱敏 */
export function maskCustomerNames<T>(items: T[]): T[] {
return items.map(v => {
const obj = v as any;
const copy = { ...obj };
if ('customer' in copy && copy.customer) copy.customer = maskCustomerName(copy.customer);
if ('customerName' in copy && copy.customerName) copy.customerName = maskCustomerName(copy.customerName);
return copy as T;
});
}
/** /**
* 通用权限过滤函数 * 通用权限过滤函数
* 适配 CachedVehicledepartment, manager, managerId和 VehicledepartmentName, customerManager, managerId * 适配 CachedVehicledepartment, manager, managerId和 VehicledepartmentName, customerManager, managerId

View File

@@ -1,6 +1,6 @@
import { Hono } from 'hono'; import { Hono } from 'hono';
import { getCache, queryDateMileage, buildDateFilters } from './cache.js'; import { getCache, queryDateMileage, buildDateFilters } from './cache.js';
import { filterByPermission } from '../../auth/permissions.js'; import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
import type { AuthUser } from '../../auth/types.js'; import type { AuthUser } from '../../auth/types.js';
import type { CachedVehicle, MonitoringFilters, MonitoringResponse } from './types.js'; import type { CachedVehicle, MonitoringFilters, MonitoringResponse } from './types.js';
@@ -115,7 +115,7 @@ app.get('/', async (c) => {
const total = filtered.length; const total = filtered.length;
return c.json({ return c.json({
vehicles: paged, vehicles: maskCustomerNames(paged),
stats, stats,
filters, filters,
total, total,

View File

@@ -3,7 +3,7 @@ import pool from '../../db.js';
import mileagePool from '../../mileage-db.js'; import mileagePool from '../../mileage-db.js';
import { getCache } from './cache.js'; import { getCache } from './cache.js';
import { fetchVehicleInfoByPlates } from './vehicle-info.js'; import { fetchVehicleInfoByPlates } from './vehicle-info.js';
import { filterByPermission } from '../../auth/permissions.js'; import { filterByPermission, maskCustomerNames } from '../../auth/permissions.js';
const app = new Hono(); const app = new Hono();
@@ -173,7 +173,7 @@ app.get('/:id/vehicles', async (c) => {
const user = (c as any).get('user') as import('../../auth/types.js').AuthUser | undefined; const user = (c as any).get('user') as import('../../auth/types.js').AuthUser | undefined;
const filtered = user ? filterByPermission(result, user) : result; const filtered = user ? filterByPermission(result, user) : result;
return c.json(filtered); return c.json(maskCustomerNames(filtered));
} catch (e: unknown) { } catch (e: unknown) {
console.error('target vehicles error:', e); console.error('target vehicles error:', e);
return c.json([], 500); return c.json([], 500);

View File

@@ -10,7 +10,7 @@ import type {
BatchGroup, BatchGroup,
InventoryTypeSummary, InventoryTypeSummary,
} from '../types.js'; } from '../types.js';
import { filterByPermission } from '../auth/permissions.js'; import { filterByPermission, maskCustomerNames, maskCustomerName } from '../auth/permissions.js';
import type { AuthUser } from '../auth/types.js'; import type { AuthUser } from '../auth/types.js';
import type { Context } from 'hono'; import type { Context } from 'hono';
@@ -312,15 +312,12 @@ async function getVehicles(): Promise<Vehicle[]> {
async function getVehiclesForUser(c: Context): Promise<Vehicle[]> { async function getVehiclesForUser(c: Context): Promise<Vehicle[]> {
const all = await getVehicles(); const all = await getVehicles();
// Hono 子路由 context 可能丢失变量,尝试多种方式获取
const user = ((c as any).get?.('user') || (c as any).var?.user) as AuthUser | undefined; const user = ((c as any).get?.('user') || (c as any).var?.user) as AuthUser | undefined;
if (user) { if (user) {
const filtered = filterByPermission(all, user); const filtered = filterByPermission(all, user);
console.log(`[vehicles] permission: ${user.permissionLevel}, user: ${user.userName}, before: ${all.length}, after: ${filtered.length}`); return maskCustomerNames(filtered);
return filtered;
} }
console.log('[vehicles] WARNING: no user in context, returning all'); return maskCustomerNames(all);
return all;
} }
function getRegionCounts(vehicles: Vehicle[], regions: readonly string[]): Record<string, number> { function getRegionCounts(vehicles: Vehicle[], regions: readonly string[]): Record<string, number> {
@@ -1009,7 +1006,8 @@ app.get('/weekly-detail', async (c) => {
return c.json([]); return c.json([]);
} }
const [rows] = await pool.query<any[]>(sql); const [rows] = await pool.query<any[]>(sql);
return c.json(rows); const masked = (rows as any[]).map(r => ({ ...r, customer_name: maskCustomerName(r.customer_name) }));
return c.json(masked);
}); });
// GET /api/vehicles/refresh — force cache refresh // GET /api/vehicles/refresh — force cache refresh