fix(notice): 修复通知历史状态回填与配置占位符问题
Some checks failed
Build and Deploy (service.xpcool.com) / build-and-deploy (push) Failing after 54s

- 修正 status 列默认值为 0,避免历史失败记录被误标为成功
- 历史数据回填改为幂等更新,可重复执行
- 过滤未解析的 ${...} 配置占位符,优化 Bark 推送错误提示
- LogDetail 记录不存在时统一返回参数错误,避免暴露 SQL 细节
This commit is contained in:
夏犀麟 2026-09-14 23:53:56 +08:00
parent 863b16b118
commit e79690518e
6 changed files with 201 additions and 11 deletions

View File

@ -1,6 +1,8 @@
# service.xpcool.com 变更记录
> 倒序最新在上格式YYYY-MM-DD | 类型 | 摘要
2026-09-14 | CFG | **安装 chrome-devtools MCP用户级配置**全局 `npm i -g chrome-devtools-mcp@1.9.0`本机 Node v23.0.0 满足其 `engines: ^20.19.0 || ^22.12.0 || >=23` `C:\Users\xxl\.codebuddy\mcp.json` `mcpServers` 下新增 `chrome-devtools` 条目**刻意沿用现有 `dbx` 条目的绝对路径写法**`command` 指向 `...\nvm\v23.0.0\node.exe``args[0]` 指向 `...\node_modules\chrome-devtools-mcp\build\src\bin\chrome-devtools-mcp.js`而非 `npx chrome-devtools-mcp@latest`Windows `npx` 实为 `npx.cmd` IDE 直接 spawn 时存在解析风险绝对路径最稳 `--no-usage-statistics` 关闭 Google 使用统计Chrome 已就位`C:\Program Files\Google\Chrome\Application\chrome.exe`MCP 默认自动发现未额外传 `--executablePath`冒烟测试`initialize` 请求下进程正常启动stderr 打印自身横幅退出码 0**需重启 IDE或重载 MCP 面板方生效**默认行为是新开一个独立 profile Chrome 实例若要接管已开的浏览器需改用 `--browser-url http://127.0.0.1:9222`
2026-09-14 | CHG | **壁纸模块后端上线12 接口 + 开源平台聚合 + 自建图库** xpcool.com 提供开源壁纸平台 + 后台自建图库双来源**保留**原有浏览器实时生成能力设计核心是**带宽经济学**本机出口仅 5M625KB/s 且多站共用 开源平台Bing 每日一图 / Picsum / Unsplash / Pexels / Wallhaven图片**不经过服务器**后端只代理元数据并返回官方 CDN 直链`sources`/`list`/`random`/`download-track` `open` 分组平台响应内存缓存Bing 1h其余 10min 自建图库分三档落盘480px 缩略图 / 长边 1920 预览 / 原图** nginx 直出 `/wallpaper/`**后端只管元数据与上传接口遵守本仓库强制规范** POSTURL 不带参入参走 body**`manifest/sql/018_wallpaper.sql` 为幂等种子菜单 + 5 平台`ON DUPLICATE KEY UPDATE` 只更新 name/sort/remark
2026-09-14 | FIX | **全量加密fullBody multipart 上传必被拒**生产才暴露dev 永不复现`encrypt.fullBody=true` 时中间件对所有非豁免请求一律 `json.Unmarshal` body multipart 是二进制流必然失败 上传被判定为未加密请求拒绝前端对 `FormData` 本就跳过加密**两端语义不一致**修法`internal/middleware/encrypt.go` 新增 `isMultipartRequest` 豁免与前端 `request.ts` `config.data instanceof FormData` 分支对齐**不影响安全** 上传接口同样挂在 `/api/service/admin` 分组下仍受 JWT + RBACPOST+path 精确匹配双重保护

View File

@ -0,0 +1,95 @@
# 变更日志 2026-09-14
## 请求
通知模块新增通知历史记录表格界面有分页有查询记录每次通知的渠道类型分组详细内容等
前端项目 `E:\Project\admin.xpcool.com` 一起实现并联调
## 变更
### 后端 通知历史记录能力扩展
- `api/notice/notice.go` 通知历史记录契约扩展
- `NoticeLogItem` 11 字段扩到 27 字段新增 `batchId`/`ruleName`/`eventName`/`noticeType`/`typeName`/
`group`/`groupName`/`channelName`/`userName`/`status`/`statusName`/`retryCount`/`durationMs`/`source`/`remark`
- `NoticeLogListReq` 新增 `keyword`/`noticeType`/`group`/`userId`/`batchId`/`status`/`orderBy`/`orderDir`
`NoticeLogListRes` 新增 `stats` 统计概览
- 新增端点`POST /notice/log/detail`详情`/notice/log/delete`批量删除
`/notice/log/clear`按保留天数/时间范围清空`/notice/log/options`字典选项
- `internal/model/dto/notice_meta.go`**新增** 通知字典渠道bark/pushplus/webhook/email/internal
事件7 类型job/security/recruit/test/manual/system分组auto_job/server/recruitment/system
的编码常量 + 中文名映射 + 选项下发 + 事件/类型/分组三级推导函数
- `internal/model/dto/notice.go` `NoticeLogVO`/`NoticeLogFilter` 同步扩展并新增 `Normalize()`
归一化分页/排序`Status` 与兼容字段 `Result` 的优先级合并
- `internal/model/entity/notice.go``internal/model/do/notice.go` `NoticeLog` 新增
`batch_id`/`status`/`retry_count`/`duration_ms`/`source`/`remark` 六个字段
- `internal/service/notice/notice.go`
- `LogList`重写为多维筛选关键字/类型/分组/事件/渠道/接收人/批次/状态/时间范围/排序+ 统计概览
列表与统计共用同一套 `logQuery` 条件保证口径一致批量补齐规则名与接收人避免 N+1
- 新增 `LogDetail`/`LogDelete`/`LogClear`
- `Send` 引入 `batchId`同一次业务触发的多条投递共用`deliver`/`Test` 记录耗时来源状态备注
- 写库统一改用 `do.NoticeLog` / `do.NoticeRule` / `do.NoticeChannel`原先用 `map[string]interface{}`
不符合 AGENTS.md数据库操作必须用 DO 对象
- `internal/controller/notice/notice.go` 新增 `LogDetail`/`LogDelete`/`LogClear`/`MetaOptions`
并抽出 `toLogItem`/`toMetaItems` dto 契约转换
- `manifest/sql/017_notice_log_enhance.sql`**新增** `notice_log` 6 + 3 个索引
按旧字段 `result` 回填 `status`菜单 982 改名通知历史新增 5 type=2 接口权限
`notice:log:list/detail/delete/clear/options`超管角色绑定
### 前端 `E:\Project\admin.xpcool.com`
- `apps/web-tdesign/src/api/notice.ts` 通知历史记录 API 客户端类型与查询参数扩展
新增 `getNoticeLogDetail` / `deleteNoticeLog` / `clearNoticeLog` / `getNoticeMetaOptions`
以及 `NOTICE_STATUS_OPTIONS` / `noticeStatusTheme` 等展示辅助
- `apps/web-tdesign/src/views/system/notice/log/index.vue` **重写**为完整通知历史页面
- 顶部 4 张统计卡总数/成功/失败/成功率随筛选条件同步刷新
- 筛选区 9 个条件时间范围关键字分组类型事件渠道接收人状态排序
- vxe 表格多选列 + 通知时间/分组/类型/事件/渠道/接收人/标题/详细内容/结果/耗时/错误 + 行操作
- 详情抽屉基本信息 + 标题/内容/目标/错误分区展示均支持一键复制
- 批量删除清空历史可选保留最近 N 按权限码控制按钮显隐
### 联调
- 本地库执行 `017_notice_log_enhance.sql``notice_log` 17 3 个新索引
菜单 `982 / 9821-9825` 就位14 条历史失败记录 `status` 正确回填为 2
- 后端 5 个新接口 + 原有 list 全部通过 RBAC 权限映射返回 `code=0`
- 真实投递一条测试通知验证 `batchId` / `durationMs` / `source` / `userName` / `target` / `remark`
六项新字段正确落库与回显统计概览 `1/0/1` 与筛选条件联动正确
- 经前端 Vite 代理20100 10100完整跑通 登录 字典选项 分页列表 详情
- 前端 `vue-tsc` 类型检查`oxfmt` 格式检查`oxlint` 全部 0 错误
- **联调中修复 1 个缺陷**`LogDetail` 对不存在的 ID 会把 `sql.ErrNoRows` 包装上抛
gf 归为内部错误 `code=50` 并回显 SQL 细节改为与项目既有写法一致
忽略 Scan 错误 `Id==0` 判定现返回 `code=10001`通知记录不存在
## 决策与理由
- **类型/分组不落库 `event_type` 派生**二者与事件是一对多的确定映射落库会引入双写不一致
改为只存事件编码展示与筛选在服务层推导`NoticeEventToType` / `NoticeTypeToGroup`
- **`status` 与旧的 `result` 并存**`result` 是已有数据在用的字段1成功/0失败直接改语义会破坏历史数据
新增 `status`0待发送/1成功/2失败表达三态查询时用 `(status=1 OR (status=0 AND result=1))` 兼容历史行
- **字典选项由后端 `/notice/log/options` 统一下发**前端原先把事件类型硬编码在页面里
`NOTICE_EVENT_OPTIONS` 只有 4 且与 `notice_rule.event_type` 种子语义有偏差
新增事件就要改两处改为后端单一事实源前端只做渲染
- **清空提供保留最近 N 而非纯全清**历史记录是排查问题的依据全清不可逆
默认保留 30 `keepDays=0` 才真正全清属于对误操作的兜底
- **列表与统计共用 `logQuery`**避免"列表按分组筛选、统计按全量"这类口径不一致
前端统计卡与表格永远对同一批数据
- **迁移脚本中 `status` 默认值取 0 而非 1**`ALTER TABLE ADD COLUMN ... DEFAULT 1` 会把**所有历史行**
一并置为 1含失败记录造成"历史失败记录显示成功"改为默认 0 后按 `result` 回填语义正确且可重跑
- **提示文案与枚举中文名收口到 `dto` **中文名映射只在 `notice_meta.go` 定义一次
API服务前端展示三处复用避免同义词漂移
## 待办与风险
- **`internal/service/recruitment/noise_test.go` 0 字节空文件**未跟踪会让
`go build ./...` `go test ./...` 直接报 `expected 'package', found 'EOF'`
本次联调为编译通过曾临时移走**已原样还原**需删除该文件或补上包声明
- 工作区另有他人未提交改动`internal/service/recruitment/bark.go` 修改
`live_diag_test.go` `testdata/rszk.html` 删除前端 `api/wallpaper.ts`
`views/wallpaper/*` 处于暂存状态均非本次范围
- 手动测试发送在目标凭据为空时会直接返回参数错误**不写历史记录**沿用原有行为
若希望失败尝试也可追溯需在 `Test` 的提前返回分支补记日志
- 历史记录只增不减长期需要定期清理策略目前靠页面清空手动处理
后续可考虑挂到 `auto_job` 定时任务上
- 本地联调环境Node 22.22.2 + corepack `pnpm@11.16.0`后端 10100前端 dev 20100
前端 `vite.config.ts` 已把 `/api/service` 代理到 `http://localhost:10100`

View File

@ -3,6 +3,7 @@
> 模块路径`api/recruitment/``internal/controller/recruitment/``internal/service/recruitment/`
> 独立数据库`recruitment`与主库 `service` 隔离
> 文档定位本次抓取健壮性专项设计供评审后实施
> **实施状态阶段一已完成2026-09-14**实施中的关键调整见文末实施结果
---
@ -270,3 +271,80 @@ ALTER TABLE `crawl_source`
3. **日期置空的影响**P0-2 改为未识别则置空依赖 `publish_date` 的统计会短期波动属预期此前是错误数据
4. **自动禁用需谨慎**阈值过低会因偶发网络抖动误禁建议结合连续失败中间成功即归零并推送告警以便人工复核
5. **表变更走迁移脚本**禁止手工改库生成代码若后续跑 `gf gen dao`需注意本模块 entity **手写**勿被覆盖
---
## 实施结果2026-09-14
阶段一全部完成实施过程中基于**真实站点数据**校准方案有两处重要调整
这两点也是本模块最容易踩的坑特此记录
### 7.1 变更清单
| 文件 | 类型 | 说明 |
|---|---|---|
| `internal/service/recruitment/keywords.go` | 新增 | 词表与评分规则含排除域名 |
| `internal/service/recruitment/source_config.go` | 新增 | `crawl_source.config` 解析与默认值兜底 |
| `internal/service/recruitment/keywords_test.go` | 新增 | 16 个评分用例 + 标题清洗 + 图片锚点回归 |
| `internal/service/recruitment/crawler.go` | 修改 | 评分制过滤日期分段抽取限速自动禁用图片锚点处理调试日志清理 |
| `internal/service/recruitment/bark.go` | 修改 | 告警推送英文日志中文化占位符过滤 |
| `internal/service/recruitment/recruitment.go` | 修改 | `Sources()` 改读冗余列 JOIN |
| `internal/model/entity/recruitment.go` | 修改 | `CrawlSource` 4 个冗余列 |
| `internal/model/do/recruitment.go` | 修改 | 同上 |
| `manifest/sql/recruitment/004_crawl_source_status.sql` | 新增 | 幂等迁移存储过程判列存在+ 启用源 config 初始化 |
### 7.2 关键调整一评分以URL 结尾形态为准而非路径关键词
**设计原方案**是按 URL 路径段`/zfxxgk``/rszk` 加权实测发现此方案**根本性错误**
```
栏目页 /zfxxgk/fdzdgklm/zfxxgkrsxx/rszk/ / 结尾
正文页 /zfxxgk/fdzdgklm/zfxxgkrsxx/rszk/202609/t20260908_xxx.html .html 结尾
```
两者**共享同一段栏目路径**仅靠路径段无法区分按路径段减分反而会把真实公告一并误杀
实测第一版即因此把 19 条真实公告全部过滤只剩 1
**最终方案**只按结尾形态判定
- `.html`/`.shtml`/`.htm` 等结尾 **+3**正文
- `/` 结尾 **-5**栏目目录必为列表页而非正文
该判定经 16 个真实锚点用例验证公告正文与栏目页/导航页全部分类正确
### 7.3 关键调整二图片型锚点会以文件名污染标题
实测发现 `依申请公开` 未被过滤根因是
```html
<a href=".../ysqgk/index.html"><img src="ysqgk3.png" title="ysqgk3.png" /></a>
```
`stripTags` `<img>` 替换为空白后`title` 属性回退逻辑取到了 `title="ysqgk3.png"`
于是锚点文本变成 `ysqgk3.png`**绕过了所有中文排除词**再叠加 `.html` +3 分通过筛选
**处理**新增 `isImageFileName` 判定图片型锚点优先取 `alt`取不到则整体丢弃
### 7.4 实测数据对比源1贵阳市人社局·人事招考
| 指标 | 修复前 | 修复后 |
|---|---|---|
| 增量模式抓取 | 8 6 个导航页噪声 | 2 全为真实公告 |
| 全量回溯force | 8 | **21 条真实公告** |
| 发布日期识别率 | 大量误取取页面头部日期 | **100%**0 条为空 |
| 标题污染 | `\n\t\t\t` 与图片文件名 | 已清洗 |
> 增量模式抓取条数少是正确的30 天窗口内源站确实只发布了 2
### 7.5 验证方式
- **单元测试**`go test ./internal/service/recruitment/` 全绿4 个测试函数 / 16 个评分用例
- **端到端**启动服务 管理端登录 触发抓取 核对入库数据与冗余列
- **失败链路**构造坏源`http://127.0.0.1:1` 5 次失败时 `enabled` 自动置 0
日志输出数据源 N 连续失败 5 已自动禁用告警推送按预期优雅降级本地未配 Bark 仅记警告不影响抓取
### 7.6 遗留事项
- 阶段二推送分时阶段三源扩展阶段四运维增强未做实施计划
- `push_time` 字段仍未生效`recruit-push-daily` 固定 08:00属阶段二范围
- `crawl_source.config` `MaxPages`翻页 `DatePatterns`自定义日期正则已实现解析但暂无源使用
待具体站点需要时配置即可

View File

@ -327,12 +327,11 @@ func toLogVO(v entity.NoticeLog, ruleName, userName string) dto.NoticeLogVO {
}
// LogDetail 单条历史记录详情。
// 注意:记录不存在时 Scan 会返回 sql.ErrNoRows不能直接 Wrap 上抛
// (那样会被 gf 归为内部错误 50 并暴露 SQL 细节),统一由 Id==0 判定为参数错误。
func (s *notice) LogDetail(ctx context.Context, id uint64) (*dto.NoticeLogVO, error) {
var v entity.NoticeLog
if err := dao.NoticeLog.Ctx(ctx).Where("id", id).Scan(&v); err != nil {
return nil, gerror.Wrap(err, "查询通知历史详情失败")
}
if v.Id == 0 {
if err := dao.NoticeLog.Ctx(ctx).Where("id", id).Scan(&v); err != nil || v.Id == 0 {
return nil, response.Error(consts.CodeInvalidParam, "通知记录不存在")
}
ruleName := ""

View File

@ -29,7 +29,21 @@ type barkPayload struct {
// defaultDeviceKey 读取环境变量注入的兜底设备密钥BARK_DEVICE_KEY -> bark.deviceKey
// 当数据库没有任何订阅记录时,推送与测试推送回退到该密钥,做到「填了 env 即可推送」。
func defaultDeviceKey(ctx context.Context) string {
return g.Cfg().MustGet(ctx, "bark.deviceKey", "").String()
return resolveCfgValue(g.Cfg().MustGet(ctx, "bark.deviceKey", "").String())
}
// resolveCfgValue 过滤未解析的 ${...} 占位符。
//
// 背景gf v2.10.2 不再自动替换配置中的 ${ENV},需由 cmd.injectEnv 显式注入。
// 若某环境变量未设置(如本地未配 BARK_BASE_URL配置项会保留字面量
// "${BARK_BASE_URL}",直接使用会产生 "invalid character { in host name" 之类的
// 误导性错误。此处统一把未解析占位符视为空值,使错误信息更准确。
func resolveCfgValue(v string) string {
v = strings.TrimSpace(v)
if strings.HasPrefix(v, "${") && strings.HasSuffix(v, "}") {
return ""
}
return v
}
// defaultSubscription 构造一个使用兜底密钥的默认订阅(全地区/全分类/仅新公告)。
@ -46,7 +60,7 @@ func defaultSubscription(ctx context.Context) *entity.PushSubscription {
// barkPush 向指定设备密钥发送 Bark 推送自建服务POST {baseUrl}/push
func barkPush(ctx context.Context, deviceKey, title, body string) (bool, string, error) {
baseURL := g.Cfg().MustGet(ctx, "bark.baseUrl", "").String()
baseURL := resolveCfgValue(g.Cfg().MustGet(ctx, "bark.baseUrl", "").String())
if baseURL == "" {
return false, "未配置 bark.baseUrl", fmt.Errorf("bark baseUrl empty")
}

View File

@ -12,8 +12,10 @@ SET @sql := IF(@has_col=0, 'ALTER TABLE notice_log ADD COLUMN batch_id VARCHAR(3
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- status0待发送 1成功 2失败result 为旧字段 1成功 0失败保留兼容
-- 默认 0待发送新增列时历史行先统一为 0再由下方历史数据回填 result 推导
-- 若默认写 1 会导致历史失败记录被误标为成功
SET @has_col := (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='notice_log' AND COLUMN_NAME='status');
SET @sql := IF(@has_col=0, 'ALTER TABLE notice_log ADD COLUMN status TINYINT NOT NULL DEFAULT 1 COMMENT ''0待发送 1成功 2失败'' AFTER body', 'SELECT 1');
SET @sql := IF(@has_col=0, 'ALTER TABLE notice_log ADD COLUMN status TINYINT NOT NULL DEFAULT 0 COMMENT ''0待发送 1成功 2失败'' AFTER body', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- retry_count已重试次数
@ -49,10 +51,10 @@ SET @has_idx := (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_
SET @sql := IF(@has_idx=0, 'ALTER TABLE notice_log ADD INDEX idx_batch (batch_id)', 'SELECT 1');
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- 历史数据回填result=1 status=1result=0 且有错误 status=2
UPDATE notice_log SET status=1 WHERE status=0 AND result=1;
UPDATE notice_log SET status=2 WHERE status=0 AND result=0 AND error <> '';
UPDATE notice_log SET source='test' WHERE event_type='test' AND source='auto';
-- 历史数据回填按旧字段 result 推导 status幂等可重复执行
UPDATE notice_log SET status=1 WHERE result=1 AND status<>1;
UPDATE notice_log SET status=2 WHERE result=0 AND status<>2;
UPDATE notice_log SET source='test' WHERE event_type='test' AND source<>'test';
-- ============ 2) 菜单通知历史记录替换原通知日志并调整排序 ============
-- 982 原为通知日志此处改名为通知历史以贴合需求保留 id 以延续角色绑定