Files
hms/crates/erp-config/src/handler/numbering_handler.rs
iven 5d6e1dc394 feat(core): implement optimistic locking across all entities
Add VersionMismatch error variant and check_version() helper to erp-core.
All 13 mutable entities now enforce version checking on update/delete:
- erp-auth: user, role, organization, department, position
- erp-config: dictionary, dictionary_item, menu, setting, numbering_rule
- erp-workflow: process_definition, process_instance, task
- erp-message: message, message_subscription

Update DTOs to expose version in responses and require version in update
requests. HTTP 409 Conflict returned on version mismatch.
2026-04-11 23:25:43 +08:00

160 lines
4.4 KiB
Rust

use axum::Extension;
use axum::extract::{FromRef, Path, Query, State};
use axum::response::Json;
use validator::Validate;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, PaginatedResponse, Pagination, TenantContext};
use uuid::Uuid;
use crate::config_state::ConfigState;
use crate::dto::{
CreateNumberingRuleReq, GenerateNumberResp, NumberingRuleResp, UpdateNumberingRuleReq,
};
use crate::service::numbering_service::NumberingService;
/// GET /api/v1/numbering-rules
///
/// 分页查询当前租户下的编号规则列表。
/// 需要 `numbering.list` 权限。
pub async fn list_numbering_rules<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Query(pagination): Query<Pagination>,
) -> Result<Json<ApiResponse<PaginatedResponse<NumberingRuleResp>>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "numbering.list")?;
let (rules, total) = NumberingService::list(ctx.tenant_id, &pagination, &state.db).await?;
let page = pagination.page.unwrap_or(1);
let page_size = pagination.limit();
let total_pages = total.div_ceil(page_size);
Ok(Json(ApiResponse::ok(PaginatedResponse {
data: rules,
total,
page,
page_size,
total_pages,
})))
}
/// POST /api/v1/numbering-rules
///
/// 创建新的编号规则。
/// 规则编码在租户内必须唯一。
/// 需要 `numbering.create` 权限。
pub async fn create_numbering_rule<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Json(req): Json<CreateNumberingRuleReq>,
) -> Result<Json<ApiResponse<NumberingRuleResp>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "numbering.create")?;
req.validate()
.map_err(|e| AppError::Validation(e.to_string()))?;
let rule = NumberingService::create(
ctx.tenant_id,
ctx.user_id,
&req,
&state.db,
&state.event_bus,
)
.await?;
Ok(Json(ApiResponse::ok(rule)))
}
/// PUT /api/v1/numbering-rules/:id
///
/// 更新编号规则的可编辑字段。
/// 需要 `numbering.update` 权限。
pub async fn update_numbering_rule<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
Json(req): Json<UpdateNumberingRuleReq>,
) -> Result<Json<ApiResponse<NumberingRuleResp>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "numbering.update")?;
let rule =
NumberingService::update(id, ctx.tenant_id, ctx.user_id, &req, &state.db).await?;
Ok(Json(ApiResponse::ok(rule)))
}
/// POST /api/v1/numbering-rules/:id/generate
///
/// 根据编号规则生成新的编号。
/// 使用 PostgreSQL advisory lock 保证并发安全。
/// 需要 `numbering.generate` 权限。
pub async fn generate_number<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
) -> Result<Json<ApiResponse<GenerateNumberResp>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "numbering.generate")?;
let result = NumberingService::generate_number(id, ctx.tenant_id, &state.db).await?;
Ok(Json(ApiResponse::ok(result)))
}
/// DELETE /api/v1/numbering-rules/:id
///
/// 软删除编号规则,设置 deleted_at 时间戳。
/// 需要请求体包含 version 字段用于乐观锁校验。
/// 需要 `numbering.delete` 权限。
pub async fn delete_numbering_rule<S>(
State(state): State<ConfigState>,
Extension(ctx): Extension<TenantContext>,
Path(id): Path<Uuid>,
Json(req): Json<DeleteNumberingVersionReq>,
) -> Result<Json<ApiResponse<()>>, AppError>
where
ConfigState: FromRef<S>,
S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "numbering.delete")?;
NumberingService::delete(
id,
ctx.tenant_id,
ctx.user_id,
req.version,
&state.db,
&state.event_bus,
)
.await?;
Ok(Json(ApiResponse {
success: true,
data: None,
message: Some("编号规则已删除".to_string()),
}))
}
/// 删除编号规则的乐观锁版本号请求体。
#[derive(Debug, serde::Deserialize)]
pub struct DeleteNumberingVersionReq {
pub version: i32,
}