fix(industry): 审计收尾 — MEDIUM + LOW 全部清零
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
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
M-1: Industries 创建弹窗添加 cold_start_template + pain_seed_categories M-3: industryStore console.warn → createLogger 结构化日志 B2: classify_with_industries 平局打破 + 归一化因子 3.0 文档化 S3: set_account_industries 验证移入事务内消除 TOCTOU T1: 4 个 SaaS 请求类型添加 deny_unknown_fields I3: store_trigger_experience Debug 格式 → signal_name 描述名 L-1: 删除 Accounts.tsx 死代码 editingIndustries L-3: Industries.tsx filters 类型补全 source 字段
This commit is contained in:
@@ -204,24 +204,25 @@ pub async fn set_account_industries(
|
||||
req: &SetAccountIndustriesRequest,
|
||||
) -> SaasResult<Vec<AccountIndustryItem>> {
|
||||
let now = chrono::Utc::now();
|
||||
|
||||
// 批量验证:一次查询所有行业是否存在且启用
|
||||
let ids: Vec<&str> = req.industries.iter().map(|e| e.industry_id.as_str()).collect();
|
||||
|
||||
// 事务:验证 + DELETE + INSERT 原子执行,消除 TOCTOU
|
||||
let mut tx = pool.begin().await.map_err(SaasError::Database)?;
|
||||
|
||||
// 验证:所有行业必须存在且启用
|
||||
let valid_count: (i64,) = sqlx::query_as(
|
||||
"SELECT COUNT(*) FROM industries WHERE id = ANY($1) AND status = 'active'"
|
||||
)
|
||||
.bind(&ids)
|
||||
.fetch_one(pool)
|
||||
.fetch_one(&mut *tx)
|
||||
.await
|
||||
.map_err(SaasError::Database)?;
|
||||
|
||||
if valid_count.0 != ids.len() as i64 {
|
||||
tx.rollback().await.ok();
|
||||
return Err(SaasError::InvalidInput("部分行业不存在或已禁用".to_string()));
|
||||
}
|
||||
|
||||
// 事务性 DELETE + INSERT
|
||||
let mut tx = pool.begin().await.map_err(SaasError::Database)?;
|
||||
|
||||
sqlx::query("DELETE FROM account_industries WHERE account_id = $1")
|
||||
.bind(account_id)
|
||||
.execute(&mut *tx)
|
||||
|
||||
@@ -36,6 +36,7 @@ pub struct IndustryListItem {
|
||||
|
||||
/// 创建行业请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct CreateIndustryRequest {
|
||||
pub id: String,
|
||||
pub name: String,
|
||||
@@ -57,6 +58,7 @@ pub struct CreateIndustryRequest {
|
||||
|
||||
/// 更新行业请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct UpdateIndustryRequest {
|
||||
pub name: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
@@ -99,12 +101,14 @@ pub struct AccountIndustryItem {
|
||||
|
||||
/// 设置用户行业请求
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct SetAccountIndustriesRequest {
|
||||
pub industries: Vec<AccountIndustryEntry>,
|
||||
}
|
||||
|
||||
/// 用户行业条目
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AccountIndustryEntry {
|
||||
pub industry_id: String,
|
||||
#[serde(default)]
|
||||
|
||||
Reference in New Issue
Block a user