feat(health): 积分规则/商品 update/delete + 标签更新端点
- 积分规则: 添加 update/delete service + handler + 路由 - 兑换商品: 添加 update/delete service + handler + 路由 - 文章标签: 添加 update service + handler + 路由 - Web 管理端: 规则/商品列表页支持编辑/删除/启用切换 - Web 管理端: 标签管理页支持编辑、删除传 version
This commit is contained in:
@@ -218,3 +218,15 @@ impl CreateTagReq {
|
||||
self.name = sanitize_string(&self.name);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateTagReq {
|
||||
pub name: String,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
impl UpdateTagReq {
|
||||
pub fn sanitize(&mut self) {
|
||||
self.name = sanitize_string(&self.name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,6 +264,18 @@ pub struct OfflineEventResp {
|
||||
// 管理端:带版本号的更新/删除包装
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateRuleWithVersion {
|
||||
pub data: UpdatePointsRuleReq,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateProductWithVersion {
|
||||
pub data: UpdatePointsProductReq,
|
||||
pub version: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
|
||||
pub struct UpdateOfflineEventWithVersion {
|
||||
pub data: UpdateOfflineEventReq,
|
||||
|
||||
@@ -29,6 +29,9 @@ pub enum HealthError {
|
||||
#[error("日常监测记录不存在")]
|
||||
DailyMonitoringNotFound,
|
||||
|
||||
#[error("积分规则不存在")]
|
||||
PointsRuleNotFound,
|
||||
|
||||
#[error("兑换商品不存在")]
|
||||
PointsProductNotFound,
|
||||
|
||||
@@ -92,6 +95,7 @@ impl From<HealthError> for AppError {
|
||||
| HealthError::FollowUpTaskNotFound
|
||||
| HealthError::ConsultationNotFound
|
||||
| HealthError::ArticleNotFound
|
||||
| HealthError::PointsRuleNotFound
|
||||
| HealthError::PointsProductNotFound
|
||||
| HealthError::PointsOrderNotFound
|
||||
| HealthError::OfflineEventNotFound
|
||||
|
||||
@@ -6,7 +6,7 @@ use erp_core::error::AppError;
|
||||
use erp_core::rbac::require_permission;
|
||||
use erp_core::types::{ApiResponse, TenantContext};
|
||||
|
||||
use crate::dto::article_dto::{CreateTagReq, TagResp};
|
||||
use crate::dto::article_dto::{CreateTagReq, TagResp, UpdateTagReq};
|
||||
use crate::service::article_tag_service;
|
||||
use crate::state::HealthState;
|
||||
|
||||
@@ -40,6 +40,24 @@ where
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn update_tag<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(id): Path<uuid::Uuid>,
|
||||
mut req: Json<UpdateTagReq>,
|
||||
) -> Result<Json<ApiResponse<TagResp>>, AppError>
|
||||
where
|
||||
HealthState: FromRef<S>,
|
||||
S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.articles.manage")?;
|
||||
req.sanitize();
|
||||
let result = article_tag_service::update_tag(
|
||||
&state, ctx.tenant_id, id, Some(ctx.user_id), req.0,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, utoipa::ToSchema)]
|
||||
pub struct DeleteTagReq {
|
||||
pub version: i32,
|
||||
|
||||
@@ -227,6 +227,38 @@ where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn update_rule<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(rule_id): Path<Uuid>,
|
||||
Json(wrapper): Json<crate::dto::points_dto::UpdateRuleWithVersion>,
|
||||
) -> Result<Json<ApiResponse<PointsRuleResp>>, AppError>
|
||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.points.manage")?;
|
||||
let mut data = wrapper.data;
|
||||
data.sanitize();
|
||||
let result = points_service::update_rule(
|
||||
&state, ctx.tenant_id, rule_id, Some(ctx.user_id), data, wrapper.version,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn delete_rule<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(rule_id): Path<Uuid>,
|
||||
Json(wrapper): Json<crate::dto::DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.points.manage")?;
|
||||
points_service::delete_rule(
|
||||
&state, ctx.tenant_id, rule_id, Some(ctx.user_id), wrapper.version,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
pub async fn admin_create_product<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
@@ -243,6 +275,38 @@ where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn admin_update_product<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(product_id): Path<Uuid>,
|
||||
Json(wrapper): Json<crate::dto::points_dto::UpdateProductWithVersion>,
|
||||
) -> Result<Json<ApiResponse<PointsProductResp>>, AppError>
|
||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.points.manage")?;
|
||||
let mut data = wrapper.data;
|
||||
data.sanitize();
|
||||
let result = points_service::update_product(
|
||||
&state, ctx.tenant_id, product_id, Some(ctx.user_id), data, wrapper.version,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(result)))
|
||||
}
|
||||
|
||||
pub async fn admin_delete_product<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
Path(product_id): Path<Uuid>,
|
||||
Json(wrapper): Json<crate::dto::DeleteWithVersion>,
|
||||
) -> Result<Json<ApiResponse<()>>, AppError>
|
||||
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
|
||||
{
|
||||
require_permission(&ctx, "health.points.manage")?;
|
||||
points_service::delete_product(
|
||||
&state, ctx.tenant_id, product_id, Some(ctx.user_id), wrapper.version,
|
||||
).await?;
|
||||
Ok(Json(ApiResponse::ok(())))
|
||||
}
|
||||
|
||||
pub async fn admin_list_orders<S>(
|
||||
State(state): State<HealthState>,
|
||||
Extension(ctx): Extension<TenantContext>,
|
||||
|
||||
@@ -372,7 +372,8 @@ impl HealthModule {
|
||||
)
|
||||
.route(
|
||||
"/health/article-tags/{id}",
|
||||
axum::routing::delete(article_tag_handler::delete_tag),
|
||||
axum::routing::put(article_tag_handler::update_tag)
|
||||
.delete(article_tag_handler::delete_tag),
|
||||
)
|
||||
// 积分商城 — 患者端
|
||||
.route(
|
||||
@@ -426,10 +427,20 @@ impl HealthModule {
|
||||
axum::routing::get(points_handler::list_rules)
|
||||
.post(points_handler::create_rule),
|
||||
)
|
||||
.route(
|
||||
"/health/admin/points/rules/{id}",
|
||||
axum::routing::put(points_handler::update_rule)
|
||||
.delete(points_handler::delete_rule),
|
||||
)
|
||||
.route(
|
||||
"/health/admin/points/products",
|
||||
axum::routing::post(points_handler::admin_create_product),
|
||||
)
|
||||
.route(
|
||||
"/health/admin/points/products/{id}",
|
||||
axum::routing::put(points_handler::admin_update_product)
|
||||
.delete(points_handler::admin_delete_product),
|
||||
)
|
||||
.route(
|
||||
"/health/admin/points/orders",
|
||||
axum::routing::get(points_handler::admin_list_orders),
|
||||
|
||||
@@ -9,7 +9,7 @@ use erp_core::audit::AuditLog;
|
||||
use erp_core::audit_service;
|
||||
use erp_core::error::check_version;
|
||||
|
||||
use crate::dto::article_dto::{CreateTagReq, TagResp};
|
||||
use crate::dto::article_dto::{CreateTagReq, TagResp, UpdateTagReq};
|
||||
use crate::entity::article_tag;
|
||||
use crate::error::{HealthError, HealthResult};
|
||||
use crate::state::HealthState;
|
||||
@@ -66,6 +66,44 @@ pub async fn create_tag(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_tag(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdateTagReq,
|
||||
) -> HealthResult<TagResp> {
|
||||
let expected_version = req.version;
|
||||
let model = article_tag::Entity::find()
|
||||
.filter(article_tag::Column::Id.eq(id))
|
||||
.filter(article_tag::Column::TenantId.eq(tenant_id))
|
||||
.filter(article_tag::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::TagNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: article_tag::ActiveModel = model.into();
|
||||
active.name = Set(req.name);
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "article_tag.updated", "article_tag")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(TagResp {
|
||||
id: m.id, name: m.name,
|
||||
created_at: m.created_at, updated_at: m.updated_at, version: m.version,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete_tag(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
|
||||
@@ -645,6 +645,89 @@ pub async fn create_product(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_product(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
product_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdatePointsProductReq,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<PointsProductResp> {
|
||||
let model = points_product::Entity::find()
|
||||
.filter(points_product::Column::Id.eq(product_id))
|
||||
.filter(points_product::Column::TenantId.eq(tenant_id))
|
||||
.filter(points_product::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PointsProductNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: points_product::ActiveModel = model.into();
|
||||
if let Some(name) = req.name { active.name = Set(name); }
|
||||
if let Some(product_type) = req.product_type { active.product_type = Set(product_type); }
|
||||
if let Some(points_cost) = req.points_cost { active.points_cost = Set(points_cost); }
|
||||
if let Some(stock) = req.stock { active.stock = Set(stock); }
|
||||
if let Some(image_url) = req.image_url { active.image_url = Set(Some(image_url)); }
|
||||
if let Some(description) = req.description { active.description = Set(Some(description)); }
|
||||
if let Some(service_config) = req.service_config { active.service_config = Set(Some(service_config)); }
|
||||
if let Some(is_active) = req.is_active { active.is_active = Set(is_active); }
|
||||
if let Some(sort_order) = req.sort_order { active.sort_order = Set(sort_order); }
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "points_product.updated", "points_product")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(PointsProductResp {
|
||||
id: m.id, name: m.name, product_type: m.product_type,
|
||||
points_cost: m.points_cost, stock: m.stock,
|
||||
image_url: m.image_url, description: m.description,
|
||||
is_active: m.is_active, sort_order: m.sort_order,
|
||||
created_at: m.created_at, updated_at: m.updated_at, version: m.version,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete_product(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
product_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = points_product::Entity::find()
|
||||
.filter(points_product::Column::Id.eq(product_id))
|
||||
.filter(points_product::Column::TenantId.eq(tenant_id))
|
||||
.filter(points_product::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PointsProductNotFound)?;
|
||||
|
||||
let _next_ver = check_version(expected_version, model.version)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: points_product::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "points_product.deleted", "points_product")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 兑换(FIFO 消费积分)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -1055,6 +1138,89 @@ pub async fn create_rule(
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn update_rule(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
rule_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
req: UpdatePointsRuleReq,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<PointsRuleResp> {
|
||||
let model = points_rule::Entity::find()
|
||||
.filter(points_rule::Column::Id.eq(rule_id))
|
||||
.filter(points_rule::Column::TenantId.eq(tenant_id))
|
||||
.filter(points_rule::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PointsRuleNotFound)?;
|
||||
|
||||
let next_ver = check_version(expected_version, model.version)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: points_rule::ActiveModel = model.into();
|
||||
if let Some(name) = req.name { active.name = Set(name); }
|
||||
if let Some(description) = req.description { active.description = Set(Some(description)); }
|
||||
if let Some(points_value) = req.points_value { active.points_value = Set(points_value); }
|
||||
if let Some(daily_cap) = req.daily_cap { active.daily_cap = Set(daily_cap); }
|
||||
if let Some(streak_7d_bonus) = req.streak_7d_bonus { active.streak_7d_bonus = Set(streak_7d_bonus); }
|
||||
if let Some(streak_14d_bonus) = req.streak_14d_bonus { active.streak_14d_bonus = Set(streak_14d_bonus); }
|
||||
if let Some(streak_30d_bonus) = req.streak_30d_bonus { active.streak_30d_bonus = Set(streak_30d_bonus); }
|
||||
if let Some(is_active) = req.is_active { active.is_active = Set(is_active); }
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(next_ver);
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "points_rule.updated", "points_rule")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(PointsRuleResp {
|
||||
id: m.id, event_type: m.event_type, name: m.name,
|
||||
description: m.description, points_value: m.points_value,
|
||||
daily_cap: m.daily_cap, streak_7d_bonus: m.streak_7d_bonus,
|
||||
streak_14d_bonus: m.streak_14d_bonus, streak_30d_bonus: m.streak_30d_bonus,
|
||||
is_active: m.is_active, created_at: m.created_at,
|
||||
updated_at: m.updated_at, version: m.version,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn delete_rule(
|
||||
state: &HealthState,
|
||||
tenant_id: Uuid,
|
||||
rule_id: Uuid,
|
||||
operator_id: Option<Uuid>,
|
||||
expected_version: i32,
|
||||
) -> HealthResult<()> {
|
||||
let model = points_rule::Entity::find()
|
||||
.filter(points_rule::Column::Id.eq(rule_id))
|
||||
.filter(points_rule::Column::TenantId.eq(tenant_id))
|
||||
.filter(points_rule::Column::DeletedAt.is_null())
|
||||
.one(&state.db)
|
||||
.await?
|
||||
.ok_or(HealthError::PointsRuleNotFound)?;
|
||||
|
||||
let _next_ver = check_version(expected_version, model.version)?;
|
||||
|
||||
let now = Utc::now();
|
||||
let mut active: points_rule::ActiveModel = model.into();
|
||||
active.deleted_at = Set(Some(now));
|
||||
active.updated_at = Set(now);
|
||||
active.updated_by = Set(operator_id);
|
||||
active.version = Set(active.version.unwrap() + 1);
|
||||
let m = active.update(&state.db).await?;
|
||||
|
||||
audit_service::record(
|
||||
AuditLog::new(tenant_id, operator_id, "points_rule.deleted", "points_rule")
|
||||
.with_resource_id(m.id),
|
||||
&state.db,
|
||||
).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 线下活动
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user