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

@@ -15,24 +15,48 @@ pub async fn list_industries(
) -> SaasResult<PaginatedResponse<IndustryListItem>> {
let (page, page_size, offset) = normalize_pagination(query.page, query.page_size);
// 动态构建参数化查询 — 所有用户输入通过 $N 绑定
let mut where_parts: Vec<String> = vec!["1=1".to_string()];
let mut param_idx = 3; // $1=LIMIT, $2=OFFSET, $3+=filters
let status_param: Option<String> = query.status.clone();
let source_param: Option<String> = query.source.clone();
// 构建 WHERE 条件 — 每个查询独立的参数编号
let mut where_parts: Vec<String> = vec!["1=1".to_string()];
// count 查询:参数从 $1 开始
let mut count_params: Vec<String> = Vec::new();
let mut count_idx = 1;
if status_param.is_some() {
where_parts.push(format!("status = ${}", param_idx));
param_idx += 1;
count_params.push(format!("status = ${}", count_idx));
count_idx += 1;
}
if source_param.is_some() {
where_parts.push(format!("source = ${}", param_idx));
param_idx += 1;
count_params.push(format!("source = ${}", count_idx));
count_idx += 1;
}
let where_sql = where_parts.join(" AND ");
let count_where = if count_params.is_empty() {
"1=1".to_string()
} else {
format!("1=1 AND {}", count_params.join(" AND "))
};
// items 查询:$1=LIMIT, $2=OFFSET, $3+=filters
let mut items_params: Vec<String> = Vec::new();
let mut items_idx = 3;
if status_param.is_some() {
items_params.push(format!("status = ${}", items_idx));
items_idx += 1;
}
if source_param.is_some() {
items_params.push(format!("source = ${}", items_idx));
items_idx += 1;
}
let items_where = if items_params.is_empty() {
"1=1".to_string()
} else {
format!("1=1 AND {}", items_params.join(" AND "))
};
// count 查询
let count_sql = format!("SELECT COUNT(*) FROM industries WHERE {}", where_sql);
let count_sql = format!("SELECT COUNT(*) FROM industries WHERE {}", count_where);
let mut count_q = sqlx::query_scalar::<_, i64>(&count_sql);
if let Some(ref s) = status_param { count_q = count_q.bind(s); }
if let Some(ref s) = source_param { count_q = count_q.bind(s); }
@@ -44,7 +68,7 @@ pub async fn list_industries(
COALESCE(jsonb_array_length(keywords), 0) as keywords_count, \
created_at, updated_at \
FROM industries WHERE {} ORDER BY source, id LIMIT $1 OFFSET $2",
where_sql
items_where
);
let mut items_q = sqlx::query_as::<_, IndustryListItem>(&items_sql)
.bind(page_size as i64)