feat(admin-users): add plan entitlement revocation flow

This commit is contained in:
ZheFox
2026-09-02 11:23:24 +08:00
parent 6540c1e46a
commit 144a28f544
19 changed files with 726 additions and 73 deletions
@@ -34,6 +34,22 @@ fn admin_user_id_from_billing_path(request_path: &str, suffix: &str) -> Option<S
}
}
fn admin_user_entitlement_ids_from_path(request_path: &str) -> Option<(String, String)> {
let rest = request_path
.trim_end_matches('/')
.strip_prefix("/api/admin/users/")?;
let mut parts = rest.split('/');
let user_id = parts.next()?.trim();
if parts.next()? != "billing" || parts.next()? != "entitlements" {
return None;
}
let entitlement_id = parts.next()?.trim();
if user_id.is_empty() || entitlement_id.is_empty() || parts.next().is_some() {
return None;
}
Some((user_id.to_string(), entitlement_id.to_string()))
}
fn admin_user_billing_operator_id(request_context: &AdminRequestContext<'_>) -> Option<String> {
request_context
.decision()
@@ -202,6 +218,60 @@ pub(in super::super) async fn build_admin_list_user_billing_entitlements_respons
}
}
pub(in super::super) async fn build_admin_revoke_user_billing_entitlement_response(
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
) -> Result<Response<Body>, GatewayError> {
let Some((user_id, entitlement_id)) =
admin_user_entitlement_ids_from_path(request_context.path())
else {
return Ok(build_admin_users_bad_request_response("缺少套餐权益 ID"));
};
if state.find_user_auth_by_id(&user_id).await?.is_none() {
return Ok((
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "用户不存在" })),
)
.into_response());
}
match state
.app()
.revoke_user_plan_entitlement(&user_id, &entitlement_id)
.await?
{
crate::LocalMutationOutcome::Applied(()) => {}
crate::LocalMutationOutcome::NotFound => {
return Ok((
http::StatusCode::NOT_FOUND,
Json(json!({ "detail": "套餐权益不存在或已失效" })),
)
.into_response());
}
crate::LocalMutationOutcome::Invalid(detail) => {
return Ok(build_admin_users_bad_request_response(detail));
}
crate::LocalMutationOutcome::Unavailable => {
return Ok(build_admin_users_data_unavailable_response());
}
}
let entitlements = match load_admin_user_entitlements_payload(state, &user_id).await? {
Some(value) => value,
None => return Ok(build_admin_users_data_unavailable_response()),
};
Ok(attach_admin_audit_response(
Json(json!({
"items": entitlements["items"].clone(),
"entitlements": entitlements["items"].clone(),
"total": entitlements["total"].clone(),
}))
.into_response(),
"admin_user_plan_revoked",
"revoke_user_billing_entitlement",
"user_plan_entitlement",
&entitlement_id,
))
}
pub(in super::super) async fn build_admin_grant_user_billing_plan_response(
state: &AdminAppState<'_>,
request_context: &AdminRequestContext<'_>,
@@ -28,6 +28,7 @@ use self::batch::{
use self::billing::{
build_admin_grant_user_billing_plan_response,
build_admin_list_user_billing_entitlements_response,
build_admin_revoke_user_billing_entitlement_response,
};
use self::groups::{
build_admin_create_user_group_response, build_admin_delete_user_group_response,
@@ -8,10 +8,11 @@ use super::{
build_admin_list_user_group_members_response, build_admin_list_user_groups_response,
build_admin_list_user_sessions_response, build_admin_list_users_response,
build_admin_replace_user_group_members_response, build_admin_resolve_user_selection_response,
build_admin_reveal_user_api_key_response, build_admin_set_default_user_group_response,
build_admin_toggle_user_api_key_lock_response, build_admin_update_user_api_key_response,
build_admin_update_user_group_response, build_admin_update_user_response,
build_admin_user_batch_action_response, build_admin_users_data_unavailable_response,
build_admin_reveal_user_api_key_response, build_admin_revoke_user_billing_entitlement_response,
build_admin_set_default_user_group_response, build_admin_toggle_user_api_key_lock_response,
build_admin_update_user_api_key_response, build_admin_update_user_group_response,
build_admin_update_user_response, build_admin_user_batch_action_response,
build_admin_users_data_unavailable_response,
};
use crate::handlers::admin::request::{AdminAppState, AdminRequestContext};
use crate::GatewayError;
@@ -58,6 +59,10 @@ fn is_admin_users_route(request_context: &AdminRequestContext<'_>) -> bool {
&& path.starts_with("/api/admin/users/")
&& path.ends_with("/billing/grant-plan")
&& path.matches('/').count() == 6)
|| (request_context.method() == http::Method::DELETE
&& path.starts_with("/api/admin/users/")
&& path.contains("/billing/entitlements/")
&& path.matches('/').count() == 7)
|| ((request_context.method() == http::Method::GET
|| request_context.method() == http::Method::PUT
|| request_context.method() == http::Method::DELETE)
@@ -155,6 +160,9 @@ pub(super) async fn maybe_build_local_admin_users_routes_response(
build_admin_grant_user_billing_plan_response(state, request_context, request_body)
.await?,
)),
Some("revoke_user_billing_entitlement") => Ok(Some(
build_admin_revoke_user_billing_entitlement_response(state, request_context).await?,
)),
Some("get_user") => Ok(Some(
build_admin_get_user_response(state, request_context).await?,
)),