diff --git a/src/persistence/pg.ts b/src/persistence/pg.ts index 79bde89..935f419 100644 --- a/src/persistence/pg.ts +++ b/src/persistence/pg.ts @@ -316,13 +316,17 @@ export class PgAssessmentStore implements AssessmentStore { return { items, total }; } - /** 各工作流状态的记录数(用于工作台统计卡)。仅统计未归档项,另返回归档总数。 */ - async countsByStatus(): Promise<{ total: number; byStatus: Record; archived: number }> { + /** 各工作流状态的记录数(用于工作台统计卡)。仅统计未归档项,另返回归档总数。可按发起人(assessorId)过滤。 */ + async countsByStatus(assessorId?: string): Promise<{ total: number; byStatus: Record; archived: number }> { + const scoped = assessorId !== undefined && assessorId !== ''; + const cond = scoped ? `AND a.assessment->>'assessorId' = $1` : ''; + const params = scoped ? [assessorId] : []; const res = await this.pool.query( `SELECT COALESCE(ws.status, 'pending_risk_review') AS status, count(*)::int AS n FROM assessments a LEFT JOIN workflow_status ws ON ws.assessment_id = a.id - WHERE a.archived = false + WHERE a.archived = false ${cond} GROUP BY 1`, + params, ); const byStatus: Record = {}; let total = 0; @@ -331,7 +335,8 @@ export class PgAssessmentStore implements AssessmentStore { total += Number(r.n); } const arc = await this.pool.query( - 'SELECT count(*)::int AS n FROM assessments WHERE archived = true', + `SELECT count(*)::int AS n FROM assessments a WHERE archived = true ${cond}`, + params, ); const archived = Number((arc.rows[0] as { n: number }).n); return { total, byStatus, archived }; diff --git a/src/server/index.ts b/src/server/index.ts index eccccbd..958ae9e 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1678,13 +1678,18 @@ app.get('/api/assessments', async (c) => { /** 工作台统计:各状态评估数。 */ app.get('/api/assessments/summary', async (c) => { + // 行级隔离:销售只统计本人发起的;风控/管理层全局。 + let scopeId = c.req.query('assessorId'); + const authU = (c as import('hono').Context).get('user') as AuthPayload | undefined; + if (authU?.role === '商务/销售' && authU.uid) scopeId = authU.uid; if (store instanceof PgAssessmentStore) { - return c.json(await store.countsByStatus()); + return c.json(await store.countsByStatus(scopeId)); } const byStatus: Record = {}; let archivedCount = 0; let activeTotal = 0; for (const r of store.getAll()) { + if (scopeId !== undefined && scopeId !== '' && (r.assessment as unknown as { assessorId?: string }).assessorId !== scopeId) continue; if (archivedById.get(r.assessment.id) === true) { archivedCount += 1; continue; diff --git a/web/src/api/client.ts b/web/src/api/client.ts index 894e529..23a997b 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -465,9 +465,10 @@ export interface AssessmentSummary { readonly archived?: number; } -/** 获取各状态评估数统计。 */ -export async function fetchSummary(): Promise { - return request('GET', '/api/assessments/summary'); +/** 获取各状态评估数统计。销售传本人 assessorId 则按本人统计(服务端对销售亦强制本人)。 */ +export async function fetchSummary(assessorId?: string): Promise { + const q = assessorId !== undefined && assessorId !== '' ? `?assessorId=${encodeURIComponent(assessorId)}` : ''; + return request('GET', `/api/assessments/summary${q}`); } /** 评估详情响应。 */ diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index ab66d55..e10d51a 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -146,7 +146,7 @@ export function Dashboard(): JSX.Element { }, [loadList]); const loadAux = useCallback(() => { - fetchSummary().then(setSummary).catch(() => undefined); + fetchSummary(scopeAssessorId).then(setSummary).catch(() => undefined); const todoStatus = TODO_STATUS[role]; if (todoStatus !== undefined) { fetchAssessmentsPage({ page: 1, pageSize: 50, status: todoStatus, ...(scopeAssessorId !== undefined ? { assessorId: scopeAssessorId } : {}) }) @@ -155,11 +155,15 @@ export function Dashboard(): JSX.Element { } else { setTodoItems([]); } - // 告警数据 - fetch(`${API_BASE}/api/assessments/expiring`).then((r) => r.json()).then(setExpiring).catch(() => setExpiring([])); - fetch(`${API_BASE}/api/assessments/overdue`).then((r) => r.json()).then(setOverdue).catch(() => setOverdue([])); - fetch(`${API_BASE}/api/reject-reason-stats`).then((r) => r.json()).then(setRejectStats).catch(() => setRejectStats([])); - fetch(`${API_BASE}/api/accuracy`).then((r) => r.json()).then(setAccuracy).catch(() => undefined); + // 组合分析(准确度/驳回Top/到期/超时)面向风控与管理层;销售视图不展示(避免跨人数据)。 + if (role === '商务/销售') { + setExpiring([]); setOverdue([]); setRejectStats([]); setAccuracy({ count: 0, avgPredictedPct: null, avgActualPct: null, avgDeviationPct: null, bias: null }); + } else { + fetch(`${API_BASE}/api/assessments/expiring`).then((r) => r.json()).then(setExpiring).catch(() => setExpiring([])); + fetch(`${API_BASE}/api/assessments/overdue`).then((r) => r.json()).then(setOverdue).catch(() => setOverdue([])); + fetch(`${API_BASE}/api/reject-reason-stats`).then((r) => r.json()).then(setRejectStats).catch(() => setRejectStats([])); + fetch(`${API_BASE}/api/accuracy`).then((r) => r.json()).then(setAccuracy).catch(() => undefined); + } // 草稿箱(仅销售展示):列出当前用户的向导草稿。 if (role === '商务/销售') { listDrafts(user?.id ?? undefined).then(setDrafts).catch(() => setDrafts([])); @@ -318,11 +322,12 @@ export function Dashboard(): JSX.Element { ]; const todoCount = summary.byStatus[TODO_STATUS[role] ?? ''] ?? 0; + const isSales = role === '商务/销售'; const summaryItems = [ - { label: '全部评估', value: summary.total, tone: colorVar('color.brand.primary') }, - { label: '我的待办', value: todoCount, tone: colorVar('color.risk.high') }, - { label: '待风控', value: summary.byStatus.pending_risk_review ?? 0, tone: colorVar('color.risk.medium') }, - { label: '已通过', value: summary.byStatus.approved ?? 0, tone: colorVar('color.risk.low') }, + { label: isSales ? '我的评估' : '全部评估', value: summary.total, tone: colorVar('color.brand.primary') }, + { label: isSales ? '被驳回(待处理)' : '我的待办', value: todoCount, tone: colorVar('color.risk.high') }, + { label: isSales ? '我的待风控' : '待风控', value: summary.byStatus.pending_risk_review ?? 0, tone: colorVar('color.risk.medium') }, + { label: isSales ? '我的已通过' : '已通过', value: summary.byStatus.approved ?? 0, tone: colorVar('color.risk.low') }, ]; const pageTitle = role === '风控' ? '待办审核' : role === '管理层' ? '待办审批' : '评估历史';