Browse Source

✨ feat(ClockIn): 增加考勤模块

Pchen. 8 months ago
parent
commit
1be73b0dd9

+ 116 - 0
apis/ClockIn/AddAttendanceItems.js

@@ -0,0 +1,116 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const db = require("../../plugin/DataBase/db");
+const AccessControl = require("../../lib/AccessControl");
+
+class AddAttendanceItems extends API {
+    constructor() {
+        super();
+        this.setPath('/Attendance');
+        this.setMethod('POST');
+    }
+
+    async onRequest(data, res) {
+        this.setAllowCORS(res);
+        let {
+            uuid,
+            session,
+            name,
+            user,
+            day_of_week,
+            loopy,
+            begintime,
+            endtime,
+            position,
+            radius,
+            address,
+            admin
+        } = data;
+
+        // 检查必需的参数是否缺失
+        if ([uuid, session, name, user, day_of_week, loopy, begintime, endtime, position, radius].some(value => value === '' || value === null || value === undefined)) {
+            res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513123
+            });
+            return;
+        }
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            res.json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+            return;
+        }
+
+        // 确认权限
+        let permission = await AccessControl.getPermission(uuid);
+        if (!permission.includes('admin') && !permission.includes('manage')) {
+            res.json({
+                ...BaseStdResponse.PERMISSION_DENIED,
+                endpoint: 481454
+            });
+            return;
+        }
+
+        user = user.split('|');
+        admin = admin.split('|');
+
+        let uuids, admins;
+        try {
+            uuids = await AccessControl.checkUser(user);
+            admins = await AccessControl.checkUser(admin);
+        } catch (error) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                endpoint: 513513,
+                msg: error.message
+            });
+        }
+
+        // 插入数据
+        const sql = `INSERT INTO kq_items (name, createUser, createTime, user, day_of_week, loopy, begintime, endtime, position, radius, address, admin)
+                     VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
+
+        const values = [
+            name,
+            uuid,
+            new Date().getTime(),
+            JSON.stringify(uuids),
+            day_of_week,
+            loopy,
+            begintime,
+            endtime,
+            JSON.stringify(position),
+            radius,
+            address,
+            JSON.stringify(admins)
+        ];
+
+        try {
+            const [result] = await db.query(sql, values);
+
+            if (result.affectedRows !== 1) {
+                res.json({
+                    ...BaseStdResponse.DATABASE_ERR,
+                    endpoint: 513513
+                });
+                return;
+            }
+
+            res.json({
+                ...BaseStdResponse.OK
+            });
+        } catch (error) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 513513,
+                msg: error.message
+            });
+        }
+    }
+}
+
+module.exports.AddAttendanceItems = AddAttendanceItems;

+ 154 - 0
apis/ClockIn/AddAttendanceRecord.js

@@ -0,0 +1,154 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const db = require("../../plugin/DataBase/db");
+const AccessControl = require("../../lib/AccessControl");
+
+class AddAttendanceRecord extends API {
+    constructor() {
+        super();
+
+        this.setPath('/AddAttendanceRecord');
+        this.setMethod('GET');
+    }
+
+    async onRequest(data, res) {
+        this.setAllowCORS(res);
+        let {
+            uuid,
+            session,
+            project_id
+        } = data;
+
+        // 检查必需的参数是否缺失
+        if ([uuid, session, project_id].some(value => value === '' || value === null || value === undefined)) {
+            res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513123
+            });
+            return;
+        }
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            res.json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+            return;
+        }
+
+        // 获取考勤项目
+        const sqlGetProject = 'SELECT user, day_of_week, loopy, begintime, endtime FROM kq_items WHERE id = ?';
+        let [projectResult] = await db.query(sqlGetProject, [project_id]);
+
+        if (!projectResult || projectResult.length === 0) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 154754511
+            });
+            return;
+        }
+
+        let projectData = projectResult[0];
+        let users = JSON.parse(projectData.user);
+
+        if (!users.includes(uuid)) {
+            res.json({
+                ...BaseStdResponse.ERR,
+                endpoint: 481454,
+                msg: '用户不在考勤名单中'
+            });
+            return;
+        }
+
+        const isWithinTime = this.isWithinAttendanceTime(projectData);
+        if (!isWithinTime) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                endpoint: 513513,
+                msg: '未在打卡时间段内'
+            });
+        }
+
+        // 检查是否已存在考勤记录
+        const sqlCheckRecord = 'SELECT id, uuid, time FROM kq_records WHERE project_id = ? AND uuid = ?';
+        let [recordsResult] = await db.query(sqlCheckRecord, [project_id, uuid]);
+
+        if (!recordsResult) {
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 513513
+            });
+        }
+
+        if (recordsResult.length !== 0 && this.hasRecord(projectData, recordsResult, uuid)) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                endpoint: 513513,
+                msg: '用户在本考勤周期已有考勤记录!'
+            });
+        }
+
+        // 插入考勤记录
+        const sqlInsertRecord = 'INSERT INTO kq_records (project_id, uuid, time) VALUES (?, ?, ?)';
+        let [insertResult] = await db.query(sqlInsertRecord, [project_id, uuid, new Date().getTime()]);
+
+        if (insertResult.affectedRows !== 1) {
+            return res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 513513
+            });
+        }
+
+        res.json({
+            ...BaseStdResponse.OK
+        });
+    }
+
+    isWithinAttendanceTime(attendanceTimes) {
+        const now = new Date();
+        const dayOfWeek = now.getDay();
+        const nowTime = now.getTime();
+
+        function timeToTodayTimestamp(timeStr) {
+            const [hours, minutes, seconds] = timeStr.split(':').map(Number);
+            const todayWithTime = new Date(now.getFullYear(), now.getMonth(), now.getDate(), hours, minutes, seconds);
+            return todayWithTime.getTime();
+        }
+
+        const { createTime, day_of_week, begintime, endtime, loopy } = attendanceTimes;
+        if (dayOfWeek === day_of_week && (loopy || (Number(createTime) + 604800000 - nowTime > 0)) && timeToTodayTimestamp(begintime) <= nowTime && timeToTodayTimestamp(endtime) >= nowTime) {
+            return true;
+        }
+        return false;
+    }
+
+    hasRecord(attendanceData, records, uuid) {
+        const { day_of_week, begintime, endtime, loopy } = attendanceData;
+        if (!loopy) {
+            return records.some(record => record.uuid === uuid);
+        }
+        const now = new Date();
+        const dayOfWeek = now.getDay();
+        const nowTime = now.getTime();
+
+        function getTimestamp(time) {
+            const daysUntilTarget = (day_of_week - dayOfWeek + 7) % 7;
+            const targetDate = new Date(now);
+            targetDate.setDate(now.getDate() + daysUntilTarget);
+            const [hours, minutes, seconds] = time.split(':').map(Number);
+            targetDate.setHours(hours, minutes, seconds, 0);
+            return targetDate.getTime();
+        }
+
+        const begin = getTimestamp(begintime);
+        const end = getTimestamp(endtime);
+
+        if (nowTime >= begin) {
+            return records.some(record => record.time >= begin && record.time <= end && record.uuid === uuid);
+        }
+        return false;
+    }
+}
+
+module.exports.AddAttendanceRecord = AddAttendanceRecord;

+ 96 - 0
apis/ClockIn/DeleteAttendanceItem.js

@@ -0,0 +1,96 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const db = require("../../plugin/DataBase/db");
+const AccessControl = require("../../lib/AccessControl");
+
+class DeleteAttendanceItem extends API {
+    constructor() {
+        super();
+
+        this.setPath('/Attendance');
+        this.setMethod('DELETE');
+    }
+
+    async onRequest(data, res) {
+        this.setAllowCORS(res);
+
+        let {
+            uuid,
+            session,
+            project_id
+        } = data;
+
+        // 检查必需的参数是否缺失
+        if ([uuid, session, project_id].some(value => value === '' || value === null || value === undefined)) {
+            res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513123
+            });
+            return;
+        }
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            res.json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+            return;
+        }
+
+        // 获取考勤项目
+        const sqlGetProject = 'SELECT user, createUser, begintime, loopy, day_of_week, admin FROM kq_items WHERE id = ?';
+        let [projectResult] = await db.query(sqlGetProject, [project_id]);
+
+        if (!projectResult || projectResult.length === 0) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 154754511
+            });
+            return;
+        }
+
+        let projectData = projectResult[0];
+
+        // 检查用户权限
+        let permission = await AccessControl.getPermission(uuid);
+        if (projectData.createUser !== uuid && !permission.includes('admin') && !Array.isArray(projectData.admin) && !JSON.parse(projectData.admin).includes(uuid)) {
+            res.json({
+                ...BaseStdResponse.PERMISSION_DENIED,
+                endpoint: 481454,
+                msg: '你不是该考勤项目管理员,无操作权限'
+            });
+            return;
+        }
+
+        // 删除考勤项目
+        const sqlDeleteProject = 'DELETE FROM kq_items WHERE id = ?';
+        let [deleteResult] = await db.query(sqlDeleteProject, [project_id]);
+
+        if (deleteResult.affectedRows !== 1) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 513513
+            });
+            return;
+        }
+
+        // 删除考勤记录
+        const sqlDeleteRecords = 'DELETE FROM kq_records WHERE project_id = ?';
+        let [deleteRecordsResult] = await db.query(sqlDeleteRecords, [project_id]);
+
+        if (!deleteRecordsResult) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 513513
+            });
+            return;
+        }
+
+        res.json({
+            ...BaseStdResponse.OK
+        });
+    }
+}
+
+module.exports.DeleteAttendanceItem = DeleteAttendanceItem;

+ 132 - 0
apis/ClockIn/EditAttendanceItems.js

@@ -0,0 +1,132 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const db = require("../../plugin/DataBase/db");
+const AccessControl = require("../../lib/AccessControl");
+
+class EditAttendanceItems extends API {
+    constructor() {
+        super();
+        this.setPath('/Attendance');
+        this.setMethod('PUT');
+    }
+
+    async onRequest(data, res) {
+        this.setAllowCORS(res);
+        let {
+            uuid,
+            session,
+            id,
+            name,
+            user,
+            day_of_week,
+            loopy,
+            begintime,
+            endtime,
+            position,
+            radius,
+            address,
+            admin
+        } = data;
+
+        // 检查必需的参数是否缺失
+        if ([uuid, session, id, name, user, day_of_week, loopy, begintime, endtime, position, radius].some(value => value === '' || value === null || value === undefined)) {
+            res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513123
+            });
+            return;
+        }
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            res.json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+            return;
+        }
+
+        // 获取考勤项目
+        const sqlGetProject = 'SELECT user, createUser, begintime, loopy, day_of_week, admin FROM kq_items WHERE id = ?';
+        let [projectResult] = await db.query(sqlGetProject, [id]);
+
+        if (!projectResult || projectResult.length === 0) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 154754511
+            });
+            return;
+        }
+
+        let projectData = projectResult[0];
+
+        // 检查用户权限
+        let permission = await AccessControl.getPermission(uuid);
+        if (projectData.createUser !== uuid && !permission.includes('admin') && !Array.isArray(projectData.admin) && !JSON.parse(projectData.admin).includes(uuid)) {
+            return res.json({
+                ...BaseStdResponse.PERMISSION_DENIED,
+                endpoint: 481454,
+                msg: '你不是该考勤项目管理员,无操作权限'
+            });
+        }
+
+        user = user.split('|');
+        admin = admin.split('|');
+
+        let uuids, admins;
+        try {
+            uuids = await AccessControl.checkUser(user);
+            admins = await AccessControl.checkUser(admin);
+        } catch (error) {
+            return res.json({
+                ...BaseStdResponse.ERR,
+                endpoint: 513513,
+                msg: error.message
+            });
+        }
+
+        // 更新考勤项目
+        const sqlUpdateProject = `
+            UPDATE kq_items 
+            SET 
+                name = ?,
+                user = ?,
+                day_of_week = ?,
+                loopy = ?,
+                begintime = ?,
+                endtime = ?,
+                position = ?,
+                radius = ?,
+                address = ?,
+                admin = ?
+            WHERE id = ?
+        `;
+        let [updateResult] = await db.query(sqlUpdateProject, [
+            name,
+            JSON.stringify(uuids),
+            day_of_week,
+            loopy,
+            begintime,
+            endtime,
+            JSON.stringify(position),
+            radius,
+            address,
+            JSON.stringify(admins),
+            id
+        ]);
+
+        if (updateResult.affectedRows !== 1) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 513513
+            });
+            return;
+        }
+
+        res.json({
+            ...BaseStdResponse.OK
+        });
+    }
+}
+
+module.exports.EditAttendanceItems = EditAttendanceItems;

+ 103 - 0
apis/ClockIn/GetAttendanceItemDetail.js

@@ -0,0 +1,103 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const db = require("../../plugin/DataBase/db");
+const AccessControl = require("../../lib/AccessControl");
+const UserInfoCache = require("../../lib/UserInfoCache");
+
+class GetAttendanceItemDetail extends API {
+    constructor() {
+        super();
+        
+        this.setPath('/GetAttendanceItemDetail');
+        this.setMethod('GET');
+    }
+
+    async onRequest(data, res) {
+        this.setAllowCORS(res);
+        const { uuid, session, project_id } = data;
+
+        // 检查必需的参数是否缺失
+        if (!uuid || !session || !project_id) {
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513123
+            });
+        }
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            return res.json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+        }
+
+        try {
+            // 获取考勤项目详情
+            const sqlGetProject = 'SELECT * FROM kq_items WHERE id = ?';
+            let [projectResult] = await db.query(sqlGetProject, [project_id]);
+
+            if (!projectResult || projectResult.length === 0) {
+                return res.json({
+                    ...BaseStdResponse.DATABASE_ERR,
+                    endpoint: 154754511
+                });
+            }
+
+            // 获取考勤记录
+            const sqlGetRecords = 'SELECT id, uuid, time, commit FROM kq_records WHERE project_id = ?';
+            let [records] = await db.query(sqlGetRecords, [project_id]);
+
+            // 收集需要查询的用户 UUID
+            const userUuids = new Set();
+            const addUserUuid = (uuid) => userUuids.add(uuid);
+
+            for (let item of projectResult) {
+                addUserUuid(item.uuid);
+                const users = JSON.parse(item.user || '[]');
+                users.forEach(addUserUuid);
+
+                if (item.admin) {
+                    const admins = JSON.parse(item.admin);
+                    admins.forEach(addUserUuid);
+                }
+
+                addUserUuid(item.createUser);
+            }
+
+            // 如果没有需要查询的用户,直接返回结果
+            if (userUuids.size === 0) {
+                return res.json({
+                    ...BaseStdResponse.OK,
+                    data: projectResult[0],
+                    userInfo: {},
+                    records
+                });
+            }
+
+            const userInfo = {};
+
+            // 从缓存中获取用户信息
+            await Promise.all(Array.from(userUuids).map(async (uuid) => {
+                const userCache = await UserInfoCache.getUserByUuid(uuid);
+                userInfo[uuid] = {
+                    userCache
+                };
+            }));
+
+            res.json({
+                ...BaseStdResponse.OK,
+                data: projectResult[0],
+                userInfo,
+                records
+            });
+        } catch (error) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 154754511
+            });
+        }
+    }
+}
+
+module.exports.GetAttendanceItemDetail = GetAttendanceItemDetail;

+ 107 - 0
apis/ClockIn/GetAttendanceItemList.js

@@ -0,0 +1,107 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const db = require("../../plugin/DataBase/db");
+const AccessControl = require("../../lib/AccessControl");
+const UserInfoCache = require("../../lib/UserInfoCache");
+
+class GetAttendanceItemList extends API {
+    constructor() {
+        super();
+        
+        this.setPath('/Attendance');
+        this.setMethod('GET');
+    }
+
+    async onRequest(data, res) {
+        this.setAllowCORS(res);
+
+        const { uuid, session } = data;
+
+        // 检查必需的参数是否缺失
+        if (!uuid || !session) {
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513123
+            });
+        }
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            return res.json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+        }
+
+        // 确认权限
+        const permission = await AccessControl.getPermission(uuid);
+        const hasPermission = ['manage', 'admin'].some(group => permission.includes(group));
+        if (!hasPermission) {
+            return res.json({
+                ...BaseStdResponse.PERMISSION_DENIED,
+                endpoint: 481454
+            });
+        }
+
+        try {
+            // 获取考勤项目列表
+            const sqlGetItems = 'SELECT * FROM kq_items ORDER BY id DESC';
+            const [items] = await db.query(sqlGetItems);
+
+            if (!items) {
+                return res.json({
+                    ...BaseStdResponse.DATABASE_ERR,
+                    endpoint: 154754511
+                });
+            }
+
+            // 收集需要查询的用户 UUID
+            const userUuids = new Set();
+            const addUserUuid = (uuid) => userUuids.add(uuid);
+
+            items.forEach(item => {
+                addUserUuid(item.uuid);
+                const users = JSON.parse(item.user || '[]');
+                users.forEach(addUserUuid);
+                if (item.admin) {
+                    const admins = JSON.parse(item.admin);
+                    admins.forEach(addUserUuid);
+                }
+                addUserUuid(item.createUser);
+            });
+
+            // 如果没有需要查询的用户,直接返回结果
+            if (userUuids.size === 0) {
+                return res.json({
+                    ...BaseStdResponse.OK,
+                    data: items,
+                    userInfo: {}
+                });
+            }
+
+            const userInfo = {};
+
+            // 从缓存中获取用户信息
+            await Promise.all(Array.from(userUuids).map(async (uuid) => {
+                const userCache = await UserInfoCache.getUserByUuid(uuid);
+                userInfo[uuid] = {
+                    userCache
+                };
+
+            }));
+
+            res.json({
+                ...BaseStdResponse.OK,
+                data: items,
+                userInfo
+            });
+        } catch (error) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 154754511
+            });
+        }
+    }
+}
+
+module.exports.GetAttendanceItemList = GetAttendanceItemList;

+ 81 - 0
apis/ClockIn/GetMyAttendanceItems.js

@@ -0,0 +1,81 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const db = require("../../plugin/DataBase/db");
+const AccessControl = require("../../lib/AccessControl");
+
+class GetMyAttendanceItems extends API {
+    constructor() {
+        super();
+
+        this.setPath('/GetMyAttendanceItems');
+        this.setMethod('GET');
+    }
+
+    async onRequest(data, res) {
+        this.setAllowCORS(res);
+        const { uuid, session } = data;
+
+        // 检查必需的参数是否缺失
+        if (!uuid || !session) {
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513123
+            });
+        }
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            return res.json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+        }
+
+        try {
+            const conn = db.getConnection();
+
+            // 查询用户考勤项目
+            const sqlGetItems = `
+                SELECT id, name, user, day_of_week, loopy, begintime, endtime, address
+                FROM kq_items
+                WHERE JSON_CONTAINS(user, JSON_QUOTE(?), '$') ORDER BY id DESC
+            `;
+            const [items] = await conn.query(sqlGetItems, [uuid]);
+
+            if (!items) {
+                return res.json({
+                    ...BaseStdResponse.DATABASE_ERR,
+                    endpoint: 154754511
+                });
+            }
+
+            // 查询用户考勤记录
+            const sqlGetRecords = `
+                SELECT id, uuid, project_id, time, commit
+                FROM kq_records
+                WHERE uuid = ?
+            `;
+            const [records] = await conn.query(sqlGetRecords, [uuid]);
+
+            if (records === undefined) {
+                return res.json({
+                    ...BaseStdResponse.DATABASE_ERR,
+                    endpoint: 154754511
+                });
+            }
+
+            res.json({
+                ...BaseStdResponse.OK,
+                list: items,
+                records: records
+            });
+        } catch (error) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 154754511
+            });
+        }
+    }
+}
+
+module.exports.GetMyAttendanceItems = GetMyAttendanceItems;

+ 155 - 0
apis/ClockIn/SupplementRecord.js

@@ -0,0 +1,155 @@
+const API = require("../../lib/API");
+const { BaseStdResponse } = require("../../BaseStdResponse");
+const db = require("../../plugin/DataBase/db");
+const AccessControl = require("../../lib/AccessControl");
+const UserInfoCache = require("../../lib/UserInfoCache");
+
+class SupplementRecord extends API {
+    constructor() {
+        super();
+
+        this.setPath('/SupplementRecord');
+        this.setMethod('GET');
+    }
+
+    async onRequest(data, res) {
+        this.setAllowCORS(res);
+        const { uuid, session, user, project_id } = data;
+
+        // 检查必需的参数是否缺失
+        if (!uuid || !session || !user || !project_id) {
+            return res.json({
+                ...BaseStdResponse.MISSING_PARAMETER,
+                endpoint: 1513123
+            });
+        }
+
+        // 检查 session 是否有效
+        if (!await AccessControl.checkSession(uuid, session)) {
+            return res.json({
+                ...BaseStdResponse.ACCESS_DENIED,
+                endpoint: 48153145
+            });
+        }
+
+        try {
+            // 获取考勤项目数据
+            const sqlGetProject = `
+                SELECT user, createUser, begintime, loopy, day_of_week, admin
+                FROM kq_items
+                WHERE id = ?
+            `;
+            const [projectData] = await db.query(sqlGetProject, [project_id]);
+
+            if (!projectData.length) {
+                return res.json({
+                    ...BaseStdResponse.DATABASE_ERR,
+                    endpoint: 154754511
+                });
+            }
+
+            const item = projectData[0];
+
+            // 检查权限
+            const permission = await AccessControl.getPermission(uuid);
+            if (item.createUser !== uuid && !permission.groups.includes('admin') && !item.admin.includes(uuid)) {
+                return res.json({
+                    ...BaseStdResponse.PERMISSION_DENIED,
+                    endpoint: 481454,
+                    msg: '你不是该考勤项目管理员,无操作权限'
+                });
+            }
+
+            // 获取用户 UUID
+            const sqlGetUserUUID = `
+                SELECT uuid
+                FROM users
+                WHERE name = ?
+            `;
+            const [userUUIDData] = await db.query(sqlGetUserUUID, [user]);
+
+            if (!userUUIDData.length) {
+                return res.json({
+                    ...BaseStdResponse.DATABASE_ERR,
+                    endpoint: 154754511,
+                    msg: '未找到用户'
+                });
+            }
+
+            const userUUID = userUUIDData[0].uuid;
+            if (!item.user.includes(userUUID)) {
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    endpoint: 481454,
+                    msg: '用户不在考勤名单中'
+                });
+            }
+
+            // 检查是否已有考勤记录
+            const sqlCheckRecords = `
+                SELECT id, uuid, time
+                FROM kq_records
+                WHERE project_id = ? AND uuid = ?
+            `;
+            const [records] = await db.query(sqlCheckRecords, [project_id, userUUID]);
+
+            if (records.some(record => this.hasRecord(item, record))) {
+                return res.json({
+                    ...BaseStdResponse.ERR,
+                    endpoint: 513523,
+                    msg: '用户在本考勤周期已有考勤记录!'
+                });
+            }
+
+            // 添加考勤记录
+            const userInfo = await UserInfoCache.getUserByUuid(uuid);
+            const sqlInsertRecord = `
+                INSERT INTO kq_records (project_id, uuid, time, commit)
+                VALUES (?, ?, ?, ?)
+            `;
+            const result = await db.query(sqlInsertRecord, [
+                project_id,
+                userUUID,
+                new Date().getTime(),
+                `${userInfo.name}补卡`
+            ]);
+
+            if (result.affectedRows !== 1) {
+                return res.json({
+                    ...BaseStdResponse.DATABASE_ERR,
+                    endpoint: 513514
+                });
+            }
+
+            res.json({
+                ...BaseStdResponse.OK
+            });
+
+        } catch (error) {
+            res.json({
+                ...BaseStdResponse.DATABASE_ERR,
+                endpoint: 154754511
+            });
+        }
+    }
+
+    hasRecord(attendanceData, record) {
+        const { day_of_week, begintime, loopy } = attendanceData;
+        const now = new Date();
+        const nowTime = now.getTime();
+
+        if (!loopy) {
+            return record.time >= nowTime;
+        }
+
+        const targetDay = (day_of_week + 7 - now.getDay()) % 7;
+        const targetDate = new Date(now);
+        targetDate.setDate(now.getDate() + targetDay);
+        const [hours, minutes, seconds] = begintime.split(':').map(Number);
+        targetDate.setHours(hours, minutes, seconds, 0);
+
+        return record.time >= targetDate.getTime() - 604800000;
+    }
+}
+
+module.exports.SupplementRecord = SupplementRecord;