refactor: 后端仓库层收口(A6:公众号助手技能包)
wechat_official_account 包原先直接拿 store.DB 读写 27 处,是仓库里仅剩的最大
一块。收口后该包不再引用 store:任务/运行记录/交付物/专员四类模型用的是 A4 就
已有的仓库,只有两个包内私有模型(文章状态、热点缓存)新增了仓库。
顺带修掉一处三态混淆:原 loadOfficialAccountArticle 把「读取出错」和「还没有」
合并成同一条路(`if err == nil { return article }`,其余一律往下建新行),于是
读路径(GET workflow / 导出 / 执行步骤)上一次读取失败会去盲目 INSERT,撞上
task_id 唯一索引才失败 —— 那是运气不是设计。改成 FindByTaskID 明确返回三种
结果,读失败就报错,不再新建。
行为变化(有意,不是等价重构):
- 热点缓存读失败、产物/运行记录查询失败,原先 web.Fail 成错误,现在按仓库层
bool 风格降级成「空」:调用方分别走不依赖热点的兜底选题路径 / 返回空时间线。
- 因此 loadFreshOfficialAccountHotspots 及其调用链上的 error 出口恒为 nil。
留着是为了不再往上传导签名改动,已在注释里写明这是行为变化。
验证(脚手架用完即删):
- 真实请求:db 副本 + httptest 走完整路由,覆盖建任务/读工作流/改配置重置/
归属隔离(非归属人 404)/唯一键下不重复建文章行/重置后旧 run+artifact 清空。
- 仓库层直测:热点过滤(0 分、过期、跨业务域)、排序三级兜底、LIMIT 40、
同 URL 不同源必须是两行、FindByTaskID 三态、DeleteByTask 只清本任务。
- 14 个变异全部被杀死(去掉各过滤条件、ORDER BY、LIMIT、冲突键里的 source_key、
空切片保护;三态退化成两态;DeleteByTask 丢掉 task_id;归属校验改成不过滤)。
其中 M6 第一轮活下来,暴露出 R3a 用同一个 source_key、单 url 也能匹配 ——
是断言写空了,补了「同 URL 不同源必须两行」才真正测住。
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"gorm.io/gorm"
|
||||
|
||||
oaamodel "eai_agentplatform/backend/internal/specialists/packages/wechat_official_account/model"
|
||||
)
|
||||
|
||||
// OfficialAccountArticle 公众号文章状态仓库(一个任务一篇,task_id 上有唯一索引)。
|
||||
//
|
||||
// 模型定义在专员技能包内(specialists/packages/wechat_official_account/model),
|
||||
// 原先这个包自己拿 store.DB 读写,收口后统一走这里。
|
||||
type OfficialAccountArticleRepo struct{ *QueryBuilder }
|
||||
|
||||
// FindByTaskID 按任务取文章状态。三种结果,**调用方必须分清**:
|
||||
// - row != nil 找到了
|
||||
// - row == nil, err == nil 该任务还没有文章行,调用方应新建一篇
|
||||
// - err != nil 读取出错,调用方应报错,**不要**当成「还没有」
|
||||
//
|
||||
// 第三种是本仓库要返回 error 而不是 bool 的原因。原实现是
|
||||
// `if err == nil { return article }` 其余一律往下走新建 —— 也就是说读取
|
||||
// 真出错时也会去建一篇新的。task_id 上有唯一索引,这种误建多半会撞唯一键
|
||||
// 而失败,但那是运气不是设计。(同 UserXAppCenterRepo.FindByUser)
|
||||
func (r OfficialAccountArticleRepo) FindByTaskID(taskID uint) (*oaamodel.OfficialAccountArticle, error) {
|
||||
var row oaamodel.OfficialAccountArticle
|
||||
err := r.Inner().Where("task_id = ?", taskID).First(&row).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &row, nil
|
||||
}
|
||||
|
||||
// Insert 新建文章状态。
|
||||
func (r OfficialAccountArticleRepo) Insert(a *oaamodel.OfficialAccountArticle) bool {
|
||||
return r.QueryBuilder.Insert(a)
|
||||
}
|
||||
|
||||
// Update 保存文章状态(改标题/提纲/正文/配图等一律走这里)。
|
||||
func (r OfficialAccountArticleRepo) Update(a *oaamodel.OfficialAccountArticle) bool {
|
||||
return r.Save(a)
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
oahmodel "eai_agentplatform/backend/internal/specialists/packages/wechat_official_account/model"
|
||||
)
|
||||
|
||||
// OfficialAccountHotspot 公众号热点缓存仓库。
|
||||
//
|
||||
// 表里存的是抓取回来的 RSS/网页条目及其领域评分,按 (source_key, url)
|
||||
// 唯一,重复抓到同一条要就地更新而不是堆新行。
|
||||
type OfficialAccountHotspotRepo struct{ *QueryBuilder }
|
||||
|
||||
// hotspotListLimit 单次取回的热点条数上限。
|
||||
//
|
||||
// 调用方(loadFreshOfficialAccountHotspots)取回后还要按关键词在内存里再滤一遍,
|
||||
// 所以这里多取一些留余量 —— 这个 40 是原实现写死的值,收口时原样保留。
|
||||
const hotspotListLimit = 40
|
||||
|
||||
// ListFresh 取某业务域内、抓取时间在 cutoff 之后的、领域分高的热点。
|
||||
//
|
||||
// 排序按「领域分 → 发布时间 → id」降序:领域分是主序(越对口越靠前),
|
||||
// 发布时间用来在同分里挑新鲜的,最后的 id 是稳定排序的兜底 ——
|
||||
// 少了它,同分同时间的记录每次查询顺序可能不一样,分页/去重就会飘。
|
||||
//
|
||||
// 返回空切片表示没有符合条件的热点,这不是错误。
|
||||
//
|
||||
// **注意:读取真出错时同样返回空**,调用方分不出「没有」和「读失败」。
|
||||
// 收口前这里返回 error,读失败会让整个热点步骤失败;现在降级成「本轮没有热点」,
|
||||
// 调用方会走到不依赖热点的兜底选题路径。这个降级是有意接受的 ——
|
||||
// 仓库层统一是 bool / 裸返回的风格(同 TaskRunRepo.ListByTask),
|
||||
// 不为一次缓存读再造一个 error 出口;但这是**行为变化**,不是等价重构。
|
||||
func (r OfficialAccountHotspotRepo) ListFresh(domainKey string, cutoff time.Time) []oahmodel.OfficialAccountHotspot {
|
||||
var items []oahmodel.OfficialAccountHotspot
|
||||
if r.Type(&items).
|
||||
Where("business_domain = ? AND fetched_at >= ? AND domain_score > 0", domainKey, cutoff).
|
||||
Order("domain_score DESC, published_at DESC, id DESC").
|
||||
Limit(hotspotListLimit).
|
||||
Find(&items) {
|
||||
return items
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpsertAll 批量写入热点,按 (source_key, url) 冲突就地更新。
|
||||
//
|
||||
// 这里返回 error 而不是 bool,是因为调用方要把 err.Error() 拼进给用户看的
|
||||
// 抓取日志(「热点入库失败:…」)——bool 装不下这句话,丢掉它用户就只看到
|
||||
// 「本轮未写入新热点」,无从知道是库的问题还是源站的问题。
|
||||
func (r OfficialAccountHotspotRepo) UpsertAll(rows []oahmodel.OfficialAccountHotspot) error {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
return r.Inner().Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "source_key"}, {Name: "url"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{
|
||||
"title", "summary", "published_at", "raw_score", "domain_score",
|
||||
"heat_label", "tags_json", "fetched_at", "updated_at",
|
||||
}),
|
||||
}).Create(&rows).Error
|
||||
}
|
||||
@@ -26,6 +26,13 @@ func (r TaskArtifactRepo) ListByTask(taskID uint) []model.TaskArtifact {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByTask 删掉某任务名下的全部交付物,条数一并返回。
|
||||
// 与 TaskRunRepo.DeleteByTask 配对使用(重置任务配置时两样一起清)。
|
||||
func (r TaskArtifactRepo) DeleteByTask(taskID uint) (int64, bool) {
|
||||
res := r.Inner().Where("task_id = ?", taskID).Delete(&model.TaskArtifact{})
|
||||
return res.RowsAffected, res.Error == nil
|
||||
}
|
||||
|
||||
// Insert 新建交付物。
|
||||
func (r TaskArtifactRepo) Insert(a *model.TaskArtifact) bool {
|
||||
return r.QueryBuilder.Insert(a)
|
||||
|
||||
@@ -28,6 +28,16 @@ func (r TaskRunRepo) ListByTask(taskID uint) []model.TaskRun {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByTask 删掉某任务名下的全部运行记录,条数一并返回。
|
||||
//
|
||||
// 用于「重置任务配置」:旧配置下的运行记录留着会把时间线和产物对错,
|
||||
// 所以整批清掉重来。这里不做级联,产物由 TaskArtifactRepo.DeleteByTask 单独清 ——
|
||||
// 调用方要的是「两样都清干净」,出错了才分得清是哪一边没清成。
|
||||
func (r TaskRunRepo) DeleteByTask(taskID uint) (int64, bool) {
|
||||
res := r.Inner().Where("task_id = ?", taskID).Delete(&model.TaskRun{})
|
||||
return res.RowsAffected, res.Error == nil
|
||||
}
|
||||
|
||||
// Insert 追加一条运行记录。
|
||||
func (r TaskRunRepo) Insert(run *model.TaskRun) bool {
|
||||
return r.QueryBuilder.Insert(run)
|
||||
|
||||
Reference in New Issue
Block a user