PHP项目如何实现年假管理?

wen java案例 3

本文目录导读:

PHP项目如何实现年假管理?

  1. 核心业务规则
  2. 数据库表设计(核心)
  3. 核心 PHP 代码逻辑(伪代码示例)
  4. 用 Laravel / ThinkPHP 注意事项

这是一个比较典型的业务场景,在 PHP 项目中实现年假管理,核心不在于复杂的算法,而在于业务规则的配置、数据的连续性(自然年/入职周年)以及审批流的串联

下面从数据库设计、核心逻辑、代码示例、以及如何集成到现有框架(如 Laravel、ThinkPHP)这几个方面来详细说明。


核心业务规则

在写代码前,需要定义清楚规则,通常有这几种:

  1. 额度计算方式
    • 按自然年:每年1月1日发放全年额度(如每年5天)。
    • 按入职周年:入职满一年后,在周年日发放新一年的额度。
    • 首年按比例折算:新员工入职当年,按月或按天折算(如5天 / 12 * 剩余月数)。
  2. 有效期
    • 当年有效,过期清零。
    • 可顺延(如顺延至次年3月31日)。
  3. 余额计算
    • 总余额 = 发放额度 - 已使用 - 已申请(待审批)

数据库表设计(核心)

需要 3-4 张表来支撑,重点是 leave_balancesleave_records

-- 1. 员工年假额度总表 (用于快速查询当前可用余额)
CREATE TABLE `leave_balances` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `user_id` INT UNSIGNED NOT NULL COMMENT '员工ID',
    `year` YEAR NOT NULL COMMENT '所属年份(按自然年或按周年年)',
    `total_days` DECIMAL(5,2) NOT NULL DEFAULT 0 COMMENT '本年度发放的总天数',
    `used_days` DECIMAL(5,2) NOT NULL DEFAULT 0 COMMENT '已使用天数(含已审批和已申请待审批)',
    `available_days` DECIMAL(5,2) NOT NULL COMMENT '可用余额 = total - used',
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY `unique_user_year` (`user_id`, `year`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 年假发放/调整记录 (细粒度记录每一笔变动)
CREATE TABLE `leave_grants` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `user_id` INT UNSIGNED NOT NULL,
    `year` YEAR NOT NULL,
    `grant_type` TINYINT NOT NULL COMMENT '1-系统发放, 2-手动调整, 3-上年度结转',
    `days` DECIMAL(5,2) NOT NULL COMMENT '变动天数(正为增加,负为扣减)',
    `remark` VARCHAR(255) DEFAULT NULL,
    `granted_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 年假申请与使用记录 (核心业务表)
CREATE TABLE `leave_requests` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `user_id` INT UNSIGNED NOT NULL,
    `leave_type` TINYINT NOT NULL DEFAULT 1 COMMENT '1-年假, 2-事假, 3-病假...',
    `start_date` DATE NOT NULL,
    `end_date` DATE NOT NULL,
    `days` DECIMAL(5,2) NOT NULL COMMENT '申请天数(支持半天0.5)',
    `status` TINYINT NOT NULL DEFAULT 0 COMMENT '0-待审批, 1-已批准, 2-已拒绝, 3-已撤回',
    `remark` TEXT DEFAULT NULL,
    `approved_by` INT UNSIGNED DEFAULT NULL,
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    INDEX `idx_user_status` (`user_id`, `status`),
    INDEX `idx_date` (`start_date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. 员工基础信息表 (需要入职日期、年假规则)
CREATE TABLE `users` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(50) NOT NULL,
    `hire_date` DATE NOT NULL COMMENT '入职日期',
    `annual_leave_rule` TINYINT NOT NULL DEFAULT 1 COMMENT '1-按自然年, 2-按入职周年',
    `annual_leave_days` DECIMAL(4,1) NOT NULL DEFAULT 5.0 COMMENT '每年标准天数(如5天/10天/15天)'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

核心 PHP 代码逻辑(伪代码示例)

以下逻辑适用于 Laravel,如果你使用 ThinkPHP 或原生 PHP,思路完全一致,替换模型调用即可。

发放年假(定时任务 / Cron Job)

一般通过每日执行一次或在员工入职周年日触发。

<?php
// 伪代码 - Laravel Command
namespace App\Console\Commands;
use Carbon\Carbon;
use App\Models\User;
use App\Models\LeaveBalance;
use App\Models\LeaveGrant;
use Illuminate\Console\Command;
class GrantAnnualLeave extends Command
{
    protected $signature = 'leave:grant';
    public function handle()
    {
        // 1. 获取所有需要发放年假的用户
        $users = User::where('status', 1)->get();
        foreach ($users as $user) {
            $today = Carbon::today();
            $shouldGrant = false;
            $year = $today->year;
            if ($user->annual_leave_rule == 1) {
                // 按自然年:1月1日发放
                if ($today->isSameDay(Carbon::create($year, 1, 1))) {
                    $shouldGrant = true;
                }
            } elseif ($user->annual_leave_rule == 2) {
                // 按入职周年:在对应的月份和日期发放
                $hireDate = Carbon::parse($user->hire_date);
                $anniversary = Carbon::create($year, $hireDate->month, $hireDate->day);
                if ($today->isSameDay($anniversary)) {
                    $shouldGrant = true;
                    $year = $anniversary->year; // 按周年年
                }
            }
            if (! $shouldGrant) {
                continue;
            }
            // 避免重复发放
            $exists = LeaveBalance::where('user_id', $user->id)
                        ->where('year', $year)
                        ->exists();
            if ($exists) {
                continue;
            }
            // 计算首次入职年份的折算
            $grantDays = $user->annual_leave_days;
            $hireYear = Carbon::parse($user->hire_date)->year;
            if ($year == $hireYear) {
                // 首年按比例折算 (假设当年每月1天)
                $months = max(0, 12 - Carbon::parse($user->hire_date)->month + 1);
                $grantDays = round(($user->annual_leave_days / 12) * $months, 2);
            }
            // 写库:插入发放记录 & 更新余额表
            \DB::transaction(function () use ($user, $year, $grantDays) {
                LeaveGrant::create([
                    'user_id' => $user->id,
                    'year' => $year,
                    'grant_type' => 1,
                    'days' => $grantDays,
                    'remark' => "{$year}年度年假自动发放",
                ]);
                LeaveBalance::updateOrCreate(
                    ['user_id' => $user->id, 'year' => $year],
                    ['total_days' => $grantDays, 'available_days' => $grantDays, 'used_days' => 0]
                );
            });
        }
    }
}

用户提交年假申请

关键点:申请前必须校验可用余额。

<?php
// 伪代码 - 在 Controller 中
use Carbon\Carbon;
use App\Models\LeaveBalance;
use App\Models\LeaveRequest;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
class LeaveController extends Controller
{
    public function store(Request $request)
    {
        $validated = $request->validate([
            'start_date' => 'required|date',
            'end_date' => 'required|date|after_or_equal:start_date',
            'days' => 'required|numeric|min:0.5|max:30', // 防止大额误提交
        ]);
        $user = auth()->user();
        $startDate = Carbon::parse($validated['start_date']);
        $endDate = Carbon::parse($validated['end_date']);
        $applyDays = $validated['days'];
        // 1. 计算扣除哪个年度的额度 (简单处理:如果假期跨年,拆分成两个请求或算入开始年份)
        $year = $startDate->year;
        // 2. 获取该员工在对应年份的余额
        $balance = LeaveBalance::where('user_id', $user->id)
                        ->where('year', $year)
                        ->first();
        if (! $balance) {
            throw ValidationException::withMessages(['days' => '该年度无可用年假额度']);
        }
        if ($balance->available_days < $applyDays) {
            throw ValidationException::withMessages([
                'days' => "可用余额不足,当前余额:{$balance->available_days}天,申请:{$applyDays}天"
            ]);
        }
        // 3. 写入申请记录,同时预扣余额 (通过事务保证数据一致性)
        DB::transaction(function () use ($user, $request, $applyDays, $year, $balance) {
            // 创建申请单
            $leaveRequest = LeaveRequest::create([
                'user_id' => $user->id,
                'leave_type' => 1,
                'start_date' => $request->start_date,
                'end_date' => $request->end_date,
                'days' => $applyDays,
                'status' => 0, // 待审批
            ]);
            // 预扣余额 (status=0 待审批也要扣掉,防止超额申请)
            $balance->decrement('available_days', $applyDays);
            $balance->increment('used_days', $applyDays);
        });
        return response()->json(['message' => '申请提交成功,等待审批']);
    }
}

审批通过 / 拒绝

<?php
// 伪代码 - 审批通过
public function approve($id)
{
    $leave = LeaveRequest::findOrFail($id);
    if ($leave->status != 0) {
        return back()->with('error', '只能审批待审批的申请');
    }
    DB::transaction(function () use ($leave) {
        $leave->update([
            'status' => 1, // 已批准
            'approved_by' => auth()->id()
        ]);
        // 余额已经在申请时预扣了,这里不需要再操作
    });
    return back()->with('success', '审批通过');
}
// 伪代码 - 审批拒绝(需要回滚余额)
public function reject($id)
{
    $leave = LeaveRequest::findOrFail($id);
    if ($leave->status != 0) {
        return back()->with('error', '只能拒绝待审批的申请');
    }
    DB::transaction(function () use ($leave) {
        $leave->update([
            'status' => 2, // 已拒绝
            'approved_by' => auth()->id()
        ]);
        // 回滚余额!这里和申请时对应
        $balance = LeaveBalance::where('user_id', $leave->user_id)
                        ->where('year', $leave->start_date->year)
                        ->first();
        if ($balance) {
            $balance->increment('available_days', $leave->days);
            $balance->decrement('used_days', $leave->days);
        }
    });
    return back()->with('success', '已拒绝');
}

查询当前余额

// 直接查 LeaveBalance 表
public function myBalance()
{
    $user = auth()->user();
    // 查看当前年份可用余额
    $currentYear = now()->year;
    $balance = LeaveBalance::where('user_id', $user->id)
                    ->where('year', $currentYear)
                    ->first();
    return response()->json([
        'total' => $balance->total_days ?? 0,
        'used' => $balance->used_days ?? 0,
        'available' => $balance->available_days ?? 0,
    ]);
}

用 Laravel / ThinkPHP 注意事项

场景 建议做法
定时任务 Laravel 用 Kernel (schedule),ThinkPHP 用 php think 或系统 Cron Job。
事务处理 凡是涉及余额变更 + 记录写入,必须使用数据库事务
余额字段类型 使用 DECIMAL(5,2),支持半天(0.5)的休假。
跨年假期 如果申请日期跨年(如 12月25日 - 1月3日),建议拆分成两笔记录,各占对应年份的额度。
手动调整 管理员后台可以调用 LeaveGrant 来直接加/减天数,并同步更新 LeaveBalance
缓存 用户首页显示的余额可以缓存 1 分钟,但不建议缓存太久,因为提交申请后余额必须实时更新。

年假管理的 难点集中在余额变更的一致性业务规则的配置

  • 不要试图在 SQL 查询时做复杂计算,应该维护一个 leave_balances 表来冗余存储余额。
  • 确保每个余额变更都有对应的记录(通过 leave_grants 记录发放,通过 leave_requests 记录消耗)。
  • PHP 本身并不限制,关键在于理解业务逻辑(自然年 vs 周年、按比例折算、跨年处理)。

按照上面这个数据结构 + 逻辑,不管是用 Laravel 的 Eloquent、ThinkPHP 的 Model,还是原生 PDO,都能稳妥实现。

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