123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462 |
- package service
- import "C"
- import (
- "client/app"
- "client/config"
- "client/wcf"
- "context"
- "fmt"
- "github.com/gogf/gf/v2/util/gconv"
- "io"
- "log"
- "math/rand"
- "net/http"
- "net/url"
- "os"
- "os/exec"
- "os/signal"
- "path/filepath"
- "regexp"
- "strings"
- "sync"
- "time"
- )
- // 全局变量定义
- // 全局消息队列和工作池
- var (
- cache = new(sync.Map) // 线程安全的缓存,用于存储对话历史等数据
- sendTalkQueue = make(chan SendTask, 1000) // 缓冲队列
- workerPool = make(chan struct{}, 5) // 并发控制
- )
- const (
- ContentTypeText string = "0" // 文字
- ContentTypeImage string = "1" // 图片
- ContentTypeLink string = "2" // 链接
- ContentTypeVideo string = "3" // 视频
- ContentTypeFile string = "4" // 文件
- )
- // init 初始化函数,在包被加载时自动执行
- func init() {
- // 初始化微信客户端连接
- app.WechatFerryInit()
- // 验证连接状态,如果连接失败则终止程序
- if app.WxClient == nil || !app.WxClient.IsLogin() {
- log.Fatal("无法连接到微信客户端")
- }
- // 确保全局变量一致
- rand.Seed(time.Now().UnixNano())
- }
- // OnMsg消息回调
- func OnMsg(ctx context.Context) {
- for {
- select {
- case <-ctx.Done():
- return
- default:
- err := app.WxClient.OnMSG(func(msg *wcf.WxMsg) {
- processMsg(msg)
- })
- if err != nil {
- log.Println(err.Error())
- }
- time.Sleep(time.Second)
- }
- }
- }
- // processMsg 处理单条微信消息的核心函数
- // msg: 包含微信消息所有信息的结构体
- func processMsg(msg *wcf.WxMsg) {
- // 结构化日志输出消息详情
- log.Println(fmt.Sprintf(`
- [消息详情]
- 是否来自自己: %v
- 是否是群消息: %v
- 消息类型: %d
- 时间戳: %d
- 消息ID: %d
- 房间ID: %s
- 发送人: %s
- 消息内容: %s
- `,
- msg.IsSelf, msg.IsGroup,
- msg.Type, msg.Ts,
- msg.Id, msg.Roomid, msg.Sender,
- msg.Content,
- ))
- if !msg.IsGroup && msg.Type == 1 {
- returnData := map[string]interface{}{
- "roomid": msg.Roomid,
- "IsSelf": msg.IsSelf,
- "content": msg.Content,
- }
- if err := client.SendMessage(gconv.String(returnData), "chatHistory"); err != nil {
- log.Println(fmt.Sprintf("发送消息到客户端失败: %v", msg.Content))
- }
- }
- }
- // WxHandle 微信消息处理入口函数
- func WxHandle() {
- ctx, cancleFn := context.WithCancel(context.Background())
- log.Println(ctx)
- // 注册Ctrl+C信号处理函数
- signalChan := make(chan os.Signal, 1)
- signal.Notify(signalChan, os.Interrupt)
- go func() {
- <-signalChan
- cancleFn()
- log.Println("感谢温柔的ctrl+c关闭,下次可直接运行程序,无需重启微信。")
- app.WxClient.Close()
- //强制杀死微信进程
- killWeChat()
- os.Exit(0)
- }()
- ret := app.WxClient.EnableRecvTxt()
- log.Println("开启接收消息状态:", ret)
- systemWxId := app.WxClient.GetSelfWXID()
- go OnMsg(ctx)
- go ConnectGRPC(systemWxId, config.Cfg.ServiceAddress)
- select {}
- }
- // GetContacts 获取并处理通讯录联系人列表
- func GetContacts() {
- // 创建联系人数据切片
- returnData := []map[string]interface{}{}
- // 遍历所有联系人
- for _, c := range app.WxClient.GetContacts() {
- // 过滤不需要的联系人类型:
- // 1. 以@openim结尾的联系人(可能是系统账号)
- // 2. 备注为空的联系人
- isavailable, phone, name := userJudge(c)
- if !isavailable {
- continue
- }
- // 构建联系人信息字典
- returnData = append(returnData, map[string]interface{}{
- "name": c.Name, // 联系人昵称
- "code": c.Code, // 联系人编码
- "wxid": c.Wxid, // 微信ID
- "remark": c.Remark, // 备注名
- "phone": phone,
- "appellation": name,
- "personName": config.Cfg.PersonName,
- })
- }
- // 将联系人列表发送给客户端
- if err := client.SendMessage(gconv.String(returnData), "getContacts"); err != nil {
- log.Println(fmt.Sprintf("发送联系人数据失败: %v", err))
- }
- }
- // 备注解析
- func extractPhoneNumber(input string) (string, string, error) {
- // 正则表达式匹配以字母开头、结尾为手机号的字符串
- //re := regexp.MustCompile(`^[A-Za-z].*?(\d{11})$`)
- re := regexp.MustCompile(`^[A-Za-z].*/([^/]+)(\d{11})$`)
- matches := re.FindStringSubmatch(input)
- if len(matches) < 3 {
- return "", "", fmt.Errorf("未找到匹配的手机号")
- }
- return matches[2], matches[1], nil
- }
- // killWeChat 关闭微信
- func killWeChat() {
- // 根据操作系统选择不同的命令
- var cmd *exec.Cmd
- // 在Windows上使用taskkill命令
- cmd = exec.Command("taskkill", "/F", "/IM", "WeChat.exe")
- // 执行命令
- err := cmd.Run()
- if err != nil {
- log.Println("Error killing process:", err)
- return
- }
- log.Println("wechat process killed successfully.")
- }
- // 用户身份判断
- func userJudge(c *wcf.RpcContact) (bool, string, string) {
- phone, name, _ := extractPhoneNumber(c.Remark)
- if strings.HasSuffix(c.Wxid, "@openim") || c.Remark == "" || phone == "" || c.Name == "语音记事本" || c.Name == "文件传输助手" {
- return false, "", ""
- }
- return true, phone, name
- }
- // 发送文本信息
- func sendText(content, wxId, appellation string) (bool, error) {
- if appellation != "" {
- appellation = string([]rune(appellation)[0])
- content = fmt.Sprintf("%s老师,%s", appellation, content)
- }
- if app.WxClient.SendTxt(content, wxId, nil) != 0 {
- return false, fmt.Errorf(fmt.Sprintf("%s%s文字消息发送失败", content, wxId))
- }
- return true, nil
- }
- // 发送图片信息
- func sendImage(url, wxId, taskId string) (bool, error) {
- imgPath, err := DownloadImage(url, "img", taskId)
- if err != nil {
- return false, fmt.Errorf("%s%s下载图片失败: %w", err)
- }
- if app.WxClient.SendIMG(imgPath, wxId) != 0 {
- return false, fmt.Errorf("%s%s图片发送失败", url, wxId)
- }
- return true, nil
- }
- // 发送视频信息
- func sendVideo(url, wxId, taskId string) (bool, error) {
- videoPath, err := DownloadImage(url, "video", taskId)
- if err != nil {
- return false, fmt.Errorf("%s%s下载视频失败: %w", err)
- }
- if app.WxClient.SendIMG(videoPath, wxId) != 0 {
- return false, fmt.Errorf("%s%s视频发送失败", url, wxId)
- }
- return true, nil
- }
- // 发送文件信息
- func sendFile(url, wxId, taskId string) (bool, error) {
- filePath, err := DownloadImage(url, "file", taskId)
- if err != nil {
- return false, fmt.Errorf("%s%s下载文件失败: %w", url, wxId, err)
- }
- if app.WxClient.SendFile(filePath, wxId) != 0 {
- return false, fmt.Errorf("%s%s文件发送失败", url, wxId)
- }
- return true, nil
- }
- // 发送任务结构
- type SendTask struct {
- DataStr string
- RetryCount int
- }
- // 初始化工作池
- func InitSendTalkWorkers() {
- for i := 0; i < cap(workerPool); i++ {
- go sendTalkWorker()
- }
- }
- // 工作goroutine
- func sendTalkWorker() {
- for task := range sendTalkQueue {
- workerPool <- struct{}{} // 获取令牌
- processSendTask(task) // 直接处理任务
- <-workerPool // 释放令牌
- }
- }
- // 处理单个任务
- func processSendTask(task SendTask) {
- defer func() {
- if r := recover(); r != nil {
- log.Printf("SendTask panic: %v", r)
- }
- }()
- if task.RetryCount > 3 {
- log.Printf("放弃重试 taskId=%s", gconv.Map(task.DataStr)["taskId"])
- return
- }
- aaa := rand.Intn(config.Cfg.InformationDelay*1000) + 5000
- time.Sleep(time.Duration(aaa) * time.Millisecond)
- if err := doSendTalk(task.DataStr); err != nil {
- log.Printf("发送失败 (重试 %d/3): %v", task.RetryCount, err)
- time.Sleep(time.Second * time.Duration(task.RetryCount))
- sendTalkQueue <- SendTask{task.DataStr, task.RetryCount + 1}
- }
- }
- // 异步入队
- func SendTalk(dataStr string) {
- task := SendTask{
- DataStr: dataStr,
- RetryCount: 0,
- }
- select {
- case sendTalkQueue <- task: // 入队成功
- log.Printf("任务已入队 taskId=%s", gconv.Map(dataStr)["taskId"])
- default:
- // 队列满时降级处理
- log.Println("警告:发送队列已满,降级同步处理")
- processSendTask(task)
- }
- }
- // 实际发送逻辑
- func doSendTalk(dataStr string) error {
- dataMap := gconv.Map(dataStr)
- taskId := gconv.String(dataMap["taskId"])
- userMap := gconv.Map(dataMap["user"])
- //replyLanguage := gconv.String(dataMap["replyLanguage"])
- wxId := gconv.String(userMap["wxid"])
- appellation := gconv.String(userMap["appellation"])
- batchCode := gconv.String(dataMap["batchCode"])
- baseUserId := gconv.String(userMap["baseUserId"])
- ok := true
- // 处理内容发送
- for _, v := range gconv.Maps(dataMap["content"]) {
- content := gconv.String(v["content"])
- switch gconv.String(v["content_type"]) {
- case ContentTypeText, ContentTypeLink:
- ok1, err := sendText(content, wxId, appellation)
- if !ok1 {
- ok = false
- log.Println(err)
- }
- case ContentTypeImage:
- ok1, err := sendImage(content, wxId, taskId)
- if !ok1 {
- ok = false
- log.Println(err)
- }
- case ContentTypeVideo:
- ok1, err := sendVideo(content, wxId, taskId)
- if !ok1 {
- ok = false
- log.Println(err)
- }
- case ContentTypeFile:
- ok1, err := sendFile(content, wxId, taskId)
- if !ok1 {
- ok = false
- log.Println(err)
- }
- }
- }
- //time.Sleep(3 * time.Second)
- /*if replyLanguage != "" {
- ok1, err := sendText(replyLanguage, wxId, "")
- if !ok1 {
- ok = false
- log.Println(err)
- }
- }*/
- // 发送回执
- returnData := map[string]interface{}{
- "taskId": taskId,
- "batch_code": batchCode,
- "base_user_id": baseUserId,
- "isSuccess": ok,
- }
- return client.SendMessage(gconv.String(returnData), "sendTalkReceipt")
- }
- // 拒绝接受回执
- func Reject(text string) {
- time.Sleep(10 * time.Second)
- // 1. 解析输入数据
- dataMap := gconv.Map(text)
- if dataMap == nil {
- log.Println("无效的输入数据")
- return
- }
- contentMap := gconv.Map(dataMap["content"])
- if contentMap == nil {
- log.Println("无效的内容数据")
- return
- }
- wxId := gconv.String(dataMap["wxId"])
- if wxId == "" {
- log.Println("缺少接收人wxId")
- return
- }
- // 2. 获取内容和类型
- contentType := gconv.String(contentMap["content_type"])
- content := gconv.String(contentMap["content"])
- if content == "" {
- log.Println("消息内容为空")
- return
- }
- switch contentType {
- case ContentTypeText, ContentTypeLink:
- sendText(content, wxId, "")
- case ContentTypeImage:
- sendImage(content, wxId, "A101")
- case ContentTypeVideo:
- sendVideo(content, wxId, "A101")
- case ContentTypeFile:
- sendFile(content, wxId, "A101")
- }
- }
- // DownloadImage 下载图片、视频、文件并保存到指定格式的路径
- func DownloadImage(url, fileType string, taskId string) (string, error) {
- // 1. 创建img目录结构 (格式: img/2025/07/04/)
- dirPath := filepath.Join(fileType, time.Now().Format(time.DateOnly), taskId)
- if err := os.MkdirAll(dirPath, 0755); err != nil {
- return "", fmt.Errorf("创建目录失败: %v", err)
- }
- // 2. 获取文件扩展名
- filename, _ := GetFullFilename(url)
- // 3. 生成文件名 (如 task_id_1.jpg)
- filePath := filepath.Join(dirPath, filename)
- // 4. 下载文件
- resp, err := http.Get(url)
- if err != nil {
- return "", fmt.Errorf("下载请求失败: %v", err)
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return "", fmt.Errorf("下载失败,状态码: %d", resp.StatusCode)
- }
- // 5. 创建并保存文件
- out, err := os.Create(filePath)
- if err != nil {
- return "", fmt.Errorf("创建文件失败: %v", err)
- }
- defer out.Close()
- if _, err := io.Copy(out, resp.Body); err != nil {
- return "", fmt.Errorf("写入文件失败: %v", err)
- }
- // 6. 获取绝对路径
- absPath, err := filepath.Abs(filePath)
- if err != nil {
- return "", fmt.Errorf("获取绝对路径失败: %v", err)
- }
- return absPath, nil
- }
- // GetFullFilename 从URL中提取完整文件名(包含扩展名)
- func GetFullFilename(rawURL string) (string, error) {
- // 解析URL(处理特殊字符和路径)
- parsedURL, err := url.Parse(rawURL)
- if err != nil {
- return "", fmt.Errorf("URL解析失败: %w", err)
- }
- // 获取路径部分(去掉域名和查询参数)
- path := parsedURL.Path
- // 提取文件名(包含扩展名)
- fullFilename := filepath.Base(path)
- // 去掉URL查询参数(如 ?xxx=yyy)
- filename := strings.Split(fullFilename, "?")[0]
- return filename, nil
- }
|