大改动:全部人员用ID关联(JWT带uid + audit actor_id + assessorId改用户ID + 看板按ID匹配 + 历史回填)
- JWT 载荷增加 uid;登录返回 id;前端持久化 uid 并在变更请求中携带 userId - 操作人服务端解析(优先JWT.uid,回退body.userId),审计写入 actor_id + 当时显示名 - audit_logs 增加 actor_id 列;持久化与加载带 actorId - assessments.assessorId 改存用户ID,列表/详情按ID解析显示名(assessorName) - 看板待办「分给我的」改为按 userId 匹配;发起人显示真实姓名 - 审批线指派按 salesId(用户ID) 计算 - scripts/backfill-actor-ids.sql 回填历史(旧账号名→当前用户ID),本地+生产已执行
This commit is contained in:
@@ -0,0 +1,14 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
/**
|
||||||
|
* 审计日志增加 actor_id(操作人用户 ID 关联)。username 保留为当时显示名快照。
|
||||||
|
*/
|
||||||
|
|
||||||
|
exports.up = (pgm) => {
|
||||||
|
pgm.sql(`ALTER TABLE audit_logs ADD COLUMN IF NOT EXISTS actor_id TEXT;`);
|
||||||
|
pgm.sql(`CREATE INDEX IF NOT EXISTS idx_audit_actor ON audit_logs(actor_id);`);
|
||||||
|
};
|
||||||
|
|
||||||
|
exports.down = (pgm) => {
|
||||||
|
pgm.sql(`DROP INDEX IF EXISTS idx_audit_actor;`);
|
||||||
|
pgm.sql(`ALTER TABLE audit_logs DROP COLUMN IF EXISTS actor_id;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
-- 历史数据回填:把按"人名字符串"关联的记录改为按用户 ID 关联。
|
||||||
|
-- 处理旧账号名→当前账号名的改名映射,再解析为用户 ID。可重复执行(幂等)。
|
||||||
|
|
||||||
|
WITH alias(oldname, newname) AS (VALUES
|
||||||
|
('销售账号','张伟'), ('销售二','王芳'), ('销售三','李娜'),
|
||||||
|
('风控账号','刘洋'), ('风控二','陈静'),
|
||||||
|
('管理账号','赵磊'), ('管理二','孙莉'),
|
||||||
|
('系统账号','周强')
|
||||||
|
)
|
||||||
|
UPDATE assessments a
|
||||||
|
SET assessment = jsonb_set(a.assessment, '{assessorId}', to_jsonb(u.id))
|
||||||
|
FROM alias al JOIN users u ON u.username = al.newname
|
||||||
|
WHERE a.assessment->>'assessorId' = al.oldname;
|
||||||
|
|
||||||
|
-- assessorId 已是当前用户名 → 也转为 ID
|
||||||
|
UPDATE assessments a
|
||||||
|
SET assessment = jsonb_set(a.assessment, '{assessorId}', to_jsonb(u.id))
|
||||||
|
FROM users u
|
||||||
|
WHERE a.assessment->>'assessorId' = u.username;
|
||||||
|
|
||||||
|
-- 审计:按旧名映射回填 actor_id,并把 username 刷新为当前账号名
|
||||||
|
WITH alias(oldname, newname) AS (VALUES
|
||||||
|
('销售账号','张伟'), ('销售二','王芳'), ('销售三','李娜'),
|
||||||
|
('风控账号','刘洋'), ('风控二','陈静'),
|
||||||
|
('管理账号','赵磊'), ('管理二','孙莉'),
|
||||||
|
('系统账号','周强')
|
||||||
|
)
|
||||||
|
UPDATE audit_logs al2
|
||||||
|
SET actor_id = u.id, username = u.username
|
||||||
|
FROM alias al JOIN users u ON u.username = al.newname
|
||||||
|
WHERE al2.actor_id IS NULL AND al2.username = al.oldname;
|
||||||
|
|
||||||
|
-- 审计:username 已是当前账号名 → 回填 actor_id
|
||||||
|
UPDATE audit_logs al2
|
||||||
|
SET actor_id = u.id
|
||||||
|
FROM users u
|
||||||
|
WHERE al2.actor_id IS NULL AND al2.username = u.username;
|
||||||
+4
-2
@@ -13,6 +13,8 @@ export type AuthRole = '商务/销售' | '风控' | '管理层' | '系统管理
|
|||||||
export interface AuthPayload {
|
export interface AuthPayload {
|
||||||
username: string;
|
username: string;
|
||||||
role: AuthRole;
|
role: AuthRole;
|
||||||
|
/** 用户 ID(稳定锚,用于记录关联)。旧 token 可能缺省。 */
|
||||||
|
uid?: string;
|
||||||
iat: number;
|
iat: number;
|
||||||
exp: number;
|
exp: number;
|
||||||
}
|
}
|
||||||
@@ -48,9 +50,9 @@ function verify(token: string): AuthPayload | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 签发 JWT(登录成功后调用)。 */
|
/** 签发 JWT(登录成功后调用)。 */
|
||||||
export function issueToken(username: string, role: AuthRole): string {
|
export function issueToken(username: string, role: AuthRole, uid?: string): string {
|
||||||
const now = Math.floor(Date.now() / 1000);
|
const now = Math.floor(Date.now() / 1000);
|
||||||
return sign({ username, role, iat: now, exp: now + 86400 });
|
return sign({ username, role, ...(uid !== undefined ? { uid } : {}), iat: now, exp: now + 86400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 无需鉴权的公共路径(登录与健康检查)。 */
|
/** 无需鉴权的公共路径(登录与健康检查)。 */
|
||||||
|
|||||||
@@ -35,7 +35,10 @@ export function poolOptionsFromEnv(): Pick<
|
|||||||
/** 操作记录条目(与服务端一致)。 */
|
/** 操作记录条目(与服务端一致)。 */
|
||||||
export interface AuditLogEntry {
|
export interface AuditLogEntry {
|
||||||
readonly role: string;
|
readonly role: string;
|
||||||
|
/** 操作人显示名(当时快照)。 */
|
||||||
readonly username: string;
|
readonly username: string;
|
||||||
|
/** 操作人用户 ID(关联锚;历史记录可能缺省)。 */
|
||||||
|
readonly actorId?: string;
|
||||||
readonly action: string;
|
readonly action: string;
|
||||||
readonly comment?: string;
|
readonly comment?: string;
|
||||||
readonly timestamp: string;
|
readonly timestamp: string;
|
||||||
@@ -362,7 +365,7 @@ export async function loadWorkflowState(pool: Pool): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const a = await pool.query(
|
const a = await pool.query(
|
||||||
'SELECT assessment_id, role, username, action, comment, ts FROM audit_logs ORDER BY id ASC',
|
'SELECT assessment_id, role, username, actor_id, action, comment, ts FROM audit_logs ORDER BY id ASC',
|
||||||
);
|
);
|
||||||
for (const r of a.rows as Array<Record<string, unknown>>) {
|
for (const r of a.rows as Array<Record<string, unknown>>) {
|
||||||
const id = String(r.assessment_id);
|
const id = String(r.assessment_id);
|
||||||
@@ -370,6 +373,7 @@ export async function loadWorkflowState(pool: Pool): Promise<{
|
|||||||
list.push({
|
list.push({
|
||||||
role: String(r.role),
|
role: String(r.role),
|
||||||
username: String(r.username),
|
username: String(r.username),
|
||||||
|
...(r.actor_id != null ? { actorId: String(r.actor_id) } : {}),
|
||||||
action: String(r.action),
|
action: String(r.action),
|
||||||
...(r.comment != null ? { comment: String(r.comment) } : {}),
|
...(r.comment != null ? { comment: String(r.comment) } : {}),
|
||||||
timestamp: toIso(r.ts),
|
timestamp: toIso(r.ts),
|
||||||
@@ -399,9 +403,9 @@ export async function persistStatus(pool: Pool, id: string, status: string): Pro
|
|||||||
/** 追加一条操作记录。 */
|
/** 追加一条操作记录。 */
|
||||||
export async function persistAudit(pool: Pool, id: string, entry: AuditLogEntry): Promise<void> {
|
export async function persistAudit(pool: Pool, id: string, entry: AuditLogEntry): Promise<void> {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`INSERT INTO audit_logs(assessment_id, role, username, action, comment, ts)
|
`INSERT INTO audit_logs(assessment_id, role, username, actor_id, action, comment, ts)
|
||||||
VALUES($1, $2, $3, $4, $5, $6)`,
|
VALUES($1, $2, $3, $4, $5, $6, $7)`,
|
||||||
[id, entry.role, entry.username, entry.action, entry.comment ?? null, entry.timestamp],
|
[id, entry.role, entry.username, entry.actorId ?? null, entry.action, entry.comment ?? null, entry.timestamp],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -55,6 +55,16 @@ export async function listUsers(pool: pg.Pool): Promise<UserRecord[]> {
|
|||||||
return (res.rows as Array<Record<string, unknown>>).map(mapRow);
|
return (res.rows as Array<Record<string, unknown>>).map(mapRow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 按 ID 取用户(不含密码)。 */
|
||||||
|
export async function getUserById(pool: pg.Pool, id: string): Promise<UserRecord | null> {
|
||||||
|
const res = await pool.query(
|
||||||
|
'SELECT id, username, display_name, role, active, created_at FROM users WHERE id = $1',
|
||||||
|
[id],
|
||||||
|
);
|
||||||
|
const r = (res.rows as Array<Record<string, unknown>>)[0];
|
||||||
|
return r ? mapRow(r) : null;
|
||||||
|
}
|
||||||
|
|
||||||
/** 按用户名取用户(含密码哈希,仅供登录校验内部使用)。 */
|
/** 按用户名取用户(含密码哈希,仅供登录校验内部使用)。 */
|
||||||
export async function getUserByUsername(
|
export async function getUserByUsername(
|
||||||
pool: pg.Pool,
|
pool: pg.Pool,
|
||||||
|
|||||||
+85
-25
@@ -72,6 +72,7 @@ import {
|
|||||||
ensureSeedUsers,
|
ensureSeedUsers,
|
||||||
USER_ROLES,
|
USER_ROLES,
|
||||||
type UserRole,
|
type UserRole,
|
||||||
|
getUserById,
|
||||||
getApprovalConfig,
|
getApprovalConfig,
|
||||||
saveApprovalConfig,
|
saveApprovalConfig,
|
||||||
ensureApprovalConfig,
|
ensureApprovalConfig,
|
||||||
@@ -123,7 +124,7 @@ import {
|
|||||||
type SynthesisContext,
|
type SynthesisContext,
|
||||||
} from '../llm/index.js';
|
} from '../llm/index.js';
|
||||||
import type { ResolvedDataPoint } from '../adapters/index.js';
|
import type { ResolvedDataPoint } from '../adapters/index.js';
|
||||||
import { authMiddleware, requireRole, issueToken, type AuthRole } from '../auth/index.js';
|
import { authMiddleware, requireRole, issueToken, type AuthRole, type AuthPayload } from '../auth/index.js';
|
||||||
import type { BusinessType, Industry } from '../domain/common.js';
|
import type { BusinessType, Industry } from '../domain/common.js';
|
||||||
import type { Indicator } from '../domain/index.js';
|
import type { Indicator } from '../domain/index.js';
|
||||||
|
|
||||||
@@ -160,8 +161,8 @@ app.post('/api/auth/login', async (c) => {
|
|||||||
if (user === null || !user.active || !verifyPassword(password, user.passwordHash)) {
|
if (user === null || !user.active || !verifyPassword(password, user.passwordHash)) {
|
||||||
return c.json({ error: '用户名或密码错误,或账号已停用' }, 401);
|
return c.json({ error: '用户名或密码错误,或账号已停用' }, 401);
|
||||||
}
|
}
|
||||||
const token = issueToken(user.username, user.role as AuthRole);
|
const token = issueToken(user.username, user.role as AuthRole, user.id);
|
||||||
return c.json({ token, username: user.username, role: user.role, displayName: user.displayName });
|
return c.json({ token, id: user.id, username: user.username, role: user.role, displayName: user.displayName });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无数据库(演示模式):按传入角色签发,兼容旧行为。
|
// 无数据库(演示模式):按传入角色签发,兼容旧行为。
|
||||||
@@ -635,9 +636,11 @@ app.post('/api/assessments/run', async (c) => {
|
|||||||
// 由销售确认资料无误后点「申报」按钮才报送风控审核。
|
// 由销售确认资料无误后点「申报」按钮才报送风控审核。
|
||||||
{
|
{
|
||||||
setStatus(result.assessment.id, 'draft');
|
setStatus(result.assessment.id, 'draft');
|
||||||
|
const actC = await resolveActor(c, body.assessorId, undefined);
|
||||||
const submitEntry: AuditLogEntry = {
|
const submitEntry: AuditLogEntry = {
|
||||||
role: '商务/销售',
|
role: '商务/销售',
|
||||||
username: result.assessment.assessorId ?? '系统',
|
username: actC.name,
|
||||||
|
...(actC.id !== null ? { actorId: actC.id } : {}),
|
||||||
action: isEdit ? '编辑并重新评估(草稿待申报)' : '创建评估(草稿待申报)',
|
action: isEdit ? '编辑并重新评估(草稿待申报)' : '创建评估(草稿待申报)',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -1538,6 +1541,36 @@ function setStatus(id: string, status: WorkflowStatus): void {
|
|||||||
workflowStatus.set(id, status);
|
workflowStatus.set(id, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析操作人:优先 JWT(uid),否则用 body 传入的 userId;并解析为当前显示名(人名)。
|
||||||
|
* 返回 { id, name },供审计 actor_id 与显示名快照使用。
|
||||||
|
*/
|
||||||
|
async function resolveActor(
|
||||||
|
c: import('hono').Context,
|
||||||
|
bodyUserId?: string,
|
||||||
|
fallbackName?: string,
|
||||||
|
): Promise<{ id: string | null; name: string }> {
|
||||||
|
const payload = c.get('user') as AuthPayload | undefined;
|
||||||
|
const id = payload?.uid ?? bodyUserId ?? null;
|
||||||
|
let name = payload?.username ?? fallbackName ?? (id ?? '系统');
|
||||||
|
if (pool !== null && id) {
|
||||||
|
const u = await getUserById(pool, id).catch(() => null);
|
||||||
|
if (u) name = u.username;
|
||||||
|
}
|
||||||
|
return { id, name };
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用户 ID → 用户名(人名) 映射,供把记录中的 ID 解析为显示名。 */
|
||||||
|
async function userIdNameMap(): Promise<Record<string, string>> {
|
||||||
|
if (pool === null) return {};
|
||||||
|
try {
|
||||||
|
const us = await listUsers(pool);
|
||||||
|
return Object.fromEntries(us.map((u) => [u.id, u.username]));
|
||||||
|
} catch {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function addAuditLog(id: string, entry: AuditLogEntry): void {
|
function addAuditLog(id: string, entry: AuditLogEntry): void {
|
||||||
const existing = auditLogs.get(id) ?? [];
|
const existing = auditLogs.get(id) ?? [];
|
||||||
auditLogs.set(id, [...existing, entry]);
|
auditLogs.set(id, [...existing, entry]);
|
||||||
@@ -1601,7 +1634,12 @@ app.get('/api/assessments', async (c) => {
|
|||||||
...(statusFilter !== undefined ? { status: statusFilter } : {}),
|
...(statusFilter !== undefined ? { status: statusFilter } : {}),
|
||||||
...(q !== undefined ? { q } : {}),
|
...(q !== undefined ? { q } : {}),
|
||||||
});
|
});
|
||||||
return c.json({ items, total, page, pageSize: size });
|
const nameMap = await userIdNameMap();
|
||||||
|
const items2 = items.map((it) => ({
|
||||||
|
...it,
|
||||||
|
assessorName: nameMap[String(it.assessorId)] ?? String(it.assessorId ?? ''),
|
||||||
|
}));
|
||||||
|
return c.json({ items: items2, total, page, pageSize: size });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 无 DB:内存过滤 + 分页(小数据量)。
|
// 无 DB:内存过滤 + 分页(小数据量)。
|
||||||
@@ -1695,6 +1733,8 @@ app.get('/api/assessments/:id', async (c) => {
|
|||||||
}
|
}
|
||||||
// 审批人指派(按审批线,软约束)。
|
// 审批人指派(按审批线,软约束)。
|
||||||
const assignment = pool !== null ? await getAssignment(pool, id).catch(() => null) : null;
|
const assignment = pool !== null ? await getAssignment(pool, id).catch(() => null) : null;
|
||||||
|
const assessorId = (record.assessment as unknown as { assessorId?: string }).assessorId;
|
||||||
|
const assessorName = assessorId !== undefined ? (await userIdNameMap())[assessorId] ?? assessorId : null;
|
||||||
return c.json({
|
return c.json({
|
||||||
assessment: record.assessment,
|
assessment: record.assessment,
|
||||||
report: record.report,
|
report: record.report,
|
||||||
@@ -1710,6 +1750,7 @@ app.get('/api/assessments/:id', async (c) => {
|
|||||||
profitabilityInputs: profitabilityInputsById.get(id) ?? null,
|
profitabilityInputs: profitabilityInputsById.get(id) ?? null,
|
||||||
expiresAt,
|
expiresAt,
|
||||||
assignment,
|
assignment,
|
||||||
|
assessorName,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1756,10 +1797,12 @@ app.post('/api/assessments/:id/archive', async (c) => {
|
|||||||
return c.json({ error: '评估记录不存在' }, 404);
|
return c.json({ error: '评估记录不存在' }, 404);
|
||||||
}
|
}
|
||||||
const body = await c.req
|
const body = await c.req
|
||||||
.json<{ archived?: boolean; user?: string }>()
|
.json<{ archived?: boolean; user?: string; userId?: string }>()
|
||||||
.catch(() => ({}) as { archived?: boolean; user?: string });
|
.catch(() => ({}) as { archived?: boolean; user?: string; userId?: string });
|
||||||
const archived = body.archived !== false; // 缺省视为归档
|
const archived = body.archived !== false; // 缺省视为归档
|
||||||
const username = body.user ?? '系统';
|
const actA = await resolveActor(c, body.userId, body.user);
|
||||||
|
const username = actA.name;
|
||||||
|
const actorIdPart = actA.id !== null ? { actorId: actA.id } : {};
|
||||||
|
|
||||||
if (archived) {
|
if (archived) {
|
||||||
const current = getStatus(id) ?? 'pending_risk_review';
|
const current = getStatus(id) ?? 'pending_risk_review';
|
||||||
@@ -1770,6 +1813,7 @@ app.post('/api/assessments/:id/archive', async (c) => {
|
|||||||
const entry: AuditLogEntry = {
|
const entry: AuditLogEntry = {
|
||||||
role: '流程',
|
role: '流程',
|
||||||
username,
|
username,
|
||||||
|
...actorIdPart,
|
||||||
action: '归档',
|
action: '归档',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -1789,6 +1833,7 @@ app.post('/api/assessments/:id/archive', async (c) => {
|
|||||||
const entry: AuditLogEntry = {
|
const entry: AuditLogEntry = {
|
||||||
role: '流程',
|
role: '流程',
|
||||||
username,
|
username,
|
||||||
|
...actorIdPart,
|
||||||
action: '取消归档(重置为待风控审核)',
|
action: '取消归档(重置为待风控审核)',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -1869,11 +1914,12 @@ app.post('/api/assessments/:id/review', requireRole('风控'), async (c) => {
|
|||||||
if (record === undefined) {
|
if (record === undefined) {
|
||||||
return c.json({ error: '评估记录不存在' }, 404);
|
return c.json({ error: '评估记录不存在' }, 404);
|
||||||
}
|
}
|
||||||
const body = await c.req.json<{ action: 'approve' | 'reject'; comment?: string; user: string }>();
|
const body = await c.req.json<{ action: 'approve' | 'reject'; comment?: string; user?: string; userId?: string }>();
|
||||||
const current = getStatus(id) ?? 'pending_risk_review';
|
const current = getStatus(id) ?? 'pending_risk_review';
|
||||||
if (current !== 'pending_risk_review') {
|
if (current !== 'pending_risk_review') {
|
||||||
return c.json({ error: `当前状态为 ${current},无法审核` }, 400);
|
return c.json({ error: `当前状态为 ${current},无法审核` }, 400);
|
||||||
}
|
}
|
||||||
|
const act = await resolveActor(c, body.userId, body.user);
|
||||||
let newStatus: WorkflowStatus;
|
let newStatus: WorkflowStatus;
|
||||||
let actionText: string;
|
let actionText: string;
|
||||||
if (body.action === 'approve') {
|
if (body.action === 'approve') {
|
||||||
@@ -1918,7 +1964,8 @@ app.post('/api/assessments/:id/review', requireRole('风控'), async (c) => {
|
|||||||
setStatus(id, newStatus);
|
setStatus(id, newStatus);
|
||||||
const reviewEntry: AuditLogEntry = {
|
const reviewEntry: AuditLogEntry = {
|
||||||
role: '风控',
|
role: '风控',
|
||||||
username: body.user,
|
username: act.name,
|
||||||
|
...(act.id !== null ? { actorId: act.id } : {}),
|
||||||
action: actionText,
|
action: actionText,
|
||||||
...(body.comment !== undefined && body.comment !== '' ? { comment: body.comment } : {}),
|
...(body.comment !== undefined && body.comment !== '' ? { comment: body.comment } : {}),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -1938,7 +1985,7 @@ app.post('/api/assessments/:id/approve', requireRole('管理层'), async (c) =>
|
|||||||
if (record === undefined) {
|
if (record === undefined) {
|
||||||
return c.json({ error: '评估记录不存在' }, 404);
|
return c.json({ error: '评估记录不存在' }, 404);
|
||||||
}
|
}
|
||||||
const body = await c.req.json<{ action: 'approve' | 'reject' | 'abandon'; comment?: string; user: string; rejectTo?: 'risk' | 'origin' }>();
|
const body = await c.req.json<{ action: 'approve' | 'reject' | 'abandon'; comment?: string; user?: string; userId?: string; rejectTo?: 'risk' | 'origin' }>();
|
||||||
const current = getStatus(id) ?? 'pending_risk_review';
|
const current = getStatus(id) ?? 'pending_risk_review';
|
||||||
if (current !== 'risk_reviewed' && current !== 'pending_risk_review') {
|
if (current !== 'risk_reviewed' && current !== 'pending_risk_review') {
|
||||||
return c.json({ error: `当前状态为 ${current},无法审批` }, 400);
|
return c.json({ error: `当前状态为 ${current},无法审批` }, 400);
|
||||||
@@ -1962,9 +2009,11 @@ app.post('/api/assessments/:id/approve', requireRole('管理层'), async (c) =>
|
|||||||
actionText = '管理层驳回(退回风控复审)';
|
actionText = '管理层驳回(退回风控复审)';
|
||||||
}
|
}
|
||||||
setStatus(id, newStatus);
|
setStatus(id, newStatus);
|
||||||
|
const act = await resolveActor(c, body.userId, body.user);
|
||||||
const approveEntry: AuditLogEntry = {
|
const approveEntry: AuditLogEntry = {
|
||||||
role: '管理层',
|
role: '管理层',
|
||||||
username: body.user,
|
username: act.name,
|
||||||
|
...(act.id !== null ? { actorId: act.id } : {}),
|
||||||
action: actionText,
|
action: actionText,
|
||||||
...(body.comment !== undefined && body.comment !== '' ? { comment: body.comment } : {}),
|
...(body.comment !== undefined && body.comment !== '' ? { comment: body.comment } : {}),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -1987,7 +2036,7 @@ app.post('/api/assessments/:id/override', requireRole('管理层'), async (c) =>
|
|||||||
if (record === undefined) {
|
if (record === undefined) {
|
||||||
return c.json({ error: '评估记录不存在' }, 404);
|
return c.json({ error: '评估记录不存在' }, 404);
|
||||||
}
|
}
|
||||||
const body = await c.req.json<{ status: WorkflowStatus; user: string; comment?: string }>();
|
const body = await c.req.json<{ status: WorkflowStatus; user?: string; userId?: string; comment?: string }>();
|
||||||
const current = getStatus(id) ?? 'pending_risk_review';
|
const current = getStatus(id) ?? 'pending_risk_review';
|
||||||
if (current !== 'approved' && current !== 'abandoned') {
|
if (current !== 'approved' && current !== 'abandoned') {
|
||||||
return c.json({ error: '仅可对「已通过」或「已放弃」的项目直接调整状态' }, 400);
|
return c.json({ error: '仅可对「已通过」或「已放弃」的项目直接调整状态' }, 400);
|
||||||
@@ -2004,9 +2053,11 @@ app.post('/api/assessments/:id/override', requireRole('管理层'), async (c) =>
|
|||||||
return c.json({ error: `非法目标状态:${body.status}` }, 400);
|
return c.json({ error: `非法目标状态:${body.status}` }, 400);
|
||||||
}
|
}
|
||||||
setStatus(id, body.status);
|
setStatus(id, body.status);
|
||||||
|
const actOv = await resolveActor(c, body.userId, body.user);
|
||||||
const entry: AuditLogEntry = {
|
const entry: AuditLogEntry = {
|
||||||
role: '管理层',
|
role: '管理层',
|
||||||
username: body.user,
|
username: actOv.name,
|
||||||
|
...(actOv.id !== null ? { actorId: actOv.id } : {}),
|
||||||
action: `管理层直接调整状态(${STATUS_CN[current]} → ${STATUS_CN[body.status]})`,
|
action: `管理层直接调整状态(${STATUS_CN[current]} → ${STATUS_CN[body.status]})`,
|
||||||
...(body.comment !== undefined && body.comment !== '' ? { comment: body.comment } : {}),
|
...(body.comment !== undefined && body.comment !== '' ? { comment: body.comment } : {}),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -2038,6 +2089,7 @@ app.put('/api/assessments/:id', async (c) => {
|
|||||||
region?: string;
|
region?: string;
|
||||||
profitabilityInputs?: ProfitabilityInputs;
|
profitabilityInputs?: ProfitabilityInputs;
|
||||||
user?: string;
|
user?: string;
|
||||||
|
userId?: string;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
// 更新评估记录字段
|
// 更新评估记录字段
|
||||||
@@ -2161,9 +2213,11 @@ app.put('/api/assessments/:id', async (c) => {
|
|||||||
await flushStore();
|
await flushStore();
|
||||||
|
|
||||||
// 审计留痕
|
// 审计留痕
|
||||||
|
const actE = await resolveActor(c, body.userId, body.user);
|
||||||
const editEntry: AuditLogEntry = {
|
const editEntry: AuditLogEntry = {
|
||||||
role: '商务/销售',
|
role: '商务/销售',
|
||||||
username: body.user ?? '销售',
|
username: actE.name,
|
||||||
|
...(actE.id !== null ? { actorId: actE.id } : {}),
|
||||||
action: '修改项目资料(驳回后调整)',
|
action: '修改项目资料(驳回后调整)',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -2185,7 +2239,7 @@ app.post('/api/assessments/:id/redline-verdict', requireRole('风控', '管理
|
|||||||
if (record === undefined) {
|
if (record === undefined) {
|
||||||
return c.json({ error: '评估记录不存在' }, 404);
|
return c.json({ error: '评估记录不存在' }, 404);
|
||||||
}
|
}
|
||||||
const body = await c.req.json<{ redlineId: string; status: '命中' | '未命中'; note?: string; user: string; role?: string; title?: string }>();
|
const body = await c.req.json<{ redlineId: string; status: '命中' | '未命中'; note?: string; user?: string; userId?: string; role?: string; title?: string }>();
|
||||||
if (body.status !== '命中' && body.status !== '未命中') {
|
if (body.status !== '命中' && body.status !== '未命中') {
|
||||||
return c.json({ error: '裁定状态须为「命中」或「未命中」' }, 400);
|
return c.json({ error: '裁定状态须为「命中」或「未命中」' }, 400);
|
||||||
}
|
}
|
||||||
@@ -2232,9 +2286,11 @@ app.post('/api/assessments/:id/redline-verdict', requireRole('风控', '管理
|
|||||||
store.put({ ...record, assessment: a, report: updatedReport, savedAt: new Date().toISOString() });
|
store.put({ ...record, assessment: a, report: updatedReport, savedAt: new Date().toISOString() });
|
||||||
await flushStore();
|
await flushStore();
|
||||||
|
|
||||||
|
const actV = await resolveActor(c, body.userId, body.user);
|
||||||
const entry: AuditLogEntry = {
|
const entry: AuditLogEntry = {
|
||||||
role: body.role ?? '风控',
|
role: body.role ?? '风控',
|
||||||
username: body.user,
|
username: actV.name,
|
||||||
|
...(actV.id !== null ? { actorId: actV.id } : {}),
|
||||||
action: `人工裁定红线「${title}」→ ${body.status}`,
|
action: `人工裁定红线「${title}」→ ${body.status}`,
|
||||||
...(note !== '' ? { comment: note } : {}),
|
...(note !== '' ? { comment: note } : {}),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -2252,12 +2308,11 @@ app.post('/api/assessments/:id/redline-verdict', requireRole('风控', '管理
|
|||||||
* 按审批线为评估指派审批人(软约束,仅展示/过滤/审计)。
|
* 按审批线为评估指派审批人(软约束,仅展示/过滤/审计)。
|
||||||
* 据提交销售解析审批线 → 指定风控/管理层;写入 assessment_assignments。
|
* 据提交销售解析审批线 → 指定风控/管理层;写入 assessment_assignments。
|
||||||
*/
|
*/
|
||||||
async function assignApprovers(id: string, salesUsername: string): Promise<void> {
|
async function assignApprovers(id: string, salesId: string | undefined): Promise<void> {
|
||||||
if (pool === null) return;
|
if (pool === null) return;
|
||||||
const cfg = await getApprovalConfig(pool);
|
const cfg = await getApprovalConfig(pool);
|
||||||
if (!cfg.assignment.enabled) return;
|
if (!cfg.assignment.enabled) return;
|
||||||
const sales = await getUserByUsername(pool, salesUsername);
|
const { riskReviewerId, managerId } = resolveAssignees(cfg.assignment, salesId);
|
||||||
const { riskReviewerId, managerId } = resolveAssignees(cfg.assignment, sales?.id);
|
|
||||||
const users = await listUsers(pool);
|
const users = await listUsers(pool);
|
||||||
const nameOf = (uid: string | null): string | null => {
|
const nameOf = (uid: string | null): string | null => {
|
||||||
if (uid === null) return null;
|
if (uid === null) return null;
|
||||||
@@ -2280,15 +2335,17 @@ app.post('/api/assessments/:id/submit', async (c) => {
|
|||||||
if (record === undefined) {
|
if (record === undefined) {
|
||||||
return c.json({ error: '评估记录不存在' }, 404);
|
return c.json({ error: '评估记录不存在' }, 404);
|
||||||
}
|
}
|
||||||
const body = await c.req.json<{ user: string }>();
|
const body = await c.req.json<{ user?: string; userId?: string }>();
|
||||||
const current = getStatus(id) ?? 'pending_risk_review';
|
const current = getStatus(id) ?? 'pending_risk_review';
|
||||||
if (current !== 'draft') {
|
if (current !== 'draft') {
|
||||||
return c.json({ error: `当前状态为 ${current},无法申报` }, 400);
|
return c.json({ error: `当前状态为 ${current},无法申报` }, 400);
|
||||||
}
|
}
|
||||||
|
const actS = await resolveActor(c, body.userId, body.user);
|
||||||
setStatus(id, 'pending_risk_review');
|
setStatus(id, 'pending_risk_review');
|
||||||
const submitEntry: AuditLogEntry = {
|
const submitEntry: AuditLogEntry = {
|
||||||
role: '商务/销售',
|
role: '商务/销售',
|
||||||
username: body.user,
|
username: actS.name,
|
||||||
|
...(actS.id !== null ? { actorId: actS.id } : {}),
|
||||||
action: '申报(报送风控审核)',
|
action: '申报(报送风控审核)',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -2296,7 +2353,7 @@ app.post('/api/assessments/:id/submit', async (c) => {
|
|||||||
if (pool !== null) {
|
if (pool !== null) {
|
||||||
await persistStatus(pool, id, 'pending_risk_review');
|
await persistStatus(pool, id, 'pending_risk_review');
|
||||||
await persistAudit(pool, id, submitEntry);
|
await persistAudit(pool, id, submitEntry);
|
||||||
await assignApprovers(id, body.user).catch(() => undefined);
|
await assignApprovers(id, actS.id ?? undefined).catch(() => undefined);
|
||||||
}
|
}
|
||||||
return c.json({ status: 'pending_risk_review' });
|
return c.json({ status: 'pending_risk_review' });
|
||||||
});
|
});
|
||||||
@@ -2308,15 +2365,17 @@ app.post('/api/assessments/:id/resubmit', async (c) => {
|
|||||||
if (record === undefined) {
|
if (record === undefined) {
|
||||||
return c.json({ error: '评估记录不存在' }, 404);
|
return c.json({ error: '评估记录不存在' }, 404);
|
||||||
}
|
}
|
||||||
const body = await c.req.json<{ user: string }>();
|
const body = await c.req.json<{ user?: string; userId?: string }>();
|
||||||
const current = getStatus(id) ?? 'pending_risk_review';
|
const current = getStatus(id) ?? 'pending_risk_review';
|
||||||
if (current !== 'rejected') {
|
if (current !== 'rejected') {
|
||||||
return c.json({ error: `当前状态为 ${current},无需重新提交` }, 400);
|
return c.json({ error: `当前状态为 ${current},无需重新提交` }, 400);
|
||||||
}
|
}
|
||||||
|
const actR = await resolveActor(c, body.userId, body.user);
|
||||||
setStatus(id, 'pending_risk_review');
|
setStatus(id, 'pending_risk_review');
|
||||||
const resubmitEntry: AuditLogEntry = {
|
const resubmitEntry: AuditLogEntry = {
|
||||||
role: '商务/销售',
|
role: '商务/销售',
|
||||||
username: body.user,
|
username: actR.name,
|
||||||
|
...(actR.id !== null ? { actorId: actR.id } : {}),
|
||||||
action: '重新提交评估',
|
action: '重新提交评估',
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -2324,6 +2383,7 @@ app.post('/api/assessments/:id/resubmit', async (c) => {
|
|||||||
if (pool !== null) {
|
if (pool !== null) {
|
||||||
await persistStatus(pool, id, 'pending_risk_review');
|
await persistStatus(pool, id, 'pending_risk_review');
|
||||||
await persistAudit(pool, id, resubmitEntry);
|
await persistAudit(pool, id, resubmitEntry);
|
||||||
|
await assignApprovers(id, actR.id ?? undefined).catch(() => undefined);
|
||||||
}
|
}
|
||||||
return c.json({ status: 'pending_risk_review' });
|
return c.json({ status: 'pending_risk_review' });
|
||||||
});
|
});
|
||||||
|
|||||||
+21
-4
@@ -24,6 +24,15 @@ function authHeader(): Record<string, string> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 当前登录用户 ID(用于在记录中以 ID 关联操作人;演示模式下作为 body 兜底)。 */
|
||||||
|
export function currentUserId(): string | undefined {
|
||||||
|
try {
|
||||||
|
return localStorage.getItem('risk-agent-uid') ?? undefined;
|
||||||
|
} catch {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/** API 调用错误。 */
|
/** API 调用错误。 */
|
||||||
export class ApiError extends Error {
|
export class ApiError extends Error {
|
||||||
constructor(
|
constructor(
|
||||||
@@ -118,6 +127,8 @@ export interface AssessmentListItem {
|
|||||||
readonly acceptability?: string;
|
readonly acceptability?: string;
|
||||||
readonly createdAt: string;
|
readonly createdAt: string;
|
||||||
readonly assessorId: string;
|
readonly assessorId: string;
|
||||||
|
/** 发起人显示名(按 assessorId 解析)。 */
|
||||||
|
readonly assessorName?: string;
|
||||||
readonly status: WorkflowStatus;
|
readonly status: WorkflowStatus;
|
||||||
readonly auditLog: AuditLogEntry[];
|
readonly auditLog: AuditLogEntry[];
|
||||||
readonly archived?: boolean;
|
readonly archived?: boolean;
|
||||||
@@ -439,6 +450,7 @@ export async function archiveAssessment(
|
|||||||
return request('POST', `/api/assessments/${id}/archive`, {
|
return request('POST', `/api/assessments/${id}/archive`, {
|
||||||
archived,
|
archived,
|
||||||
...(user !== undefined ? { user } : {}),
|
...(user !== undefined ? { user } : {}),
|
||||||
|
...(currentUserId() !== undefined ? { userId: currentUserId() } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -473,6 +485,8 @@ export interface AssessmentDetailResponse {
|
|||||||
readonly expiresAt?: string | null;
|
readonly expiresAt?: string | null;
|
||||||
/** 审批人指派(按审批线,软约束)。 */
|
/** 审批人指派(按审批线,软约束)。 */
|
||||||
readonly assignment?: { riskReviewerName: string | null; managerName: string | null } | null;
|
readonly assignment?: { riskReviewerName: string | null; managerName: string | null } | null;
|
||||||
|
/** 发起人显示名(按 assessorId 解析)。 */
|
||||||
|
readonly assessorName?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 获取单条评估详情。 */
|
/** 获取单条评估详情。 */
|
||||||
@@ -510,6 +524,7 @@ export async function reviewAssessment(
|
|||||||
return request('POST', `/api/assessments/${id}/review`, {
|
return request('POST', `/api/assessments/${id}/review`, {
|
||||||
action,
|
action,
|
||||||
user,
|
user,
|
||||||
|
...(currentUserId() !== undefined ? { userId: currentUserId() } : {}),
|
||||||
...(comment !== undefined && comment !== '' ? { comment } : {}),
|
...(comment !== undefined && comment !== '' ? { comment } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -525,6 +540,7 @@ export async function approveAssessment(
|
|||||||
return request('POST', `/api/assessments/${id}/approve`, {
|
return request('POST', `/api/assessments/${id}/approve`, {
|
||||||
action,
|
action,
|
||||||
user,
|
user,
|
||||||
|
...(currentUserId() !== undefined ? { userId: currentUserId() } : {}),
|
||||||
...(comment !== undefined && comment !== '' ? { comment } : {}),
|
...(comment !== undefined && comment !== '' ? { comment } : {}),
|
||||||
...(rejectTo !== undefined ? { rejectTo } : {}),
|
...(rejectTo !== undefined ? { rejectTo } : {}),
|
||||||
});
|
});
|
||||||
@@ -532,12 +548,12 @@ export async function approveAssessment(
|
|||||||
|
|
||||||
/** 重新提交评估。 */
|
/** 重新提交评估。 */
|
||||||
export async function resubmitAssessment(id: string, user: string): Promise<{ status: WorkflowStatus }> {
|
export async function resubmitAssessment(id: string, user: string): Promise<{ status: WorkflowStatus }> {
|
||||||
return request('POST', `/api/assessments/${id}/resubmit`, { user });
|
return request('POST', `/api/assessments/${id}/resubmit`, { user, ...(currentUserId() !== undefined ? { userId: currentUserId() } : {}) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 申报:将草稿评估报送风控审核。 */
|
/** 申报:将草稿评估报送风控审核。 */
|
||||||
export async function submitAssessment(id: string, user: string): Promise<{ status: WorkflowStatus }> {
|
export async function submitAssessment(id: string, user: string): Promise<{ status: WorkflowStatus }> {
|
||||||
return request('POST', `/api/assessments/${id}/submit`, { user });
|
return request('POST', `/api/assessments/${id}/submit`, { user, ...(currentUserId() !== undefined ? { userId: currentUserId() } : {}) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 应用预测准确度校准(调整目标净利率基准,管理层)。 */
|
/** 应用预测准确度校准(调整目标净利率基准,管理层)。 */
|
||||||
@@ -550,7 +566,7 @@ export async function submitRedlineVerdict(
|
|||||||
id: string,
|
id: string,
|
||||||
params: { redlineId: string; status: '命中' | '未命中'; note?: string; user: string; role?: string; title?: string },
|
params: { redlineId: string; status: '命中' | '未命中'; note?: string; user: string; role?: string; title?: string },
|
||||||
): Promise<{ redlineId: string; status: string; acceptability: string }> {
|
): Promise<{ redlineId: string; status: string; acceptability: string }> {
|
||||||
return request('POST', `/api/assessments/${id}/redline-verdict`, params);
|
return request('POST', `/api/assessments/${id}/redline-verdict`, { ...params, ...(currentUserId() !== undefined ? { userId: currentUserId() } : {}) });
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 管理层对「已通过」或「已放弃」项目直接调整工作流状态(留痕)。 */
|
/** 管理层对「已通过」或「已放弃」项目直接调整工作流状态(留痕)。 */
|
||||||
@@ -563,6 +579,7 @@ export async function overrideAssessmentStatus(
|
|||||||
return request('POST', `/api/assessments/${id}/override`, {
|
return request('POST', `/api/assessments/${id}/override`, {
|
||||||
status,
|
status,
|
||||||
user,
|
user,
|
||||||
|
...(currentUserId() !== undefined ? { userId: currentUserId() } : {}),
|
||||||
...(comment !== undefined && comment !== '' ? { comment } : {}),
|
...(comment !== undefined && comment !== '' ? { comment } : {}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -987,7 +1004,7 @@ export async function updateAssessment(
|
|||||||
const res = await fetch(`${API_BASE}/api/assessments/${id}`, {
|
const res = await fetch(`${API_BASE}/api/assessments/${id}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json', ...authHeader() },
|
headers: { 'Content-Type': 'application/json', ...authHeader() },
|
||||||
body: JSON.stringify(data),
|
body: JSON.stringify({ ...data, ...(currentUserId() !== undefined ? { userId: currentUserId() } : {}) }),
|
||||||
});
|
});
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const err = await res.json().catch(() => ({})) as { error?: string };
|
const err = await res.json().catch(() => ({})) as { error?: string };
|
||||||
|
|||||||
@@ -159,7 +159,7 @@ export function Dashboard(): JSX.Element {
|
|||||||
fetch(`${API_BASE}/api/accuracy`).then((r) => r.json()).then(setAccuracy).catch(() => undefined);
|
fetch(`${API_BASE}/api/accuracy`).then((r) => r.json()).then(setAccuracy).catch(() => undefined);
|
||||||
// 草稿箱(仅销售展示):列出当前用户的向导草稿。
|
// 草稿箱(仅销售展示):列出当前用户的向导草稿。
|
||||||
if (role === '商务/销售') {
|
if (role === '商务/销售') {
|
||||||
listDrafts(user?.username ?? undefined).then(setDrafts).catch(() => setDrafts([]));
|
listDrafts(user?.id ?? undefined).then(setDrafts).catch(() => setDrafts([]));
|
||||||
} else {
|
} else {
|
||||||
setDrafts([]);
|
setDrafts([]);
|
||||||
}
|
}
|
||||||
@@ -207,7 +207,7 @@ export function Dashboard(): JSX.Element {
|
|||||||
<span style={{ fontWeight: 700, color: colorVar('color.text.primary'), whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
<span style={{ fontWeight: 700, color: colorVar('color.text.primary'), whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>
|
||||||
{display}
|
{display}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary'), whiteSpace: 'nowrap' }}>发起人:{r.assessorId}</span>
|
<span style={{ ...typographyStyle('caption'), color: colorVar('color.text.secondary'), whiteSpace: 'nowrap' }}>发起人:{r.assessorName ?? r.assessorId}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -517,16 +517,16 @@ export function Dashboard(): JSX.Element {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{todoItems.length > 0 && (() => {
|
{todoItems.length > 0 && (() => {
|
||||||
const myName = user?.username;
|
const myId = user?.id;
|
||||||
const assignedToMe = (id: string): boolean => {
|
const assignedToMe = (id: string): boolean => {
|
||||||
const a = assignments[id];
|
const a = assignments[id];
|
||||||
if (a === undefined) return false;
|
if (a === undefined) return false;
|
||||||
return role === '风控' ? a.riskReviewerName === myName : role === '管理层' ? a.managerName === myName : false;
|
return role === '风控' ? a.riskReviewerId === myId : role === '管理层' ? a.managerId === myId : false;
|
||||||
};
|
};
|
||||||
const isAssigned = (id: string): boolean => {
|
const isAssigned = (id: string): boolean => {
|
||||||
const a = assignments[id];
|
const a = assignments[id];
|
||||||
if (a === undefined) return false;
|
if (a === undefined) return false;
|
||||||
return role === '风控' ? a.riskReviewerName !== null : role === '管理层' ? a.managerName !== null : false;
|
return role === '风控' ? a.riskReviewerId !== null : role === '管理层' ? a.managerId !== null : false;
|
||||||
};
|
};
|
||||||
// 软约束:默认只看分给我的;未指派的也展示(避免遗漏)。
|
// 软约束:默认只看分给我的;未指派的也展示(避免遗漏)。
|
||||||
const shown = onlyMine ? todoItems.filter((t) => assignedToMe(t.id) || !isAssigned(t.id)) : todoItems;
|
const shown = onlyMine ? todoItems.filter((t) => assignedToMe(t.id) || !isAssigned(t.id)) : todoItems;
|
||||||
@@ -536,8 +536,9 @@ export function Dashboard(): JSX.Element {
|
|||||||
render: (r) => {
|
render: (r) => {
|
||||||
const a = assignments[r.id];
|
const a = assignments[r.id];
|
||||||
const name = a !== undefined ? (role === '管理层' ? a.managerName : a.riskReviewerName) : null;
|
const name = a !== undefined ? (role === '管理层' ? a.managerName : a.riskReviewerName) : null;
|
||||||
|
const aid = a !== undefined ? (role === '管理层' ? a.managerId : a.riskReviewerId) : null;
|
||||||
if (name === null || name === undefined) return <span style={{ color: colorVar('color.text.secondary') }}>未指派</span>;
|
if (name === null || name === undefined) return <span style={{ color: colorVar('color.text.secondary') }}>未指派</span>;
|
||||||
const mine = name === myName;
|
const mine = aid === myId;
|
||||||
return <span style={{ ...typographyStyle('caption'), fontWeight: 600, color: mine ? '#15803D' : colorVar('color.text.primary') }}>{name}{mine ? '(我)' : ''}</span>;
|
return <span style={{ ...typographyStyle('caption'), fontWeight: 600, color: mine ? '#15803D' : colorVar('color.text.primary') }}>{name}{mine ? '(我)' : ''}</span>;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -340,7 +340,7 @@ export function NewAssessment(): JSX.Element {
|
|||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
void saveDraft({
|
void saveDraft({
|
||||||
id: draftId,
|
id: draftId,
|
||||||
assessorId: user?.username ?? null,
|
assessorId: user?.id ?? null,
|
||||||
sourceAssessmentId,
|
sourceAssessmentId,
|
||||||
projectName: projectName.trim() !== '' ? projectName : null,
|
projectName: projectName.trim() !== '' ? projectName : null,
|
||||||
form: buildSnapshot(),
|
form: buildSnapshot(),
|
||||||
@@ -369,7 +369,7 @@ export function NewAssessment(): JSX.Element {
|
|||||||
try {
|
try {
|
||||||
await saveDraft({
|
await saveDraft({
|
||||||
id: draftId,
|
id: draftId,
|
||||||
assessorId: user?.username ?? null,
|
assessorId: user?.id ?? null,
|
||||||
sourceAssessmentId,
|
sourceAssessmentId,
|
||||||
projectName: projectName.trim() !== '' ? projectName : null,
|
projectName: projectName.trim() !== '' ? projectName : null,
|
||||||
form: buildSnapshot(),
|
form: buildSnapshot(),
|
||||||
@@ -700,7 +700,7 @@ export function NewAssessment(): JSX.Element {
|
|||||||
...(effectiveEditId !== null ? { assessmentId: effectiveEditId, useLlm: false } : {}),
|
...(effectiveEditId !== null ? { assessmentId: effectiveEditId, useLlm: false } : {}),
|
||||||
...(effectiveEditId !== null && editSavedAt !== null ? { expectedSavedAt: editSavedAt } : {}),
|
...(effectiveEditId !== null && editSavedAt !== null ? { expectedSavedAt: editSavedAt } : {}),
|
||||||
...(Number(clientTotalHeadcount) > 0 ? { clientTotalHeadcount: Number(clientTotalHeadcount) } : {}),
|
...(Number(clientTotalHeadcount) > 0 ? { clientTotalHeadcount: Number(clientTotalHeadcount) } : {}),
|
||||||
...(user?.username !== undefined ? { assessorId: user.username } : {}),
|
...(user?.id !== undefined ? { assessorId: user.id } : {}),
|
||||||
knownData: Object.entries(answers)
|
knownData: Object.entries(answers)
|
||||||
.filter((e): e is [string, number] => typeof e[1] === 'number')
|
.filter((e): e is [string, number] => typeof e[1] === 'number')
|
||||||
.map(([k, v]) => [k, v]),
|
.map(([k, v]) => [k, v]),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const TEST_ACCOUNTS: readonly {
|
|||||||
/** 认证状态。 */
|
/** 认证状态。 */
|
||||||
export interface AuthState {
|
export interface AuthState {
|
||||||
readonly isAuthenticated: boolean;
|
readonly isAuthenticated: boolean;
|
||||||
readonly user: { username: string; role: AuthRole; displayName?: string } | null;
|
readonly user: { id?: string; username: string; role: AuthRole; displayName?: string } | null;
|
||||||
readonly error: string | null;
|
readonly error: string | null;
|
||||||
|
|
||||||
login(username: string, password: string): Promise<boolean>;
|
login(username: string, password: string): Promise<boolean>;
|
||||||
@@ -80,10 +80,12 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const role = d.role as AuthRole;
|
const role = d.role as AuthRole;
|
||||||
const user = { username: String(d.username ?? username), role, ...(d.displayName ? { displayName: String(d.displayName) } : {}) };
|
const user = { ...(d.id ? { id: String(d.id) } : {}), username: String(d.username ?? username), role, ...(d.displayName ? { displayName: String(d.displayName) } : {}) };
|
||||||
saveToStorage(user);
|
saveToStorage(user);
|
||||||
if (typeof d.token === 'string' && d.token !== '') localStorage.setItem('risk-agent-token', d.token);
|
if (typeof d.token === 'string' && d.token !== '') localStorage.setItem('risk-agent-token', d.token);
|
||||||
else localStorage.removeItem('risk-agent-token');
|
else localStorage.removeItem('risk-agent-token');
|
||||||
|
if (d.id) localStorage.setItem('risk-agent-uid', String(d.id));
|
||||||
|
else localStorage.removeItem('risk-agent-uid');
|
||||||
set({ isAuthenticated: true, user, error: null });
|
set({ isAuthenticated: true, user, error: null });
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -94,7 +96,7 @@ export const useAuthStore = create<AuthState>((set) => ({
|
|||||||
|
|
||||||
logout: () => {
|
logout: () => {
|
||||||
saveToStorage(null);
|
saveToStorage(null);
|
||||||
try { localStorage.removeItem('risk-agent-token'); } catch { /* ignore */ }
|
try { localStorage.removeItem('risk-agent-token'); localStorage.removeItem('risk-agent-uid'); } catch { /* ignore */ }
|
||||||
set({ isAuthenticated: false, user: null, error: null });
|
set({ isAuthenticated: false, user: null, error: null });
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user