fix: audit remediation — SSE user scoping, FK transactional safety, UI error handling

- H4: scoped SSE import progress to exact userId match; non-HTTP events excluded from all subscribers
- H2: moved PRAGMA foreign_key_check inside SQLite transaction before COMMIT; violations rollback preserving old tables
- M1: removed dead axios-style error branch from extractErrorMessage (interceptor already unwraps)
- M2: split handleSave try/catch — save errors vs reload errors shown distinctly
- M3: added provider field validation before AI config test request
- Added SSE scoping regression tests (import service + controller)
- Added FK check failure rollback test (database-migrations.spec)
- Updated controller spec expectations for userId parameter

Co-authored-by: Code Review <branch-review>
This commit is contained in:
2026-07-12 22:59:03 +08:00
parent b6fca99390
commit cc4f4dae4e
69 changed files with 6262 additions and 1980 deletions

View File

@@ -480,7 +480,7 @@ export class DingTalkService {
return records.map((r) => ({
userId: r.userId,
userName: '',
workDate: new Date(r.workDate).toISOString().slice(0, 10),
workDate: new Date(r.workDate + 8 * 60 * 60 * 1000).toISOString().slice(0, 10),
timeResult: r.timeResult ?? r.sourceType ?? '',
locationResult: r.locationResult ?? r.locationMethod ?? r.userAddress ?? '',
planCheckTime: '',
@@ -728,6 +728,47 @@ export class DingTalkService {
return all;
}
async deleteAttendanceGroup(groupId: number, opUserId = 'manager'): Promise<void> {
if (!this.configured) throw new ServiceUnavailableException('钉钉未配置');
const token = await this.getAccessToken();
await this.rateLimit();
const keyResponse = await fetch(
`https://oapi.dingtalk.com/topapi/attendance/groups/idtokey?access_token=${token}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ op_user_id: opUserId, group_id: groupId }),
},
);
const keyData = await keyResponse.json() as {
errcode: number;
errmsg: string;
result?: string;
};
if (keyData.errcode !== 0 || !keyData.result) {
throw new Error(`钉钉考勤组ID转换失败: ${keyData.errmsg} (code=${keyData.errcode})`);
}
await this.rateLimit();
const deleteResponse = await fetch(
`https://oapi.dingtalk.com/topapi/attendance/group/delete?access_token=${token}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ op_userid: opUserId, group_key: keyData.result }),
},
);
const deleteData = await deleteResponse.json() as {
errcode: number;
errmsg: string;
success?: boolean;
};
if (deleteData.errcode !== 0 || deleteData.success !== true) {
throw new Error(`钉钉删除考勤组失败: ${deleteData.errmsg} (code=${deleteData.errcode})`);
}
}
// ═══════════════════════════════════════════
// 考勤排班 — 排班分配