本文目录导读:

我来详细介绍在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
这个请假管理系统实现了以下功能:
- 完整的请假申请、审批流程
- 假期余额自动管理
- 工作日自动计算(排除周末和节假日)
- 审批权限控制
- 数据一致性保障
- 操作日志记录
- 邮件/通知提醒
你可以根据实际需求进行扩展,比如增加:
- 多级审批流程
- 请假规则配置
- 报表统计分析
- 考勤数据对接
- 移动端适配等
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播