- C-1: Add tenant_id to settings unique index to prevent cross-tenant conflicts - C-2: Move pg_advisory_xact_lock inside the transaction for correct concurrency (previously lock was released before the numbering transaction started) - H-5: Add CORS middleware (permissive for dev, TODO: restrict in production)
380 lines
13 KiB
Rust
380 lines
13 KiB
Rust
use chrono::{Datelike, NaiveDate, Utc};
|
||
use sea_orm::{
|
||
ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set,
|
||
Statement, ConnectionTrait, DatabaseBackend, TransactionTrait,
|
||
};
|
||
use uuid::Uuid;
|
||
|
||
use crate::dto::{CreateNumberingRuleReq, GenerateNumberResp, NumberingRuleResp};
|
||
use crate::entity::numbering_rule;
|
||
use crate::error::{ConfigError, ConfigResult};
|
||
use erp_core::events::EventBus;
|
||
use erp_core::types::Pagination;
|
||
|
||
/// 编号规则 CRUD 服务 -- 创建、查询、更新、软删除编号规则,
|
||
/// 以及线程安全地生成编号序列。
|
||
pub struct NumberingService;
|
||
|
||
impl NumberingService {
|
||
/// 分页查询编号规则列表。
|
||
pub async fn list(
|
||
tenant_id: Uuid,
|
||
pagination: &Pagination,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> ConfigResult<(Vec<NumberingRuleResp>, 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) as u64;
|
||
let models = paginator
|
||
.fetch_page(page_index)
|
||
.await
|
||
.map_err(|e| ConfigError::Validation(e.to_string()))?;
|
||
|
||
let resps: Vec<NumberingRuleResp> = models
|
||
.iter()
|
||
.map(|m| Self::model_to_resp(m))
|
||
.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<NumberingRuleResp> {
|
||
// 检查 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 }),
|
||
));
|
||
|
||
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()),
|
||
})
|
||
}
|
||
|
||
/// 更新编号规则的可编辑字段。
|
||
pub async fn update(
|
||
id: Uuid,
|
||
tenant_id: Uuid,
|
||
operator_id: Uuid,
|
||
req: &crate::dto::UpdateNumberingRuleReq,
|
||
db: &sea_orm::DatabaseConnection,
|
||
) -> ConfigResult<NumberingRuleResp> {
|
||
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 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);
|
||
|
||
let updated = active
|
||
.update(db)
|
||
.await
|
||
.map_err(|e| ConfigError::Validation(e.to_string()))?;
|
||
|
||
Ok(Self::model_to_resp(&updated))
|
||
}
|
||
|
||
/// 软删除编号规则。
|
||
pub async fn delete(
|
||
id: Uuid,
|
||
tenant_id: Uuid,
|
||
operator_id: Uuid,
|
||
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 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
|
||
.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 }),
|
||
));
|
||
|
||
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<GenerateNumberResp> {
|
||
// 先读取规则获取 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<C>(
|
||
rule_id: Uuid,
|
||
tenant_id: Uuid,
|
||
txn: &C,
|
||
) -> ConfigResult<String>
|
||
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 separator = &rule.separator;
|
||
let mut parts = vec![rule.prefix.clone()];
|
||
|
||
// 日期部分(如果配置了 date_format)
|
||
if let Some(date_fmt) = &rule.date_format {
|
||
let date_part = Utc::now().format(date_fmt).to_string();
|
||
parts.push(date_part);
|
||
}
|
||
|
||
// 序列号补零
|
||
let seq_padded = format!("{:0>width$}", seq_current, width = rule.seq_length as usize);
|
||
parts.push(seq_padded);
|
||
|
||
let number = parts.join(separator);
|
||
|
||
Ok(number)
|
||
}
|
||
|
||
/// 根据重置周期判断是否需要重置序列号。
|
||
///
|
||
/// 如果需要重置,返回 `seq_start`;否则返回原值。
|
||
fn maybe_reset_sequence(
|
||
seq_current: i64,
|
||
seq_start: i64,
|
||
reset_cycle: &str,
|
||
last_reset_date: Option<NaiveDate>,
|
||
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()),
|
||
}
|
||
}
|
||
}
|