use chrono::{Datelike, NaiveDate, Utc}; use sea_orm::{ ActiveModelTrait, ColumnTrait, ConnectionTrait, DatabaseBackend, EntityTrait, PaginatorTrait, QueryFilter, Set, Statement, TransactionTrait, }; use uuid::Uuid; use crate::dto::{CreateNumberingRuleReq, GenerateNumberResp, NumberingRuleResp}; use crate::entity::numbering_rule; use crate::error::{ConfigError, ConfigResult}; use erp_core::audit::AuditLog; use erp_core::audit_service; use erp_core::error::check_version; use erp_core::events::EventBus; use erp_core::types::Pagination; /// 格式化编号字符串。 /// /// 拼接规则: /// 1. 以 `prefix` 开头 /// 2. 若 `prefix` 非空,追加 `separator` /// 3. 若 `date_part` 为 `Some` 且非空,追加 `date_part` + `separator` /// 4. 追加零填充的 `seq_current`(填充到 `seq_length` 位,最少 1 位) pub(crate) fn format_number( prefix: &str, separator: &str, date_part: Option<&str>, seq_current: i64, seq_length: i32, ) -> String { let mut result = String::with_capacity(32); result.push_str(prefix); if !prefix.is_empty() { result.push_str(separator); } if let Some(dp) = date_part { if !dp.is_empty() { result.push_str(dp); result.push_str(separator); } } let width = (seq_length.max(1)) as usize; let seq_padded = format!("{:0>width$}", seq_current, width = width); result.push_str(&seq_padded); result } /// 编号规则 CRUD 服务 -- 创建、查询、更新、软删除编号规则, /// 以及线程安全地生成编号序列。 pub struct NumberingService; impl NumberingService { /// 分页查询编号规则列表。 pub async fn list( tenant_id: Uuid, pagination: &Pagination, db: &sea_orm::DatabaseConnection, ) -> ConfigResult<(Vec, u64)> { let paginator = numbering_rule::Entity::find() .filter(numbering_rule::Column::TenantId.eq(tenant_id)) .filter(numbering_rule::Column::DeletedAt.is_null()) .paginate(db, pagination.limit()); let total = paginator .num_items() .await .map_err(|e| ConfigError::Validation(e.to_string()))?; let page_index = pagination.page.unwrap_or(1).saturating_sub(1); let models = paginator .fetch_page(page_index) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; let resps: Vec = models.iter().map(Self::model_to_resp).collect(); Ok((resps, total)) } /// 创建编号规则。 /// /// 检查 code 在租户内唯一后插入。 pub async fn create( tenant_id: Uuid, operator_id: Uuid, req: &CreateNumberingRuleReq, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> ConfigResult { // 检查 code 唯一性 let existing = numbering_rule::Entity::find() .filter(numbering_rule::Column::TenantId.eq(tenant_id)) .filter(numbering_rule::Column::Code.eq(&req.code)) .filter(numbering_rule::Column::DeletedAt.is_null()) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; if existing.is_some() { return Err(ConfigError::DuplicateKey(format!( "编号规则编码已存在: {}", req.code ))); } let now = Utc::now(); let id = Uuid::now_v7(); let seq_start = req.seq_start.unwrap_or(1); let model = numbering_rule::ActiveModel { id: Set(id), tenant_id: Set(tenant_id), name: Set(req.name.clone()), code: Set(req.code.clone()), prefix: Set(req.prefix.clone().unwrap_or_default()), date_format: Set(req.date_format.clone()), seq_length: Set(req.seq_length.unwrap_or(4)), seq_start: Set(seq_start), seq_current: Set(seq_start as i64), separator: Set(req.separator.clone().unwrap_or_else(|| "-".to_string())), reset_cycle: Set(req .reset_cycle .clone() .unwrap_or_else(|| "never".to_string())), last_reset_date: Set(Some(Utc::now().date_naive())), created_at: Set(now), updated_at: Set(now), created_by: Set(operator_id), updated_by: Set(operator_id), deleted_at: Set(None), version: Set(1), }; model .insert(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; event_bus .publish( erp_core::events::DomainEvent::new( "numbering_rule.created", tenant_id, serde_json::json!({ "rule_id": id, "code": req.code }), ), db, ) .await; audit_service::record( AuditLog::new( tenant_id, Some(operator_id), "numbering_rule.create", "numbering_rule", ) .with_resource_id(id), db, ) .await; Ok(NumberingRuleResp { id, name: req.name.clone(), code: req.code.clone(), prefix: req.prefix.clone().unwrap_or_default(), date_format: req.date_format.clone(), seq_length: req.seq_length.unwrap_or(4), seq_start, seq_current: seq_start as i64, separator: req.separator.clone().unwrap_or_else(|| "-".to_string()), reset_cycle: req .reset_cycle .clone() .unwrap_or_else(|| "never".to_string()), last_reset_date: Some(Utc::now().date_naive().to_string()), version: 1, }) } /// 更新编号规则的可编辑字段。使用乐观锁校验版本。 pub async fn update( id: Uuid, tenant_id: Uuid, operator_id: Uuid, req: &crate::dto::UpdateNumberingRuleReq, db: &sea_orm::DatabaseConnection, ) -> ConfigResult { let model = numbering_rule::Entity::find_by_id(id) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none()) .ok_or_else(|| ConfigError::NotFound(format!("编号规则不存在: {id}")))?; let next_version = check_version(req.version, model.version).map_err(|_| ConfigError::VersionMismatch)?; let mut active: numbering_rule::ActiveModel = model.into(); if let Some(name) = &req.name { active.name = Set(name.clone()); } if let Some(prefix) = &req.prefix { active.prefix = Set(prefix.clone()); } if let Some(date_format) = &req.date_format { active.date_format = Set(Some(date_format.clone())); } if let Some(seq_length) = req.seq_length { active.seq_length = Set(seq_length); } if let Some(separator) = &req.separator { active.separator = Set(separator.clone()); } if let Some(reset_cycle) = &req.reset_cycle { active.reset_cycle = Set(reset_cycle.clone()); } active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(next_version); let updated = active .update(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; audit_service::record( AuditLog::new( tenant_id, Some(operator_id), "numbering_rule.update", "numbering_rule", ) .with_resource_id(id), db, ) .await; Ok(Self::model_to_resp(&updated)) } /// 软删除编号规则。使用乐观锁校验版本。 pub async fn delete( id: Uuid, tenant_id: Uuid, operator_id: Uuid, version: i32, db: &sea_orm::DatabaseConnection, event_bus: &EventBus, ) -> ConfigResult<()> { let model = numbering_rule::Entity::find_by_id(id) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none()) .ok_or_else(|| ConfigError::NotFound(format!("编号规则不存在: {id}")))?; let next_version = check_version(version, model.version).map_err(|_| ConfigError::VersionMismatch)?; let mut active: numbering_rule::ActiveModel = model.into(); active.deleted_at = Set(Some(Utc::now())); active.updated_at = Set(Utc::now()); active.updated_by = Set(operator_id); active.version = Set(next_version); active .update(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; event_bus .publish( erp_core::events::DomainEvent::new( "numbering_rule.deleted", tenant_id, serde_json::json!({ "rule_id": id }), ), db, ) .await; audit_service::record( AuditLog::new( tenant_id, Some(operator_id), "numbering_rule.delete", "numbering_rule", ) .with_resource_id(id), db, ) .await; Ok(()) } /// 线程安全地生成编号。 /// /// 使用 PostgreSQL advisory lock 保证并发安全: /// 1. 在事务内获取 pg_advisory_xact_lock /// 2. 在同一事务内读取规则、检查重置周期、递增序列、更新数据库 /// 3. 拼接编号字符串返回 pub async fn generate_number( rule_id: Uuid, tenant_id: Uuid, db: &sea_orm::DatabaseConnection, ) -> ConfigResult { // 先读取规则获取 code(用于 advisory lock) let rule = numbering_rule::Entity::find_by_id(rule_id) .one(db) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none()) .ok_or_else(|| ConfigError::NotFound(format!("编号规则不存在: {rule_id}")))?; let rule_code = rule.code.clone(); let tenant_id_str = tenant_id.to_string(); // 在同一个事务内获取 advisory lock 并执行编号生成 // pg_advisory_xact_lock 是事务级别的,锁会在事务结束时自动释放 let number = db .transaction(|txn| { let rule_code = rule_code.clone(); let tenant_id_str = tenant_id_str.clone(); Box::pin(async move { // 在事务内获取 advisory lock txn.execute(Statement::from_sql_and_values( DatabaseBackend::Postgres, "SELECT pg_advisory_xact_lock(abs(hashtext($1)), abs(hashtext($2))::int)", [rule_code.into(), tenant_id_str.into()], )) .await .map_err(|e| ConfigError::Validation(format!("获取编号锁失败: {e}")))?; // 在同一个事务内执行编号生成 Self::generate_number_in_txn(rule_id, tenant_id, txn).await }) }) .await?; Ok(GenerateNumberResp { number }) } /// 事务内执行编号生成逻辑。 /// /// 检查重置周期,必要时重置序列,然后递增并拼接编号。 async fn generate_number_in_txn( rule_id: Uuid, tenant_id: Uuid, txn: &C, ) -> ConfigResult where C: ConnectionTrait, { let rule = numbering_rule::Entity::find_by_id(rule_id) .one(txn) .await .map_err(|e| ConfigError::Validation(e.to_string()))? .filter(|r| r.tenant_id == tenant_id && r.deleted_at.is_none()) .ok_or_else(|| ConfigError::NotFound(format!("编号规则不存在: {rule_id}")))?; let today = Utc::now().date_naive(); let mut seq_current = rule.seq_current; // 检查是否需要重置序列 seq_current = Self::maybe_reset_sequence( seq_current, rule.seq_start as i64, &rule.reset_cycle, rule.last_reset_date, today, ); // 递增序列 let next_seq = seq_current + 1; // 检查序列是否超出 seq_length 能表示的最大值 let max_val = 10i64.pow(rule.seq_length as u32) - 1; if next_seq > max_val { return Err(ConfigError::NumberingExhausted(format!( "编号序列已耗尽,当前序列号 {next_seq} 超出长度 {} 的最大值", rule.seq_length ))); } // 更新数据库中的 seq_current 和 last_reset_date let mut active: numbering_rule::ActiveModel = rule.clone().into(); active.seq_current = Set(next_seq); active.last_reset_date = Set(Some(today)); active.updated_at = Set(Utc::now()); active .update(txn) .await .map_err(|e| ConfigError::Validation(e.to_string()))?; // 拼接编号字符串: {prefix}{separator}{date_part}{separator}{seq_padded} let date_part = rule.date_format.as_ref().map(|fmt| Utc::now().format(fmt).to_string()); let number = format_number( &rule.prefix, &rule.separator, date_part.as_deref(), seq_current, rule.seq_length, ); Ok(number) } /// 根据重置周期判断是否需要重置序列号。 /// /// 如果需要重置,返回 `seq_start`;否则返回原值。 fn maybe_reset_sequence( seq_current: i64, seq_start: i64, reset_cycle: &str, last_reset_date: Option, today: NaiveDate, ) -> i64 { let last_reset = match last_reset_date { Some(d) => d, None => return seq_start, // 从未重置过,使用 seq_start }; match reset_cycle { "daily" => { if last_reset != today { seq_start } else { seq_current } } "monthly" => { if last_reset.month() != today.month() || last_reset.year() != today.year() { seq_start } else { seq_current } } "yearly" => { if last_reset.year() != today.year() { seq_start } else { seq_current } } _ => seq_current, // "never" 或其他值不重置 } } /// 将数据库模型转换为响应 DTO。 fn model_to_resp(m: &numbering_rule::Model) -> NumberingRuleResp { NumberingRuleResp { id: m.id, name: m.name.clone(), code: m.code.clone(), prefix: m.prefix.clone(), date_format: m.date_format.clone(), seq_length: m.seq_length, seq_start: m.seq_start, seq_current: m.seq_current, separator: m.separator.clone(), reset_cycle: m.reset_cycle.clone(), last_reset_date: m.last_reset_date.map(|d| d.to_string()), version: m.version, } } } #[cfg(test)] mod tests { use super::*; use chrono::NaiveDate; /// 辅助:构造 NaiveDate fn date(y: i32, m: u32, d: u32) -> NaiveDate { NaiveDate::from_ymd_opt(y, m, d).unwrap() } // ---- maybe_reset_sequence 测试 ---- #[test] fn reset_never_keeps_current() { // "never" 周期:永远不重置,保持 seq_current let result = NumberingService::maybe_reset_sequence( 100, 1, "never", Some(date(2025, 1, 1)), date(2026, 4, 15), ); assert_eq!(result, 100); } #[test] fn reset_unknown_cycle_keeps_current() { // 未知周期值等同于不重置 let result = NumberingService::maybe_reset_sequence( 50, 1, "weekly", Some(date(2025, 1, 1)), date(2026, 4, 15), ); assert_eq!(result, 50); } #[test] fn reset_daily_same_day_keeps_current() { // 同一天内不重置 let today = date(2026, 4, 15); let result = NumberingService::maybe_reset_sequence(42, 1, "daily", Some(today), today); assert_eq!(result, 42); } #[test] fn reset_daily_different_day_resets() { // 不同天重置为 seq_start let result = NumberingService::maybe_reset_sequence( 42, 1, "daily", Some(date(2026, 4, 14)), date(2026, 4, 15), ); assert_eq!(result, 1); } #[test] fn reset_daily_resets_with_custom_start() { // 重置时使用自定义 seq_start let result = NumberingService::maybe_reset_sequence( 99, 10, "daily", Some(date(2026, 4, 10)), date(2026, 4, 15), ); assert_eq!(result, 10); } #[test] fn reset_monthly_same_month_keeps_current() { // 同月不重置 let result = NumberingService::maybe_reset_sequence( 30, 1, "monthly", Some(date(2026, 4, 1)), date(2026, 4, 15), ); assert_eq!(result, 30); } #[test] fn reset_monthly_different_month_resets() { // 不同月份重置 let result = NumberingService::maybe_reset_sequence( 30, 1, "monthly", Some(date(2026, 3, 31)), date(2026, 4, 1), ); assert_eq!(result, 1); } #[test] fn reset_monthly_same_month_different_year_resets() { // 不同年份但相同月份数字,仍然重置 let result = NumberingService::maybe_reset_sequence( 20, 5, "monthly", Some(date(2025, 4, 15)), date(2026, 4, 15), ); assert_eq!(result, 5); } #[test] fn reset_yearly_same_year_keeps_current() { // 同年不重置 let result = NumberingService::maybe_reset_sequence( 50, 1, "yearly", Some(date(2026, 1, 1)), date(2026, 12, 31), ); assert_eq!(result, 50); } #[test] fn reset_yearly_different_year_resets() { // 不同年份重置 let result = NumberingService::maybe_reset_sequence( 50, 1, "yearly", Some(date(2025, 12, 31)), date(2026, 1, 1), ); assert_eq!(result, 1); } #[test] fn reset_no_last_reset_date_returns_seq_start() { // 从未重置过,使用 seq_start let result = NumberingService::maybe_reset_sequence(999, 1, "daily", None, date(2026, 4, 15)); assert_eq!(result, 1); } #[test] fn reset_no_last_reset_date_uses_custom_start() { // 从未重置过,使用自定义 seq_start let result = NumberingService::maybe_reset_sequence(999, 42, "monthly", None, date(2026, 4, 15)); assert_eq!(result, 42); } // ---- model_to_resp 测试 ---- #[test] fn model_to_resp_maps_fields_correctly() { let id = Uuid::now_v7(); let tenant_id = Uuid::now_v7(); let now = Utc::now(); let today = now.date_naive(); let model = numbering_rule::Model { id, tenant_id, name: "订单编号".to_string(), code: "ORDER".to_string(), prefix: "ORD".to_string(), date_format: Some("%Y%m%d".to_string()), seq_length: 6, seq_start: 1, seq_current: 42, separator: "-".to_string(), reset_cycle: "daily".to_string(), last_reset_date: Some(today), created_at: now, updated_at: now, created_by: tenant_id, updated_by: tenant_id, deleted_at: None, version: 3, }; let resp = NumberingService::model_to_resp(&model); assert_eq!(resp.id, id); assert_eq!(resp.name, "订单编号"); assert_eq!(resp.code, "ORDER"); assert_eq!(resp.prefix, "ORD"); assert_eq!(resp.date_format, Some("%Y%m%d".to_string())); assert_eq!(resp.seq_length, 6); assert_eq!(resp.seq_start, 1); assert_eq!(resp.seq_current, 42); assert_eq!(resp.separator, "-"); assert_eq!(resp.reset_cycle, "daily"); assert_eq!(resp.last_reset_date, Some(today.to_string())); assert_eq!(resp.version, 3); } #[test] fn model_to_resp_none_fields() { let id = Uuid::now_v7(); let tenant_id = Uuid::now_v7(); let now = Utc::now(); let model = numbering_rule::Model { id, tenant_id, name: "简单编号".to_string(), code: "SIMPLE".to_string(), prefix: "".to_string(), date_format: None, seq_length: 4, seq_start: 1, seq_current: 1, separator: "-".to_string(), reset_cycle: "never".to_string(), last_reset_date: None, created_at: now, updated_at: now, created_by: tenant_id, updated_by: tenant_id, deleted_at: None, version: 1, }; let resp = NumberingService::model_to_resp(&model); assert_eq!(resp.date_format, None); assert_eq!(resp.last_reset_date, None); assert_eq!(resp.prefix, ""); } // ---- format_number 测试 ---- #[test] fn format_basic_prefix_no_date() { // 基础:前缀 + 序列号 let result = format_number("ORD", "/", None, 1, 5); assert_eq!(result, "ORD/00001"); } #[test] fn format_with_date_part() { // 前缀 + 日期 + 序列号 let result = format_number("INV", "-", Some("20260430"), 42, 4); assert_eq!(result, "INV-20260430-0042"); } #[test] fn format_no_prefix() { // 无前缀,直接输出序列号 let result = format_number("", "/", None, 7, 3); assert_eq!(result, "007"); } #[test] fn format_no_prefix_no_date() { // 无前缀无日期,仅序列号 let result = format_number("", "-", None, 99, 6); assert_eq!(result, "000099"); } #[test] fn format_seq_length_zero_pads_to_one() { // seq_length=0 时仍至少填充 1 位 let result = format_number("", "", None, 5, 0); assert_eq!(result, "5"); } }