- 积分规则: 添加 update/delete service + handler + 路由 - 兑换商品: 添加 update/delete service + handler + 路由 - 文章标签: 添加 update service + handler + 路由 - Web 管理端: 规则/商品列表页支持编辑/删除/启用切换 - Web 管理端: 标签管理页支持编辑、删除传 version
454 lines
17 KiB
Rust
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()))
|
|
}
|