怎样在PHP项目中实现请假管理?

wen java案例 5

本文目录导读:

怎样在PHP项目中实现请假管理?

  1. 数据库设计
  2. PHP核心功能实现
  3. 前端界面示例(HTML + Vue.js)
  4. 安全性和最佳实践
  5. 部署配置建议

我来详细介绍在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

这个请假管理系统实现了以下功能:

  1. 完整的请假申请、审批流程
  2. 假期余额自动管理
  3. 工作日自动计算(排除周末和节假日)
  4. 审批权限控制
  5. 数据一致性保障
  6. 操作日志记录
  7. 邮件/通知提醒

你可以根据实际需求进行扩展,比如增加:

  • 多级审批流程
  • 请假规则配置
  • 报表统计分析
  • 考勤数据对接
  • 移动端适配等

抱歉,评论功能暂时关闭!