如何在Laravel+MySQL中实现按年重置的自定义自增发票ID
带年份且每年重置的发票ID最优实现方案(Laravel+MySQL)
核心思路
不能直接用单一自增列实现,必须拆分年份和年度序号两个字段来维护:
- 保留原有的自增主键
id作为内部唯一标识(不对外展示),保证数据库操作的基础性能 - 新增
invoice_year(存储年份)和invoice_number(存储当年自增序号)两个字段,通过联合索引确保同一年的序号唯一,同时兼顾查询性能
具体实现步骤
1. 数据库表结构调整
用Laravel迁移修改发票表:
Schema::table('invoices', function (Blueprint $table) { // 存储发票年份,例如2023、2024 $table->unsignedSmallInteger('invoice_year')->after('id'); // 存储当年的自增序号,每年从1开始 $table->unsignedInteger('invoice_number')->after('invoice_year'); // 新增联合唯一索引,确保同一年的序号不重复,同时优化查询 $table->unique(['invoice_year', 'invoice_number']); // 单独给invoice_year加索引,方便按年份筛选发票 $table->index('invoice_year'); });
2. 自动生成年度序号逻辑
在Invoice模型的creating事件中,自动计算当前年份的下一个序号:
class Invoice extends Model { protected static function boot() { parent::boot(); static::creating(function ($invoice) { $currentYear = date('Y'); $invoice->invoice_year = $currentYear; // 查询当前年份最大的发票序号,不存在则设为0,加1后作为新序号 $lastNumber = self::where('invoice_year', $currentYear)->max('invoice_number') ?? 0; $invoice->invoice_number = $lastNumber + 1; }); } }
如果并发量较高,建议加数据库事务避免重复序号:
static::creating(function ($invoice) { $currentYear = date('Y'); DB::transaction(function () use ($invoice, $currentYear) { $lastNumber = self::where('invoice_year', $currentYear)->lockForUpdate()->max('invoice_number') ?? 0; $invoice->invoice_year = $currentYear; $invoice->invoice_number = $lastNumber + 1; }); });
3. 对外展示格式化发票ID
在模型中定义访问器,拼接成用户需要的YYYY-NNN格式:
public function getFormattedInvoiceIdAttribute() { return "{$this->invoice_year}-{$this->invoice_number}"; }
之后在代码中可以直接通过$invoice->formatted_invoice_id获取格式化后的发票ID。
索引性能说明
- 原有的自增主键
id依然是主键索引,数据库的增删改查性能不受影响 - 联合唯一索引
(invoice_year, invoice_number)是高效的:整数类型的索引比字符串索引更快,前缀为年份的联合索引在按年份查询发票时,能快速定位到目标数据范围 - 不要直接用字符串字段存储
2023-344这类格式:字符串索引的查询效率远低于整数索引,且无法自动维护年度自增逻辑,还会增加存储成本
不推荐的方案
- 直接修改MySQL自增列:MySQL自增列无法按年份重置,强行用触发器实现会导致锁表、性能下降等问题
- 用单一字符串字段存储格式化ID:无法保证唯一性,索引性能差,维护成本高
内容的提问来源于stack exchange,提问作者Yusuf Bouzekri
相关产品推荐
相关产品推荐

