fix(admin): 行业选择500修复 + 管理员切换订阅计划
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- fix(industry): list_industries SQL参数编号错位 — count查询和items查询
  共用WHERE子句但参数从$3开始,sqlx bind按$1/$2顺序绑定导致500
- feat(billing): 新增 PUT /admin/accounts/:id/subscription 端点 (super_admin)
  验证目标计划 → 取消当前订阅 → 创建新订阅(30天) → 同步配额
- feat(admin-v2): Accounts.tsx 编辑弹窗新增「订阅计划」选择区
  显示所有活跃计划,保存时调用admin switch plan API
This commit is contained in:
iven
2026-04-14 19:06:58 +08:00
parent d2a0c8efc0
commit 6721a1cc6e
8 changed files with 218 additions and 13 deletions

View File

@@ -300,6 +300,93 @@ pub async fn increment_dimension_by(
Ok(())
}
/// 管理员切换用户订阅计划(仅 super_admin 调用)
///
/// 1. 验证目标 plan_id 存在且 active
/// 2. 取消用户当前 active 订阅
/// 3. 创建新订阅status=active, 30 天周期)
/// 4. 更新当月 usage quota 的 max_* 列
pub async fn admin_switch_plan(
pool: &PgPool,
account_id: &str,
target_plan_id: &str,
) -> SaasResult<Subscription> {
// 1. 验证目标计划存在且 active
let plan = get_plan(pool, target_plan_id).await?
.ok_or_else(|| crate::error::SaasError::NotFound("目标计划不存在或已下架".into()))?;
// 2. 检查是否已订阅该计划
if let Some(current_sub) = get_active_subscription(pool, account_id).await? {
if current_sub.plan_id == target_plan_id {
return Err(crate::error::SaasError::InvalidInput("用户已订阅该计划".into()));
}
}
let mut tx = pool.begin().await
.map_err(|e| crate::error::SaasError::Internal(format!("开启事务失败: {}", e)))?;
let now = chrono::Utc::now();
// 3. 取消当前活跃订阅
sqlx::query(
"UPDATE billing_subscriptions SET status = 'canceled', canceled_at = $1, updated_at = $1 \
WHERE account_id = $2 AND status IN ('trial', 'active', 'past_due')"
)
.bind(&now)
.bind(account_id)
.execute(&mut *tx)
.await?;
// 4. 创建新订阅
let sub_id = uuid::Uuid::new_v4().to_string();
let period_start = now;
let period_end = now + chrono::Duration::days(30);
sqlx::query(
"INSERT INTO billing_subscriptions \
(id, account_id, plan_id, status, current_period_start, current_period_end, created_at, updated_at) \
VALUES ($1, $2, $3, 'active', $4, $5, $6, $6)"
)
.bind(&sub_id)
.bind(account_id)
.bind(&target_plan_id)
.bind(&period_start)
.bind(&period_end)
.bind(&now)
.execute(&mut *tx)
.await?;
// 5. 同步当月 usage quota 的 max_* 列
let limits: PlanLimits = serde_json::from_value(plan.limits.clone())
.unwrap_or_else(|_| PlanLimits::free());
sqlx::query(
"UPDATE billing_usage_quotas SET max_input_tokens=$1, max_output_tokens=$2, \
max_relay_requests=$3, max_hand_executions=$4, max_pipeline_runs=$5, updated_at=NOW() \
WHERE account_id=$6 AND period_start = DATE_TRUNC('month', NOW())"
)
.bind(limits.max_input_tokens_monthly)
.bind(limits.max_output_tokens_monthly)
.bind(limits.max_relay_requests_monthly)
.bind(limits.max_hand_executions_monthly)
.bind(limits.max_pipeline_runs_monthly)
.bind(account_id)
.execute(&mut *tx)
.await?;
tx.commit().await
.map_err(|e| crate::error::SaasError::Internal(format!("事务提交失败: {}", e)))?;
// 查询返回新订阅
let sub = sqlx::query_as::<_, Subscription>(
"SELECT * FROM billing_subscriptions WHERE id = $1"
)
.bind(&sub_id)
.fetch_one(pool)
.await?;
Ok(sub)
}
/// 检查用量配额
///
/// P1-7 修复: 从当前 Plan 读取限额(而非 stale 的 usage 表冗余列)