feat(health): 线下活动管理端 CRUD + 积分统计 API + 前端页面 (Chunk 4)
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

后端:
- 线下活动管理: create/update/delete/list/checkin 5 个管理端接口
- 活动签到自动发放积分 (事务内原子操作)
- 积分统计 API: 总发放/总消耗/总过期/活跃账户/Top10排行

前端:
- OfflineEventList: 活动管理页面 (创建/编辑/删除/状态筛选)
- points.ts 扩展: 线下活动 + 统计 API 方法
- 侧边栏新增线下活动入口
This commit is contained in:
iven
2026-04-25 17:34:54 +08:00
parent eb937d3d02
commit 7b18a7398d
8 changed files with 987 additions and 0 deletions

View File

@@ -262,6 +262,111 @@ where HealthState: FromRef<S>, S: Clone + Send + Sync + 'static,
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
// ---------------------------------------------------------------------------