From a06ae5c8cf0358c8f238f43aa70cea430aab6668 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Sat, 11 Apr 2026 23:55:25 +0800 Subject: [PATCH] 1 --- cmd/server/wire_gen.go | 2 +- cookies/0d86b848uu2183uu4a08/xhs.json | 2 +- go.mod | 1 + go.sum | 2 + html/page.html | 2 +- internal/biz/public.go | 46 ++- internal/data/model/token.gen.go | 1 + internal/entitys/publish.go | 15 + internal/entitys/request.go | 3 +- internal/manager/publish_manager.go | 53 ++- internal/publisher/base.go | 12 + internal/publisher/sphsp.go | 535 +++++--------------------- internal/server/router/app.go | 1 + internal/service/app.go | 58 +-- internal/service/login.go | 17 + internal/service/publish.go | 9 +- pkg/func.go | 11 + tmpl/dataTemp/queryTempl.go | 15 +- 18 files changed, 293 insertions(+), 492 deletions(-) diff --git a/cmd/server/wire_gen.go b/cmd/server/wire_gen.go index 033d04f..ab5a6ea 100644 --- a/cmd/server/wire_gen.go +++ b/cmd/server/wire_gen.go @@ -28,7 +28,7 @@ func InitializeApp(configConfig *config.Config, allLogger log.AllLogger) (*serve publishImpl := impl.NewPublishImpl(db) loginRelationImpl := impl.NewLoginRelationImpl(db) publishBiz := biz.NewPublishBiz(configConfig, publishImpl, userImpl, platImpl, tokenImpl, loginRelationImpl) - appService := service.NewAppService(configConfig, tokenImpl, userImpl, platImpl, publishBiz) + appService := service.NewAppService(configConfig, tokenImpl, userImpl, platImpl, publishBiz, loginRelationImpl) loginService := service.NewLoginService(configConfig, publishBiz) publishService := service.NewPublishService(configConfig, publishBiz, db) appModule := router.NewAppModule(configConfig, appService, loginService, publishService) diff --git a/cookies/0d86b848uu2183uu4a08/xhs.json b/cookies/0d86b848uu2183uu4a08/xhs.json index 77af278..c55db4d 100644 --- a/cookies/0d86b848uu2183uu4a08/xhs.json +++ b/cookies/0d86b848uu2183uu4a08/xhs.json @@ -1 +1 @@ -[{"name":"gid","value":"yjfKDJiJ0J7SyjfKDJi8Dqf884ufUMYf3MlIVqfYTYl3D3q8Svu0VW888yyYW4Y8JD0j8Yd2","domain":".xiaohongshu.com","path":"/","expires":1810372429.581067,"size":75,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"ets","value":"1775619738206","domain":".xiaohongshu.com","path":"/","expires":1778211738.206388,"size":16,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"a1","value":"19d6b2f0b04zdps8dison8k8oibcyzaju3ji7d03d30000118748","domain":".xiaohongshu.com","path":"/","expires":1807155738,"size":54,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customer-sso-sid","value":"68c5176262287866115194944uxxdffhcemiwvwt","domain":".xiaohongshu.com","path":"/","expires":1776224554.091467,"size":56,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"x-user-id-creator.xiaohongshu.com","value":"65d74a4c0000000005032a98","domain":".xiaohongshu.com","path":"/","expires":1810179755.091531,"size":57,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"access-token-creator.xiaohongshu.com","value":"customer.creator.AT-68c517626228786611519495vggizcwmifuvnaaw","domain":".xiaohongshu.com","path":"/","expires":1778211754.091569,"size":96,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy_creator_session_id","value":"nWC5PzTFCCSJsLYiRFLFORIWUoUf5c4Egr3v","domain":".xiaohongshu.com","path":"/","expires":1778211755.091586,"size":61,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"webId","value":"f840cc8b10e0a01b0bdb839482af19d1","domain":".xiaohongshu.com","path":"/","expires":1807155738,"size":37,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"xsecappid","value":"ugc","domain":".xiaohongshu.com","path":"/","expires":1807379925,"size":12,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"loadts","value":"1775843925008","domain":".xiaohongshu.com","path":"/","expires":1807379925,"size":19,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy.creator.beaker.session.id","value":"1775619757404082990054","domain":".xiaohongshu.com","path":"/","expires":1778211755.091605,"size":54,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"websectiga","value":"2a3d3ea002e7d92b5c9743590ebd24010cf3710ff3af8029153751e41a6af4a3","domain":".xiaohongshu.com","path":"/","expires":1776103125,"size":74,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"sec_poison_id","value":"680df974-cc97-44bd-9c18-b9efaafb66cc","domain":".xiaohongshu.com","path":"/","expires":1775844530,"size":49,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"acw_tc","value":"0a00076417758431730566823e6256f5349e529f992a6ba74dc0835439d93a","domain":"creator.xiaohongshu.com","path":"/","expires":1775844709.365943,"size":68,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customerClientId","value":"231145420384063","domain":".xiaohongshu.com","path":"/","expires":1810179755.091552,"size":31,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443}] \ No newline at end of file +[{"name":"webId","value":"f840cc8b10e0a01b0bdb839482af19d1","domain":".xiaohongshu.com","path":"/","expires":1807155738,"size":37,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"gid","value":"yjfKDJiJ0J7SyjfKDJi8Dqf884ufUMYf3MlIVqfYTYl3D3q8Svu0VW888yyYW4Y8JD0j8Yd2","domain":".xiaohongshu.com","path":"/","expires":1810372429.581067,"size":75,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"x-user-id-creator.xiaohongshu.com","value":"65d74a4c0000000005032a98","domain":".xiaohongshu.com","path":"/","expires":1810179755.091531,"size":57,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy_creator_session_id","value":"nWC5PzTFCCSJsLYiRFLFORIWUoUf5c4Egr3v","domain":".xiaohongshu.com","path":"/","expires":1778211755.091586,"size":61,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"acw_tc","value":"0a00075317759153837631021e2d5e31fa378daf9c77578ea082108e84e3cf","domain":"creator.xiaohongshu.com","path":"/","expires":1775916920.153024,"size":68,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"loadts","value":"1775915120761","domain":".xiaohongshu.com","path":"/","expires":1807451120,"size":19,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"websectiga","value":"cffd9dcea65962b05ab048ac76962acee933d26157113bb213105a116241fa6c","domain":".xiaohongshu.com","path":"/","expires":1776174321,"size":74,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"xsecappid","value":"ugc","domain":".xiaohongshu.com","path":"/","expires":1807451120,"size":12,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customerClientId","value":"231145420384063","domain":".xiaohongshu.com","path":"/","expires":1810179755.091552,"size":31,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"customer-sso-sid","value":"68c5176262287866115194944uxxdffhcemiwvwt","domain":".xiaohongshu.com","path":"/","expires":1776224554.091467,"size":56,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"ets","value":"1775619738206","domain":".xiaohongshu.com","path":"/","expires":1778211738.206388,"size":16,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"a1","value":"19d6b2f0b04zdps8dison8k8oibcyzaju3ji7d03d30000118748","domain":".xiaohongshu.com","path":"/","expires":1807155738,"size":54,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"sec_poison_id","value":"d361d271-b86e-4565-8105-4f11e54cd97c","domain":".xiaohongshu.com","path":"/","expires":1775915726,"size":49,"httpOnly":false,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"access-token-creator.xiaohongshu.com","value":"customer.creator.AT-68c517626228786611519495vggizcwmifuvnaaw","domain":".xiaohongshu.com","path":"/","expires":1778211754.091569,"size":96,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443},{"name":"galaxy.creator.beaker.session.id","value":"1775619757404082990054","domain":".xiaohongshu.com","path":"/","expires":1778211755.091605,"size":54,"httpOnly":true,"secure":false,"session":false,"priority":"Medium","sameParty":false,"sourceScheme":"Secure","sourcePort":443}] \ No newline at end of file diff --git a/go.mod b/go.mod index b3a9bb5..c817443 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( require ( filippo.io/edwards25519 v1.1.0 // indirect + gitea.cdlsxd.cn/self-tools/l_request v1.0.8 // indirect github.com/andybalholm/brotli v1.1.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect diff --git a/go.sum b/go.sum index b56aa68..710f68a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= +gitea.cdlsxd.cn/self-tools/l_request v1.0.8 h1:FaKRql9mCVcSoaGqPeBOAruZ52slzRngQ6VRTYKNSsA= +gitea.cdlsxd.cn/self-tools/l_request v1.0.8/go.mod h1:Qf4hVXm2Eu5vOvwXk8D7U0q/aekMCkZ4Fg9wnRKlasQ= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a h1:lSA0F4e9A2NcQSqGqTOXqu2aRi/XEQxDCBwM8yJtE6s= gitea.com/xorm/sqlfiddle v0.0.0-20180821085327-62ce714f951a/go.mod h1:EXuID2Zs0pAQhH8yz+DNjUbjppKQzKFAn28TMYPB6IU= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= diff --git a/html/page.html b/html/page.html index 47c0bf4..b606a50 100644 --- a/html/page.html +++ b/html/page.html @@ -70,7 +70,7 @@ ">
  • 关注者数据
  • 视频数据
  • 图文数据
  • 直播数据
  • 带货数据
  • 设置
  • + ">
  • 人员设置
  • 肖像授权管理
  • 服务菜单
  • diff --git a/internal/biz/public.go b/internal/biz/public.go index f472a86..c45165a 100644 --- a/internal/biz/public.go +++ b/internal/biz/public.go @@ -2,6 +2,11 @@ package biz import ( "context" + "fmt" + "geo/internal/entitys" + "geo/pkg" + "gitea.cdlsxd.cn/self-tools/l_request" + "github.com/gofiber/fiber/v2/log" "time" "geo/internal/config" @@ -101,5 +106,44 @@ func (b *PublishBiz) UpdateLoginStatus(ctx context.Context, userIndex, platIndex cond := builder.NewCond(). And(builder.Eq{"user_index": userIndex}). And(builder.Eq{"plat_index": platIndex}) - return b.loginRelationImpl.UpdateByCond(ctx, &cond, &model.User{Status: loginStatus}) + return b.loginRelationImpl.UpdateByCond(ctx, &cond, &model.LoginRelation{LoginStatus: loginStatus}) +} + +func (b *PublishBiz) Notify(data *entitys.NotifyData) (err error) { + if data.NotifyUrl != "" { + if data.TokenId == 0 { + return + } + cond := builder.NewCond(). + And(builder.Eq{"id": data.TokenId}). + And(builder.Eq{"status": 1}) + + tokenInfo := &model.Token{} + err = b.tokenImpl.GetOneBySearchStructWithOutCtx(&cond, tokenInfo) + if err != nil { + return + } + if tokenInfo.ID == 0 { + return fmt.Errorf("未找到用户信息") + } + data.NotifyUrl = tokenInfo.Notifyurl + } + if data.NotifyUrl == "" { + return + } + sendData, _ := pkg.StructToMap(data) + req := l_request.Request{ + Method: "POST", + Url: data.NotifyUrl, + Json: sendData, + Headers: map[string]string{ + "Content-Type": "application/json", + }, + } + res, err := req.Send() + log.Infof("回调完成:%v", res) + if err != nil { + return fmt.Errorf("请求失败,err: %v", err) + } + return nil } diff --git a/internal/data/model/token.gen.go b/internal/data/model/token.gen.go index f2f1719..950c7ab 100644 --- a/internal/data/model/token.gen.go +++ b/internal/data/model/token.gen.go @@ -15,6 +15,7 @@ type Token struct { ID int32 `gorm:"column:id;primaryKey" json:"id"` Name string `gorm:"column:name" json:"name"` Secret string `gorm:"column:secret;not null" json:"secret"` + Notifyurl string `gorm:"column:notify_url;not null" json:"notify_url"` AccessToken string `gorm:"column:access_token;not null" json:"access_token"` UserLimit int32 `gorm:"column:user_limit;primaryKey" json:"user_limit"` ExpireTime time.Time `gorm:"column:expire_time;not null" json:"expire_time"` diff --git a/internal/entitys/publish.go b/internal/entitys/publish.go index 341d22d..30489d5 100644 --- a/internal/entitys/publish.go +++ b/internal/entitys/publish.go @@ -25,3 +25,18 @@ type PublishTaskDetail struct { LoginedUrl string `db:"logined_url"` Desc string `db:"desc"` } + +type NotifyData struct { + NotifyUrl string + TokenId int + Type NotifyType + Status int + Msg string +} + +type NotifyType int8 + +const ( + NotifyTypePublish NotifyType = 1 + NotifyTypeToken NotifyType = 1 +) diff --git a/internal/entitys/request.go b/internal/entitys/request.go index 1270140..cffdcc8 100644 --- a/internal/entitys/request.go +++ b/internal/entitys/request.go @@ -15,7 +15,8 @@ type ( } DelUserRequest struct { - ID int `json:"id" validate:"required" zh:"用户ID"` + AccessToken string `json:"access_token" validate:"required" zh:"access_token"` + UserIndex string `json:"user_index" validate:"required" zh:"用户索引"` } GetAppRequest struct { diff --git a/internal/manager/publish_manager.go b/internal/manager/publish_manager.go index f1cc5b1..1014beb 100644 --- a/internal/manager/publish_manager.go +++ b/internal/manager/publish_manager.go @@ -3,6 +3,7 @@ package manager import ( "context" "fmt" + "geo/internal/biz" "geo/internal/config" "geo/internal/entitys" "geo/internal/publisher" @@ -41,7 +42,8 @@ type PublishManager struct { stopCh chan struct{} db *utils.Db stopOnce sync.Once - + stopOnceMu sync.Mutex + publicBiz *biz.PublishBiz // 并发控制 workerNum int // 并发worker数量 workerWg sync.WaitGroup // 等待所有worker退出 @@ -53,7 +55,7 @@ var ( ) // GetPublishManager 获取单例实例 -func GetPublishManager(config *config.Config, db *utils.Db) (*PublishManager, error) { +func GetPublishManager(config *config.Config, db *utils.Db, publicBiz *biz.PublishBiz) (*PublishManager, error) { if config == nil || db == nil { return nil, fmt.Errorf("config和db参数不能为空") } @@ -64,6 +66,7 @@ func GetPublishManager(config *config.Config, db *utils.Db) (*PublishManager, er Conf: config, stopCh: make(chan struct{}), db: db, + publicBiz: publicBiz, } }) return publishManager, nil @@ -80,13 +83,11 @@ func (pm *PublishManager) getTaskLogger(requestID string) (*log.Logger, *os.File logsDir = "./logs" } - dateDir := time.Now().Format("2006-01-02") - taskLogDir := filepath.Join(logsDir, "tasks", dateDir) - if err := os.MkdirAll(taskLogDir, 0755); err != nil { + if err := os.MkdirAll(logsDir, 0755); err != nil { return nil, nil, fmt.Errorf("创建日志目录失败: %v", err) } - logPath := filepath.Join(taskLogDir, fmt.Sprintf("%s.log", requestID)) + logPath := filepath.Join(logsDir, fmt.Sprintf("%s.log", requestID)) logFile, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644) if err != nil { @@ -125,6 +126,11 @@ func (pm *PublishManager) Start(tokenID int, workerNum int) bool { pm.workerNum = workerNum pm.stopCh = make(chan struct{}) + // ✅ 修改:重置 sync.Once(关键修复) + pm.stopOnceMu.Lock() + pm.stopOnce = sync.Once{} + pm.stopOnceMu.Unlock() + for i := 0; i < workerNum; i++ { pm.workerWg.Add(1) go pm.workerLoop(i) @@ -134,7 +140,6 @@ func (pm *PublishManager) Start(tokenID int, workerNum int) bool { return true } -// Stop 停止自动发布 func (pm *PublishManager) Stop() bool { pm.mu.Lock() if !pm.AutoStatus { @@ -144,9 +149,12 @@ func (pm *PublishManager) Stop() bool { pm.AutoStatus = false pm.mu.Unlock() + // ✅ 修改:使用互斥锁保护 Once 调用 + pm.stopOnceMu.Lock() pm.stopOnce.Do(func() { close(pm.stopCh) }) + pm.stopOnceMu.Unlock() done := make(chan struct{}) go func() { @@ -322,7 +330,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys. if r := recover(); r != nil { errMsg := fmt.Sprintf("任务执行发生panic: %v", r) taskLogger.Printf("❌ CRITICAL: %s", errMsg) - pm.updatePublishStatus(publishData.ID, StatusFailed, errMsg) + pm.updatePublishStatus(publishData, StatusFailed, errMsg) } }() @@ -330,7 +338,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys. params, sourceUrl := pm.extractTaskParams(publishData, taskLogger) if params == nil { - pm.updatePublishStatus(publishData.ID, StatusFailed, "提取任务参数失败") + pm.updatePublishStatus(publishData, StatusFailed, "提取任务参数失败") return &SingleResult{Success: false, Message: "提取任务参数失败", RequestId: publishData.RequestID} } params.Headless = headless @@ -339,7 +347,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys. if publisherClass == nil { errMsg := fmt.Sprintf("不支持的平台: %s", params.PlatIndex) taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg) - pm.updatePublishStatus(publishData.ID, StatusFailed, errMsg) + pm.updatePublishStatus(publishData, StatusFailed, errMsg) return &SingleResult{Success: false, Message: errMsg, RequestId: publishData.RequestID} } @@ -347,7 +355,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys. if err != nil { errMsg := fmt.Sprintf("准备文件失败: %v", err) taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg) - pm.updatePublishStatus(publishData.ID, StatusFailed, errMsg) + pm.updatePublishStatus(publishData, StatusFailed, errMsg) return &SingleResult{Success: false, Message: errMsg, RequestId: publishData.RequestID} } defer pm.cleanupFiles(docPath, imgPath, taskLogger, publishData.RequestID) @@ -356,7 +364,7 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys. if err != nil { errMsg := fmt.Sprintf("提取文档内容失败: %v", err) taskLogger.Printf("[任务 %s] ❌ %s", publishData.RequestID, errMsg) - pm.updatePublishStatus(publishData.ID, StatusFailed, errMsg) + pm.updatePublishStatus(publishData, StatusFailed, errMsg) return &SingleResult{Success: false, Message: errMsg, RequestId: publishData.RequestID} } params.ImagePath = imgPath @@ -368,10 +376,10 @@ func (pm *PublishManager) processTask(ctx context.Context, publishData *entitys. if success { taskLogger.Printf("[任务 %s] ✅ 发布成功: %s", publishData.RequestID, message) - pm.updatePublishStatus(publishData.ID, StatusSuccess, message) + pm.updatePublishStatus(publishData, StatusSuccess, message) } else { taskLogger.Printf("[任务 %s] ❌ 发布失败: %s", publishData.RequestID, message) - pm.updatePublishStatus(publishData.ID, StatusFailed, message) + pm.updatePublishStatus(publishData, StatusFailed, message) } taskLogger.Printf(strings.Repeat("=", 80)) @@ -408,6 +416,7 @@ func (pm *PublishManager) GetTaskByRequestID(requestID string, tokenId int) (*en SELECT p.id, p.request_id, + p.token_id, p.plat_index, p.title, p.tag, @@ -524,19 +533,25 @@ func (pm *PublishManager) extractContent(docPath string, publisherClass *publish } // updatePublishStatus 更新发布状态 -func (pm *PublishManager) updatePublishStatus(id int, status int, message string) error { - if id == 0 { +func (pm *PublishManager) updatePublishStatus(publishData *entitys.PublishTaskDetail, status int, message string) error { + if publishData.ID == 0 { return fmt.Errorf("id不能为空") } var err error if message != "" { - _, err = pm.db.Execute("UPDATE publish SET status = ?, msg = ? WHERE id=?", status, message, id) + _, err = pm.db.Execute("UPDATE publish SET status = ?, msg = ? WHERE id=?", status, message, publishData.ID) } else { - _, err = pm.db.Execute("UPDATE publish SET status = ? WHERE id=?", status, id) + _, err = pm.db.Execute("UPDATE publish SET status = ? WHERE id=?", status, publishData.ID) } if err != nil { - log.Printf("更新发布状态失败: id=%s, status=%d, error=%v", id, status, err) + log.Printf("更新发布状态失败: id=%s, status=%d, error=%v", publishData.ID, status, err) } + _ = pm.publicBiz.Notify(&entitys.NotifyData{ + TokenId: publishData.TokenID, + Type: entitys.NotifyTypePublish, + Status: status, + Msg: message, + }) return err } diff --git a/internal/publisher/base.go b/internal/publisher/base.go index 19d1f72..1e6cd06 100644 --- a/internal/publisher/base.go +++ b/internal/publisher/base.go @@ -264,6 +264,18 @@ func (b *BasePublisher) JSClick(element *rod.Element) error { return err } +func (b *BasePublisher) JSClick2(element *rod.Element) error { + if element == nil { + b.Logger.Printf("element is nil") + return fmt.Errorf("element is nil") + } + err := element.Click(proto.InputMouseButtonLeft, 1) + if err != nil { + b.Logger.Printf("click fail:" + err.Error()) + } + return err +} + func (b *BasePublisher) ScrollToElement(element *rod.Element) error { _, err := element.Evaluate(&rod.EvalOptions{ JS: `el => el.scrollIntoView({block: 'center', behavior: 'smooth'})`, diff --git a/internal/publisher/sphsp.go b/internal/publisher/sphsp.go index 831513b..794a9ee 100644 --- a/internal/publisher/sphsp.go +++ b/internal/publisher/sphsp.go @@ -2,12 +2,10 @@ package publisher import ( "context" - "encoding/base64" "fmt" "geo/internal/config" + "github.com/go-rod/rod" "log" - "os" - "path/filepath" "strings" "time" ) @@ -15,6 +13,7 @@ import ( // ShipinhaoVideoPublisher 视频号视频发布器 type ShipinhaoVideoPublisher struct { *BasePublisher + iframe *rod.Page } // NewShipinhaoVideoPublisher 创建视频号发布器 @@ -24,6 +23,67 @@ func NewShipinhaoVideoPublisher(ctx context.Context, task *TaskParams, config *c } } +func (p *ShipinhaoVideoPublisher) CheckLogin() (bool, string) { + p.LogInfo("检查登录状态...") + + if err := p.SetupDriver(); err != nil { + return false, fmt.Sprintf("浏览器启动失败: %v", err) + } + defer p.Close() + + p.Page.MustNavigate(p.LoginedURL) + p.Sleep(3) + p.WaitForPageReady(5) + + if p.CheckLoginStatus() { + p.SaveCookies() + return true, "已登录" + } + return false, "未登录" +} + +func (p *ShipinhaoVideoPublisher) CheckLoginStatus() bool { + currentURL := p.GetCurrentURL() + if strings.Contains(currentURL, "login") || strings.Contains(currentURL, "passport") { + return false + } + if strings.Contains(currentURL, "channels.weixin.qq.com") { + return true + } + return false +} + +func (p *ShipinhaoVideoPublisher) WaitLogin() (bool, string) { + p.LogInfo("开始等待登录...") + + if err := p.SetupDriver(); err != nil { + return false, fmt.Sprintf("浏览器启动失败: %v", err) + } + defer p.Close() + + p.Page.MustNavigate(p.LoginURL) + p.Sleep(3) + + if p.CheckLoginStatus() { + p.SaveCookies() + return true, "already_logged_in" + } + + p.LogInfo("请扫描二维码登录...") + + startTime := time.Now() + timeout := 120 + for time.Since(startTime) < time.Duration(timeout)*time.Second { + if p.CheckLoginStatus() { + p.SaveCookies() + return true, "login_success" + } + p.SleepMs(2000) + } + + return false, "登录超时" +} + // PublishNote 发布视频主流程 func (p *ShipinhaoVideoPublisher) PublishNote() (bool, string) { p.StartNote() @@ -32,7 +92,7 @@ func (p *ShipinhaoVideoPublisher) PublishNote() (bool, string) { if err := p.SetupDriver(); err != nil { return false, fmt.Sprintf("浏览器启动失败: %v", err) } - defer p.Close() + defer p.Browser.MustClose() // 3. 加载 cookies 并检查登录状态 if err := p.LoadCookies(); err == nil { p.Page.Navigate(p.EditorURL) @@ -58,10 +118,6 @@ func (p *ShipinhaoVideoPublisher) PublishNote() (bool, string) { fn func() error }{ {"CDP拦截上传", p.uploadViaCDPIntercept}, - //{"拖拽事件上传", p.uploadViaDragEvent}, - //{"网络拦截上传", p.uploadViaNetworkIntercept}, - //{"React事件上传", p.uploadViaReactEvent}, - //{"文件输入框上传", p.uploadViaFileInput}, } for _, method := range methods { @@ -97,7 +153,7 @@ func (p *ShipinhaoVideoPublisher) PublishNote() (bool, string) { } // 10. 等待发布完成 - p.Sleep(10) + currentURL := p.GetCurrentURL() if strings.Contains(currentURL, "https://channels.weixin.qq.com/platform/post/list") { p.LogInfo("发布完成") @@ -125,7 +181,7 @@ func (p *ShipinhaoVideoPublisher) ensureInEditorIframe() { if err == nil && exist { frame, err := frameElement.Frame() if err == nil && frame != nil { - p.Page = frame + p.iframe = frame p.LogInfo(fmt.Sprintf("已切换到 iframe: %s", selector)) return } @@ -135,43 +191,14 @@ func (p *ShipinhaoVideoPublisher) ensureInEditorIframe() { p.LogInfo("未找到 iframe,使用主页面") } -// uploadViaFileInput 通过文件输入框上传(最基础的方法) -func (p *ShipinhaoVideoPublisher) uploadViaFileInput(filePath string) error { - p.LogInfo("使用文件输入框上传...") - - // 确保在正确的 iframe 中 - p.ensureInEditorIframe() - - // 查找文件输入框(使用非阻塞方式) - fileInputs, err := p.Page.Elements("input[type='file']") - if err != nil { - return fmt.Errorf("查找文件输入框失败: %v", err) - } - - if len(fileInputs) == 0 { - return fmt.Errorf("未找到文件输入框") - } - - err = fileInputs[0].SetFiles([]string{filePath}) - if err != nil { - return fmt.Errorf("设置文件失败: %v", err) - } - - p.LogInfo(fmt.Sprintf("文件已选择: %s", filepath.Base(filePath))) - return nil -} - // uploadViaCDPIntercept 使用 CDP 拦截并注入文件上传 func (p *ShipinhaoVideoPublisher) uploadViaCDPIntercept() error { p.LogInfo("使用 CDP 协议拦截文件上传...") - // 确保在正确的 iframe 中 - p.ensureInEditorIframe() - p.LogInfo("已切换到 iframe,开始查找文件输入框") p.Sleep(1) // 先在当前 iframe 中查找文件输入框 - fileInputs, err := p.Page.Elements("input[type='file'][accept*='video']") + fileInputs, err := p.iframe.Elements("input[type='file'][accept*='video']") if err != nil { p.LogInfo(fmt.Sprintf("查找文件输入框失败: %v", err)) } @@ -185,303 +212,6 @@ func (p *ShipinhaoVideoPublisher) uploadViaCDPIntercept() error { } p.LogInfo(fmt.Sprintf("直接设置文件失败: %v", err)) } - //filePath := p.SourcePath - //// 读取文件为 Base64 - //fileData, err := os.ReadFile(filePath) - //if err != nil { - // return fmt.Errorf("读取文件失败: %v", err) - //} - //base64Data := base64.StdEncoding.EncodeToString(fileData) - //fileName := filepath.Base(filePath) - //// 如果直接设置失败,使用 JS 注入方式 - //p.LogInfo("使用 JS 注入方式上传文件") - // - //// 注入 JS 代码模拟文件上传 - //script := fmt.Sprintf(` - // (function() { - // // 创建 File 对象 - // var byteCharacters = atob('%s'); - // var byteNumbers = new Array(byteCharacters.length); - // for (var i = 0; i < byteCharacters.length; i++) { - // byteNumbers[i] = byteCharacters.charCodeAt(i); - // } - // var byteArray = new Uint8Array(byteNumbers); - // var blob = new Blob([byteArray], {type: 'video/mp4'}); - // var file = new File([blob], '%s', {type: 'video/mp4'}); - // - // // 查找隐藏的文件输入框 - // var fileInput = document.querySelector('input[type="file"][accept*="video"]'); - // if (!fileInput) { - // fileInput = document.querySelector('input[type="file"]'); - // } - // - // if (!fileInput) { - // // 如果还是找不到,创建一个 - // fileInput = document.createElement('input'); - // fileInput.type = 'file'; - // fileInput.accept = 'video/mp4,video/x-m4v,video/*'; - // fileInput.multiple = true; - // fileInput.style.display = 'none'; - // document.body.appendChild(fileInput); - // } - // - // // 临时显示文件输入框(如果需要) - // var originalDisplay = fileInput.style.display; - // fileInput.style.display = 'block'; - // - // // 使用 DataTransfer 设置文件 - // var dataTransfer = new DataTransfer(); - // dataTransfer.items.add(file); - // fileInput.files = dataTransfer.files; - // - // // 恢复原始显示状态 - // fileInput.style.display = originalDisplay; - // - // // 触发 change 事件 - // var changeEvent = new Event('change', { bubbles: true }); - // fileInput.dispatchEvent(changeEvent); - // - // // 触发 input 事件 - // var inputEvent = new Event('input', { bubbles: true }); - // fileInput.dispatchEvent(inputEvent); - // - // // 查找上传区域并触发点击 - // var uploadArea = document.querySelector('.upload-wrap, .video-plugin-title-action, [class*="upload"]'); - // if (uploadArea) { - // uploadArea.click(); - // } - // - // // 模拟拖拽事件 - // var dropZones = document.querySelectorAll('.upload-wrap, [class*="upload"], [class*="drop"]'); - // for (var i = 0; i < dropZones.length; i++) { - // var zone = dropZones[i]; - // if (zone.offsetParent !== null) { - // var dragOverEvent = new DragEvent('dragover', { - // bubbles: true, - // cancelable: true, - // dataTransfer: dataTransfer - // }); - // zone.dispatchEvent(dragOverEvent); - // - // var dropEvent = new DragEvent('drop', { - // bubbles: true, - // cancelable: true, - // dataTransfer: dataTransfer - // }); - // zone.dispatchEvent(dropEvent); - // break; - // } - // } - // - // return {success: true, fileName: '%s', hasFileInput: !!fileInput}; - // })(); - //`, base64Data, fileName, fileName) - // - //result, err := p.Page.Eval(script) - //if err != nil { - // return fmt.Errorf("CDP 注入失败: %v", err) - //} - // - //p.LogInfo(fmt.Sprintf("CDP 注入完成,结果: %v", result)) - return nil -} - -// uploadViaDragEvent 通过模拟拖拽事件上传 -func (p *ShipinhaoVideoPublisher) uploadViaDragEvent(filePath string) error { - p.LogInfo("模拟拖拽事件上传...") - - // 读取文件为 Base64 - fileData, err := os.ReadFile(filePath) - if err != nil { - return fmt.Errorf("读取文件失败: %v", err) - } - base64Data := base64.StdEncoding.EncodeToString(fileData) - fileName := filepath.Base(filePath) - - // 确保在正确的 iframe 中 - p.ensureInEditorIframe() - p.Sleep(1) - - script := fmt.Sprintf(` - (function() { - // 创建模拟文件 - var byteCharacters = atob('%s'); - var byteNumbers = new Array(byteCharacters.length); - for (var i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); - } - var byteArray = new Uint8Array(byteNumbers); - var blob = new Blob([byteArray], {type: 'video/mp4'}); - var file = new File([blob], '%s', {type: 'video/mp4'}); - - var dataTransfer = new DataTransfer(); - dataTransfer.items.add(file); - - // 查找上传区域 - var dropZones = document.querySelectorAll('[class*="upload"], [class*="drop"], [class*="video"]'); - var targetZone = null; - - for (var i = 0; i < dropZones.length; i++) { - var zone = dropZones[i]; - if (zone.offsetParent !== null && - (zone.innerText.includes('上传') || - zone.innerText.includes('时长') || - zone.className.includes('upload'))) { - targetZone = zone; - break; - } - } - - if (!targetZone) { - targetZone = document.body; - } - - // 模拟拖拽事件 - var dragOverEvent = new DragEvent('dragover', { - bubbles: true, - cancelable: true, - dataTransfer: dataTransfer - }); - targetZone.dispatchEvent(dragOverEvent); - - var dropEvent = new DragEvent('drop', { - bubbles: true, - cancelable: true, - dataTransfer: dataTransfer - }); - targetZone.dispatchEvent(dropEvent); - - return {success: true, message: '拖拽事件已触发'}; - })(); - `, base64Data, fileName) - - _, err = p.Page.Eval(script) - if err != nil { - return fmt.Errorf("拖拽事件上传失败: %v", err) - } - - p.LogInfo("拖拽事件已触发") - return nil -} - -// uploadViaNetworkIntercept 通过拦截网络请求上传 -func (p *ShipinhaoVideoPublisher) uploadViaNetworkIntercept(filePath string) error { - p.LogInfo("尝试网络拦截上传...") - - // 确保在正确的 iframe 中 - p.ensureInEditorIframe() - - // 点击上传区域 - clickScript := ` - var areas = document.querySelectorAll('[class*="upload"]'); - for (var i = 0; i < areas.length; i++) { - if (areas[i].offsetParent !== null) { - areas[i].click(); - return true; - } - } - return false; - ` - _, err := p.Page.Eval(clickScript) - if err != nil { - p.LogInfo(fmt.Sprintf("点击上传区域失败: %v", err)) - } - p.Sleep(1) - - // 使用 CDP 设置文件输入 - fileInputs, err := p.Page.Elements("input[type='file']") - if err != nil || len(fileInputs) == 0 { - return fmt.Errorf("未找到文件输入框") - } - - err = fileInputs[0].SetFiles([]string{filePath}) - if err != nil { - return fmt.Errorf("设置文件失败: %v", err) - } - - p.LogInfo("网络拦截上传完成") - return nil -} - -// uploadViaReactEvent 通过 React 内部事件上传 -func (p *ShipinhaoVideoPublisher) uploadViaReactEvent(filePath string) error { - p.LogInfo("尝试 React 事件上传...") - - // 读取文件为 Base64 - fileData, err := os.ReadFile(filePath) - if err != nil { - return fmt.Errorf("读取文件失败: %v", err) - } - base64Data := base64.StdEncoding.EncodeToString(fileData) - fileName := filepath.Base(filePath) - - // 确保在正确的 iframe 中 - p.ensureInEditorIframe() - - script := fmt.Sprintf(` - (function() { - // 查找所有 DOM 元素 - var allElements = document.querySelectorAll('*'); - var uploadComponent = null; - - for (var i = 0; i < allElements.length; i++) { - var el = allElements[i]; - // 检查是否有 React 内部属性 - if (el._reactRootContainer || - Object.keys(el).some(key => key.startsWith('__react'))) { - // 查找包含上传相关文本的元素 - if (el.innerText && - (el.innerText.includes('上传') || - el.innerText.includes('时长'))) { - uploadComponent = el; - break; - } - } - } - - if (uploadComponent) { - // 创建模拟文件 - var byteCharacters = atob('%s'); - var byteNumbers = new Array(byteCharacters.length); - for (var i = 0; i < byteCharacters.length; i++) { - byteNumbers[i] = byteCharacters.charCodeAt(i); - } - var byteArray = new Uint8Array(byteNumbers); - var blob = new Blob([byteArray], {type: 'video/mp4'}); - var file = new File([blob], '%s', {type: 'video/mp4'}); - - var dataTransfer = new DataTransfer(); - dataTransfer.items.add(file); - - // 触发 change 事件 - var fileInput = document.querySelector('input[type="file"]'); - if (fileInput) { - fileInput.files = dataTransfer.files; - var event = new Event('change', {bubbles: true}); - fileInput.dispatchEvent(event); - } - - // 尝试触发 React 的 onChange - var syntheticEvent = new Event('change', {bubbles: true}); - syntheticEvent.target = {files: dataTransfer.files}; - uploadComponent.dispatchEvent(syntheticEvent); - - return {success: true}; - } - - return {success: false, message: '未找到 React 组件'}; - })(); - `, base64Data, fileName) - - result, err := p.Page.Eval(script) - if err != nil { - return fmt.Errorf("React 事件上传失败: %v", err) - } - - // 检查结果 - if result != nil { - p.LogInfo(fmt.Sprintf("React 事件触发结果: %v", result)) - } return nil } @@ -493,7 +223,7 @@ func (p *ShipinhaoVideoPublisher) waitForUploadComplete(timeout int) (bool, stri for time.Since(startTime).Seconds() < float64(timeout) { // 检查是否还存在上传区域特征 - exists, _, err := p.Page.Has(".form-item.flex-start") + exists, _, err := p.iframe.Has(".form-item.flex-start") if err == nil && exists { p.LogInfo("视频上传成功") p.Sleep(2) @@ -524,116 +254,41 @@ func (p *ShipinhaoVideoPublisher) inputTitleAndDescription() (bool, string) { p.LogInfo(fmt.Sprintf("目标内容: %s", fullContent)) // 确保在正确的 iframe 中 - p.ensureInEditorIframe() - p.Sleep(1) - // 使用 JavaScript 直接设置编辑器内容 - script := ` - function setEditorContent(content) { - // 定位编辑器元素 - var editor = document.querySelector('.post-desc-box .input-editor, .input-editor, [contenteditable="true"]'); - if (!editor) { - console.error('Editor element not found'); - return false; - } - - // 聚焦编辑器 - editor.focus(); - - // 清空现有内容 - editor.innerText = ''; - - // 设置新内容 - editor.innerText = content; - - // 触发所有可能的事件 - var events = ['input', 'change', 'blur', 'focus', 'keyup', 'keydown']; - events.forEach(function(eventType) { - var event = new Event(eventType, { bubbles: true, cancelable: true }); - editor.dispatchEvent(event); - }); - - // 尝试触发 React 的合成事件 - var reactKey = Object.keys(editor).find(function(key) { - return key.startsWith('__reactEventHandlers'); - }); - if (reactKey && editor[reactKey] && editor[reactKey].onChange) { - var syntheticEvent = { target: { value: content, innerText: content }, type: 'change' }; - editor[reactKey].onChange(syntheticEvent); - } - - console.log('Content set successfully, final value:', editor.innerText); - return true; - } - return setEditorContent(arguments[0]); - ` - - result, err := p.Page.Eval(script, fullContent) + exist, input, err := p.iframe.Has("div[data-placeholder*='描述']") if err != nil { - return false, fmt.Sprintf("设置内容失败: %v", err) + return false, "未找到编辑器元素" + } + if !exist { + return false, "未找到编辑器元素" } - if result != nil { - p.LogInfo("通过 JS 成功设置内容") - p.Sleep(1) - return true, "内容输入成功" - } - - return false, "未找到编辑器元素" + input.Input(fullContent) + return true, "标签设置完成" } +// clickPublish 点击发布按钮 + +//func (p *ShipinhaoVideoPublisher) clickPublish() (bool, string) { +// p.LogInfo("点击发布按钮...") +// p.Sleep(5) +// // 直接用 JS 选择器,不经过 rod 元素 +// _, err := p.iframe.Eval(`document.querySelector('div.form-btns button.weui-desktop-btn_primary')?.click()`) +// if err != nil { +// return false, fmt.Sprintf("点击按钮失败: %v", err) +// } +// +// p.LogInfo("成功点击发表按钮") +// return true, "" +//} + // clickPublish 点击发布按钮 func (p *ShipinhaoVideoPublisher) clickPublish() (bool, string) { p.LogInfo("点击发布按钮...") + p.Sleep(5) + // 直接在 iframe 上执行 JS,通过选择器找到并点击 + p.iframe.Eval(`document.querySelector('div.form-btns button.weui-desktop-btn_primary')?.click()`) - // 滚动到底部 - p.Page.Eval(`window.scrollTo(0, document.body.scrollHeight);`) - p.Sleep(1) - - // 确保在正确的 iframe 中 - p.ensureInEditorIframe() - p.Sleep(1) - - // 方法1: 通过文本 "发表" 查找按钮 - script := ` - var buttons = document.querySelectorAll('button'); - for (var i = 0; i < buttons.length; i++) { - var btn = buttons[i]; - var text = btn.innerText || btn.textContent || ''; - if (text.trim() === '发表' && btn.offsetParent !== null) { - btn.click(); - return true; - } - } - return false; - ` - - result, err := p.Page.Eval(script) - if err == nil && result != nil { - p.LogInfo("已点击发表按钮") - return true, "已点击发表" - } - - // 方法2: 通过 CSS 选择器查找 - publishSelectors := []string{ - ".weui-desktop-btn.weui-desktop-btn_primary", - "button.weui-desktop-btn_primary", - ".weui-desktop-btn_wrp button", - "button[class*='primary']", - } - - for _, selector := range publishSelectors { - btn, err := p.Page.Element(selector) - if err == nil && btn != nil { - visible, _ := btn.Visible() - if visible { - p.JSClick(btn) - p.LogInfo(fmt.Sprintf("通过选择器 %s 点击发表按钮", selector)) - return true, "已点击发表" - } - } - } - - p.LogError("所有方法都未找到发表按钮") - return false, "未找到发表按钮" + p.LogInfo("成功点击发表按钮") + return true, "" } diff --git a/internal/server/router/app.go b/internal/server/router/app.go index 03ab349..2604ba7 100644 --- a/internal/server/router/app.go +++ b/internal/server/router/app.go @@ -44,4 +44,5 @@ func (m *AppModule) Register(router fiber.Router) { router.Post("/get_publish_list", vali(m.publishService.GetPublishList, &entitys.GetPublishListRequest{})) router.Post("/login_platform", vali(m.loginService.LoginPlatform, &entitys.LoginPlatformRequest{})) router.Post("/logout_platform", vali(m.loginService.LogoutPlatform, &entitys.LogoutPlatformRequest{})) + router.Get("/logs/:request_id", m.loginService.Log) } diff --git a/internal/service/app.go b/internal/service/app.go index bf0998a..ba546f8 100644 --- a/internal/service/app.go +++ b/internal/service/app.go @@ -17,11 +17,12 @@ import ( ) type AppService struct { - cfg *config.Config - tokenImpl *impl.TokenImpl - userImpl *impl.UserImpl - platImpl *impl.PlatImpl - publishBiz *biz.PublishBiz + cfg *config.Config + tokenImpl *impl.TokenImpl + userImpl *impl.UserImpl + platImpl *impl.PlatImpl + loginRelationImpl *impl.LoginRelationImpl + publishBiz *biz.PublishBiz } func NewAppService( @@ -31,14 +32,15 @@ func NewAppService( userImpl *impl.UserImpl, platImpl *impl.PlatImpl, publishBiz *biz.PublishBiz, + loginRelationImpl *impl.LoginRelationImpl, ) *AppService { return &AppService{ - cfg: cfg, - tokenImpl: tokenImpl, - - userImpl: userImpl, - platImpl: platImpl, - publishBiz: publishBiz, + cfg: cfg, + tokenImpl: tokenImpl, + loginRelationImpl: loginRelationImpl, + userImpl: userImpl, + platImpl: platImpl, + publishBiz: publishBiz, } } @@ -59,11 +61,18 @@ func (a *AppService) LoginApp(c *fiber.Ctx, req *entitys.LoginAppRequest) error if err != nil { return err } - + a.publishBiz.Notify(&entitys.NotifyData{ + TokenId: int(tokenInfo.ID), + NotifyUrl: tokenInfo.Notifyurl, + Type: entitys.NotifyTypeToken, + Status: 1, + Msg: accessToken, + }) return pkg.HandleResponse(c, fiber.Map{ "access_token": accessToken, "user_limit": tokenInfo.UserLimit, "expire_time": tokenInfo.ExpireTime, + "user_name": tokenInfo.Name, }) } @@ -82,7 +91,7 @@ func (a *AppService) GetUserAndAutoStatus(c *fiber.Ctx, req *entitys.GetUserAndA return err } - pm, err := manager.GetPublishManager(a.cfg, a.tokenImpl.GetDb()) + pm, err := manager.GetPublishManager(a.cfg, a.tokenImpl.GetDb(), a.publishBiz) if err != nil { return err } @@ -124,22 +133,24 @@ func (a *AppService) AddUser(c *fiber.Ctx, req *entitys.AddUserRequest) error { // 获取平台列表并关联 platCond := builder.NewCond().And(builder.Eq{"status": 1}) plats := make([]model.Plat, 0) - _, err = a.platImpl.GetListToStruct(c.UserContext(), &platCond, nil, &plats, "") + _, err = a.platImpl.GetListToStruct(c.UserContext(), &platCond, nil, &plats, "id desc") if err != nil { return err } - + loginRelations := make([]model.LoginRelation, 0, len(plats)) for _, plat := range plats { - loginRelation := &model.LoginRelation{ + loginRelations = append(loginRelations, model.LoginRelation{ UserIndex: userIndex, PlatIndex: plat.Index, LoginStatus: 2, Status: 1, - } - // 这里需要调用 loginRelationImpl 的 Add 方法 - _ = loginRelation - } + }) + } + err = a.loginRelationImpl.Add(c.UserContext(), loginRelations) + if err != nil { + return err + } return pkg.HandleResponse(c, fiber.Map{ "user_name": req.Name, "user_index": userIndex, @@ -148,8 +159,12 @@ func (a *AppService) AddUser(c *fiber.Ctx, req *entitys.AddUserRequest) error { func (a *AppService) DelUser(c *fiber.Ctx, req *entitys.DelUserRequest) error { // 需要从请求中获取 access_token + _, err := a.publishBiz.ValidateAccessToken(c.UserContext(), req.AccessToken) + if err != nil { + return err + } // 这里简化处理 - err := a.userImpl.DeleteByKey(c.UserContext(), a.userImpl.PrimaryKey(), int64(req.ID)) + err = a.userImpl.DeleteByKey(c.UserContext(), "user_index", req.UserIndex) if err != nil { return err } @@ -164,7 +179,6 @@ func (a *AppService) GetApp(c *fiber.Ctx, req *entitys.GetAppRequest) error { cond := builder.NewCond(). And(builder.Eq{"login_relation.user_index": req.UserIndex}). - And(builder.Eq{"login_relation.status": 1}). And(builder.Eq{"plat.status": 1}) result, err := a.platImpl.GetPlatListWithLoginStatus(c.UserContext(), &cond) diff --git a/internal/service/login.go b/internal/service/login.go index 11e01ce..9b0d616 100644 --- a/internal/service/login.go +++ b/internal/service/login.go @@ -95,3 +95,20 @@ func (s *LoginService) ServeQrcode(c *fiber.Ctx, filename string) error { } return c.SendFile(filepath) } + +func (s *LoginService) Log(c *fiber.Ctx) error { + r_id := c.Params("request_id", "") + if r_id == "" { + return errcode.ParamErr("request_id未传") + } + logFile := filepath.Join(s.cfg.Sys.LogsDir, r_id+".log") + if _, err := os.Stat(logFile); os.IsNotExist(err) { + return errcode.NotFound("未找到日志") + } + content, err := os.ReadFile(logFile) + if err != nil { + return errcode.SysErr("读取日志失败") + } + + return pkg.HandleResponse(c, fiber.Map{"content": string(content)}) +} diff --git a/internal/service/publish.go b/internal/service/publish.go index 1b77ce3..08e7c35 100644 --- a/internal/service/publish.go +++ b/internal/service/publish.go @@ -84,7 +84,7 @@ func (s *PublishService) PublishOn(c *fiber.Ctx, req *entitys.PublishOnRequest) return err } - pm, err := manager.GetPublishManager(s.cfg, s.db) + pm, err := manager.GetPublishManager(s.cfg, s.db, s.publishBiz) if err != nil { return err } @@ -102,7 +102,7 @@ func (s *PublishService) PublishOff(c *fiber.Ctx, req *entitys.PublishOffRequest return err } - pm, err := manager.GetPublishManager(s.cfg, s.db) + pm, err := manager.GetPublishManager(s.cfg, s.db, s.publishBiz) if err != nil { return err } @@ -118,7 +118,7 @@ func (s *PublishService) PublishStatus(c *fiber.Ctx, req *entitys.PublishStatusR return err } - pm, err := manager.GetPublishManager(s.cfg, s.db) + pm, err := manager.GetPublishManager(s.cfg, s.db, s.publishBiz) if err != nil { return err } @@ -152,11 +152,12 @@ func (s *PublishService) PublishExecuteRetry(c *fiber.Ctx, req *entitys.PublishE return err } - pm, err := manager.GetPublishManager(s.cfg, s.db) + pm, err := manager.GetPublishManager(s.cfg, s.db, s.publishBiz) if err != nil { return err } result := pm.RetryTask(c.UserContext(), int(tokenInfo.ID), req.RequestID) + return pkg.HandleResponse(c, result) } diff --git a/pkg/func.go b/pkg/func.go index 8129fb0..a32cddb 100644 --- a/pkg/func.go +++ b/pkg/func.go @@ -331,3 +331,14 @@ func GetFreePort() (int, error) { return l.Addr().(*net.TCPAddr).Port, nil } + +// StructToMap 将结构体转换为 map[string]any +func StructToMap(v any) (map[string]any, error) { + b, err := json.Marshal(v) + if err != nil { + return nil, err + } + var m map[string]any + err = json.Unmarshal(b, &m) + return m, err +} diff --git a/tmpl/dataTemp/queryTempl.go b/tmpl/dataTemp/queryTempl.go index c871f3b..ced03ca 100644 --- a/tmpl/dataTemp/queryTempl.go +++ b/tmpl/dataTemp/queryTempl.go @@ -163,6 +163,16 @@ func (k DataTemp) GetOneBySearchStruct(ctx context.Context, cond *builder.Cond, return errcode.SqlErr(err) } + return + +} + +func (k DataTemp) GetOneBySearchStructWithOutCtx(cond *builder.Cond, data interface{}) (err error) { + query, _ := builder.ToBoundSQL(*cond) + if err = k.Db.Model(k.modelInstance()).Where(query).Limit(1).Find(&data).Error; err != nil { + return errcode.SqlErr(err) + } + return } @@ -261,9 +271,10 @@ func (k DataTemp) GetByKey(ctx context.Context, key string, value interface{}, d return } -func (k DataTemp) DeleteByKey(ctx context.Context, key string, value int64) error { +func (k DataTemp) DeleteByKey(ctx context.Context, key string, value interface{}) error { result := k.Db.WithContext(ctx).Model(k.modelInstance()).Where(fmt.Sprintf("%s = ?", key), value). - Update("deleted_at", gorm.Expr("CURRENT_TIMESTAMP")) + Update("deleted_at", gorm.Expr("CURRENT_TIMESTAMP")). + Update("status", 0) if result.Error != nil { return result.Error