🎨 添加联系人同步功能,更新联系人模型以支持更多字段;优化定时任务管理
All checks were successful
BuildImage / build-image (push) Successful in 2m0s

This commit is contained in:
李寻欢 2025-04-09 16:15:16 +08:00
parent 80a3de9906
commit dc28090064
5 changed files with 338 additions and 10 deletions

View File

@ -384,3 +384,56 @@ func GetWechatMessages(ctx context.Context, containerHost, robotWxId string) (ms
msg = response.Data.AddMsgs msg = response.Data.AddMsgs
return return
} }
// GetContactList
// @description: 获取联系人微信Id
// @param ctx
// @param containerHost
// @param robotWxId
// @return list
// @return err
func GetContactList(ctx context.Context, containerHost, robotWxId string) (list []string, err error) {
client := newHTTPClient()
url := fmt.Sprintf("http://%s/GetContractList", containerHost)
var response BaseResponse[ContactListResponse]
_, err = client.R().
SetContext(ctx).
SetBody(map[string]any{
"CurrentChatroomContactSeq": 0,
"CurrentWxcontactSeq": 0,
"Wxid": robotWxId,
}).
SetResult(&response).
Post(url)
list = response.Data.ContactUsernameList
return
}
// GetContactDetail
// @description: 获取联系人详细信息
// @param ctx
// @param containerHost
// @param robotWxId
// @param wxId
// @return info
// @return err
func GetContactDetail(ctx context.Context, containerHost, robotWxId string, wxId []string) (info []ContactDetailInfoItem, err error) {
client := newHTTPClient()
url := fmt.Sprintf("http://%s/GetContact", containerHost)
var response BaseResponse[ContactDetailInfoResponse]
_, err = client.R().
SetContext(ctx).
SetBody(map[string]any{
"Chatroom": "",
"RequestWxids": strings.Join(wxId, ","),
"Wxid": robotWxId,
}).
SetResult(&response).
Post(url)
info = response.Data.ContactList
return
}

View File

@ -103,3 +103,116 @@ type Message struct {
MsgSeq int `json:"MsgSeq"` MsgSeq int `json:"MsgSeq"`
PushContent string `json:"PushContent,omitempty"` PushContent string `json:"PushContent,omitempty"`
} }
// ===================================================================
type ContactListResponse struct {
BaseResponse struct {
Ret int `json:"ret"`
ErrMsg struct {
String string `json:"string"`
} `json:"errMsg"`
} `json:"BaseResponse"`
CurrentWxcontactSeq int `json:"CurrentWxcontactSeq"`
CurrentChatRoomContactSeq int `json:"CurrentChatRoomContactSeq"`
CountinueFlag int `json:"CountinueFlag"`
ContactUsernameList []string `json:"ContactUsernameList"` // 联系人微信Id列表
}
// ContactDetailInfo
// @description: 联系人详情
type ContactDetailInfoResponse struct {
BaseResponse struct {
Ret int `json:"ret"`
ErrMsg struct {
} `json:"errMsg"`
} `json:"BaseResponse"`
ContactCount int `json:"ContactCount"`
ContactList []ContactDetailInfoItem `json:"ContactList"`
Ret []int `json:"Ret"`
Ticket []struct {
} `json:"Ticket"`
}
// ContactDetailInfoItem
// @description: 详情
type ContactDetailInfoItem struct {
UserName struct {
String string `json:"string"`
} `json:"UserName"` // 微信Id
NickName struct {
String string `json:"string"`
} `json:"NickName"` // 昵称
Pyinitial struct {
String string `json:"string"`
} `json:"Pyinitial"` // 昵称拼音首字母大写
QuanPin struct {
String string `json:"string"`
} `json:"QuanPin"` // 昵称拼音全拼小写
Sex int `json:"Sex"` // 性别 0未知 1男 2
ImgBuf struct {
ILen int `json:"iLen"`
} `json:"ImgBuf"`
BitMask int64 `json:"BitMask"`
BitVal int `json:"BitVal"`
ImgFlag int `json:"ImgFlag"`
Remark struct {
} `json:"Remark"`
RemarkPyinitial struct {
} `json:"RemarkPyinitial"`
RemarkQuanPin struct {
} `json:"RemarkQuanPin"`
ContactType int `json:"ContactType"`
RoomInfoCount int `json:"RoomInfoCount"`
DomainList struct {
} `json:"DomainList"`
ChatRoomNotify int `json:"ChatRoomNotify"`
AddContactScene int `json:"AddContactScene"`
Province string `json:"Province"` // 省份
City string `json:"City"` // 城市
Signature string `json:"Signature"` // 个性签名
PersonalCard int `json:"PersonalCard"`
HasWeiXinHdHeadImg int `json:"HasWeiXinHdHeadImg"`
VerifyFlag int `json:"VerifyFlag"`
Level int `json:"Level"`
Source int `json:"Source"`
Alias string `json:"Alias"` // 微信号
WeiboFlag int `json:"WeiboFlag"`
AlbumStyle int `json:"AlbumStyle"`
AlbumFlag int `json:"AlbumFlag"`
SnsUserInfo struct {
SnsFlag int `json:"SnsFlag"`
SnsBgimgId string `json:"SnsBgimgId"` // 朋友圈背景图
SnsBgobjectId float64 `json:"SnsBgobjectId"`
SnsFlagEx int `json:"SnsFlagEx"`
} `json:"SnsUserInfo"`
Country string `json:"Country"` // 国家
BigHeadImgUrl string `json:"BigHeadImgUrl"` // 大头像地址
SmallHeadImgUrl string `json:"SmallHeadImgUrl"` // 小头像地址
MyBrandList string `json:"MyBrandList"`
CustomizedInfo struct {
BrandFlag int `json:"BrandFlag"`
} `json:"CustomizedInfo"`
HeadImgMd5 string `json:"HeadImgMd5"`
EncryptUserName string `json:"EncryptUserName"`
AdditionalContactList struct {
LinkedinContactItem struct {
} `json:"LinkedinContactItem"`
} `json:"AdditionalContactList"`
ChatroomVersion int `json:"ChatroomVersion"`
ChatroomMaxCount int `json:"ChatroomMaxCount"`
ChatroomAccessType int `json:"ChatroomAccessType"`
NewChatroomData struct {
MemberCount int `json:"MemberCount"`
InfoMask int `json:"InfoMask"`
} `json:"NewChatroomData"`
DeleteFlag int `json:"DeleteFlag"`
LabelIdlist string `json:"LabelIdlist"`
PhoneNumListInfo struct {
Count int `json:"Count"`
} `json:"PhoneNumListInfo"`
ChatroomInfoVersion int `json:"ChatroomInfoVersion"`
DeleteContactScene int `json:"DeleteContactScene"`
ChatroomStatus int `json:"ChatroomStatus"`
ExtFlag int `json:"ExtFlag"`
}

View File

@ -11,12 +11,21 @@ const (
// Contact 表示微信联系人,包括好友和群组 // Contact 表示微信联系人,包括好友和群组
type Contact struct { type Contact struct {
BaseModel BaseModel
RobotID uint `gorm:"column:robot_id;index" json:"robot_id"` RobotID uint `gorm:"column:robot_id;index:deleted,unique" json:"robot_id"`
WechatID string `gorm:"column:wechat_id;index:idx_contact_wechat_id,length:64" json:"wechat_id"` // 添加索引长度 WechatID string `gorm:"column:wechat_id;index:deleted,unique" json:"wechat_id"` // 添加索引长度
Nickname string `gorm:"column:nickname" json:"nickname"` Alias string `gorm:"column:alias" json:"alias"` // 微信号
Avatar string `gorm:"column:avatar" json:"avatar"` Nickname string `gorm:"column:nickname" json:"nickname"`
Type ContactType `gorm:"column:type" json:"type"` Avatar string `gorm:"column:avatar" json:"avatar"`
Remark string `gorm:"column:remark" json:"remark"` Type ContactType `gorm:"column:type" json:"type"`
Remark string `gorm:"column:remark" json:"remark"`
Pyinitial string `gorm:"column:pyinitial" json:"pyinitial"` // 昵称拼音首字母大写
QuanPin string `gorm:"column:quan_pin" json:"quan_pin"` // 昵称拼音全拼小写
Sex int `gorm:"column:sex" json:"sex"` // 性别 0未知 1男 2
Country string `gorm:"column:country" json:"country"` // 国家
Province string `gorm:"column:province" json:"province"` // 省份
City string `gorm:"column:city" json:"city"` // 城市
Signature string `gorm:"column:signature" json:"signature"` // 个性签名
SnsBackground string `gorm:"column:sns_background" json:"sns_background"` // 朋友圈背景图
} }
// TableName 指定表名 // TableName 指定表名

115
internal/tasks/contract.go Normal file
View File

@ -0,0 +1,115 @@
package tasks
import (
"context"
"gitee.ltd/lxh/wechat-robot/internal/docker"
"gitee.ltd/lxh/wechat-robot/internal/model"
"github.com/gofiber/fiber/v2/log"
"slices"
"strings"
"time"
)
// syncContact
// @description: 同步联系人
// @param containerHost 容器接口地址
// @param robotWxId 机器人微信号
// @param robotId 机器人ID
func syncContact(containerHost, robotWxId string, robotId uint) {
ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
defer cancel()
// 先获取全部id
ids, err := docker.GetContactList(ctx, containerHost, robotWxId)
if err != nil {
// 处理错误
log.Errorf("[%s]获取联系人列表失败: %v", robotWxId, err)
return
}
// 过滤掉特殊微信Id
var specialId = []string{"filehelper", "newsapp", "fmessage", "weibo", "qqmail", "tmessage", "qmessage", "qqsync",
"floatbottle", "lbsapp", "shakeapp", "medianote", "qqfriend", "readerapp", "blogapp", "facebookapp", "masssendapp",
"meishiapp", "feedsapp", "voip", "blogappweixin", "weixin", "brandsessionholder", "weixinreminder", "officialaccounts",
"notification_messages", "wxitil", "userexperience_alarm", "notification_messages", "exmail_tool", "mphelper"}
ids = slices.DeleteFunc(ids, func(id string) bool {
return slices.Contains(specialId, id) || strings.HasPrefix(id, "gh_") || strings.TrimSpace(id) == ""
})
// 获取昵称等详细信息
contacts, err := docker.GetContactDetail(ctx, containerHost, robotWxId, ids)
if err != nil {
// 处理错误
log.Errorf("[%s]获取联系人详情失败: %v", robotWxId, err)
return
}
// 循环联系人信息,打印一下
db := model.GetDB()
nowIds := make([]string, 0)
for _, contact := range contacts {
//log.Infof("[%s]联系人信息: %+v", robotWxId, contact)
if strings.TrimSpace(contact.UserName.String) == "" {
// 微信号为空,跳过
continue
}
nowIds = append(nowIds, contact.UserName.String)
// 判断数据库是否存在当前数据,不存在就新建,存在就更新
var existId uint
db.Model(&model.Contact{}).Where("robot_id = ?", robotId).Where("wechat_id = ?", contact.UserName.String).Pluck("id", &existId)
if existId > 0 {
// 存在,修改
pm := map[string]any{
"alias": contact.Alias,
"nickname": contact.NickName.String,
"avatar": contact.BigHeadImgUrl,
"pyinitial": contact.Pyinitial.String,
"quan_pin": contact.QuanPin.String,
"sex": contact.Sex,
"country": contact.Country,
"province": contact.Province,
"city": contact.City,
"signature": contact.Signature,
"sns_background": contact.SnsUserInfo.SnsBgimgId,
}
if contact.BigHeadImgUrl == "" {
pm["avatar"] = contact.SmallHeadImgUrl
}
err = db.Model(&model.Contact{}).Where("id = ?", existId).Updates(pm).Error
} else {
// 组装联系人信息,然后存入数据库
var c model.Contact
c.RobotID = robotId
c.WechatID = contact.UserName.String
c.Alias = contact.Alias
c.Nickname = contact.NickName.String
c.Avatar = contact.BigHeadImgUrl
if contact.BigHeadImgUrl == "" {
c.Avatar = contact.SmallHeadImgUrl
}
c.Type = "friend"
if strings.HasSuffix(contact.UserName.String, "@chatroom") {
// 群聊
c.Type = "group"
}
c.Pyinitial = contact.Pyinitial.String
c.QuanPin = contact.QuanPin.String
c.Sex = contact.Sex
c.Country = contact.Country
c.Province = contact.Province
c.City = contact.City
c.Signature = contact.Signature
c.SnsBackground = contact.SnsUserInfo.SnsBgimgId
err = db.Create(&c).Error
}
if err != nil {
log.Debugf("[%s]保存联系人失败: %v", robotWxId, err)
}
}
// 清理掉不存在的联系人
db.Model(&model.Contact{}).Where("robot_id = ?", robotId).Where("wechat_id NOT IN ?", nowIds).Delete(&model.Contact{})
}

View File

@ -12,7 +12,8 @@ import (
var scheduler gocron.Scheduler var scheduler gocron.Scheduler
// 已启动定时任务的微信机器人 // 已启动定时任务的微信机器人
var enabledMap = sync.Map{} var enabledSyncMessageMap = sync.Map{}
var enabledSyncContactMap = sync.Map{}
// Start // Start
// @description: 启动任务 // @description: 启动任务
@ -41,7 +42,18 @@ func Start() {
log.Panicf("添加定时任务失败: %v", err) log.Panicf("添加定时任务失败: %v", err)
} }
// 添加到已启动的任务列表 // 添加到已启动的任务列表
enabledMap.Store(robot.ID, job.ID()) enabledSyncMessageMap.Store(robot.ID, job.ID())
// 添加联系人同步任务
job, err = scheduler.NewJob(
gocron.CronJob("0 */1 * * *", true), // 每小时同步一次
gocron.NewTask(syncContact, robot.ContainerHost, robot.WechatID, robot.ID),
)
if err != nil {
log.Panicf("添加联系人同步任务失败: %v", err)
}
// 添加到已启动的任务列表
enabledSyncContactMap.Store(robot.ID, job.ID())
} }
// 启动定时任务 // 启动定时任务
@ -61,7 +73,18 @@ func AddJob(robot model.Robot) {
return return
} }
// 添加到已启动的任务列表 // 添加到已启动的任务列表
enabledMap.Store(robot.ID, job.ID()) enabledSyncMessageMap.Store(robot.ID, job.ID())
// 添加联系人同步任务
job, err = scheduler.NewJob(
gocron.CronJob("0 */1 * * *", true), // 每小时同步一次
gocron.NewTask(syncContact, robot.ContainerHost, robot.WechatID, robot.ID),
)
if err != nil {
log.Panicf("添加联系人同步任务失败: %v", err)
}
// 添加到已启动的任务列表
enabledSyncContactMap.Store(robot.ID, job.ID())
} }
// DeleteJob // DeleteJob
@ -69,7 +92,7 @@ func AddJob(robot model.Robot) {
// @param robotId // @param robotId
func DeleteJob(robotId uint) { func DeleteJob(robotId uint) {
// 先取出任务Id // 先取出任务Id
jobId, ok := enabledMap.Load(robotId) jobId, ok := enabledSyncMessageMap.Load(robotId)
if !ok { if !ok {
log.Printf("定时任务不存在robotId: %d", robotId) log.Printf("定时任务不存在robotId: %d", robotId)
return return
@ -78,4 +101,19 @@ func DeleteJob(robotId uint) {
log.Printf("删除定时任务失败: %v", err) log.Printf("删除定时任务失败: %v", err)
return return
} }
// 删除已启动的任务列表
enabledSyncMessageMap.Delete(robotId)
// 删除联系人同步任务
jobId, ok = enabledSyncContactMap.Load(robotId)
if !ok {
log.Printf("联系人同步任务不存在robotId: %d", robotId)
return
}
if err := scheduler.RemoveJob(jobId.(uuid.UUID)); err != nil {
log.Printf("删除联系人同步任务失败: %v", err)
return
}
// 删除已启动的任务列表
enabledSyncContactMap.Delete(robotId)
} }