From 1875e7dd4c96528db042334953031d429f2b0266 Mon Sep 17 00:00:00 2001 From: eaiadmin Date: Sat, 19 Sep 2026 08:55:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E5=90=8E=E7=AB=AF=E4=BB=93?= =?UTF-8?q?=E5=BA=93=E5=B1=82=E6=94=B6=E5=8F=A3=EF=BC=88A6=EF=BC=9A?= =?UTF-8?q?=E5=85=AC=E4=BC=97=E5=8F=B7=E5=8A=A9=E6=89=8B=E6=8A=80=E8=83=BD?= =?UTF-8?q?=E5=8C=85=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../repository/official_account_article.go | 46 ++++++++++++ .../repository/official_account_hotspot.go | 64 ++++++++++++++++ .../internal/repository/task_artifact.go | 7 ++ .../internal/repository/task_run.go | 10 +++ .../api/official_account_article_service.go | 26 ++++--- .../api/official_account_generated_image.go | 10 +-- .../api/official_account_hotspot_service.go | 23 ++---- .../api/official_account_workflow.go | 74 +++++++++---------- .../wechat_official_account/api/repos.go | 29 ++++++++ 9 files changed, 217 insertions(+), 72 deletions(-) create mode 100644 eai_agentplatform/backend-go/internal/repository/official_account_article.go create mode 100644 eai_agentplatform/backend-go/internal/repository/official_account_hotspot.go create mode 100644 eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/repos.go diff --git a/eai_agentplatform/backend-go/internal/repository/official_account_article.go b/eai_agentplatform/backend-go/internal/repository/official_account_article.go new file mode 100644 index 0000000..86e96e9 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/repository/official_account_article.go @@ -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) +} diff --git a/eai_agentplatform/backend-go/internal/repository/official_account_hotspot.go b/eai_agentplatform/backend-go/internal/repository/official_account_hotspot.go new file mode 100644 index 0000000..90cb48a --- /dev/null +++ b/eai_agentplatform/backend-go/internal/repository/official_account_hotspot.go @@ -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 +} diff --git a/eai_agentplatform/backend-go/internal/repository/task_artifact.go b/eai_agentplatform/backend-go/internal/repository/task_artifact.go index 82886ab..11579e5 100644 --- a/eai_agentplatform/backend-go/internal/repository/task_artifact.go +++ b/eai_agentplatform/backend-go/internal/repository/task_artifact.go @@ -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) diff --git a/eai_agentplatform/backend-go/internal/repository/task_run.go b/eai_agentplatform/backend-go/internal/repository/task_run.go index bf037da..ff1f887 100644 --- a/eai_agentplatform/backend-go/internal/repository/task_run.go +++ b/eai_agentplatform/backend-go/internal/repository/task_run.go @@ -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) diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_article_service.go b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_article_service.go index 3634e2c..069473d 100644 --- a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_article_service.go +++ b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_article_service.go @@ -2,26 +2,28 @@ package wechatofficialaccountapi import ( "encoding/json" + "errors" "fmt" "sort" "strings" "eai_agentplatform/backend/internal/model" officialaccountmodel "eai_agentplatform/backend/internal/specialists/packages/wechat_official_account/model" - "eai_agentplatform/backend/internal/store" ) -func loadOfficialAccountArticle(taskID uint) (*officialaccountmodel.OfficialAccountArticle, error) { - var article officialaccountmodel.OfficialAccountArticle - if err := store.DB.Where("task_id = ?", taskID).First(&article).Error; err != nil { +// ensureOfficialAccountArticle 取该任务的文章状态,没有就按当前工作流新建一篇。 +// +// 三种情况要分开走,不能把「读取出错」和「还没有」合并: +// - 读到了 → 直接用 +// - 确实没有 → 新建(正常路径,每个任务第一次进来都会走这里) +// - 读取出错 → 原样返回错误,**不要**去建新的 —— task_id 上有唯一索引, +// 误建多半会撞唯一键而失败,但那是运气不是设计 +func ensureOfficialAccountArticle(task model.TaskRecord, workflow officialAccountWorkflowState) (*officialaccountmodel.OfficialAccountArticle, error) { + article, err := oaArticleRepo.FindByTaskID(task.ID) + if err != nil { return nil, err } - return &article, nil -} - -func ensureOfficialAccountArticle(task model.TaskRecord, workflow officialAccountWorkflowState) (*officialaccountmodel.OfficialAccountArticle, error) { - article, err := loadOfficialAccountArticle(task.ID) - if err == nil { + if article != nil { return article, nil } row := &officialaccountmodel.OfficialAccountArticle{ @@ -39,8 +41,8 @@ func ensureOfficialAccountArticle(task model.TaskRecord, workflow officialAccoun MaxContentImages: normalizeOfficialAccountMaxContentImages(workflow.Form.MaxContentImages), Status: "draft", } - if err := store.DB.Create(row).Error; err != nil { - return nil, err + if !oaArticleRepo.Insert(row) { + return nil, errors.New("创建公众号文章状态失败") } return row, nil } diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_generated_image.go b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_generated_image.go index f3d33d1..d637d5d 100644 --- a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_generated_image.go +++ b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_generated_image.go @@ -3,6 +3,7 @@ package wechatofficialaccountapi import ( "crypto/sha1" "encoding/base64" + "errors" "fmt" "os" "path/filepath" @@ -13,7 +14,6 @@ import ( "eai_agentplatform/backend/internal/config" "eai_agentplatform/backend/internal/model" officialaccountmodel "eai_agentplatform/backend/internal/specialists/packages/wechat_official_account/model" - "eai_agentplatform/backend/internal/store" "eai_agentplatform/backend/internal/web" ) @@ -44,11 +44,11 @@ func normalizeOfficialAccountMediaForResponse(task *model.TaskRecord, workflow * syncOfficialAccountArticleFromWorkflow(article, *workflow) task.ContextJSON = marshalOfficialAccountWorkflow(*workflow) - if err := store.DB.Save(article).Error; err != nil { - return err + if !oaArticleRepo.Update(article) { + return errors.New("保存公众号文章状态失败") } - if err := store.DB.Save(task).Error; err != nil { - return err + if !taskRecordRepo.Update(task) { + return errors.New("保存公众号任务失败") } return nil } diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_hotspot_service.go b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_hotspot_service.go index ab0b58a..12b94c2 100644 --- a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_hotspot_service.go +++ b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_hotspot_service.go @@ -14,11 +14,8 @@ import ( "strings" "time" - "golang.org/x/net/html/charset" - "gorm.io/gorm/clause" - officialaccountmodel "eai_agentplatform/backend/internal/specialists/packages/wechat_official_account/model" - "eai_agentplatform/backend/internal/store" + "golang.org/x/net/html/charset" ) type officialAccountFetchedHotspot struct { @@ -115,16 +112,9 @@ func ensureOfficialAccountHotspots(form officialAccountForm, force bool) ([]offi } func loadFreshOfficialAccountHotspots(domainKey, keyword string, maxAge time.Duration) ([]officialaccountmodel.OfficialAccountHotspot, error) { - var items []officialaccountmodel.OfficialAccountHotspot - cutoff := time.Now().Add(-maxAge) - err := store.DB. - Where("business_domain = ? AND fetched_at >= ? AND domain_score > 0", domainKey, cutoff). - Order("domain_score DESC, published_at DESC, id DESC"). - Limit(40). - Find(&items).Error - if err != nil { - return nil, err - } + // keyword 不在 SQL 里过滤,是取回来在内存里再滤一遍(见下)—— + // 库里那 40 条是「该业务域里分数最高的」,关键词只用来在其中挑相关的。 + items := oaHotspotRepo.ListFresh(domainKey, time.Now().Add(-maxAge)) filtered := filterOfficialAccountHotspotsByKeyword(items, keyword) if len(filtered) > 0 { return filtered, nil @@ -294,10 +284,7 @@ func upsertOfficialAccountHotspots(domainKey, keyword string, items []officialAc if len(upserts) == 0 { return 0, logs } - if err := store.DB.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(&upserts).Error; err != nil { + if err := oaHotspotRepo.UpsertAll(upserts); err != nil { logs = append(logs, "热点入库失败:"+err.Error()) return 0, logs } diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_workflow.go b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_workflow.go index ff94ab3..24db875 100644 --- a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_workflow.go +++ b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/official_account_workflow.go @@ -13,9 +13,7 @@ import ( "eai_agentplatform/backend/internal/config" "eai_agentplatform/backend/internal/middleware" "eai_agentplatform/backend/internal/model" - specialistmodel "eai_agentplatform/backend/internal/specialists/model" officialaccountmodel "eai_agentplatform/backend/internal/specialists/packages/wechat_official_account/model" - "eai_agentplatform/backend/internal/store" "eai_agentplatform/backend/internal/web" ) @@ -184,8 +182,10 @@ func CreateOfficialAccountTask(c *gin.Context) { return } - var specialist specialistmodel.Specialist - if err := store.DB.Where("key = ?", officialAccountSpecialistKey).First(&specialist).Error; err != nil { + // 按 key 取专员,不限 state —— 与 SpecialistRepo.GetByKey 的口径一致: + // 任务挂在已下线的专员上,历史记录仍要解释得通。 + specialist, found := specialistRepo.GetByKey(officialAccountSpecialistKey) + if !found { web.Fail(c, web.NewNotFoundError("公众号助手尚未配置")) return } @@ -206,12 +206,13 @@ func CreateOfficialAccountTask(c *gin.Context) { if user != nil { task.CreatedBy = &user.ID } - if err := store.DB.Create(&task).Error; err != nil { + if !taskRecordRepo.Insert(&task) { web.Fail(c, web.NewBadRequest("创建公众号任务失败")) return } if _, err := ensureOfficialAccountArticle(task, workflow); err != nil { - _ = store.DB.Delete(&task).Error + // 文章状态没建起来,这个任务就是个空壳,回滚掉别留在库里 + taskRecordRepo.Delete(&task) web.Fail(c, web.NewBadRequest("初始化公众号文章状态失败")) return } @@ -319,11 +320,11 @@ func UpdateOfficialAccountTask(c *gin.Context) { task.CurrentResult = "" task.CurrentRunID = nil task.LastTriggeredAt = nil - if err := store.DB.Where("task_id = ?", task.ID).Delete(&model.TaskRun{}).Error; err != nil { + if _, ok := taskRunRepo.DeleteByTask(task.ID); !ok { web.Fail(c, web.NewBadRequest("清理旧运行记录失败")) return } - if err := store.DB.Where("task_id = ?", task.ID).Delete(&model.TaskArtifact{}).Error; err != nil { + if _, ok := taskArtifactRepo.DeleteByTask(task.ID); !ok { web.Fail(c, web.NewBadRequest("清理旧产物失败")) return } @@ -335,11 +336,11 @@ func UpdateOfficialAccountTask(c *gin.Context) { } task.ContextJSON = marshalOfficialAccountWorkflow(workflow) - if err := store.DB.Save(&task).Error; err != nil { + if !taskRecordRepo.Update(&task) { web.Fail(c, web.NewBadRequest("更新任务配置失败")) return } - if err := store.DB.Save(article).Error; err != nil { + if !oaArticleRepo.Update(article) { web.Fail(c, web.NewBadRequest("保存公众号文章状态失败")) return } @@ -375,10 +376,10 @@ func UpdateOfficialAccountTask(c *gin.Context) { StartedAt: now, FinishedAt: &now, } - if err := store.DB.Create(&run).Error; err == nil { + if taskRunRepo.Insert(&run) { task.CurrentRunID = &run.ID task.LastTriggeredAt = &now - _ = store.DB.Save(&task).Error + taskRecordRepo.Update(&task) } } @@ -431,7 +432,7 @@ func ExecuteOfficialAccountWorkflowStep(c *gin.Context) { if appErr != nil { setOfficialAccountStepError(&workflow, stepKey, appErr.Message, now) task.ContextJSON = marshalOfficialAccountWorkflow(workflow) - _ = store.DB.Save(&task).Error + taskRecordRepo.Update(&task) web.Fail(c, appErr) return } @@ -473,7 +474,7 @@ func ExecuteOfficialAccountWorkflowStep(c *gin.Context) { StartedAt: now, FinishedAt: &now, } - if err := store.DB.Create(&run).Error; err != nil { + if !taskRunRepo.Insert(&run) { web.Fail(c, web.NewBadRequest("保存步骤运行记录失败")) return } @@ -493,17 +494,17 @@ func ExecuteOfficialAccountWorkflowStep(c *gin.Context) { SourceRefsJSON: string(sourceRefsJSON), CreatedByRunID: &run.ID, } - if err := store.DB.Create(artifact).Error; err != nil { + if !taskArtifactRepo.Insert(artifact) { web.Fail(c, web.NewBadRequest("保存步骤产物失败")) return } } - if err := store.DB.Save(article).Error; err != nil { + if !oaArticleRepo.Update(article) { web.Fail(c, web.NewBadRequest("保存公众号文章状态失败")) return } - if err := store.DB.Save(&task).Error; err != nil { + if !taskRecordRepo.Update(&task) { web.Fail(c, web.NewBadRequest("更新公众号任务失败")) return } @@ -603,7 +604,7 @@ func RegenerateOfficialAccountImage(c *gin.Context) { StartedAt: now, FinishedAt: &now, } - if err := store.DB.Create(&run).Error; err != nil { + if !taskRunRepo.Insert(&run) { web.Fail(c, web.NewBadRequest("保存单张图片重生成记录失败")) return } @@ -620,16 +621,16 @@ func RegenerateOfficialAccountImage(c *gin.Context) { SourceRefsJSON: "[]", CreatedByRunID: &run.ID, } - if err := store.DB.Create(artifact).Error; err != nil { + if !taskArtifactRepo.Insert(artifact) { web.Fail(c, web.NewBadRequest("保存单张图片结果失败")) return } - if err := store.DB.Save(article).Error; err != nil { + if !oaArticleRepo.Update(article) { web.Fail(c, web.NewBadRequest("保存公众号文章状态失败")) return } - if err := store.DB.Save(&task).Error; err != nil { + if !taskRecordRepo.Update(&task) { web.Fail(c, web.NewBadRequest("更新公众号任务失败")) return } @@ -652,17 +653,11 @@ func respondOfficialAccountWorkflow(c *gin.Context, task model.TaskRecord) { return } - var artifacts []model.TaskArtifact - if err := store.DB.Where("task_id = ?", task.ID).Order("created_at DESC, id DESC").Find(&artifacts).Error; err != nil { - web.Fail(c, web.NewBadRequest("查询公众号产物失败")) - return - } - - var runs []model.TaskRun - if err := store.DB.Where("task_id = ?", task.ID).Order("started_at DESC, id DESC").Find(&runs).Error; err != nil { - web.Fail(c, web.NewBadRequest("查询公众号运行记录失败")) - return - } + // 收口前这两条查询出错会 web.Fail 成 400;ListByTask 是 bool 风格、失败返回 nil, + // 所以现在读失败表现为「产物/运行记录为空」+200。这是行为变化,不是等价重构, + // 换的是与仓库层其余部分一致(TaskArtifactRepo.ListByTask 自 A4 起就是这个口径)。 + artifacts := taskArtifactRepo.ListByTask(task.ID) + runs := taskRunRepo.ListByTask(task.ID) artifacts = compactOfficialAccountArtifactsForResponse(artifacts) runs = compactOfficialAccountRunsForResponse(runs) @@ -1562,13 +1557,18 @@ func loadAccessibleTask(c *gin.Context, user *model.User) (model.TaskRecord, boo return model.TaskRecord{}, false } - query := store.DB.Model(&model.TaskRecord{}).Where("id = ?", id) + // 归属口径:管理员不设限,其余人只能看挂在自己名下的(FullName 或 Username 命中)。 + // 「owners 为空 = 什么都看不到」这条由 GetByIDForOwners 保证,不会反过来变成看全部。 + var ( + task model.TaskRecord + found bool + ) if user != nil && user.Role != "admin" { - query = query.Where("owner IN ?", []string{user.FullName, user.Username}) + task, found = taskRecordRepo.GetByIDForOwners(id, []string{user.FullName, user.Username}) + } else { + task, found = taskRecordRepo.GetByID(id) } - - var task model.TaskRecord - if err := query.First(&task).Error; err != nil { + if !found { web.Fail(c, web.NewNotFoundError("事项不存在")) return model.TaskRecord{}, false } diff --git a/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/repos.go b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/repos.go new file mode 100644 index 0000000..370a298 --- /dev/null +++ b/eai_agentplatform/backend-go/internal/specialists/packages/wechat_official_account/api/repos.go @@ -0,0 +1,29 @@ +package wechatofficialaccountapi + +import "eai_agentplatform/backend/internal/repository" + +// 本包用到的仓库,集中声明(便于测试时覆写)。 +// +// 收口前这个包直接拿 store.DB 读写了 27 处,是本仓库仅剩的最大一块; +// 其中任务/运行记录/交付物/专员四类模型早就有仓库了(A4 收的), +// 这里多数不是新增能力,是改用既有能力。 +// +// 声明集中在同一个文件,是因为它们被本包 4 个文件共用 —— +// 两份变量持有同一个仓库时,测试里覆写一份、另一份照旧,行为会静默分叉。 +var ( + specialistRepo repository.SpecialistRepo + taskRecordRepo repository.TaskRecordRepo + taskRunRepo repository.TaskRunRepo + taskArtifactRepo repository.TaskArtifactRepo + oaArticleRepo repository.OfficialAccountArticleRepo + oaHotspotRepo repository.OfficialAccountHotspotRepo +) + +func init() { + specialistRepo = repository.SpecialistRepo{} + taskRecordRepo = repository.TaskRecordRepo{} + taskRunRepo = repository.TaskRunRepo{} + taskArtifactRepo = repository.TaskArtifactRepo{} + oaArticleRepo = repository.OfficialAccountArticleRepo{} + oaHotspotRepo = repository.OfficialAccountHotspotRepo{} +}