From d17e488dd77cf3cb02de405d7464775a5dc5eaa7 Mon Sep 17 00:00:00 2001 From: renzhiyuan <465386466@qq.com> Date: Wed, 7 Jan 2026 14:19:06 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=BC=98=E5=8C=96=E4=BC=81=E4=B8=9A?= =?UTF-8?q?=E5=BE=AE=E4=BF=A1=E6=B6=88=E6=81=AF=E5=8F=91=E9=80=81=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- config/config.yaml | 2 +- config/config_test.yaml | 9 +-- internal/biz/group_config.go | 2 +- internal/biz/handle/qywx/auth.go | 8 +++ internal/biz/handle/qywx/group.go | 86 +++++++++++++++++++---- internal/biz/handle/qywx/other.go | 103 ++++++++++++++++++++++++++++ internal/biz/handle/qywx/types.go | 59 +++++++++++++++- internal/biz/qywx_app.go | 39 ++++++++--- internal/pkg/l_request/request.go | 8 ++- internal/server/cron.go | 5 ++ internal/services/cron.go | 12 ++++ internal/services/dtalk_bot_test.go | 3 +- 12 files changed, 299 insertions(+), 37 deletions(-) create mode 100644 internal/biz/handle/qywx/other.go diff --git a/config/config.yaml b/config/config.yaml index 2e6903f..33fa025 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -161,7 +161,7 @@ qywx: app_secret: "uYqtdwdtdH4Uv_P4is2AChuGzBCoB6cQDyRvpbW0Vmk" token: "zJdukry6" aes_key: "4VLH47qRGUogc2d3QLWuUhvJlk8Y0YuRjXzeBquBq8B" - init_account: "les." + init_account: "les.,FuZhongYun" chat_id_len: 16 default_config_id: 1 bot_group_id: diff --git a/config/config_test.yaml b/config/config_test.yaml index 7355304..ec63488 100644 --- a/config/config_test.yaml +++ b/config/config_test.yaml @@ -156,16 +156,11 @@ qywx: app_secret: "uYqtdwdtdH4Uv_P4is2AChuGzBCoB6cQDyRvpbW0Vmk" token: "zJdukry6" aes_key: "4VLH47qRGUogc2d3QLWuUhvJlk8Y0YuRjXzeBquBq8B" -# -# corp_id: "wx5823bf96d3bd56c7" -# token: "QDG6eK" -# aes_key: "jWmYm7qr5nMoAUwZRjGtBxmz3KA1tkAj3ykkR6q2B2C" -# app_secret: "uYqtdwdtdH4Uv_P4is2AChuGzBCoB6cQDyRvpbW0Vmk" - init_account: "les." + init_account: "les.,FuZhongYun" chat_id_len: 16 default_config_id: 1 bot_group_id: - bbxt: 23 + bbxt: 35 default_prompt: img_recognize: diff --git a/internal/biz/group_config.go b/internal/biz/group_config.go index d8c58ed..c2e2ee5 100644 --- a/internal/biz/group_config.go +++ b/internal/biz/group_config.go @@ -81,7 +81,7 @@ func (g *GroupConfigBiz) GetReportLists(ctx context.Context, groupConfig *model. if err != nil { return } - //product = []string{"优酷周卡", "优酷季卡", "优酷年卡", "爱奇艺黄金会员天卡"} + //追加电商充值系统统计 - 返回统一使用[]*bbxt.ReportRes rechargeReports, err := g.rechargeDailyReport(ctx, time.Now(), nil, g.ossClient) if err != nil || len(rechargeReports) == 0 { diff --git a/internal/biz/handle/qywx/auth.go b/internal/biz/handle/qywx/auth.go index c0c9737..130c23b 100644 --- a/internal/biz/handle/qywx/auth.go +++ b/internal/biz/handle/qywx/auth.go @@ -80,3 +80,11 @@ func (a *Auth) getNewAccessToken(ctx context.Context, corpid string, corpsecret } return } + +type UploadMediaRes struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` + Type string `json:"type"` + MediaId string `json:"media_id"` + CreatedAt string `json:"created_at"` +} diff --git a/internal/biz/handle/qywx/group.go b/internal/biz/handle/qywx/group.go index 53f064a..1aedbfa 100644 --- a/internal/biz/handle/qywx/group.go +++ b/internal/biz/handle/qywx/group.go @@ -33,19 +33,17 @@ func NewGroup(groupImpl *impl.BotGroupQywxImpl, auth *Auth) *Group { // - GroupCreateResp: 创建群聊的响应结果 // - error: 错误信息,如果请求失败则返回错误 func (g *Group) Create(ctx context.Context, req GroupCreateReq, corpid string, corpsecret string) (GroupCreateResp, error) { - // 声明一个GroupCreateResp结构体变量res,用于存储响应结果 + var res GroupCreateResp - // 将请求结构体req转换为map类型的参数param - // 如果转换失败,忽略错误 + param, _ := util.StructToMap(req) - // 发送HTTP请求到企业微信API创建群聊 - // 参数依次为:上下文、请求参数、请求URL、响应结果存储指针、企业ID、应用密钥 + _, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/create", &res, corpid, corpsecret) - // 如果请求过程中发生错误,返回响应结果和错误 + if err != nil { return res, err } - // 请求成功,返回响应结果和nil错误 + return res, nil } @@ -60,18 +58,76 @@ func (g *Group) Create(ctx context.Context, req GroupCreateReq, corpid string, c // - error: 操作过程中发生的错误,如果成功则为nil func (g *Group) SendMarkDown(ctx context.Context, req GroupSendMarkDownReq, corpid string, corpsecret string) error { - // 设置消息类型为Markdown - req.Msgtype = "markdown" - // 将请求结构体转换为map类型,便于后续的HTTP请求参数处理 + req.Msgtype = "markdown_v2" + param, _ := util.StructToMap(req) - // 调用request方法发送HTTP请求到企业微信API - // 参数依次为:上下文、请求参数、API URL、额外请求头、corpid、corpsecret - _, err := g.request(ctx, param, " https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret) - // 如果请求过程中发生错误,直接返回错误 + + _, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret) + if err != nil { return err } - // 请求成功,返回nil + + return nil +} + +func (g *Group) SendNews(ctx context.Context, req GroupSendNewsReq, corpid string, corpsecret string) error { + + req.Msgtype = "news" + + param, _ := util.StructToMap(req) + + _, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret) + + if err != nil { + return err + } + + return nil +} + +func (g *Group) SendImg(ctx context.Context, req GroupSendImgReq, corpid string, corpsecret string) error { + + req.Msgtype = "image" + + param, _ := util.StructToMap(req) + + _, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret) + + if err != nil { + return err + } + + return nil +} + +func (g *Group) SendMpNews(ctx context.Context, req GroupSendMpNewsReq, corpid string, corpsecret string) error { + + req.Msgtype = "mpnews" + + param, _ := util.StructToMap(req) + + _, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret) + + if err != nil { + return err + } + + return nil +} + +func (g *Group) SendText(ctx context.Context, req GroupSendTextReq, corpid string, corpsecret string) error { + + req.Msgtype = "text" + + param, _ := util.StructToMap(req) + + _, err := g.request(ctx, param, "https://qyapi.weixin.qq.com/cgi-bin/appchat/send", nil, corpid, corpsecret) + + if err != nil { + return err + } + return nil } diff --git a/internal/biz/handle/qywx/other.go b/internal/biz/handle/qywx/other.go new file mode 100644 index 0000000..61f0df2 --- /dev/null +++ b/internal/biz/handle/qywx/other.go @@ -0,0 +1,103 @@ +package qywx + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "mime/multipart" + "net/http" + "path" + "time" +) + +type Other struct { + auth *Auth +} + +func NewOther(auth *Auth) *Other { + return &Other{ + + auth: auth, + } +} + +func (g *Other) UploadMediaWithUrl(ctx context.Context, fileUrl, mediaType, corpid, corpsecret string) (uploadRes *UploadMediaRes, err error) { + // 1. 获取AccessToken + auth, err := g.auth.GetAccessToken(ctx, corpid, corpsecret) + if err != nil { + return nil, fmt.Errorf("获取AccessToken失败: %v", err) + } + + // 2. 下载文件 + client := &http.Client{Timeout: 10 * time.Second} + resp, err := client.Get(fileUrl) + if err != nil { + return nil, fmt.Errorf("下载文件失败: %v", err) + } + defer resp.Body.Close() + + // 检查响应状态 + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("下载文件状态码错误: %d", resp.StatusCode) + } + + // 3. 准备上传请求(修正点:使用正确的上传API) + uploadUrl := fmt.Sprintf("https://qyapi.weixin.qq.com/cgi-bin/media/upload?access_token=%s&type=%s", + auth.AccessToken, mediaType) + + // 4. 创建multipart表单 + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // 从URL提取文件名(或使用默认名) + filename := path.Base(fileUrl) + if filename == "/" || filename == "." { + filename = fmt.Sprintf("file_%d.dat", time.Now().Unix()) + } + + part, err := writer.CreateFormFile("media", filename) + if err != nil { + return nil, fmt.Errorf("创建表单文件失败: %v", err) + } + + // 5. 流式传输文件内容 + if _, err = io.Copy(part, resp.Body); err != nil { + return nil, fmt.Errorf("写入文件内容失败: %v", err) + } + + // 6. 关闭writer + if err = writer.Close(); err != nil { + return nil, fmt.Errorf("关闭writer失败: %v", err) + } + + // 7. 发送上传请求 + req, err := http.NewRequest("POST", uploadUrl, body) + if err != nil { + return nil, fmt.Errorf("创建请求失败: %v", err) + } + req.Header.Set("Content-Type", writer.FormDataContentType()) + + uploadResp, err := client.Do(req) + if err != nil { + return nil, fmt.Errorf("上传请求失败: %v", err) + } + defer uploadResp.Body.Close() + + // 8. 解析响应 + respBody, err := io.ReadAll(uploadResp.Body) + if err != nil { + return nil, fmt.Errorf("读取响应失败: %v", err) + } + + if err = json.Unmarshal(respBody, &uploadRes); err != nil { + return nil, fmt.Errorf("解析响应失败: %v", err) + } + + if uploadRes.Errcode != 0 { + return nil, fmt.Errorf("上传失败 [code=%d]: %s", uploadRes.Errcode, uploadRes.Errmsg) + } + + return +} diff --git a/internal/biz/handle/qywx/types.go b/internal/biz/handle/qywx/types.go index 38b2d34..515824c 100644 --- a/internal/biz/handle/qywx/types.go +++ b/internal/biz/handle/qywx/types.go @@ -37,9 +37,66 @@ type commonResp struct { type GroupSendMarkDownReq struct { Chatid string `json:"chatid"` Msgtype string `json:"msgtype"` - Markdown MarkDown `json:"markdown"` + Markdown MarkDown `json:"markdown_v2"` } type MarkDown struct { Content string `json:"content"` } + +type GroupSendNewsReq struct { + Chatid string `json:"chatid"` + Msgtype string `json:"msgtype"` + News News `json:"news"` + Safe int `json:"safe"` +} + +type News struct { + Articles []Articles `json:"articles"` +} +type Articles struct { + Title string `json:"title"` + Description string `json:"description"` + Url string `json:"url"` + Picurl string `json:"picurl"` +} + +type GroupSendImgReq struct { + Chatid string `json:"chatid"` + Msgtype string `json:"msgtype"` + Image Image `json:"image"` + Safe int `json:"safe"` +} +type Image struct { + MediaId string `json:"media_id"` +} + +type GroupSendMpNewsReq struct { + Chatid string `json:"chatid"` + Msgtype string `json:"msgtype"` + Mpnews Mpnews `json:"mpnews"` + Safe int `json:"safe"` +} + +type Mpnews struct { + Articles []ArticlesMpnews `json:"articles"` +} +type ArticlesMpnews struct { + Title string `json:"title"` + ThumbMediaId string `json:"thumb_media_id"` + Author string `json:"author"` + ContentSourceUrl string `json:"content_source_url"` + Content string `json:"content"` + Digest string `json:"digest"` +} + +type GroupSendTextReq struct { + Chatid string `json:"chatid"` + Msgtype string `json:"msgtype"` + Text Text `json:"text"` + Safe int `json:"safe"` +} + +type Text struct { + Content string `json:"content"` +} diff --git a/internal/biz/qywx_app.go b/internal/biz/qywx_app.go index 694fb1d..3f7fbe0 100644 --- a/internal/biz/qywx_app.go +++ b/internal/biz/qywx_app.go @@ -8,7 +8,7 @@ import ( "ai_scheduler/internal/pkg" "ai_scheduler/internal/tools/bbxt" "context" - "fmt" + "strings" "time" "ai_scheduler/internal/config" @@ -21,6 +21,7 @@ type QywxAppBiz struct { conf *config.Config botGroupQywxImpl *impl.BotGroupQywxImpl qywxGroupHandle *qywx.Group + qywxOtherHandle *qywx.Other } // NewDingTalkBotBiz @@ -28,11 +29,13 @@ func NewQywxAppBiz( conf *config.Config, botGroupQywxImpl *impl.BotGroupQywxImpl, qywxGroupHandle *qywx.Group, + qywxOtherHandle *qywx.Other, ) *QywxAppBiz { return &QywxAppBiz{ conf: conf, botGroupQywxImpl: botGroupQywxImpl, qywxGroupHandle: qywxGroupHandle, + qywxOtherHandle: qywxOtherHandle, } } @@ -51,11 +54,9 @@ func (q *QywxAppBiz) InitGroup(ctx context.Context) (string, error) { resp, err := q.qywxGroupHandle.Create( ctx, qywx.GroupCreateReq{ - Name: GroupInfo.Title, - Chatid: GroupInfo.ChatID, - Userlist: []string{ - q.conf.Qywx.InitAccount, - }, + Name: GroupInfo.Title, + Chatid: GroupInfo.ChatID, + Userlist: strings.Split(q.conf.Qywx.InitAccount, ","), }, q.conf.Qywx.CorpId, GroupInfo.AppSecret, @@ -77,12 +78,30 @@ func (q *QywxAppBiz) GetGroupInfo(ctx context.Context, groupId int) (group model } func (q *QywxAppBiz) SendReport(ctx context.Context, groupInfo *model.AiBotGroupQywx, report *bbxt.ReportRes) (err error) { - confitent := fmt.Sprintf("%s\n%s", report.Title, fmt.Sprintf("![图片](%s)", report.Url)) - err = q.qywxGroupHandle.SendMarkDown(ctx, qywx.GroupSendMarkDownReq{ + //confitent := fmt.Sprintf("%s\n%s", report.Title, fmt.Sprintf("![图片](%s)", report.Url)) + + upload, err := q.qywxOtherHandle.UploadMediaWithUrl(ctx, report.Url, "image", q.conf.Qywx.CorpId, groupInfo.AppSecret) + if err != nil { + return + } + + err = q.qywxGroupHandle.SendText(ctx, qywx.GroupSendTextReq{ Chatid: groupInfo.ChatID, - Markdown: qywx.MarkDown{ - Content: confitent, + Text: qywx.Text{ + Content: report.ReportName + "\n" + report.Title, }, }, q.conf.Qywx.CorpId, groupInfo.AppSecret) + if err != nil { + return + } + err = q.qywxGroupHandle.SendImg(ctx, qywx.GroupSendImgReq{ + Chatid: groupInfo.ChatID, + Image: qywx.Image{ + MediaId: upload.MediaId, + }, + }, q.conf.Qywx.CorpId, groupInfo.AppSecret) + if err != nil { + return + } return } diff --git a/internal/pkg/l_request/request.go b/internal/pkg/l_request/request.go index d47730c..9adf464 100644 --- a/internal/pkg/l_request/request.go +++ b/internal/pkg/l_request/request.go @@ -8,6 +8,8 @@ import ( "net/url" "strings" "time" + + "github.com/go-kratos/kratos/v2/log" ) // 请求结构体 @@ -108,7 +110,11 @@ func (r *Request) prepare() *http.Request { Method := r.getMethod() Url := r.getUrl() Data := r.getData() - req, _ := http.NewRequest(Method, Url, Data) + req, err := http.NewRequest(Method, Url, Data) + if err != nil { + log.Error(err) + return nil + } r.addHeaders(req) return req } diff --git a/internal/server/cron.go b/internal/server/cron.go index 3b662e1..4760826 100644 --- a/internal/server/cron.go +++ b/internal/server/cron.go @@ -44,6 +44,11 @@ func (c *CronServer) InitJobs(ctx context.Context) { Name: "直连天下报表推送", Schedule: "0 12,18,23 * * *", }, + { + Func: c.cronService.CronReportSendQywx, + Name: "直连天下报表推送", + Schedule: "0 12,18,23 * * *", + }, } } diff --git a/internal/services/cron.go b/internal/services/cron.go index 1cc49e1..cbb6c04 100644 --- a/internal/services/cron.go +++ b/internal/services/cron.go @@ -35,10 +35,16 @@ func (d *CronService) CronReportSendDingTalk(ctx context.Context) error { if err != nil { return err } + if groupInfo.GroupID == 0 { + return nil + } groupConfig, err := d.groupConfigBiz.GetGroupConfig(ctx, groupInfo.ConfigID) if err != nil { return err } + if groupConfig.ConfigID == 0 { + return nil + } reports, err := d.groupConfigBiz.GetReportLists(ctx, groupConfig) if err != nil { return err @@ -60,10 +66,16 @@ func (d *CronService) CronReportSendQywx(ctx context.Context) error { if err != nil { return err } + if groupInfo.GroupID == 0 { + return nil + } groupConfig, err := d.groupConfigBiz.GetGroupConfig(ctx, groupInfo.ConfigID) if err != nil { return err } + if groupConfig.ConfigID == 0 { + return nil + } reports, err := d.groupConfigBiz.GetReportLists(ctx, groupConfig) if err != nil { return err diff --git a/internal/services/dtalk_bot_test.go b/internal/services/dtalk_bot_test.go index c5303e0..f5f8a50 100644 --- a/internal/services/dtalk_bot_test.go +++ b/internal/services/dtalk_bot_test.go @@ -117,7 +117,8 @@ func run() { botGroupQywxImpl := impl.NewBotGroupQywxImpl(db) qywxAuth := qywx.NewAuth(configConfig, rdb) group := qywx.NewGroup(botGroupQywxImpl, qywxAuth) - qywxAppBiz := biz.NewQywxAppBiz(configConfig, botGroupQywxImpl, group) + other := qywx.NewOther(qywxAuth) + qywxAppBiz := biz.NewQywxAppBiz(configConfig, botGroupQywxImpl, group, other) groupConfigBiz := biz.NewGroupConfigBiz(toolRegis, utils_ossClient, botGroupConfigImpl, registry, configConfig) dingTalkBotBiz := biz.NewDingTalkBotBiz(doDo, handle, botConfigImpl, botGroupImpl, user, botChatHisImpl, manager, configConfig, sendCardClient, groupConfigBiz) // 初始化钉钉机器人服务