☰
ERP系统单据编号生成方案与Oracle实现
2026/10/28 6:18:08 网站建设 项目流程

1. 项目背景与需求分析

在ERP系统开发中,单据编号自增是一个基础但至关重要的功能。不同于简单的数据库自增ID,ERP单据编号通常需要满足以下业务需求:

  • 按单据类型区分(如采购单PO开头、销售单SO开头)
  • 包含日期信息(如20230715)
  • 支持多级编号规则(部门+类型+日期+序号)
  • 高并发环境下保证编号唯一性

传统方案直接在数据库层面使用序列(Sequence)或自增字段存在明显局限:

  1. 无法实现"前缀+日期+序号"的复杂格式
  2. 集群环境下可能产生重复编号
  3. 业务规则变更时需要修改数据库对象

2. 技术选型与方案设计

2.1 核心组件说明

Oracle 19c:企业级数据库选择,提供:

  • 高性能序列对象
  • 事务隔离级别控制
  • 表锁机制保证数据一致性

SQLSugar:轻量级ORM框架优势:

  • 简化数据库操作
  • 内置多数据库支持
  • 高性能批量操作
  • 完善的文档和社区支持

2.2 设计方案对比

方案实现方式优点缺点
数据库序列直接使用Oracle序列实现简单无法满足复杂编号规则
触发器通过数据库触发器生成业务透明调试困难,性能瓶颈
程序控制应用层代码生成灵活可控需要处理并发问题

最终选择程序控制+数据库事务的混合方案:

  1. 使用Oracle序列作为序号基础
  2. 应用层组装完整编号
  3. 通过数据库事务保证原子性

3. 具体实现步骤

3.1 数据库准备

-- 创建序列 CREATE SEQUENCE SEQ_ERP_ORDER_NO START WITH 100000 INCREMENT BY 1 NOCACHE NOCYCLE; -- 创建单据表 CREATE TABLE ERP_ORDERS ( ORDER_ID VARCHAR2(50) PRIMARY KEY, ORDER_TYPE VARCHAR2(10), ORDER_DATE DATE, -- 其他业务字段... CREATE_TIME TIMESTAMP DEFAULT SYSTIMESTAMP );

3.2 C#实现代码

public class OrderService { private readonly SqlSugarClient _db; public OrderService(SqlSugarClient db) { _db = db; } public string GenerateOrderNo(string orderType) { // 获取当前日期部分 string datePart = DateTime.Now.ToString("yyyyMMdd"); // 获取序列值 long seqNo = _db.Ado.GetInt64("SELECT SEQ_ERP_ORDER_NO.NEXTVAL FROM DUAL"); // 组合完整编号 return $"{orderType}-{datePart}-{seqNo:D5}"; } public bool CreateOrder(Order order) { try { _db.Ado.BeginTran(); // 生成单据编号 order.OrderId = GenerateOrderNo(order.OrderType); // 设置创建时间 order.CreateTime = DateTime.Now; // 插入数据 int count = _db.Insertable(order).ExecuteCommand(); _db.Ado.CommitTran(); return count > 0; } catch { _db.Ado.RollbackTran(); throw; } } }

3.3 并发控制实现

// 使用锁机制防止并发问题 private static readonly object _lockObj = new object(); public string SafeGenerateOrderNo(string orderType) { lock(_lockObj) { return GenerateOrderNo(orderType); } }

4. 高级应用与优化

4.1 按单据类型区分序列

-- 为每种单据类型创建独立序列 CREATE SEQUENCE SEQ_PO_NO START WITH 1000; CREATE SEQUENCE SEQ_SO_NO START WITH 1000;

4.2 使用Oracle高级队列

对于超高并发场景:

// 使用Oracle AQ实现消息队列 public string QueueGenerateOrderNo(string orderType) { var queue = _db.Ado.GetOracleAQQueue("ORDER_NO_QUEUE"); var msg = new OracleAQMessage { Payload = orderType }; queue.Enqueue(msg); // ...处理返回消息 }

4.3 编号规则配置化

通过配置表实现灵活规则:

CREATE TABLE NO_GENERATE_RULE ( RULE_ID VARCHAR2(20), PREFIX VARCHAR2(10), DATE_FORMAT VARCHAR2(20), SEQ_NAME VARCHAR2(30), PADDING_LENGTH NUMBER );

5. 常见问题与解决方案

5.1 性能问题排查

现象可能原因解决方案
编号生成慢序列缓存设置过小调整CACHE参数
高并发重复事务隔离级别问题使用SERIALIZABLE隔离级别
编号不连续事务回滚导致序列跳号改用NOCACHE序列

5.2 分布式环境处理

集群环境下建议:

  1. 使用中心化编号服务
  2. 每个节点分配固定前缀
  3. 结合Redis等分布式锁
// 使用Redis分布式锁 public string DistributedGenerateNo(string orderType) { using(var redLock = redLockFactory.CreateLock("OrderNoLock", TimeSpan.FromSeconds(10))) { if(redLock.IsAcquired) { return GenerateOrderNo(orderType); } throw new Exception("获取编号锁超时"); } }

6. 最佳实践建议

  1. 编号长度控制:建议不超过20字符,考虑数据库索引性能
  2. 可读性设计:使用有意义的前缀(PO-、SO-)
  3. 历史数据迁移:旧系统迁移时保留原编号,新建表记录映射关系
  4. 监控机制:记录编号生成日志,便于审计和问题追踪

重要提示:生产环境务必进行压力测试,模拟至少200并发下的编号生成情况

7. 扩展思考

  1. 断号处理:业务上是否需要严格连续?可考虑定期维护任务填补断号
  2. 多时区支持:全球化系统需明确使用服务器时区还是用户时区
  3. 离线模式:移动端离线工作时,可采用"临时编号+同步后替换"的方案

实际项目中,我们曾遇到单据编号被恶意猜测的安全问题,最终解决方案是:

  • 在编号中加入随机校验位
  • 建立编号使用记录表
  • 设置每日编号生成上限

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询