feat: 结算时同步钉钉已审批请假并标记为请假

This commit is contained in:
2026-08-05 18:43:02 +08:00
parent 32343b271b
commit c8316e9e8e
19 changed files with 746 additions and 58 deletions

View File

@@ -0,0 +1,95 @@
// aislop-ignore-file: duplicate-block -- 钉钉 API 调用块结构相似(端点/参数不同)
import type {
DingTalkLeaveResult,
DingTalkServiceContext,
} from './dingtalk.types';
interface DingTalkGetUpdateDataResponse {
errcode: number;
errmsg: string;
result?: {
userid?: string;
work_date?: string;
approve_list?: Array<{
procInst_id?: string;
tag_name?: string;
sub_type?: string;
biz_type?: number;
begin_time?: string;
end_time?: string;
gmt_finished?: string;
duration?: string;
duration_unit?: string;
}>;
};
}
/**
* 钉钉请假数据客户端。
*
* 使用「获取用户考勤数据」接口topapi/attendance/getupdatedata按用户+工作日
* 返回当天打卡结果与审批单列表;这里只取 biz_type=3请假且已审批完成
* gmt_finished 非空)的记录,保证结算时不会把审批中的请假误判为请假。
*/
export class DingTalkLeaveClient {
constructor(private readonly context: DingTalkServiceContext) {}
async fetchDailyLeaveStatus(
userId: string,
workDate: string,
): Promise<DingTalkLeaveResult[]> {
if (!(await this.context.isConfigured())) throw new Error('DingTalk not configured');
if (!userId) throw new Error('钉钉请假查询 userId 不能为空');
const token = await this.context.getAccessToken();
await this.context.rateLimit();
const res = await fetch(
`https://oapi.dingtalk.com/topapi/attendance/getupdatedata?access_token=${token}`,
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
userid: userId,
work_date: workDate.includes(' ') ? workDate : `${workDate} 00:00:00`,
}),
},
);
const data = (await res.json()) as DingTalkGetUpdateDataResponse;
if (data.errcode !== 0) {
throw new Error(`钉钉请假数据获取失败: ${data.errmsg}`);
}
const result = data.result;
if (!result) return [];
const approveList = result.approve_list ?? [];
return approveList
.filter(
(approval) =>
approval.biz_type === 3 &&
approval.gmt_finished &&
approval.procInst_id &&
approval.begin_time &&
approval.end_time,
)
.map((approval) => ({
userId: result.userid ?? userId,
workDate,
procInstId: approval.procInst_id!,
tagName: approval.tag_name ?? '请假',
leaveType: approval.sub_type ?? '',
beginTime: this.parseDingDate(approval.begin_time!),
endTime: this.parseDingDate(approval.end_time!),
approvedAt: this.parseDingDate(approval.gmt_finished!),
duration: approval.duration ?? '',
durationUnit: approval.duration_unit ?? '',
}));
}
/** 钉钉返回的日期可能是 '2026-08-01' 或 '2026-08-01 09:00:00',统一按东八区解析。 */
private parseDingDate(value: string): Date {
const normalized = value.includes(' ') ? value.replace(' ', 'T') : `${value}T00:00:00`;
const date = new Date(`${normalized}+08:00`);
return Number.isNaN(date.getTime()) ? new Date(normalized) : date;
}
}