数据隔离:销售只可见本人发起的评估(行级权限)
- listPage 支持 assessorId 过滤;列表端点对已鉴权销售强制按 JWT.uid 过滤(防伪造) - 看板历史与待办对销售按本人 user.id 过滤;风控/管理层仍可见全部 - 前端 fetchAssessmentsPage 支持 assessorId 参数
This commit is contained in:
@@ -52,6 +52,8 @@ export interface ListPageOptions {
|
|||||||
q?: string;
|
q?: string;
|
||||||
/** 归档过滤:'active'(默认,仅未归档)| 'archived'(仅归档)| 'all'(全部)。 */
|
/** 归档过滤:'active'(默认,仅未归档)| 'archived'(仅归档)| 'all'(全部)。 */
|
||||||
archived?: 'active' | 'archived' | 'all';
|
archived?: 'active' | 'archived' | 'all';
|
||||||
|
/** 仅返回该发起人(用户ID)的评估(销售按本人过滤)。 */
|
||||||
|
assessorId?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 列表行(SQL 分页返回)。 */
|
/** 列表行(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})`,
|
`(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 whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
||||||
|
|
||||||
const countRes = await this.pool.query(
|
const countRes = await this.pool.query(
|
||||||
|
|||||||
@@ -1586,6 +1586,12 @@ app.get('/api/assessments', async (c) => {
|
|||||||
const pageSize = Number(c.req.query('pageSize') ?? 20);
|
const pageSize = Number(c.req.query('pageSize') ?? 20);
|
||||||
const statusFilter = c.req.query('status');
|
const statusFilter = c.req.query('status');
|
||||||
const q = c.req.query('q');
|
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 archivedRaw = c.req.query('archived');
|
||||||
const archived: 'active' | 'archived' | 'all' =
|
const archived: 'active' | 'archived' | 'all' =
|
||||||
archivedRaw === 'archived' ? 'archived' : archivedRaw === 'all' ? 'all' : 'active';
|
archivedRaw === 'archived' ? 'archived' : archivedRaw === 'all' ? 'all' : 'active';
|
||||||
@@ -1633,6 +1639,7 @@ app.get('/api/assessments', async (c) => {
|
|||||||
archived,
|
archived,
|
||||||
...(statusFilter !== undefined ? { status: statusFilter } : {}),
|
...(statusFilter !== undefined ? { status: statusFilter } : {}),
|
||||||
...(q !== undefined ? { q } : {}),
|
...(q !== undefined ? { q } : {}),
|
||||||
|
...(assessorFilter !== undefined && assessorFilter !== '' ? { assessorId: assessorFilter } : {}),
|
||||||
});
|
});
|
||||||
const nameMap = await userIdNameMap();
|
const nameMap = await userIdNameMap();
|
||||||
const items2 = items.map((it) => ({
|
const items2 = items.map((it) => ({
|
||||||
@@ -1652,6 +1659,9 @@ app.get('/api/assessments', async (c) => {
|
|||||||
if (statusFilter !== undefined && statusFilter !== '' && statusFilter !== 'all') {
|
if (statusFilter !== undefined && statusFilter !== '' && statusFilter !== 'all') {
|
||||||
rows = rows.filter((r) => r.status === statusFilter);
|
rows = rows.filter((r) => r.status === statusFilter);
|
||||||
}
|
}
|
||||||
|
if (assessorFilter !== undefined && assessorFilter !== '') {
|
||||||
|
rows = rows.filter((r) => r.assessorId === assessorFilter);
|
||||||
|
}
|
||||||
if (q !== undefined && q.trim() !== '') {
|
if (q !== undefined && q.trim() !== '') {
|
||||||
const kw = q.trim();
|
const kw = q.trim();
|
||||||
rows = rows.filter(
|
rows = rows.filter(
|
||||||
|
|||||||
@@ -425,6 +425,7 @@ export async function fetchAssessmentsPage(params: {
|
|||||||
readonly status?: string;
|
readonly status?: string;
|
||||||
readonly q?: string;
|
readonly q?: string;
|
||||||
readonly archived?: 'active' | 'archived' | 'all';
|
readonly archived?: 'active' | 'archived' | 'all';
|
||||||
|
readonly assessorId?: string;
|
||||||
}): Promise<AssessmentPage> {
|
}): Promise<AssessmentPage> {
|
||||||
const sp = new URLSearchParams();
|
const sp = new URLSearchParams();
|
||||||
sp.set('page', String(params.page));
|
sp.set('page', String(params.page));
|
||||||
@@ -438,6 +439,9 @@ export async function fetchAssessmentsPage(params: {
|
|||||||
if (params.archived !== undefined && params.archived !== 'active') {
|
if (params.archived !== undefined && params.archived !== 'active') {
|
||||||
sp.set('archived', params.archived);
|
sp.set('archived', params.archived);
|
||||||
}
|
}
|
||||||
|
if (params.assessorId !== undefined && params.assessorId !== '') {
|
||||||
|
sp.set('assessorId', params.assessorId);
|
||||||
|
}
|
||||||
return request<AssessmentPage>('GET', `/api/assessments?${sp.toString()}`);
|
return request<AssessmentPage>('GET', `/api/assessments?${sp.toString()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,9 +126,12 @@ export function Dashboard(): JSX.Element {
|
|||||||
return () => clearTimeout(t);
|
return () => clearTimeout(t);
|
||||||
}, [searchInput]);
|
}, [searchInput]);
|
||||||
|
|
||||||
|
// 销售仅可见本人发起的评估(行级数据隔离);风控/管理层可见全部。
|
||||||
|
const scopeAssessorId = role === '商务/销售' ? user?.id : undefined;
|
||||||
|
|
||||||
const loadList = useCallback(() => {
|
const loadList = useCallback(() => {
|
||||||
setLoading(true);
|
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) => {
|
.then((res) => {
|
||||||
setItems(res.items);
|
setItems(res.items);
|
||||||
setTotal(res.total);
|
setTotal(res.total);
|
||||||
@@ -136,7 +139,7 @@ export function Dashboard(): JSX.Element {
|
|||||||
})
|
})
|
||||||
.catch((err: unknown) => setError(err instanceof Error ? err.message : '加载失败'))
|
.catch((err: unknown) => setError(err instanceof Error ? err.message : '加载失败'))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [page, pageSize, statusFilter, search, archivedView]);
|
}, [page, pageSize, statusFilter, search, archivedView, scopeAssessorId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadList();
|
loadList();
|
||||||
@@ -146,7 +149,7 @@ export function Dashboard(): JSX.Element {
|
|||||||
fetchSummary().then(setSummary).catch(() => undefined);
|
fetchSummary().then(setSummary).catch(() => undefined);
|
||||||
const todoStatus = TODO_STATUS[role];
|
const todoStatus = TODO_STATUS[role];
|
||||||
if (todoStatus !== undefined) {
|
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))
|
.then((res) => setTodoItems(res.items))
|
||||||
.catch(() => setTodoItems([]));
|
.catch(() => setTodoItems([]));
|
||||||
} else {
|
} else {
|
||||||
@@ -169,7 +172,7 @@ export function Dashboard(): JSX.Element {
|
|||||||
} else {
|
} else {
|
||||||
setAssignments({});
|
setAssignments({});
|
||||||
}
|
}
|
||||||
}, [role, user?.username]);
|
}, [role, user?.username, user?.id, scopeAssessorId]);
|
||||||
|
|
||||||
/** 删除一条草稿并刷新草稿箱。 */
|
/** 删除一条草稿并刷新草稿箱。 */
|
||||||
const removeDraft = useCallback(async (id: string): Promise<void> => {
|
const removeDraft = useCallback(async (id: string): Promise<void> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user