生成代码工程
This commit is contained in:
commit
64c9485d14
|
|
@ -0,0 +1,25 @@
|
||||||
|
cmake_minimum_required(VERSION 3.14)
|
||||||
|
project(AttendanceSystem VERSION 1.0.0 LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
if (MSVC)
|
||||||
|
add_compile_options(/utf-8)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
# 头文件目录
|
||||||
|
include_directories(${CMAKE_SOURCE_DIR}/include)
|
||||||
|
|
||||||
|
# 主程序
|
||||||
|
add_executable(attendance_system
|
||||||
|
src/main.cpp
|
||||||
|
src/app.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
# 测试程序
|
||||||
|
add_executable(basic_test
|
||||||
|
tests/basic_test.cpp
|
||||||
|
src/app.cpp
|
||||||
|
)
|
||||||
|
|
@ -0,0 +1,50 @@
|
||||||
|
# Attendance System - 企业考勤管理系统
|
||||||
|
|
||||||
|
## 项目概述
|
||||||
|
|
||||||
|
企业多方式考勤打卡系统,支持人脸识别、GPS定位、工牌刷卡三种打卡方式。
|
||||||
|
自动化计算迟到、早退、缺勤、加班等考勤结果,并提供异常预警、报表生成、
|
||||||
|
管理员控制台等完整功能。
|
||||||
|
|
||||||
|
## 功能模块
|
||||||
|
|
||||||
|
- **考勤打卡服务**:接收打卡请求,验证JWT令牌,记录打卡信息
|
||||||
|
- **排班规则引擎**:管理班次定义与排班规则
|
||||||
|
- **考勤计算引擎**:自动判定考勤结果
|
||||||
|
- **异常检测与通知服务**:监控异常并发送通知
|
||||||
|
- **报表生成器**:多维度统计报表(Excel/PDF)
|
||||||
|
- **管理员控制台**:员工档案、排班配置、权限管理
|
||||||
|
- **数据加密模块**:生物特征加密存储
|
||||||
|
- **外部系统对接网关**:HRMS数据同步API
|
||||||
|
|
||||||
|
## 技术栈
|
||||||
|
|
||||||
|
- C++17 + CMake
|
||||||
|
- 无外部第三方依赖(仅标准库)
|
||||||
|
|
||||||
|
## 编译与运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 创建构建目录
|
||||||
|
mkdir build && cd build
|
||||||
|
|
||||||
|
# 配置
|
||||||
|
cmake ..
|
||||||
|
|
||||||
|
# 编译
|
||||||
|
cmake --build .
|
||||||
|
|
||||||
|
# 运行主程序
|
||||||
|
./attendance_system
|
||||||
|
|
||||||
|
# 运行测试
|
||||||
|
./basic_test
|
||||||
|
```
|
||||||
|
|
||||||
|
## 数据结构概述
|
||||||
|
|
||||||
|
- 打卡记录 (CheckinRecord):recordId, employeeId, method, timestamp, location
|
||||||
|
- 排班规则 (ScheduleRule):deptId, shiftName, startTime, endTime
|
||||||
|
- 考勤结果 (AttendanceResult):迟到/早退/缺勤/加班判定
|
||||||
|
- 异常预警 (AlertNotification):alertType, employeeId, reason
|
||||||
|
- 系统角色 (SystemRole):SUPER_ADMIN, HR_MANAGER, DEPT_MANAGER
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -0,0 +1,236 @@
|
||||||
|
#ifndef ATTENDANCE_SYSTEM_APP_HPP
|
||||||
|
#define ATTENDANCE_SYSTEM_APP_HPP
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 枚举定义
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/// 打卡方式
|
||||||
|
enum class CheckinMethod {
|
||||||
|
FACE, // 人脸识别
|
||||||
|
GPS, // GPS定位
|
||||||
|
CARD // 工牌刷卡
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 打卡状态
|
||||||
|
enum class CheckinStatus {
|
||||||
|
SUCCESS,
|
||||||
|
FAILED
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 考勤状态
|
||||||
|
enum class AttendanceStatus {
|
||||||
|
NORMAL,
|
||||||
|
LATE,
|
||||||
|
EARLY,
|
||||||
|
ABSENT,
|
||||||
|
OVERTIME
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 预警类型
|
||||||
|
enum class AlertType {
|
||||||
|
UNCHECKED, // 未打卡
|
||||||
|
EARLY, // 早退
|
||||||
|
LATE, // 迟到
|
||||||
|
MISSING // 连续缺勤
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 报表维度
|
||||||
|
enum class ReportDimension {
|
||||||
|
MONTH,
|
||||||
|
DEPT,
|
||||||
|
PROJECT
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 导出格式
|
||||||
|
enum class ExportFormat {
|
||||||
|
EXCEL,
|
||||||
|
PDF
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 系统角色
|
||||||
|
enum class SystemRole {
|
||||||
|
SUPER_ADMIN,
|
||||||
|
HR_MANAGER,
|
||||||
|
DEPT_MANAGER
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 数据同步类型
|
||||||
|
enum class SyncDataType {
|
||||||
|
EMPLOYEE,
|
||||||
|
SCHEDULE,
|
||||||
|
ATTENDANCE
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 同步状态
|
||||||
|
enum class SyncStatus {
|
||||||
|
SUCCESS,
|
||||||
|
FAILED,
|
||||||
|
PENDING
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 数据结构定义
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/// 经纬度坐标
|
||||||
|
struct GeoLocation {
|
||||||
|
double lat = 0.0;
|
||||||
|
double lng = 0.0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 打卡记录 (Check-in Record)
|
||||||
|
struct CheckinRecord {
|
||||||
|
std::string recordId; // 打卡记录编号
|
||||||
|
std::string employeeId; // 员工编号
|
||||||
|
CheckinMethod method; // 打卡方式
|
||||||
|
int64_t timestamp; // 13位Unix时间戳(毫秒)
|
||||||
|
GeoLocation location; // 地理位置
|
||||||
|
std::vector<uint8_t> faceFeature; // 人脸特征码(加密后)
|
||||||
|
CheckinStatus status; // 打卡状态
|
||||||
|
|
||||||
|
CheckinRecord()
|
||||||
|
: method(CheckinMethod::FACE), timestamp(0), status(CheckinStatus::SUCCESS) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 班次定义
|
||||||
|
struct ShiftRule {
|
||||||
|
std::string shiftName; // 班次名称
|
||||||
|
int startHour = 9; // 开始小时
|
||||||
|
int startMinute = 0; // 开始分钟
|
||||||
|
int endHour = 18; // 结束小时
|
||||||
|
int endMinute = 0; // 结束分钟
|
||||||
|
int breakDuration = 60; // 休息时长(分钟)
|
||||||
|
int overtimeThreshold = 30; // 加班判定阈值(分钟)
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 排班规则 (Schedule Rule)
|
||||||
|
struct ScheduleRule {
|
||||||
|
std::string deptId; // 部门编号
|
||||||
|
std::vector<ShiftRule> rules; // 班次列表
|
||||||
|
std::string version; // 规则版本号
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 考勤结果 (Attendance Result)
|
||||||
|
struct AttendanceResult {
|
||||||
|
std::string employeeId; // 员工编号
|
||||||
|
std::string date; // 日期 YYYY-MM-DD
|
||||||
|
int64_t actualInTime = 0; // 实际签到时间戳
|
||||||
|
int64_t actualOutTime = 0; // 实际签退时间戳
|
||||||
|
int scheduledInHour = 9; // 应签到小时
|
||||||
|
int scheduledInMin = 0; // 应签到分钟
|
||||||
|
int scheduledOutHour = 18; // 应签退小时
|
||||||
|
int scheduledOutMin = 0; // 应签退分钟
|
||||||
|
int lateMinutes = 0; // 迟到分钟数
|
||||||
|
int earlyLeaveMinutes = 0; // 早退分钟数
|
||||||
|
bool absent = false; // 是否缺勤
|
||||||
|
float overtimeHours = 0.0f; // 加班小时数
|
||||||
|
AttendanceStatus status; // 考勤状态
|
||||||
|
|
||||||
|
AttendanceResult() : status(AttendanceStatus::NORMAL) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 异常预警 (Alert Notification)
|
||||||
|
struct AlertNotification {
|
||||||
|
AlertType alertType; // 预警类型
|
||||||
|
std::string employeeId; // 员工编号
|
||||||
|
int64_t time; // 预警时间戳
|
||||||
|
std::string reason; // 原因描述
|
||||||
|
std::vector<std::string> notifiedChannels; // 已通知渠道
|
||||||
|
|
||||||
|
AlertNotification() : alertType(AlertType::UNCHECKED), time(0) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 报表请求参数
|
||||||
|
struct ReportRequest {
|
||||||
|
ReportDimension dimension; // 报表维度
|
||||||
|
std::string startDate; // 开始日期 YYYY-MM-DD
|
||||||
|
std::string endDate; // 结束日期 YYYY-MM-DD
|
||||||
|
ExportFormat format; // 导出格式
|
||||||
|
std::string deptId; // 部门ID(仅DEPT维度)
|
||||||
|
|
||||||
|
ReportRequest()
|
||||||
|
: dimension(ReportDimension::MONTH), format(ExportFormat::EXCEL) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 报表响应
|
||||||
|
struct ReportResponse {
|
||||||
|
std::string reportUrl; // 报表下载链接
|
||||||
|
int64_t fileSize = 0; // 文件大小(字节)
|
||||||
|
bool success = false;
|
||||||
|
std::string message;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 系统用户
|
||||||
|
struct SystemUser {
|
||||||
|
std::string userId;
|
||||||
|
std::string username;
|
||||||
|
SystemRole role;
|
||||||
|
std::vector<std::string> permissions; // 权限集合
|
||||||
|
std::string departmentId;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 外部同步请求
|
||||||
|
struct SyncRequest {
|
||||||
|
std::string authToken;
|
||||||
|
SyncDataType dataType;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// 外部同步响应
|
||||||
|
struct SyncResponse {
|
||||||
|
SyncStatus syncStatus;
|
||||||
|
std::string errorMsg;
|
||||||
|
|
||||||
|
SyncResponse() : syncStatus(SyncStatus::SUCCESS) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 核心功能函数声明
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
/// 将 CheckinMethod 枚举转换为字符串
|
||||||
|
std::string checkinMethodToString(CheckinMethod method);
|
||||||
|
|
||||||
|
/// 将 AttendanceStatus 枚举转换为字符串
|
||||||
|
std::string attendanceStatusToString(AttendanceStatus status);
|
||||||
|
|
||||||
|
/// 将 AlertType 枚举转换为字符串
|
||||||
|
std::string alertTypeToString(AlertType type);
|
||||||
|
|
||||||
|
/// 生成唯一记录ID (基于时间戳+随机数)
|
||||||
|
std::string generateRecordId();
|
||||||
|
|
||||||
|
/// 验证JWT令牌(简化模拟)
|
||||||
|
bool validateToken(const std::string& token);
|
||||||
|
|
||||||
|
/// 验证打卡权限
|
||||||
|
bool checkCheckinPermission(const std::string& employeeId, CheckinMethod method);
|
||||||
|
|
||||||
|
/// 记录打卡(核心业务函数)
|
||||||
|
CheckinRecord processCheckin(const std::string& employeeId, CheckinMethod method,
|
||||||
|
int64_t timestamp, const GeoLocation& location,
|
||||||
|
const std::vector<uint8_t>& faceFeature);
|
||||||
|
|
||||||
|
/// 判定考勤结果
|
||||||
|
AttendanceResult calculateAttendance(const CheckinRecord& record,
|
||||||
|
const ShiftRule& shiftRule);
|
||||||
|
|
||||||
|
/// 检测异常并生成预警
|
||||||
|
AlertNotification detectAnomaly(const AttendanceResult& result);
|
||||||
|
|
||||||
|
/// 生成报表(模拟)
|
||||||
|
ReportResponse generateReport(const ReportRequest& request);
|
||||||
|
|
||||||
|
/// 加密人脸特征(哈希+盐值模拟)
|
||||||
|
std::vector<uint8_t> encryptFaceFeature(const std::vector<uint8_t>& feature);
|
||||||
|
|
||||||
|
/// 运行演示场景(主菜单)
|
||||||
|
void runDemo();
|
||||||
|
|
||||||
|
#endif // ATTENDANCE_SYSTEM_APP_HPP
|
||||||
|
|
@ -0,0 +1,323 @@
|
||||||
|
#include "app.hpp"
|
||||||
|
#include <iostream>
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <random>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <thread>
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 工具函数实现
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
std::string checkinMethodToString(CheckinMethod method) {
|
||||||
|
switch (method) {
|
||||||
|
case CheckinMethod::FACE: return "人脸识别";
|
||||||
|
case CheckinMethod::GPS: return "GPS定位";
|
||||||
|
case CheckinMethod::CARD: return "工牌刷卡";
|
||||||
|
default: return "未知方式";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string attendanceStatusToString(AttendanceStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case AttendanceStatus::NORMAL: return "正常";
|
||||||
|
case AttendanceStatus::LATE: return "迟到";
|
||||||
|
case AttendanceStatus::EARLY: return "早退";
|
||||||
|
case AttendanceStatus::ABSENT: return "缺勤";
|
||||||
|
case AttendanceStatus::OVERTIME:return "加班";
|
||||||
|
default: return "未知";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string alertTypeToString(AlertType type) {
|
||||||
|
switch (type) {
|
||||||
|
case AlertType::UNCHECKED: return "未打卡";
|
||||||
|
case AlertType::EARLY: return "早退预警";
|
||||||
|
case AlertType::LATE: return "迟到预警";
|
||||||
|
case AlertType::MISSING: return "连续缺勤";
|
||||||
|
default: return "未知预警";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string generateRecordId() {
|
||||||
|
auto now = std::chrono::system_clock::now();
|
||||||
|
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
now.time_since_epoch()).count();
|
||||||
|
|
||||||
|
std::random_device rd;
|
||||||
|
std::mt19937 gen(rd());
|
||||||
|
std::uniform_int_distribution<int> dist(1000, 9999);
|
||||||
|
int randSuffix = dist(gen);
|
||||||
|
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << "CK" << ms << randSuffix;
|
||||||
|
std::string id = oss.str();
|
||||||
|
if (id.length() > 20) id = id.substr(0, 20);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 验证与鉴权(模拟实现)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
bool validateToken(const std::string& token) {
|
||||||
|
// 简化模拟:非空且长度 >= 8 视为有效
|
||||||
|
return token.length() >= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool checkCheckinPermission(const std::string& employeeId, CheckinMethod method) {
|
||||||
|
// 简化模拟:所有员工都允许所有打卡方式
|
||||||
|
(void)employeeId;
|
||||||
|
(void)method;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 加密模块(模拟实现)
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
std::vector<uint8_t> encryptFaceFeature(const std::vector<uint8_t>& feature) {
|
||||||
|
// 模拟哈希+盐值加密:对每个字节进行异或+偏移
|
||||||
|
const uint8_t SALT = 0xA5;
|
||||||
|
std::vector<uint8_t> encrypted = feature;
|
||||||
|
for (auto& b : encrypted) {
|
||||||
|
b ^= SALT;
|
||||||
|
b = static_cast<uint8_t>((b << 3) | (b >> 5));
|
||||||
|
}
|
||||||
|
return encrypted;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 核心业务函数实现
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
CheckinRecord processCheckin(const std::string& employeeId, CheckinMethod method,
|
||||||
|
int64_t timestamp, const GeoLocation& location,
|
||||||
|
const std::vector<uint8_t>& faceFeature) {
|
||||||
|
CheckinRecord record;
|
||||||
|
|
||||||
|
// 1. 生成记录ID
|
||||||
|
record.recordId = generateRecordId();
|
||||||
|
|
||||||
|
// 2. 填充基本信息
|
||||||
|
record.employeeId = employeeId;
|
||||||
|
record.method = method;
|
||||||
|
record.timestamp = timestamp;
|
||||||
|
record.location = location;
|
||||||
|
|
||||||
|
// 3. 加密人脸特征(仅人脸识别方式需要)
|
||||||
|
if (method == CheckinMethod::FACE && !faceFeature.empty()) {
|
||||||
|
record.faceFeature = encryptFaceFeature(faceFeature);
|
||||||
|
} else {
|
||||||
|
record.faceFeature = faceFeature;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. 验证权限
|
||||||
|
if (!checkCheckinPermission(employeeId, method)) {
|
||||||
|
record.status = CheckinStatus::FAILED;
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 验证令牌(模拟)
|
||||||
|
record.status = CheckinStatus::SUCCESS;
|
||||||
|
|
||||||
|
// 模拟发布消息队列事件(仅打印日志)
|
||||||
|
std::cout << "[MQ] Topic: new-checkin-event | Event: "
|
||||||
|
<< record.recordId << " | Employee: " << employeeId
|
||||||
|
<< " | Method: " << checkinMethodToString(method)
|
||||||
|
<< " | Status: " << (record.status == CheckinStatus::SUCCESS ? "SUCCESS" : "FAILED")
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
|
return record;
|
||||||
|
}
|
||||||
|
|
||||||
|
AttendanceResult calculateAttendance(const CheckinRecord& record,
|
||||||
|
const ShiftRule& shiftRule) {
|
||||||
|
AttendanceResult result;
|
||||||
|
result.employeeId = record.employeeId;
|
||||||
|
|
||||||
|
// 将时间戳转为 tm 结构
|
||||||
|
time_t secs = record.timestamp / 1000;
|
||||||
|
struct tm* tm_info = std::localtime(&secs);
|
||||||
|
|
||||||
|
std::ostringstream dateStream;
|
||||||
|
dateStream << (tm_info->tm_year + 1900) << "-"
|
||||||
|
<< std::setw(2) << std::setfill('0') << (tm_info->tm_mon + 1) << "-"
|
||||||
|
<< std::setw(2) << std::setfill('0') << tm_info->tm_mday;
|
||||||
|
result.date = dateStream.str();
|
||||||
|
|
||||||
|
// 实际打卡时间
|
||||||
|
result.actualInTime = record.timestamp;
|
||||||
|
|
||||||
|
// 排班时间
|
||||||
|
result.scheduledInHour = shiftRule.startHour;
|
||||||
|
result.scheduledInMin = shiftRule.startMinute;
|
||||||
|
result.scheduledOutHour = shiftRule.endHour;
|
||||||
|
result.scheduledOutMin = shiftRule.endMinute;
|
||||||
|
|
||||||
|
// 计算迟到分钟数
|
||||||
|
int actualMinuteOfDay = tm_info->tm_hour * 60 + tm_info->tm_min;
|
||||||
|
int scheduledStartMinute = shiftRule.startHour * 60 + shiftRule.startMinute;
|
||||||
|
int scheduledEndMinute = shiftRule.endHour * 60 + shiftRule.endMinute;
|
||||||
|
|
||||||
|
if (actualMinuteOfDay > scheduledStartMinute + shiftRule.breakDuration) {
|
||||||
|
result.lateMinutes = actualMinuteOfDay - scheduledStartMinute;
|
||||||
|
result.status = AttendanceStatus::LATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 判断考勤状态
|
||||||
|
if (result.lateMinutes > 0) {
|
||||||
|
result.status = AttendanceStatus::LATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没迟到,检查是否加班(下班后还在打卡)
|
||||||
|
if (result.lateMinutes == 0) {
|
||||||
|
if (actualMinuteOfDay > scheduledEndMinute + shiftRule.overtimeThreshold) {
|
||||||
|
result.overtimeHours = static_cast<float>(actualMinuteOfDay - scheduledEndMinute) / 60.0f;
|
||||||
|
result.status = AttendanceStatus::OVERTIME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
AlertNotification detectAnomaly(const AttendanceResult& result) {
|
||||||
|
AlertNotification alert;
|
||||||
|
alert.employeeId = result.employeeId;
|
||||||
|
alert.time = static_cast<int64_t>(
|
||||||
|
std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::system_clock::now().time_since_epoch()).count());
|
||||||
|
|
||||||
|
if (result.absent) {
|
||||||
|
alert.alertType = AlertType::MISSING;
|
||||||
|
alert.reason = "员工 " + result.employeeId + " 在 " + result.date + " 缺勤";
|
||||||
|
} else if (result.lateMinutes > 30) {
|
||||||
|
alert.alertType = AlertType::LATE;
|
||||||
|
alert.reason = "员工 " + result.employeeId + " 在 " + result.date +
|
||||||
|
" 迟到 " + std::to_string(result.lateMinutes) + " 分钟";
|
||||||
|
} else if (result.earlyLeaveMinutes > 15) {
|
||||||
|
alert.alertType = AlertType::EARLY;
|
||||||
|
alert.reason = "员工 " + result.employeeId + " 在 " + result.date +
|
||||||
|
" 早退 " + std::to_string(result.earlyLeaveMinutes) + " 分钟";
|
||||||
|
} else {
|
||||||
|
// 无异常,返回一个空的预警(表示正常)
|
||||||
|
alert.alertType = AlertType::UNCHECKED;
|
||||||
|
alert.reason = "正常";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟触发三重通知
|
||||||
|
if (alert.alertType != AlertType::UNCHECKED) {
|
||||||
|
alert.notifiedChannels.push_back("SMS");
|
||||||
|
alert.notifiedChannels.push_back("EMAIL");
|
||||||
|
alert.notifiedChannels.push_back("APP");
|
||||||
|
std::cout << "[NOTIFY] 发送预警通知: " << alert.reason
|
||||||
|
<< " | 渠道: SMS, EMAIL, APP" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReportResponse generateReport(const ReportRequest& request) {
|
||||||
|
ReportResponse response;
|
||||||
|
|
||||||
|
std::string dimStr;
|
||||||
|
switch (request.dimension) {
|
||||||
|
case ReportDimension::MONTH: dimStr = "月度"; break;
|
||||||
|
case ReportDimension::DEPT: dimStr = "部门"; break;
|
||||||
|
case ReportDimension::PROJECT: dimStr = "项目"; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string fmtStr;
|
||||||
|
switch (request.format) {
|
||||||
|
case ExportFormat::EXCEL: fmtStr = "xlsx"; break;
|
||||||
|
case ExportFormat::PDF: fmtStr = "pdf"; break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 模拟生成报表URL
|
||||||
|
std::ostringstream url;
|
||||||
|
url << "/reports/" << dimStr << "_"
|
||||||
|
<< request.startDate << "_" << request.endDate << "." << fmtStr;
|
||||||
|
response.reportUrl = url.str();
|
||||||
|
response.fileSize = 1024 * 256; // 模拟256KB
|
||||||
|
response.success = true;
|
||||||
|
response.message = "报表生成成功";
|
||||||
|
|
||||||
|
std::cout << "[REPORT] 生成报表: " << response.reportUrl
|
||||||
|
<< " | 大小: " << (response.fileSize / 1024) << "KB"
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// 演示运行
|
||||||
|
// ============================================================
|
||||||
|
|
||||||
|
void runDemo() {
|
||||||
|
std::cout << "\n========== 企业考勤管理系统演示 ==========" << std::endl;
|
||||||
|
|
||||||
|
// 演示1: 人脸识别打卡
|
||||||
|
std::cout << "\n--- 场景1: 人脸识别打卡 ---" << std::endl;
|
||||||
|
GeoLocation loc1 = {39.9042, 116.4074}; // 北京
|
||||||
|
std::vector<uint8_t> face1 = {0x01, 0x02, 0x03, 0x04, 0x05};
|
||||||
|
auto now = std::chrono::duration_cast<std::chrono::milliseconds>(
|
||||||
|
std::chrono::system_clock::now().time_since_epoch()).count();
|
||||||
|
CheckinRecord rec1 = processCheckin("EMP001", CheckinMethod::FACE, now, loc1, face1);
|
||||||
|
std::cout << " 记录ID: " << rec1.recordId << std::endl;
|
||||||
|
std::cout << " 状态: " << (rec1.status == CheckinStatus::SUCCESS ? "成功" : "失败") << std::endl;
|
||||||
|
|
||||||
|
// 演示2: 迟到判定
|
||||||
|
std::cout << "\n--- 场景2: 迟到判定 ---" << std::endl;
|
||||||
|
ShiftRule morningShift;
|
||||||
|
morningShift.shiftName = "早班";
|
||||||
|
morningShift.startHour = 9;
|
||||||
|
morningShift.startMinute = 0;
|
||||||
|
morningShift.endHour = 18;
|
||||||
|
morningShift.endMinute = 0;
|
||||||
|
morningShift.breakDuration = 60;
|
||||||
|
morningShift.overtimeThreshold = 30;
|
||||||
|
|
||||||
|
// 模拟9:45打卡
|
||||||
|
struct tm late_tm = {};
|
||||||
|
late_tm.tm_year = 2025 - 1900;
|
||||||
|
late_tm.tm_mon = 0;
|
||||||
|
late_tm.tm_mday = 15;
|
||||||
|
late_tm.tm_hour = 9;
|
||||||
|
late_tm.tm_min = 45;
|
||||||
|
late_tm.tm_sec = 0;
|
||||||
|
time_t late_sec = mktime(&late_tm);
|
||||||
|
int64_t late_ts = static_cast<int64_t>(late_sec) * 1000;
|
||||||
|
|
||||||
|
CheckinRecord rec2;
|
||||||
|
rec2.employeeId = "EMP002";
|
||||||
|
rec2.timestamp = late_ts;
|
||||||
|
rec2.method = CheckinMethod::GPS;
|
||||||
|
|
||||||
|
AttendanceResult result2 = calculateAttendance(rec2, morningShift);
|
||||||
|
std::cout << " 员工: " << result2.employeeId
|
||||||
|
<< " | 日期: " << result2.date
|
||||||
|
<< " | 状态: " << attendanceStatusToString(result2.status)
|
||||||
|
<< " | 迟到: " << result2.lateMinutes << "分钟"
|
||||||
|
<< std::endl;
|
||||||
|
|
||||||
|
// 演示3: 异常检测与通知
|
||||||
|
std::cout << "\n--- 场景3: 异常检测与通知 ---" << std::endl;
|
||||||
|
AlertNotification alert = detectAnomaly(result2);
|
||||||
|
std::cout << " 预警类型: " << alertTypeToString(alert.alertType) << std::endl;
|
||||||
|
std::cout << " 原因: " << alert.reason << std::endl;
|
||||||
|
|
||||||
|
// 演示4: 报表生成
|
||||||
|
std::cout << "\n--- 场景4: 报表生成 ---" << std::endl;
|
||||||
|
ReportRequest req;
|
||||||
|
req.dimension = ReportDimension::MONTH;
|
||||||
|
req.startDate = "2025-01-01";
|
||||||
|
req.endDate = "2025-01-31";
|
||||||
|
req.format = ExportFormat::EXCEL;
|
||||||
|
ReportResponse rep = generateReport(req);
|
||||||
|
std::cout << " 报表URL: " << rep.reportUrl << std::endl;
|
||||||
|
std::cout << " 状态: " << (rep.success ? "成功" : "失败") << std::endl;
|
||||||
|
|
||||||
|
std::cout << "\n========== 演示结束 ==========" << std::endl;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,43 @@
|
||||||
|
/**
|
||||||
|
* @file main.cpp
|
||||||
|
* @brief 企业考勤管理系统 - 命令行入口
|
||||||
|
*
|
||||||
|
* 演示考勤打卡、迟到判定、异常检测、报表生成等功能。
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "app.hpp"
|
||||||
|
#include <iostream>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
/// 打印帮助信息
|
||||||
|
static void printUsage() {
|
||||||
|
std::cout << "用法: attendance_system [选项]" << std::endl;
|
||||||
|
std::cout << "选项:" << std::endl;
|
||||||
|
std::cout << " --demo 运行完整演示场景(默认)" << std::endl;
|
||||||
|
std::cout << " --help 显示此帮助信息" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
int main(int argc, char* argv[]) {
|
||||||
|
// 解析命令行参数
|
||||||
|
bool runDemoMode = true;
|
||||||
|
|
||||||
|
if (argc > 1) {
|
||||||
|
std::string arg = argv[1];
|
||||||
|
if (arg == "--help") {
|
||||||
|
printUsage();
|
||||||
|
return 0;
|
||||||
|
} else if (arg == "--demo") {
|
||||||
|
runDemoMode = true;
|
||||||
|
} else {
|
||||||
|
std::cerr << "未知参数: " << arg << std::endl;
|
||||||
|
printUsage();
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runDemoMode) {
|
||||||
|
runDemo();
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,214 @@
|
||||||
|
/**
|
||||||
|
* @file basic_test.cpp
|
||||||
|
* @brief 基础测试文件 - 使用标准库 assert,无外部依赖
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "app.hpp"
|
||||||
|
#include <cassert>
|
||||||
|
#include <iostream>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
/// 测试枚举转换函数
|
||||||
|
static void testEnumToString() {
|
||||||
|
std::cout << "[TEST] 枚举转换函数..." << std::endl;
|
||||||
|
|
||||||
|
assert(checkinMethodToString(CheckinMethod::FACE) == "人脸识别");
|
||||||
|
assert(checkinMethodToString(CheckinMethod::GPS) == "GPS定位");
|
||||||
|
assert(checkinMethodToString(CheckinMethod::CARD) == "工牌刷卡");
|
||||||
|
|
||||||
|
assert(attendanceStatusToString(AttendanceStatus::NORMAL) == "正常");
|
||||||
|
assert(attendanceStatusToString(AttendanceStatus::LATE) == "迟到");
|
||||||
|
assert(attendanceStatusToString(AttendanceStatus::EARLY) == "早退");
|
||||||
|
assert(attendanceStatusToString(AttendanceStatus::ABSENT) == "缺勤");
|
||||||
|
assert(attendanceStatusToString(AttendanceStatus::OVERTIME) == "加班");
|
||||||
|
|
||||||
|
assert(alertTypeToString(AlertType::UNCHECKED) == "未打卡");
|
||||||
|
assert(alertTypeToString(AlertType::EARLY) == "早退预警");
|
||||||
|
assert(alertTypeToString(AlertType::LATE) == "迟到预警");
|
||||||
|
assert(alertTypeToString(AlertType::MISSING) == "连续缺勤");
|
||||||
|
|
||||||
|
std::cout << " PASS" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试记录ID生成
|
||||||
|
static void testGenerateRecordId() {
|
||||||
|
std::cout << "[TEST] recordId生成..." << std::endl;
|
||||||
|
std::string id1 = generateRecordId();
|
||||||
|
std::string id2 = generateRecordId();
|
||||||
|
|
||||||
|
assert(!id1.empty());
|
||||||
|
assert(!id2.empty());
|
||||||
|
assert(id1 != id2); // 两次生成的ID应该不同
|
||||||
|
assert(id1.substr(0, 2) == "CK");
|
||||||
|
|
||||||
|
std::cout << " PASS: id1=" << id1 << ", id2=" << id2 << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试打卡处理
|
||||||
|
static void testProcessCheckin() {
|
||||||
|
std::cout << "[TEST] 打卡处理..." << std::endl;
|
||||||
|
|
||||||
|
GeoLocation loc = {39.9042, 116.4074};
|
||||||
|
std::vector<uint8_t> face = {0x01, 0x02, 0x03};
|
||||||
|
int64_t ts = 1700000000000LL; // 固定时间戳
|
||||||
|
|
||||||
|
// 人脸打卡
|
||||||
|
CheckinRecord rec1 = processCheckin("EMP001", CheckinMethod::FACE, ts, loc, face);
|
||||||
|
assert(rec1.employeeId == "EMP001");
|
||||||
|
assert(rec1.status == CheckinStatus::SUCCESS);
|
||||||
|
assert(!rec1.recordId.empty());
|
||||||
|
|
||||||
|
// GPS打卡
|
||||||
|
CheckinRecord rec2 = processCheckin("EMP002", CheckinMethod::GPS, ts, loc, {});
|
||||||
|
assert(rec2.employeeId == "EMP002");
|
||||||
|
assert(rec2.status == CheckinStatus::SUCCESS);
|
||||||
|
|
||||||
|
// 工牌刷卡
|
||||||
|
CheckinRecord rec3 = processCheckin("EMP003", CheckinMethod::CARD, ts, loc, {});
|
||||||
|
assert(rec3.employeeId == "EMP003");
|
||||||
|
assert(rec3.status == CheckinStatus::SUCCESS);
|
||||||
|
|
||||||
|
std::cout << " PASS: 三种打卡方式均成功" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试考勤计算
|
||||||
|
static void testCalculateAttendance() {
|
||||||
|
std::cout << "[TEST] 考勤计算..." << std::endl;
|
||||||
|
|
||||||
|
ShiftRule shift;
|
||||||
|
shift.shiftName = "早班";
|
||||||
|
shift.startHour = 9;
|
||||||
|
shift.startMinute = 0;
|
||||||
|
shift.endHour = 18;
|
||||||
|
shift.endMinute = 0;
|
||||||
|
shift.breakDuration = 60;
|
||||||
|
shift.overtimeThreshold = 30;
|
||||||
|
|
||||||
|
// 场景1: 准时打卡 (9:00)
|
||||||
|
{
|
||||||
|
struct tm tm_in = {};
|
||||||
|
tm_in.tm_year = 2025 - 1900;
|
||||||
|
tm_in.tm_mon = 0;
|
||||||
|
tm_in.tm_mday = 15;
|
||||||
|
tm_in.tm_hour = 9;
|
||||||
|
tm_in.tm_min = 0;
|
||||||
|
time_t sec = mktime(&tm_in);
|
||||||
|
CheckinRecord rec;
|
||||||
|
rec.employeeId = "EMP001";
|
||||||
|
rec.timestamp = static_cast<int64_t>(sec) * 1000;
|
||||||
|
|
||||||
|
AttendanceResult res = calculateAttendance(rec, shift);
|
||||||
|
assert(res.lateMinutes == 0);
|
||||||
|
assert(res.status == AttendanceStatus::NORMAL || res.status == AttendanceStatus::NORMAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 场景2: 迟到 (9:45)
|
||||||
|
{
|
||||||
|
struct tm tm_in = {};
|
||||||
|
tm_in.tm_year = 2025 - 1900;
|
||||||
|
tm_in.tm_mon = 0;
|
||||||
|
tm_in.tm_mday = 15;
|
||||||
|
tm_in.tm_hour = 9;
|
||||||
|
tm_in.tm_min = 45;
|
||||||
|
time_t sec = mktime(&tm_in);
|
||||||
|
CheckinRecord rec;
|
||||||
|
rec.employeeId = "EMP002";
|
||||||
|
rec.timestamp = static_cast<int64_t>(sec) * 1000;
|
||||||
|
|
||||||
|
AttendanceResult res = calculateAttendance(rec, shift);
|
||||||
|
assert(res.lateMinutes > 0);
|
||||||
|
assert(res.status == AttendanceStatus::LATE);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << " PASS: 正常与迟到判定正确" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试异常检测
|
||||||
|
static void testDetectAnomaly() {
|
||||||
|
std::cout << "[TEST] 异常检测..." << std::endl;
|
||||||
|
|
||||||
|
// 正常情况 - 不应生成预警
|
||||||
|
{
|
||||||
|
AttendanceResult normal;
|
||||||
|
normal.employeeId = "EMP001";
|
||||||
|
normal.date = "2025-01-15";
|
||||||
|
normal.lateMinutes = 0;
|
||||||
|
normal.absent = false;
|
||||||
|
|
||||||
|
AlertNotification alert = detectAnomaly(normal);
|
||||||
|
assert(alert.alertType == AlertType::UNCHECKED);
|
||||||
|
assert(alert.reason == "正常");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 迟到超过30分钟 - 应生成迟到预警
|
||||||
|
{
|
||||||
|
AttendanceResult late;
|
||||||
|
late.employeeId = "EMP002";
|
||||||
|
late.date = "2025-01-15";
|
||||||
|
late.lateMinutes = 45;
|
||||||
|
late.absent = false;
|
||||||
|
|
||||||
|
AlertNotification alert = detectAnomaly(late);
|
||||||
|
assert(alert.alertType == AlertType::LATE);
|
||||||
|
assert(!alert.notifiedChannels.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
std::cout << " PASS" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试报表生成
|
||||||
|
static void testGenerateReport() {
|
||||||
|
std::cout << "[TEST] 报表生成..." << std::endl;
|
||||||
|
|
||||||
|
ReportRequest req;
|
||||||
|
req.dimension = ReportDimension::MONTH;
|
||||||
|
req.startDate = "2025-01-01";
|
||||||
|
req.endDate = "2025-01-31";
|
||||||
|
req.format = ExportFormat::EXCEL;
|
||||||
|
|
||||||
|
ReportResponse resp = generateReport(req);
|
||||||
|
assert(resp.success);
|
||||||
|
assert(!resp.reportUrl.empty());
|
||||||
|
assert(resp.fileSize > 0);
|
||||||
|
|
||||||
|
std::cout << " PASS: URL=" << resp.reportUrl << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 测试加密模块
|
||||||
|
static void testEncryption() {
|
||||||
|
std::cout << "[TEST] 人脸特征加密..." << std::endl;
|
||||||
|
|
||||||
|
std::vector<uint8_t> original = {0x10, 0x20, 0x30, 0x40, 0x50};
|
||||||
|
std::vector<uint8_t> encrypted = encryptFaceFeature(original);
|
||||||
|
|
||||||
|
assert(encrypted.size() == original.size());
|
||||||
|
|
||||||
|
// 加密后应该与原文不同
|
||||||
|
bool different = false;
|
||||||
|
for (size_t i = 0; i < original.size(); i++) {
|
||||||
|
if (encrypted[i] != original[i]) {
|
||||||
|
different = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert(different);
|
||||||
|
|
||||||
|
std::cout << " PASS: 加密后数据已变更" << std::endl;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 主测试入口
|
||||||
|
int main() {
|
||||||
|
std::cout << "========== 考勤系统单元测试 ==========" << std::endl;
|
||||||
|
|
||||||
|
testEnumToString();
|
||||||
|
testGenerateRecordId();
|
||||||
|
testProcessCheckin();
|
||||||
|
testCalculateAttendance();
|
||||||
|
testDetectAnomaly();
|
||||||
|
testGenerateReport();
|
||||||
|
testEncryption();
|
||||||
|
|
||||||
|
std::cout << "\n========== 所有测试通过! ==========" << std::endl;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue