96 lines
3.2 KiB
TypeScript
96 lines
3.2 KiB
TypeScript
// 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;
|
||
}
|
||
}
|