Files
hms/crates/erp-health/src/handler/points_handler.rs
iven 55ec57b2c0
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
feat(health): 积分规则/商品 update/delete + 标签更新端点
- 积分规则: 添加 update/delete service + handler + 路由
- 兑换商品: 添加 update/delete service + handler + 路由
- 文章标签: 添加 update service + handler + 路由
- Web 管理端: 规则/商品列表页支持编辑/删除/启用切换
- Web 管理端: 标签管理页支持编辑、删除传 version
2026-04-26 14:07:21 +08:00

454 lines
17 KiB
Rust

use axum::Extension;
use axum::extract::{FromRef, Json, Path, Query, State};
use serde::Deserialize;
use utoipa::IntoParams;
use uuid::Uuid;
use erp_core::error::AppError;
use erp_core::rbac::require_permission;
use erp_core::types::{ApiResponse, PaginatedResponse, TenantContext};
use crate::dto::points_dto::*;
use crate::service::points_service;
use crate::state::HealthState;
#[derive(Debug, Deserialize, IntoParams)]
pub struct PaginationParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct ProductTypeParam {
pub product_type: Option<String>,
}
// ---------------------------------------------------------------------------
// 患者端:积分账户 + 打卡
// ---------------------------------------------------------------------------
pub async fn get_my_account<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<PointsAccountResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
let result = points_service::get_account(&state, ctx.tenant_id, patient_id).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn daily_checkin<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<CheckinStatusResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
let result = points_service::daily_checkin(
&state, ctx.tenant_id, patient_id, Some(ctx.user_id),
).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn get_checkin_status<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<CheckinStatusResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
let result = points_service::get_checkin_status(&state, ctx.tenant_id, patient_id).await?;
Ok(Json(ApiResponse::ok(result)))
}
// ---------------------------------------------------------------------------
// 患者端:积分流水 + 商品 + 兑换
// ---------------------------------------------------------------------------
pub async fn list_my_transactions<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<PaginationParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<PointsTransactionResp>>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = points_service::list_transactions(
&state, ctx.tenant_id, patient_id, page, page_size,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn list_products<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<ProductTypeParam>,
Query(page): Query<PaginationParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<PointsProductResp>>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let p = page.page.unwrap_or(1);
let ps = page.page_size.unwrap_or(20);
let result = points_service::list_products(
&state, ctx.tenant_id, params.product_type, p, ps,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn get_product<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(product_id): Path<Uuid>,
) -> Result<Json<ApiResponse<PointsProductResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let result = points_service::get_product(&state, ctx.tenant_id, product_id).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn exchange_product<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Json(req): Json<ExchangeReq>,
) -> Result<Json<ApiResponse<PointsOrderResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
let result = points_service::exchange_product(
&state, ctx.tenant_id, patient_id, req, Some(ctx.user_id),
).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn list_my_orders<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<PaginationParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<PointsOrderResp>>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = points_service::list_orders(
&state, ctx.tenant_id, patient_id, page, page_size,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
// ---------------------------------------------------------------------------
// 线下活动(患者端)
// ---------------------------------------------------------------------------
pub async fn list_offline_events<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<PaginationParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<OfflineEventResp>>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = points_service::list_offline_events(
&state, ctx.tenant_id, page, page_size,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn register_event<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(event_id): Path<Uuid>,
) -> Result<Json<ApiResponse<()>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.health-data.manage")?;
let patient_id = resolve_patient_id(&state, ctx.tenant_id, ctx.user_id).await?;
points_service::register_event(
&state, ctx.tenant_id, event_id, patient_id, Some(ctx.user_id),
).await?;
Ok(Json(ApiResponse::ok(())))
}
// ---------------------------------------------------------------------------
// 管理端:核销 + 规则管理 + 商品管理
// ---------------------------------------------------------------------------
pub async fn verify_order<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Json(req): Json<VerifyOrderReq>,
) -> Result<Json<ApiResponse<PointsOrderResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.manage")?;
let result = points_service::verify_order(
&state, ctx.tenant_id, req.qr_code, ctx.user_id,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn list_rules<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<Vec<PointsRuleResp>>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.list")?;
let result = points_service::list_rules(&state, ctx.tenant_id).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn create_rule<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Json(req): Json<CreatePointsRuleReq>,
) -> Result<Json<ApiResponse<PointsRuleResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.manage")?;
let mut req = req;
req.sanitize();
let result = points_service::create_rule(
&state, ctx.tenant_id, Some(ctx.user_id), req,
).await?;
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>,
Json(req): Json<CreatePointsProductReq>,
) -> Result<Json<ApiResponse<PointsProductResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.manage")?;
let mut req = req;
req.sanitize();
let result = points_service::create_product(
&state, ctx.tenant_id, Some(ctx.user_id), req,
).await?;
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>,
Query(params): Query<PaginationParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<PointsOrderResp>>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
// 管理端查看所有订单 — 不按 patient_id 过滤
let result = points_service::admin_list_orders(
&state, ctx.tenant_id, page, page_size,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
// ---------------------------------------------------------------------------
// 线下活动 — 管理端 CRUD + 签到
// ---------------------------------------------------------------------------
pub async fn admin_create_event<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Json(req): Json<CreateOfflineEventReq>,
) -> Result<Json<ApiResponse<OfflineEventResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.manage")?;
let mut req = req;
req.sanitize();
let result = points_service::create_offline_event(
&state, ctx.tenant_id, Some(ctx.user_id), req,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn admin_update_event<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(event_id): Path<Uuid>,
Json(wrapper): Json<UpdateOfflineEventWithVersion>,
) -> Result<Json<ApiResponse<OfflineEventResp>>, 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_offline_event(
&state, ctx.tenant_id, event_id, Some(ctx.user_id), data, wrapper.version,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn admin_delete_event<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(event_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_offline_event(
&state, ctx.tenant_id, event_id, Some(ctx.user_id), wrapper.version,
).await?;
Ok(Json(ApiResponse::ok(())))
}
#[derive(Debug, Deserialize, IntoParams)]
pub struct AdminListEventsParams {
pub page: Option<u64>,
pub page_size: Option<u64>,
pub status: Option<String>,
}
pub async fn admin_list_events<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Query(params): Query<AdminListEventsParams>,
) -> Result<Json<ApiResponse<PaginatedResponse<OfflineEventResp>>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.list")?;
let page = params.page.unwrap_or(1);
let page_size = params.page_size.unwrap_or(20);
let result = points_service::admin_list_offline_events(
&state, ctx.tenant_id, params.status, page, page_size,
).await?;
Ok(Json(ApiResponse::ok(result)))
}
pub async fn admin_checkin_event<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
Path(event_id): Path<Uuid>,
Json(req): Json<AdminCheckinReq>,
) -> Result<Json<ApiResponse<()>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.manage")?;
points_service::admin_checkin_event(
&state, ctx.tenant_id, event_id, req.patient_id, Some(ctx.user_id),
).await?;
Ok(Json(ApiResponse::ok(())))
}
// ---------------------------------------------------------------------------
// 积分统计 — 管理端
// ---------------------------------------------------------------------------
pub async fn get_points_statistics<S>(
State(state): State<HealthState>,
Extension(ctx): Extension<TenantContext>,
) -> Result<Json<ApiResponse<PointsStatisticsResp>>, AppError>
where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
{
require_permission(&ctx, "health.points.list")?;
let result = points_service::get_points_statistics(&state, ctx.tenant_id).await?;
Ok(Json(ApiResponse::ok(result)))
}
// ---------------------------------------------------------------------------
// 辅助:通过 user_id 解析 patient_id
// ---------------------------------------------------------------------------
async fn resolve_patient_id(
state: &HealthState,
tenant_id: Uuid,
user_id: Uuid,
) -> Result<Uuid, AppError> {
use crate::entity::patient;
use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
let result: Option<patient::Model> = patient::Entity::find()
.filter(patient::Column::TenantId.eq(tenant_id))
.filter(patient::Column::UserId.eq(user_id))
.filter(patient::Column::DeletedAt.is_null())
.one(&state.db)
.await
.map_err(|e: sea_orm::DbErr| AppError::Internal(e.to_string()))?;
result
.map(|p| p.id)
.ok_or_else(|| AppError::NotFound("当前用户未关联患者档案".into()))
}