wx.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462
  1. package service
  2. import "C"
  3. import (
  4. "client/app"
  5. "client/config"
  6. "client/wcf"
  7. "context"
  8. "fmt"
  9. "github.com/gogf/gf/v2/util/gconv"
  10. "io"
  11. "log"
  12. "math/rand"
  13. "net/http"
  14. "net/url"
  15. "os"
  16. "os/exec"
  17. "os/signal"
  18. "path/filepath"
  19. "regexp"
  20. "strings"
  21. "sync"
  22. "time"
  23. )
  24. // 全局变量定义
  25. // 全局消息队列和工作池
  26. var (
  27. cache = new(sync.Map) // 线程安全的缓存,用于存储对话历史等数据
  28. sendTalkQueue = make(chan SendTask, 1000) // 缓冲队列
  29. workerPool = make(chan struct{}, 5) // 并发控制
  30. )
  31. const (
  32. ContentTypeText string = "0" // 文字
  33. ContentTypeImage string = "1" // 图片
  34. ContentTypeLink string = "2" // 链接
  35. ContentTypeVideo string = "3" // 视频
  36. ContentTypeFile string = "4" // 文件
  37. )
  38. // init 初始化函数,在包被加载时自动执行
  39. func init() {
  40. // 初始化微信客户端连接
  41. app.WechatFerryInit()
  42. // 验证连接状态,如果连接失败则终止程序
  43. if app.WxClient == nil || !app.WxClient.IsLogin() {
  44. log.Fatal("无法连接到微信客户端")
  45. }
  46. // 确保全局变量一致
  47. rand.Seed(time.Now().UnixNano())
  48. }
  49. // OnMsg消息回调
  50. func OnMsg(ctx context.Context) {
  51. for {
  52. select {
  53. case <-ctx.Done():
  54. return
  55. default:
  56. err := app.WxClient.OnMSG(func(msg *wcf.WxMsg) {
  57. processMsg(msg)
  58. })
  59. if err != nil {
  60. log.Println(err.Error())
  61. }
  62. time.Sleep(time.Second)
  63. }
  64. }
  65. }
  66. // processMsg 处理单条微信消息的核心函数
  67. // msg: 包含微信消息所有信息的结构体
  68. func processMsg(msg *wcf.WxMsg) {
  69. // 结构化日志输出消息详情
  70. log.Println(fmt.Sprintf(`
  71. [消息详情]
  72. 是否来自自己: %v
  73. 是否是群消息: %v
  74. 消息类型: %d
  75. 时间戳: %d
  76. 消息ID: %d
  77. 房间ID: %s
  78. 发送人: %s
  79. 消息内容: %s
  80. `,
  81. msg.IsSelf, msg.IsGroup,
  82. msg.Type, msg.Ts,
  83. msg.Id, msg.Roomid, msg.Sender,
  84. msg.Content,
  85. ))
  86. if !msg.IsGroup && msg.Type == 1 {
  87. returnData := map[string]interface{}{
  88. "roomid": msg.Roomid,
  89. "IsSelf": msg.IsSelf,
  90. "content": msg.Content,
  91. }
  92. if err := client.SendMessage(gconv.String(returnData), "chatHistory"); err != nil {
  93. log.Println(fmt.Sprintf("发送消息到客户端失败: %v", msg.Content))
  94. }
  95. }
  96. }
  97. // WxHandle 微信消息处理入口函数
  98. func WxHandle() {
  99. ctx, cancleFn := context.WithCancel(context.Background())
  100. log.Println(ctx)
  101. // 注册Ctrl+C信号处理函数
  102. signalChan := make(chan os.Signal, 1)
  103. signal.Notify(signalChan, os.Interrupt)
  104. go func() {
  105. <-signalChan
  106. cancleFn()
  107. log.Println("感谢温柔的ctrl+c关闭,下次可直接运行程序,无需重启微信。")
  108. app.WxClient.Close()
  109. //强制杀死微信进程
  110. killWeChat()
  111. os.Exit(0)
  112. }()
  113. ret := app.WxClient.EnableRecvTxt()
  114. log.Println("开启接收消息状态:", ret)
  115. systemWxId := app.WxClient.GetSelfWXID()
  116. go OnMsg(ctx)
  117. go ConnectGRPC(systemWxId, config.Cfg.ServiceAddress)
  118. select {}
  119. }
  120. // GetContacts 获取并处理通讯录联系人列表
  121. func GetContacts() {
  122. // 创建联系人数据切片
  123. returnData := []map[string]interface{}{}
  124. // 遍历所有联系人
  125. for _, c := range app.WxClient.GetContacts() {
  126. // 过滤不需要的联系人类型:
  127. // 1. 以@openim结尾的联系人(可能是系统账号)
  128. // 2. 备注为空的联系人
  129. isavailable, phone, name := userJudge(c)
  130. if !isavailable {
  131. continue
  132. }
  133. // 构建联系人信息字典
  134. returnData = append(returnData, map[string]interface{}{
  135. "name": c.Name, // 联系人昵称
  136. "code": c.Code, // 联系人编码
  137. "wxid": c.Wxid, // 微信ID
  138. "remark": c.Remark, // 备注名
  139. "phone": phone,
  140. "appellation": name,
  141. "personName": config.Cfg.PersonName,
  142. })
  143. }
  144. // 将联系人列表发送给客户端
  145. if err := client.SendMessage(gconv.String(returnData), "getContacts"); err != nil {
  146. log.Println(fmt.Sprintf("发送联系人数据失败: %v", err))
  147. }
  148. }
  149. // 备注解析
  150. func extractPhoneNumber(input string) (string, string, error) {
  151. // 正则表达式匹配以字母开头、结尾为手机号的字符串
  152. //re := regexp.MustCompile(`^[A-Za-z].*?(\d{11})$`)
  153. re := regexp.MustCompile(`^[A-Za-z].*/([^/]+)(\d{11})$`)
  154. matches := re.FindStringSubmatch(input)
  155. if len(matches) < 3 {
  156. return "", "", fmt.Errorf("未找到匹配的手机号")
  157. }
  158. return matches[2], matches[1], nil
  159. }
  160. // killWeChat 关闭微信
  161. func killWeChat() {
  162. // 根据操作系统选择不同的命令
  163. var cmd *exec.Cmd
  164. // 在Windows上使用taskkill命令
  165. cmd = exec.Command("taskkill", "/F", "/IM", "WeChat.exe")
  166. // 执行命令
  167. err := cmd.Run()
  168. if err != nil {
  169. log.Println("Error killing process:", err)
  170. return
  171. }
  172. log.Println("wechat process killed successfully.")
  173. }
  174. // 用户身份判断
  175. func userJudge(c *wcf.RpcContact) (bool, string, string) {
  176. phone, name, _ := extractPhoneNumber(c.Remark)
  177. if strings.HasSuffix(c.Wxid, "@openim") || c.Remark == "" || phone == "" || c.Name == "语音记事本" || c.Name == "文件传输助手" {
  178. return false, "", ""
  179. }
  180. return true, phone, name
  181. }
  182. // 发送文本信息
  183. func sendText(content, wxId, appellation string) (bool, error) {
  184. if appellation != "" {
  185. appellation = string([]rune(appellation)[0])
  186. content = fmt.Sprintf("%s老师,%s", appellation, content)
  187. }
  188. if app.WxClient.SendTxt(content, wxId, nil) != 0 {
  189. return false, fmt.Errorf(fmt.Sprintf("%s%s文字消息发送失败", content, wxId))
  190. }
  191. return true, nil
  192. }
  193. // 发送图片信息
  194. func sendImage(url, wxId, taskId string) (bool, error) {
  195. imgPath, err := DownloadImage(url, "img", taskId)
  196. if err != nil {
  197. return false, fmt.Errorf("%s%s下载图片失败: %w", err)
  198. }
  199. if app.WxClient.SendIMG(imgPath, wxId) != 0 {
  200. return false, fmt.Errorf("%s%s图片发送失败", url, wxId)
  201. }
  202. return true, nil
  203. }
  204. // 发送视频信息
  205. func sendVideo(url, wxId, taskId string) (bool, error) {
  206. videoPath, err := DownloadImage(url, "video", taskId)
  207. if err != nil {
  208. return false, fmt.Errorf("%s%s下载视频失败: %w", err)
  209. }
  210. if app.WxClient.SendIMG(videoPath, wxId) != 0 {
  211. return false, fmt.Errorf("%s%s视频发送失败", url, wxId)
  212. }
  213. return true, nil
  214. }
  215. // 发送文件信息
  216. func sendFile(url, wxId, taskId string) (bool, error) {
  217. filePath, err := DownloadImage(url, "file", taskId)
  218. if err != nil {
  219. return false, fmt.Errorf("%s%s下载文件失败: %w", url, wxId, err)
  220. }
  221. if app.WxClient.SendFile(filePath, wxId) != 0 {
  222. return false, fmt.Errorf("%s%s文件发送失败", url, wxId)
  223. }
  224. return true, nil
  225. }
  226. // 发送任务结构
  227. type SendTask struct {
  228. DataStr string
  229. RetryCount int
  230. }
  231. // 初始化工作池
  232. func InitSendTalkWorkers() {
  233. for i := 0; i < cap(workerPool); i++ {
  234. go sendTalkWorker()
  235. }
  236. }
  237. // 工作goroutine
  238. func sendTalkWorker() {
  239. for task := range sendTalkQueue {
  240. workerPool <- struct{}{} // 获取令牌
  241. processSendTask(task) // 直接处理任务
  242. <-workerPool // 释放令牌
  243. }
  244. }
  245. // 处理单个任务
  246. func processSendTask(task SendTask) {
  247. defer func() {
  248. if r := recover(); r != nil {
  249. log.Printf("SendTask panic: %v", r)
  250. }
  251. }()
  252. if task.RetryCount > 3 {
  253. log.Printf("放弃重试 taskId=%s", gconv.Map(task.DataStr)["taskId"])
  254. return
  255. }
  256. aaa := rand.Intn(config.Cfg.InformationDelay*1000) + 5000
  257. time.Sleep(time.Duration(aaa) * time.Millisecond)
  258. if err := doSendTalk(task.DataStr); err != nil {
  259. log.Printf("发送失败 (重试 %d/3): %v", task.RetryCount, err)
  260. time.Sleep(time.Second * time.Duration(task.RetryCount))
  261. sendTalkQueue <- SendTask{task.DataStr, task.RetryCount + 1}
  262. }
  263. }
  264. // 异步入队
  265. func SendTalk(dataStr string) {
  266. task := SendTask{
  267. DataStr: dataStr,
  268. RetryCount: 0,
  269. }
  270. select {
  271. case sendTalkQueue <- task: // 入队成功
  272. log.Printf("任务已入队 taskId=%s", gconv.Map(dataStr)["taskId"])
  273. default:
  274. // 队列满时降级处理
  275. log.Println("警告:发送队列已满,降级同步处理")
  276. processSendTask(task)
  277. }
  278. }
  279. // 实际发送逻辑
  280. func doSendTalk(dataStr string) error {
  281. dataMap := gconv.Map(dataStr)
  282. taskId := gconv.String(dataMap["taskId"])
  283. userMap := gconv.Map(dataMap["user"])
  284. //replyLanguage := gconv.String(dataMap["replyLanguage"])
  285. wxId := gconv.String(userMap["wxid"])
  286. appellation := gconv.String(userMap["appellation"])
  287. batchCode := gconv.String(dataMap["batchCode"])
  288. baseUserId := gconv.String(userMap["baseUserId"])
  289. ok := true
  290. // 处理内容发送
  291. for _, v := range gconv.Maps(dataMap["content"]) {
  292. content := gconv.String(v["content"])
  293. switch gconv.String(v["content_type"]) {
  294. case ContentTypeText, ContentTypeLink:
  295. ok1, err := sendText(content, wxId, appellation)
  296. if !ok1 {
  297. ok = false
  298. log.Println(err)
  299. }
  300. case ContentTypeImage:
  301. ok1, err := sendImage(content, wxId, taskId)
  302. if !ok1 {
  303. ok = false
  304. log.Println(err)
  305. }
  306. case ContentTypeVideo:
  307. ok1, err := sendVideo(content, wxId, taskId)
  308. if !ok1 {
  309. ok = false
  310. log.Println(err)
  311. }
  312. case ContentTypeFile:
  313. ok1, err := sendFile(content, wxId, taskId)
  314. if !ok1 {
  315. ok = false
  316. log.Println(err)
  317. }
  318. }
  319. }
  320. //time.Sleep(3 * time.Second)
  321. /*if replyLanguage != "" {
  322. ok1, err := sendText(replyLanguage, wxId, "")
  323. if !ok1 {
  324. ok = false
  325. log.Println(err)
  326. }
  327. }*/
  328. // 发送回执
  329. returnData := map[string]interface{}{
  330. "taskId": taskId,
  331. "batch_code": batchCode,
  332. "base_user_id": baseUserId,
  333. "isSuccess": ok,
  334. }
  335. return client.SendMessage(gconv.String(returnData), "sendTalkReceipt")
  336. }
  337. // 拒绝接受回执
  338. func Reject(text string) {
  339. time.Sleep(10 * time.Second)
  340. // 1. 解析输入数据
  341. dataMap := gconv.Map(text)
  342. if dataMap == nil {
  343. log.Println("无效的输入数据")
  344. return
  345. }
  346. contentMap := gconv.Map(dataMap["content"])
  347. if contentMap == nil {
  348. log.Println("无效的内容数据")
  349. return
  350. }
  351. wxId := gconv.String(dataMap["wxId"])
  352. if wxId == "" {
  353. log.Println("缺少接收人wxId")
  354. return
  355. }
  356. // 2. 获取内容和类型
  357. contentType := gconv.String(contentMap["content_type"])
  358. content := gconv.String(contentMap["content"])
  359. if content == "" {
  360. log.Println("消息内容为空")
  361. return
  362. }
  363. switch contentType {
  364. case ContentTypeText, ContentTypeLink:
  365. sendText(content, wxId, "")
  366. case ContentTypeImage:
  367. sendImage(content, wxId, "A101")
  368. case ContentTypeVideo:
  369. sendVideo(content, wxId, "A101")
  370. case ContentTypeFile:
  371. sendFile(content, wxId, "A101")
  372. }
  373. }
  374. // DownloadImage 下载图片、视频、文件并保存到指定格式的路径
  375. func DownloadImage(url, fileType string, taskId string) (string, error) {
  376. // 1. 创建img目录结构 (格式: img/2025/07/04/)
  377. dirPath := filepath.Join(fileType, time.Now().Format(time.DateOnly), taskId)
  378. if err := os.MkdirAll(dirPath, 0755); err != nil {
  379. return "", fmt.Errorf("创建目录失败: %v", err)
  380. }
  381. // 2. 获取文件扩展名
  382. filename, _ := GetFullFilename(url)
  383. // 3. 生成文件名 (如 task_id_1.jpg)
  384. filePath := filepath.Join(dirPath, filename)
  385. // 4. 下载文件
  386. resp, err := http.Get(url)
  387. if err != nil {
  388. return "", fmt.Errorf("下载请求失败: %v", err)
  389. }
  390. defer resp.Body.Close()
  391. if resp.StatusCode != http.StatusOK {
  392. return "", fmt.Errorf("下载失败,状态码: %d", resp.StatusCode)
  393. }
  394. // 5. 创建并保存文件
  395. out, err := os.Create(filePath)
  396. if err != nil {
  397. return "", fmt.Errorf("创建文件失败: %v", err)
  398. }
  399. defer out.Close()
  400. if _, err := io.Copy(out, resp.Body); err != nil {
  401. return "", fmt.Errorf("写入文件失败: %v", err)
  402. }
  403. // 6. 获取绝对路径
  404. absPath, err := filepath.Abs(filePath)
  405. if err != nil {
  406. return "", fmt.Errorf("获取绝对路径失败: %v", err)
  407. }
  408. return absPath, nil
  409. }
  410. // GetFullFilename 从URL中提取完整文件名(包含扩展名)
  411. func GetFullFilename(rawURL string) (string, error) {
  412. // 解析URL(处理特殊字符和路径)
  413. parsedURL, err := url.Parse(rawURL)
  414. if err != nil {
  415. return "", fmt.Errorf("URL解析失败: %w", err)
  416. }
  417. // 获取路径部分(去掉域名和查询参数)
  418. path := parsedURL.Path
  419. // 提取文件名(包含扩展名)
  420. fullFilename := filepath.Base(path)
  421. // 去掉URL查询参数(如 ?xxx=yyy)
  422. filename := strings.Split(fullFilename, "?")[0]
  423. return filename, nil
  424. }