diff --git a/src/persistence/pg.ts b/src/persistence/pg.ts index cfd8294..79bde89 100644 --- a/src/persistence/pg.ts +++ b/src/persistence/pg.ts @@ -52,6 +52,8 @@ export interface ListPageOptions { q?: string; /** 归档过滤:'active'(默认,仅未归档)| 'archived'(仅归档)| 'all'(全部)。 */ archived?: 'active' | 'archived' | 'all'; + /** 仅返回该发起人(用户ID)的评估(销售按本人过滤)。 */ + assessorId?: string; } /** 列表行(SQL 分页返回)。 */ @@ -245,6 +247,10 @@ export class PgAssessmentStore implements AssessmentStore { `(a.assessment->>'projectDescription' ILIKE $${i} OR a.assessment->>'businessType' ILIKE $${i} OR a.assessment->>'industry' ILIKE $${i} OR a.assessment->>'assessorId' ILIKE $${i})`, ); } + if (opts.assessorId !== undefined && opts.assessorId !== '') { + params.push(opts.assessorId); + where.push(`a.assessment->>'assessorId' = $${params.length}`); + } const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : ''; const countRes = await this.pool.query( diff --git a/src/server/index.ts b/src/server/index.ts index 9510705..eccccbd 100644 --- a/src/server/index.ts +++ b/src/server/index.ts @@ -1586,6 +1586,12 @@ app.get('/api/assessments', async (c) => { const pageSize = Number(c.req.query('pageSize') ?? 20); const statusFilter = c.req.query('status'); const q = c.req.query('q'); + let assessorFilter = c.req.query('assessorId'); + // 行级隔离:已鉴权的销售只能看本人发起的评估(强制按 JWT 的 uid 过滤,忽略前端传值)。 + const authUser = (c as import('hono').Context).get('user') as AuthPayload | undefined; + if (authUser?.role === '商务/销售' && authUser.uid) { + assessorFilter = authUser.uid; + } const archivedRaw = c.req.query('archived'); const archived: 'active' | 'archived' | 'all' = archivedRaw === 'archived' ? 'archived' : archivedRaw === 'all' ? 'all' : 'active'; @@ -1633,6 +1639,7 @@ app.get('/api/assessments', async (c) => { archived, ...(statusFilter !== undefined ? { status: statusFilter } : {}), ...(q !== undefined ? { q } : {}), + ...(assessorFilter !== undefined && assessorFilter !== '' ? { assessorId: assessorFilter } : {}), }); const nameMap = await userIdNameMap(); const items2 = items.map((it) => ({ @@ -1652,6 +1659,9 @@ app.get('/api/assessments', async (c) => { if (statusFilter !== undefined && statusFilter !== '' && statusFilter !== 'all') { rows = rows.filter((r) => r.status === statusFilter); } + if (assessorFilter !== undefined && assessorFilter !== '') { + rows = rows.filter((r) => r.assessorId === assessorFilter); + } if (q !== undefined && q.trim() !== '') { const kw = q.trim(); rows = rows.filter( diff --git a/web/src/api/client.ts b/web/src/api/client.ts index e6ff864..894e529 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -425,6 +425,7 @@ export async function fetchAssessmentsPage(params: { readonly status?: string; readonly q?: string; readonly archived?: 'active' | 'archived' | 'all'; + readonly assessorId?: string; }): Promise { const sp = new URLSearchParams(); sp.set('page', String(params.page)); @@ -438,6 +439,9 @@ export async function fetchAssessmentsPage(params: { if (params.archived !== undefined && params.archived !== 'active') { sp.set('archived', params.archived); } + if (params.assessorId !== undefined && params.assessorId !== '') { + sp.set('assessorId', params.assessorId); + } return request('GET', `/api/assessments?${sp.toString()}`); } diff --git a/web/src/pages/Dashboard.tsx b/web/src/pages/Dashboard.tsx index 38dcc47..ab66d55 100644 --- a/web/src/pages/Dashboard.tsx +++ b/web/src/pages/Dashboard.tsx @@ -126,9 +126,12 @@ export function Dashboard(): JSX.Element { return () => clearTimeout(t); }, [searchInput]); + // 销售仅可见本人发起的评估(行级数据隔离);风控/管理层可见全部。 + const scopeAssessorId = role === '商务/销售' ? user?.id : undefined; + const loadList = useCallback(() => { setLoading(true); - fetchAssessmentsPage({ page, pageSize, status: statusFilter, q: search, archived: archivedView }) + fetchAssessmentsPage({ page, pageSize, status: statusFilter, q: search, archived: archivedView, ...(scopeAssessorId !== undefined ? { assessorId: scopeAssessorId } : {}) }) .then((res) => { setItems(res.items); setTotal(res.total); @@ -136,7 +139,7 @@ export function Dashboard(): JSX.Element { }) .catch((err: unknown) => setError(err instanceof Error ? err.message : '加载失败')) .finally(() => setLoading(false)); - }, [page, pageSize, statusFilter, search, archivedView]); + }, [page, pageSize, statusFilter, search, archivedView, scopeAssessorId]); useEffect(() => { loadList(); @@ -146,7 +149,7 @@ export function Dashboard(): JSX.Element { fetchSummary().then(setSummary).catch(() => undefined); const todoStatus = TODO_STATUS[role]; if (todoStatus !== undefined) { - fetchAssessmentsPage({ page: 1, pageSize: 50, status: todoStatus }) + fetchAssessmentsPage({ page: 1, pageSize: 50, status: todoStatus, ...(scopeAssessorId !== undefined ? { assessorId: scopeAssessorId } : {}) }) .then((res) => setTodoItems(res.items)) .catch(() => setTodoItems([])); } else { @@ -169,7 +172,7 @@ export function Dashboard(): JSX.Element { } else { setAssignments({}); } - }, [role, user?.username]); + }, [role, user?.username, user?.id, scopeAssessorId]); /** 删除一条草稿并刷新草稿箱。 */ const removeDraft = useCallback(async (id: string): Promise => {