fix: 全维度审查修复(安全/正确性/性能/稳定性/客户端/前端)

安全
- /api/ziniao/** 五个匿名接口加管理员鉴权(此前可匿名换取任意员工店铺登录令牌)
- 删除 Flask 遗留后门:默认密码建超管 + 每次启动写生产 users 表(服务端与客户端各一份)
- 进度/详情接口归属过滤:新增 TaskProgressOwnershipSupport,11 模块 progress/light 与
  /tasks/batch 接入,DTO 补 userId,前端 13 个查询封装补传(未传时后端不过滤,兼容旧端)
- 代理提取链接(含账密)不再明文入日志(新增 common/util/SecretMasking)
- 全局异常兜底不再回传原始异常信息;内部令牌比较改常量时间
- 登录加失败计数与锁定(10 次锁 15 分钟);品牌源文件下载加 SSRF 防护
- AdminApiGuardFilter 覆盖前缀从 2 扩到 15(开关默认 false,行为不变,为收紧做准备)
- 生产关闭 springdoc/knife4j(/doc.html 匿名可读全部接口定义)

正确性
- 40901/40902 拆分:锁竞争不再被伪装成 success=true(此前客户端停止重试、分片静默丢失)
- 假成功收敛:集采明细批量写失败改为抛出、去重 worker 异常标失败、4 个 worker 改判
  success 字段、publish 空 ASIN 行参与批次 flush、巡店删除全失败带 error 上报
- 客户端心跳 discard 移入 finally(7 模块,失败路径不再留僵尸 RUNNING 任务)
- 状态机条件更新:跟价停止循环、集采 activate/fail、imagevideo 归档回填、店铺匹配提交

性能
- 前端入口包 JS 1.05MB→204KB、CSS 355KB→10.7KB(Element Plus 改按需 + el-config-provider)
- 载荷引用计数按指针里的 taskId 收敛(原 JSON 列 IN 全表扫且逐行调用)
- 店铺明细多值批量 INSERT;快照 upsert 预载缓存;结果文件列改单条 UPDATE
- 新增迁移 V120(补 3 个缺失索引)/V121(删 4 个被覆盖的冗余索引)/V122(URL 前缀索引)

稳定性
- 新增 common/util/ThreadPools 有界线程池替换 5 处无界队列(防堆积 OOM)
- Redis 锁释放改 Lua 原子校验(原裸 delete 会误删他人已过期的锁)
- imagevideo 加死节点接管;锁续期失败重试;调度池 4→16;openStream 全部加超时
- 事务内远程对象删除移到提交后;启动恢复锁按实例命名

客户端
- 不再 taskkill /f /im chrome.exe(改为按调试端口精准回收,不杀用户自己的浏览器)
- 密码检测不再无条件杀紫鸟进程;品牌检测加全局互斥(代理池不再互相覆盖)
- base_dir 统一到 exe 目录(原被 os.getcwd() 覆盖,日志/缓存会分裂两个目录)
- 缓存加定时清理;图片下载加超时;mkstemp 句柄托管

测试
- 同步更新受影响的契约测试(构造器签名/条件更新/方法改名/新增接口方法等)
- 修复 FaultInjectionTest 等 3 处 mock 未 stub 流式 read 导致的读循环 OOM
- mvn test 2795 个测试全绿
This commit is contained in:
2026-09-14 04:15:36 +08:00
parent c448f49e30
commit 6d46506726
135 changed files with 3222 additions and 829 deletions
@@ -21,3 +21,16 @@ aiimage:
instance-id: ${AIIMAGE_INSTANCE_ID:}
result-file-job:
mq-enabled: ${AIIMAGE_RESULT_FILE_JOB_MQ_ENABLED:true}
# 生产关闭 API 文档(2026-09 全维度审查):/doc.html 与 /v3/api-docs 匿名可达,
# 会向未授权访问者暴露全部接口定义(内部路径、参数结构、字段名)。
# actuator 仅保留 healthSpring Boot 默认只暴露 health,且 details 默认 never);
# 它是负载均衡/面板的健康探针,需保持匿名可达。
springdoc:
api-docs:
enabled: false
swagger-ui:
enabled: false
knife4j:
enable: false
@@ -0,0 +1,71 @@
-- V120: 补齐三处缺失索引(2026-09 全维度代码审查发现)
--
-- 背景与影响:
-- 1) biz_file_task(status, updated_at)
-- StaleTaskRepairService.repairFileTaskStaleRunning 每 10 分钟按
-- status='RUNNING' AND updated_at < cutoff 扫描;该表现有索引无一以 status 打头
-- V1 是 (module_type,status)V92 是 (owner_instance_id,status,updated_at)),
-- 导致每轮全表扫描。代码侧已同步改为「先查 id(limit 500)再按主键更新」,本索引进一步消除扫描。
-- 2) brand_crawl_tasks(status, updated_at)
-- 同上,StaleTaskRepairService.repairBrandStale 的 brand 分支;该表仅有 (user_id,status)。
-- 3) biz_shop_data_crawl_item(daily_file_id)
-- 该列作为 DELETE/UPDATE 条件使用(ShopDataCrawlItemMapper.deleteByDailyFileId、
-- ShopDataCrawlItemStoreService 跨天滚动改指),但 V111/V112 的索引清单里没有它
-- → 每次删档/改指全表扫描并对命中行加锁。该表按月累积,是增长最快的明细表。
--
-- 风险:ALTER TABLE ADD INDEX 需申请元数据锁(MySQL 8 在线 DDLINPLACE),
-- 建议在低峰窗口执行;本迁移已在代码层做幂等判存(重放不会因索引已存在而失败)。
-- 回滚:
-- ALTER TABLE biz_file_task DROP INDEX idx_biz_file_task_status_updated;
-- ALTER TABLE brand_crawl_tasks DROP INDEX idx_brand_crawl_task_status_updated;
-- ALTER TABLE biz_shop_data_crawl_item DROP INDEX idx_sdc_item_daily_file;
SET @db_name = DATABASE();
-- 1) biz_file_task(status, updated_at)
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'biz_file_task'
AND INDEX_NAME = 'idx_biz_file_task_status_updated'
);
SET @sql := IF(@idx_exists = 0,
'ALTER TABLE biz_file_task ADD INDEX idx_biz_file_task_status_updated (status, updated_at)',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 2) brand_crawl_tasks(status, updated_at)
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'brand_crawl_tasks'
AND INDEX_NAME = 'idx_brand_crawl_task_status_updated'
);
SET @sql := IF(@idx_exists = 0,
'ALTER TABLE brand_crawl_tasks ADD INDEX idx_brand_crawl_task_status_updated (status, updated_at)',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 3) biz_shop_data_crawl_item(daily_file_id)
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'biz_shop_data_crawl_item'
AND INDEX_NAME = 'idx_sdc_item_daily_file'
);
SET @sql := IF(@idx_exists = 0,
'ALTER TABLE biz_shop_data_crawl_item ADD INDEX idx_sdc_item_daily_file (daily_file_id)',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 验证:应返回 3 行
-- SELECT TABLE_NAME, INDEX_NAME FROM information_schema.STATISTICS
-- WHERE TABLE_SCHEMA = DATABASE()
-- AND INDEX_NAME IN ('idx_biz_file_task_status_updated','idx_brand_crawl_task_status_updated','idx_sdc_item_daily_file')
-- GROUP BY TABLE_NAME, INDEX_NAME;
@@ -0,0 +1,83 @@
-- V121: 清理被覆盖索引完全覆盖的冗余二级索引(2026-09 全维度代码审查发现)
--
-- 判定原则:被删索引的列集合是另一索引的**严格前缀**(或完全同列),
-- 即所有能用它的查询都能被保留的索引覆盖,删除无功能损失,只减少写入时的索引维护与 buffer pool 占用。
--
-- 1) biz_file_result.idx_file_result_patrol_history_desc (module_type,user_id,created_at DESC,id DESC)
-- 与 V42 的 idx_file_result_patrol_history (module_type,user_id,created_at,id) 同列。
-- MySQL 8 反向扫描 ASC 索引即可满足全 DESC 排序,DESC 变体只在「混合升降序」的 ORDER BY 下才有优势,
-- 本表查询(巡逻删除历史)为全 DESC,故 V46 的这份可删。
-- 2) biz_task_result_item.idx_result_item_task (task_id,module_type)
-- 是 idx_result_item_country_asin (task_id,module_type,country_code,asin) 的前缀。
-- country_code 在写入侧恒 NULL、从不查询,被覆盖关系成立。)
-- 3) biz_task_chunk.idx_task_scope (task_id,module_type,scope_hash)
-- 是 uk_task_scope_chunk (task_id,module_type,scope_hash,chunk_index) 的前缀。
-- 4) biz_task_chunk.idx_task_module (task_id,module_type)
-- 同为 uk_task_scope_chunk 的前缀。
--
-- 风险:DROP INDEX 为在线操作(INPLACE),但会立即改变执行计划;均为「被覆盖」判定,
-- 保留索引可完整替代。若线上观察到计划退化,按末尾注释回滚即可。
--
-- 回滚:
-- ALTER TABLE biz_file_result ADD INDEX idx_file_result_patrol_history_desc (module_type, user_id, created_at DESC, id DESC);
-- ALTER TABLE biz_task_result_item ADD INDEX idx_result_item_task (task_id, module_type);
-- ALTER TABLE biz_task_chunk ADD INDEX idx_task_scope (task_id, module_type, scope_hash);
-- ALTER TABLE biz_task_chunk ADD INDEX idx_task_module (task_id, module_type);
SET @db_name = DATABASE();
-- 1) biz_file_result.idx_file_result_patrol_history_desc
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'biz_file_result'
AND INDEX_NAME = 'idx_file_result_patrol_history_desc'
);
SET @sql := IF(@idx_exists > 0,
'ALTER TABLE biz_file_result DROP INDEX idx_file_result_patrol_history_desc',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 2) biz_task_result_item.idx_result_item_task
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'biz_task_result_item'
AND INDEX_NAME = 'idx_result_item_task'
);
SET @sql := IF(@idx_exists > 0,
'ALTER TABLE biz_task_result_item DROP INDEX idx_result_item_task',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 3) biz_task_chunk.idx_task_scope
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'biz_task_chunk'
AND INDEX_NAME = 'idx_task_scope'
);
SET @sql := IF(@idx_exists > 0,
'ALTER TABLE biz_task_chunk DROP INDEX idx_task_scope',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 4) biz_task_chunk.idx_task_module
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'biz_task_chunk'
AND INDEX_NAME = 'idx_task_module'
);
SET @sql := IF(@idx_exists > 0,
'ALTER TABLE biz_task_chunk DROP INDEX idx_task_module',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
@@ -0,0 +1,50 @@
-- V122: 为两处「按结果文件 URL 反查引用」的计数查询补前缀索引
--
-- 背景:ShopDataCrawlTaskService 删除结果对象前会反查两张表是否仍被引用:
-- SELECT COUNT(*) FROM biz_file_result WHERE result_file_url = ?
-- SELECT COUNT(*) FROM biz_shop_data_crawl_daily_file WHERE result_file_url = ?
-- 两列均为 VARCHAR(1000) 且都没有索引 → 每次删除结果对象 = 两次全表扫描。
--
-- 索引列长度取 191utf8mb4 下 191*4=764 字节,在 InnoDB 索引长度限制内;
-- 结果 URL 是 OSS 直链/对象 key 形态的路径串,前 191 字符足以区分。
--
-- 风险:ALTER TABLE ADD INDEX 需元数据锁(MySQL 8 INPLACE),建议低峰执行。
-- 回滚:
-- ALTER TABLE biz_file_result DROP INDEX idx_file_result_url_prefix;
-- ALTER TABLE biz_shop_data_crawl_daily_file DROP INDEX idx_sdc_daily_file_url_prefix;
SET @db_name = DATABASE();
-- 1) biz_file_result.result_file_url
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'biz_file_result'
AND INDEX_NAME = 'idx_file_result_url_prefix'
);
SET @sql := IF(@idx_exists = 0,
'ALTER TABLE biz_file_result ADD INDEX idx_file_result_url_prefix (result_file_url(191))',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 2) biz_shop_data_crawl_daily_file.result_file_url
SET @idx_exists := (
SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = @db_name AND TABLE_NAME = 'biz_shop_data_crawl_daily_file'
AND INDEX_NAME = 'idx_sdc_daily_file_url_prefix'
);
SET @sql := IF(@idx_exists = 0,
'ALTER TABLE biz_shop_data_crawl_daily_file ADD INDEX idx_sdc_daily_file_url_prefix (result_file_url(191))',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- 验证:应返回 2 行
-- SELECT TABLE_NAME, INDEX_NAME FROM information_schema.STATISTICS
-- WHERE TABLE_SCHEMA = DATABASE()
-- AND INDEX_NAME IN ('idx_file_result_url_prefix','idx_sdc_daily_file_url_prefix')
-- GROUP BY TABLE_NAME, INDEX_NAME;