这个需求非常常见,通常用于为文章、产品等生成唯一的、有序的编号,NO.202505210001、NO.202505210002。
在织梦的二次开发中,实现“递增”功能最核心、最可靠的方法是利用数据库,直接在PHP代码中用一个变量来递增是不可行的,因为并发请求会导致数据错乱。
下面我将提供两种最主流的实现方案,并给出完整的代码示例。
核心思想
无论是哪种方案,核心思想都是:
- 锁定:在读取和更新编号时,必须锁定相关的资源,防止其他进程同时操作,导致编号重复或跳跃。
- 读取:从数据库中读取当前已经用到的最大编号。
- 计算:将最大编号加一,得到新的编号。
- 更新:将新的编号写回数据库,并释放锁。
使用独立的数据表(推荐,最稳定)
这是最规范、最稳定的方法,我们创建一个专门用于存储和管理序列号的表。
步骤 1:创建数据表
在你的数据库中(建议使用 dede_ 前缀,根据你的实际情况修改),执行以下SQL语句,创建一个 dede_serial_number 表:
CREATE TABLE `dede_serial_number` ( `id` int(11) NOT NULL AUTO_INCREMENT, `type` varchar(50) NOT NULL COMMENT '编号类型, article, product', `prefix` varchar(20) NOT NULL COMMENT '编号前缀, NO, PROD', `current_num` int(11) NOT NULL DEFAULT '0' COMMENT '当前已使用的最大编号', `date` date DEFAULT NULL COMMENT '日期,可用于按天/月/年重置', PRIMARY KEY (`id`), UNIQUE KEY `uk_type_date` (`type`, `date`) -- 联合唯一索引,确保同一天同类型的编号唯一 ) ENGINE=MyISAM DEFAULT CHARSET=utf8;
表结构说明:
type: 用于区分不同业务的编号,比如文章编号、产品编号。prefix: 编号的前缀,方便识别。current_num: 存储当前递增的数字。date: 存储日期,如果需要按天重置编号,这个字段就很有用。uk_type_date: 这个唯一索引是关键,它保证了在同一个type和date下,只能有一条记录,防止重复插入。
步骤 2:编写PHP递增函数
在你的织梦程序中,找到 include 目录,创建一个新文件,inc_serial_functions.php,然后把下面的函数代码放进去。
<?php
/**
* 获取一个递增的序列号
* @param string $type 编号类型 ( 'article', 'product')
* @param string $prefix 编号前缀 ( 'NO', 'PROD')
* @param bool $resetDaily 是否按天重置
* @return string 返回格式化后的序列号, NO.202505210001
*/
function getIncrementalNumber($type, $prefix = 'NO', $resetDaily = false)
{
global $dsql;
$date = '';
if ($resetDaily) {
$date = date('Ymd');
}
// 表名,根据你的前缀修改
$table = 'dede_serial_number';
// 使用事务和锁来确保原子性
$dsql->ExecuteNoneQuery("START TRANSACTION");
// 尝试获取锁,防止并发
$dsql->ExecuteNoneQuery("SELECT GET_LOCK('serial_number_{$type}_{$date}', 10)"); // 锁定10秒
try {
// 1. 尝试更新现有记录(乐观锁)
$current_num = 0;
$sql = "SELECT current_num FROM `{$table}` WHERE type = '{$type}' AND date = '{$date}'";
$row = $dsql->GetOne($sql);
if ($row) {
// 如果记录存在,则更新
$current_num = $row['current_num'];
$new_num = $current_num + 1;
$dsql->ExecuteNoneQuery("UPDATE `{$table}` SET current_num = {$new_num} WHERE type = '{$type}' AND date = '{$date}'");
} else {
// 如果记录不存在(第一次或日期已变更),则插入
$new_num = 1;
$dsql->ExecuteNoneQuery("INSERT INTO `{$table}` (type, prefix, current_num, date) VALUES ('{$type}', '{$prefix}', {$new_num}, '{$date}')");
}
// 2. 生成最终的编号字符串
$serialNumber = '';
if ($resetDaily) {
$serialNumber = $prefix . '.' . $date . str_pad($new_num, 4, '0', STR_PAD_LEFT);
} else {
// 如果不按天重置,日期可以为空或使用其他逻辑
$serialNumber = $prefix . '.' . str_pad($new_num, 6, '0', STR_PAD_LEFT);
}
// 提交事务
$dsql->ExecuteNoneQuery("COMMIT");
return $serialNumber;
} catch (Exception $e) {
// 发生异常,回滚事务
$dsql->ExecuteNoneQuery("ROLLBACK");
// 释放锁
$dsql->ExecuteNoneQuery("SELECT RELEASE_LOCK('serial_number_{$type}_{$date}')");
return 'ERROR: ' . $e->getMessage();
} finally {
// 确保锁被释放
$dsql->ExecuteNoneQuery("SELECT RELEASE_LOCK('serial_number_{$type}_{$date}')");
}
}
?>
步骤 3:在模板或PHP文件中调用
方法A:在文章发布页(article_add.php)中自动生成
打开 /dede/article_add.php 文件,找到 if($dopost=='save') 部分,在保存文章之前调用这个函数。
// 引入我们的函数文件
require_once(DEDEINC.'/inc_serial_functions.php');
// ... 其他代码 ...
if($dopost=='save')
{
// 获取递增的编号
$articleSerialNumber = getIncrementalNumber('article', 'ARTICLE', true); // true表示按天重置
// 将编号存入一个自定义字段,或者直接存入标题等
// 假设你有一个自定义字段叫 `serial_number`
$_POST['serial_number'] = $articleSerialNumber;
// ... 继续原有的保存逻辑 ...
// $arc->Save(); 这行代码不变
}
方法B:在内容模板(article_article.htm)中显示
如果你的编号已经保存在文章的自定义字段 serial_number 中,那么在模板里直接调用即可。
{dede:field.serial_number/}
如果是在PHP代码里处理,也可以直接使用 $arc->Fields['serial_number']。
利用系统自带的 sys_enum 表(简单,但不够灵活)
织梦有一个系统表 dede_sys_enum,常用来存储一些枚举值(如性别、栏目类型等),我们可以“借用”它来存储一个简单的计数器。此方法不推荐用于高并发或需要复杂逻辑的场景,因为它的设计初衷并非如此。
步骤 1:准备数据
手动在 dede_sys_enum 表中插入一条记录作为计数器的起点。
INSERT INTO `dede_sys_enum` (
`id`, `evalue`, `ename`, `eorder`, `group`, `issign`
) VALUES (
NULL, '0', 'article_counter', '0', 'serial_counter', '0'
);
evalue: 存储当前的数字,我们从0开始。ename: 一个唯一的名称,用来标识这个计数器。group: 分组,用来区分不同的计数器。
步骤 2:编写PHP递增函数
同样,在 include/inc_serial_functions.php 中添加如下函数:
/**
* 使用sys_enum表获取递增编号(简单版,不推荐高并发)
* @param string $counter_name 计数器名称
* @param string $prefix 编号前缀
* @return string
*/
function getIncrementalNumberFromEnum($counter_name, $prefix = 'NO')
{
global $dsql;
// 1. 获取当前计数 (使用锁防止并发读取到旧值)
$dsql->ExecuteNoneQuery("SELECT GET_LOCK('enum_counter_{$counter_name}', 5)");
$row = $dsql->GetOne("SELECT evalue FROM `dede_sys_enum` WHERE ename = '{$counter_name}'");
$current_num = isset($row['evalue']) ? intval($row['evalue']) : 0;
$new_num = $current_num + 1;
// 2. 更新计数
$dsql->ExecuteNoneQuery("UPDATE `dede_sys_enum` SET evalue = '{$new_num}' WHERE ename = '{$counter_name}'");
// 3. 释放锁
$dsql->ExecuteNoneQuery("SELECT RELEASE_LOCK('enum_counter_{$counter_name}')");
// 4. 格式化并返回
return $prefix . '.' . str_pad($new_num, 6, '0', STR_PAD_LEFT);
}
步骤 3:调用方法
调用方式与方案一类似,只需调用 getIncrementalNumberFromEnum 函数即可。
require_once(DEDEINC.'/inc_serial_functions.php');
$number = getIncrementalNumberFromEnum('article_counter', 'ARTICLE');
方案对比与总结
| 特性 | 方案一(独立数据表) | 方案二(sys_enum表) |
|---|---|---|
| 稳定性 | 极高,专为计数设计,结构清晰。 | 一般,非设计用途,可能受系统更新影响。 |
| 灵活性 | 极高,支持按天/月/年重置、多类型管理等。 | 较低,逻辑简单,难以实现复杂规则。 |
| 并发安全性 | 高,通过事务和行级锁(或MySQL应用锁)保证。 | 中等,依赖MySQL GET_LOCK,但表结构不适合高并发更新。 |
| 维护性 | 好,表结构独立,易于理解和维护。 | 差,数据与系统配置混在一起,不易管理。 |
| 推荐度 | 强烈推荐 | 不推荐,仅用于非常简单的、低频的内部需求。 |
对于任何正式的、有一定业务量的网站,请务必选择方案一,它虽然多建了一张表,但带来的稳定性、灵活性和可维护性是完全值得的,方案二可以作为临时或简单的测试方案,但请勿在生产环境中依赖它。
