本文目录导读:

我来详细介绍在PHP项目中实现请假管理的完整方案。
数据库设计
首先设计请假相关的数据表:
-- 请假申请表 CREATE TABLE `leave_requests` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL COMMENT '申请人ID', `leave_type` tinyint(4) NOT NULL COMMENT '请假类型:1年假,2病假,3事假,4婚假,5产假', `start_date` date NOT NULL COMMENT '开始日期', `end_date` date NOT NULL COMMENT '结束日期', `total_days` decimal(5,1) NOT NULL COMMENT '总天数', `reason` text COMMENT '请假原因', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0待审批,1已批准,2已拒绝,3已撤销', `approver_id` int(11) DEFAULT NULL COMMENT '审批人ID', `approve_remark` text COMMENT '审批意见', `approve_time` datetime DEFAULT NULL COMMENT '审批时间', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `user_id` (`user_id`), KEY `approver_id` (`approver_id`), KEY `status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 请假类型表 CREATE TABLE `leave_types` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '类型名称', `days_per_year` int(11) NOT NULL COMMENT '每年可用天数', `is_paid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否带薪', `is_active` tinyint(1) NOT NULL DEFAULT '1', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 员工假期余额表 CREATE TABLE `leave_balances` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `leave_type_id` int(11) NOT NULL, `year` year(4) NOT NULL, `total_days` decimal(5,1) NOT NULL DEFAULT '0', `used_days` decimal(5,1) NOT NULL DEFAULT '0', `remaining_days` decimal(5,1) GENERATED ALWAYS AS (total_days - used_days) STORED, PRIMARY KEY (`id`), UNIQUE KEY `unique_user_type_year` (`user_id`,`leave_type_id`,`year`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 审批记录表 CREATE TABLE `approval_records` ( `id` int(11) NOT NULL AUTO_INCREMENT, `leave_id` int(11) NOT NULL, `approver_id` int(11) NOT NULL, `action` varchar(20) NOT NULL COMMENT 'approve/reject', `remark` text, `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `leave_id` (`leave_id`), KEY `approver_id` (`approver_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
PHP核心功能实现
LeaveController.php
<?php
namespace App\Controllers;
use App\Models\LeaveRequest;
use App\Models\LeaveBalance;
use App\Helpers\DateHelper;
use App\Exceptions\LeaveException;
class LeaveController extends BaseController
{
private $leaveModel;
private $balanceModel;
public function __construct() {
parent::__construct();
$this->leaveModel = new LeaveRequest();
$this->balanceModel = new LeaveBalance();
}
/**
* 提交请假申请
*/
public function submitLeave($request) {
try {
// 验证输入
$this->validateLeaveInput($request);
// 计算请假天数(排除周末和节假日)
$totalDays = DateHelper::calculateWorkingDays(
$request['start_date'],
$request['end_date']
);
if ($totalDays <= 0) {
throw new LeaveException('请假天数必须大于0');
}
// 检查假期余额
$this->checkLeaveBalance(
$this->userId,
$request['leave_type'],
$totalDays
);
// 创建请假记录
$leaveData = [
'user_id' => $this->userId,
'leave_type' => $request['leave_type'],
'start_date' => $request['start_date'],
'end_date' => $request['end_date'],
'total_days' => $totalDays,
'reason' => $request['reason'] ?? '',
'status' => 0 // 待审批
];
$leaveId = $this->leaveModel->create($leaveData);
// 更新假期余额(预扣)
$this->balanceModel->deductBalance(
$this->userId,
$request['leave_type'],
$totalDays
);
// 发送通知给审批人
$this->sendApprovalNotification($leaveId);
return $this->successResponse('请假申请提交成功', ['id' => $leaveId]);
} catch (LeaveException $e) {
return $this->errorResponse($e->getMessage());
}
}
/**
* 审批请假
*/
public function approveLeave($request) {
try {
$leaveId = $request['leave_id'];
$action = $request['action']; // approve 或 reject
$remark = $request['remark'] ?? '';
// 验证审批权限
$leave = $this->leaveModel->findById($leaveId);
if (!$leave) {
throw new LeaveException('请假记录不存在');
}
if (!$this->hasApprovalPermission($this->userId, $leave['user_id'])) {
throw new LeaveException('您没有权限审批该请假');
}
if ($leave['status'] != 0) {
throw new LeaveException('该请假已经审批过');
}
// 更新状态
$updateData = [
'status' => $action === 'approve' ? 1 : 2,
'approver_id' => $this->userId,
'approve_remark' => $remark,
'approve_time' => date('Y-m-d H:i:s')
];
$this->leaveModel->update($leaveId, $updateData);
// 记录审批日志
$this->createApprovalRecord($leaveId, $action, $remark);
// 如果拒绝,恢复假期余额
if ($action === 'reject') {
$this->balanceModel->restoreBalance(
$leave['user_id'],
$leave['leave_type'],
$leave['total_days']
);
}
// 发送通知给申请人
$this->sendLeaveNotification($leave['user_id'], $action);
return $this->successResponse('审批完成');
} catch (LeaveException $e) {
return $this->errorResponse($e->getMessage());
}
}
/**
* 查询请假记录
*/
public function getLeaveList($request) {
$page = $request['page'] ?? 1;
$pageSize = $request['page_size'] ?? 20;
$userId = $request['user_id'] ?? $this->userId;
$status = $request['status'] ?? null;
$startDate = $request['start_date'] ?? null;
$endDate = $request['end_date'] ?? null;
$filters = [
'user_id' => $userId,
'status' => $status,
'start_date' => $startDate,
'end_date' => $endDate
];
$result = $this->leaveModel->getList($filters, $page, $pageSize);
return $this->successResponse('查询成功', $result);
}
/**
* 撤销请假申请
*/
public function cancelLeave($leaveId) {
try {
$leave = $this->leaveModel->findById($leaveId);
if (!$leave || $leave['user_id'] != $this->userId) {
throw new LeaveException('无权限撤销该申请');
}
if ($leave['status'] != 0) {
throw new LeaveException('只能撤销待审批的申请');
}
// 更新状态为已撤销
$this->leaveModel->update($leaveId, ['status' => 3]);
// 恢复假期余额
$this->balanceModel->restoreBalance(
$leave['user_id'],
$leave['leave_type'],
$leave['total_days']
);
return $this->successResponse('请假已撤销');
} catch (LeaveException $e) {
return $this->errorResponse($e->getMessage());
}
}
private function validateLeaveInput($request) {
$rules = [
'leave_type' => 'required|numeric|exists:leave_types,id',
'start_date' => 'required|date|after_or_equal:today',
'end_date' => 'required|date|after_or_equal:start_date',
'reason' => 'required|max:500'
];
$validator = new Validator($request, $rules);
if ($validator->fails()) {
throw new LeaveException($validator->errors()->first());
}
}
private function checkLeaveBalance($userId, $leaveType, $days) {
$balance = $this->balanceModel->getCurrentBalance($userId, $leaveType);
if (!$balance || $balance['remaining_days'] < $days) {
throw new LeaveException('假期余额不足');
}
}
}
DateHelper.php - 日期计算工具
<?php
namespace App\Helpers;
class DateHelper
{
// 节假日配置(实际应用中可从数据库读取)
private static $holidays = [
'2024-01-01', // 元旦
'2024-02-10', '2024-02-11', '2024-02-12', // 春节
// 更多节假日...
];
/**
* 计算工作日天数
*/
public static function calculateWorkingDays($startDate, $endDate) {
$start = new \DateTime($startDate);
$end = new \DateTime($endDate);
$end = $end->modify('+1 day'); // 包含结束日期
$workingDays = 0;
$period = new \DatePeriod($start, new \DateInterval('P1D'), $end);
foreach ($period as $date) {
$dateStr = $date->format('Y-m-d');
// 排除周末
if ($date->format('N') >= 6) {
continue;
}
// 排除法定节假日
if (in_array($dateStr, self::$holidays)) {
continue;
}
$workingDays++;
}
return $workingDays;
}
/**
* 计算自然天数
*/
public static function calculateCalendarDays($startDate, $endDate) {
$start = new \DateTime($startDate);
$end = new \DateTime($endDate);
$interval = $start->diff($end);
return $interval->days + 1; // 包含首尾
}
}
前端界面示例(HTML + Vue.js)
<!-- 请假申请页面 -->
<div id="leaveApp">
<el-form :model="leaveForm" :rules="rules" ref="leaveForm" label-width="120px">
<el-form-item label="请假类型" prop="leaveType">
<el-select v-model="leaveForm.leaveType" placeholder="请选择请假类型">
<el-option
v-for="type in leaveTypes"
:key="type.id"
:label="type.name"
:value="type.id">
</el-option>
</el-select>
</el-form-item>
<el-form-item label="开始日期" prop="startDate">
<el-date-picker
v-model="leaveForm.startDate"
type="date"
placeholder="选择开始日期"
:picker-options="startDateOptions">
</el-date-picker>
</el-form-item>
<el-form-item label="结束日期" prop="endDate">
<el-date-picker
v-model="leaveForm.endDate"
type="date"
placeholder="选择结束日期"
:picker-options="endDateOptions">
</el-date-picker>
</el-form-item>
<el-form-item label="请假天数">
<el-input :value="totalDays" disabled></el-input>
</el-form-item>
<el-form-item label="假期余额">
<el-input :value="balanceInfo" disabled></el-input>
</el-form-item>
<el-form-item label="请假原因" prop="reason">
<el-input
type="textarea"
v-model="leaveForm.reason"
:rows="4"
placeholder="请输入请假原因">
</el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitLeave">提交申请</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</el-form>
</div>
<script>
new Vue({
el: '#leaveApp',
data() {
return {
leaveForm: {
leaveType: '',
startDate: '',
endDate: '',
reason: ''
},
leaveTypes: [],
totalDays: 0,
balanceInfo: '',
rules: {
leaveType: [
{ required: true, message: '请选择请假类型' }
],
startDate: [
{ required: true, message: '请选择开始日期' }
],
endDate: [
{ required: true, message: '请选择结束日期' }
],
reason: [
{ required: true, message: '请输入请假原因' },
{ max: 500, message: '原因不超过500字' }
]
}
}
},
computed: {
startDateOptions() {
return {
disabledDate: (time) => {
return time.getTime() < Date.now() - 86400000;
}
}
},
endDateOptions() {
return {
disabledDate: (time) => {
return this.leaveForm.startDate
? time.getTime() < new Date(this.leaveForm.startDate).getTime()
: time.getTime() < Date.now() - 86400000;
}
}
}
},
watch: {
'leaveForm.startDate'() {
this.calculateDays();
},
'leaveForm.endDate'() {
this.calculateDays();
}
},
methods: {
async calculateDays() {
if (this.leaveForm.startDate && this.leaveForm.endDate) {
try {
const response = await axios.post('/api/leave/calculate-days', {
start_date: this.leaveForm.startDate,
end_date: this.leaveForm.endDate,
leave_type: this.leaveForm.leaveType
});
this.totalDays = response.data.days;
this.balanceInfo = `${response.data.remaining_days}天`;
} catch (error) {
console.error('计算天数失败', error);
}
}
},
async submitLeave() {
this.$refs.leaveForm.validate(async (valid) => {
if (valid) {
try {
const response = await axios.post('/api/leave/submit', {
leave_type: this.leaveForm.leaveType,
start_date: this.leaveForm.startDate,
end_date: this.leaveForm.endDate,
total_days: this.totalDays,
reason: this.leaveForm.reason
});
this.$message.success('请假申请提交成功');
this.resetForm();
} catch (error) {
this.$message.error(error.response.data.message || '提交失败');
}
}
});
},
resetForm() {
this.$refs.leaveForm.resetFields();
this.totalDays = 0;
this.balanceInfo = '';
}
},
mounted() {
// 加载请假类型
axios.get('/api/leave-types').then(response => {
this.leaveTypes = response.data;
});
}
});
</script>
安全性和最佳实践
权限控制中间件
<?php
namespace App\Middlewares;
class LeavePermissionMiddleware
{
public function handle($request, $next) {
// 检查用户是否登录
if (!Auth::check()) {
return redirect('/login');
}
// 根据路由检查权限
$route = $request->route()->getName();
$permissions = [
'leave.submit' => ['employee', 'manager'],
'leave.approve' => ['manager', 'admin'],
'leave.list' => ['employee', 'manager', 'admin'],
'leave.cancel' => ['employee']
];
if (isset($permissions[$route])) {
if (!in_array(Auth::user()->role, $permissions[$route])) {
return response()->json(['error' => '无权限'], 403);
}
}
return $next($request);
}
}
数据库查询优化
// 使用索引优化查询
public function getPendingLeaves($managerId) {
$sql = "SELECT l.*, u.name as applicant_name, lt.name as leave_type_name
FROM leave_requests l
INNER JOIN users u ON l.user_id = u.id
INNER JOIN leave_types lt ON l.leave_type = lt.id
WHERE l.status = 0
AND l.user_id IN (
SELECT user_id FROM manager_subordinates WHERE manager_id = ?
)
ORDER BY l.created_at DESC
LIMIT 20";
return $this->db->query($sql, [$managerId])->fetchAll();
}
事务处理
// 确保数据一致性
public function submitLeaveWithTransaction($data) {
$this->db->beginTransaction();
try {
// 1. 创建请假记录
$leaveId = $this->leaveModel->create($data);
// 2. 扣除假期余额
$this->balanceModel->deductBalance(
$data['user_id'],
$data['leave_type'],
$data['total_days']
);
// 3. 记录操作日志
$this->logModel->create([
'action' => 'leave_submit',
'target_id' => $leaveId,
'user_id' => $data['user_id'],
'description' => '提交请假申请'
]);
$this->db->commit();
return $leaveId;
} catch (\Exception $e) {
$this->db->rollback();
throw $e;
}
}
部署配置建议
Nginx配置
server {
listen 80;
server_name leave.example.com;
root /var/www/leave-system/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.0-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 限制上传大小
client_max_body_size 10M;
# 安全配置
location ~ \.env {
deny all;
}
# 缓存配置
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
expires 1y;
add_header Cache-Control public;
}
}
环境变量配置
DB_HOST=localhost DB_PORT=3306 DB_DATABASE=leave_system DB_USERNAME=leave_user DB_PASSWORD=secure_password APP_ENV=production APP_DEBUG=false APP_URL=https://leave.example.com MAIL_HOST=smtp.example.com MAIL_PORT=587 MAIL_USERNAME=notification@example.com MAIL_PASSWORD=mail_password
这个请假管理系统实现了以下功能:
- 完整的请假申请、审批流程
- 假期余额自动管理
- 工作日自动计算(排除周末和节假日)
- 审批权限控制
- 数据一致性保障
- 操作日志记录
- 邮件/通知提醒
你可以根据实际需求进行扩展,比如增加:
- 多级审批流程
- 请假规则配置
- 报表统计分析
- 考勤数据对接
- 移动端适配等