1497 lines
37 KiB
Go
1497 lines
37 KiB
Go
package windowsv3
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"encoding/hex"
|
||
"fmt"
|
||
"regexp"
|
||
"sort"
|
||
"strings"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/fsnotify/fsnotify"
|
||
_ "github.com/mattn/go-sqlite3"
|
||
"github.com/rs/zerolog/log"
|
||
|
||
"github.com/sjzar/chatlog/internal/errors"
|
||
"github.com/sjzar/chatlog/internal/model"
|
||
"github.com/sjzar/chatlog/internal/wechatdb/datasource/dbm"
|
||
"github.com/sjzar/chatlog/pkg/util"
|
||
)
|
||
|
||
const (
|
||
Message = "message"
|
||
Contact = "contact"
|
||
Image = "image"
|
||
Video = "video"
|
||
File = "file"
|
||
Voice = "voice"
|
||
|
||
talkerCacheTTL = 30 * time.Second
|
||
)
|
||
|
||
var Groups = []*dbm.Group{
|
||
{
|
||
Name: Message,
|
||
Pattern: `^MSG([0-9]?[0-9])?\.db$`,
|
||
BlackList: []string{},
|
||
},
|
||
{
|
||
Name: Contact,
|
||
Pattern: `^MicroMsg\.db$`,
|
||
BlackList: []string{},
|
||
},
|
||
{
|
||
Name: Image,
|
||
Pattern: `^HardLinkImage\.db$`,
|
||
BlackList: []string{},
|
||
},
|
||
{
|
||
Name: Video,
|
||
Pattern: `^HardLinkVideo\.db$`,
|
||
BlackList: []string{},
|
||
},
|
||
{
|
||
Name: File,
|
||
Pattern: `^HardLinkFile\.db$`,
|
||
BlackList: []string{},
|
||
},
|
||
{
|
||
Name: Voice,
|
||
Pattern: `^MediaMSG([0-9]?[0-9])?\.db$`,
|
||
BlackList: []string{},
|
||
},
|
||
}
|
||
|
||
// MessageDBInfo 保存消息数据库的信息
|
||
type MessageDBInfo struct {
|
||
FilePath string
|
||
StartTime time.Time
|
||
EndTime time.Time
|
||
TalkerMap map[string]int
|
||
}
|
||
|
||
// DataSource 实现了 DataSource 接口
|
||
type DataSource struct {
|
||
path string
|
||
dbm *dbm.DBManager
|
||
|
||
// 消息数据库信息
|
||
messageInfos []MessageDBInfo
|
||
|
||
talkerCacheMu sync.RWMutex
|
||
talkerCache []string
|
||
talkerCacheExpiry time.Time
|
||
}
|
||
|
||
// New 创建一个新的 WindowsV3DataSource
|
||
func New(path string) (*DataSource, error) {
|
||
ds := &DataSource{
|
||
path: path,
|
||
dbm: dbm.NewDBManager(path),
|
||
messageInfos: make([]MessageDBInfo, 0),
|
||
}
|
||
|
||
for _, g := range Groups {
|
||
ds.dbm.AddGroup(g)
|
||
}
|
||
|
||
if err := ds.dbm.Start(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
if err := ds.initMessageDbs(); err != nil {
|
||
return nil, errors.DBInitFailed(err)
|
||
}
|
||
|
||
ds.dbm.AddCallback(Message, func(event fsnotify.Event) error {
|
||
if !(event.Op.Has(fsnotify.Create) || event.Op.Has(fsnotify.Write) || event.Op.Has(fsnotify.Rename)) {
|
||
return nil
|
||
}
|
||
if err := ds.initMessageDbs(); err != nil {
|
||
log.Err(err).Msgf("Failed to reinitialize message DBs: %s", event.Name)
|
||
}
|
||
ds.invalidateTalkerCache()
|
||
return nil
|
||
})
|
||
|
||
ds.dbm.AddCallback(Contact, func(event fsnotify.Event) error {
|
||
if !(event.Op.Has(fsnotify.Create) || event.Op.Has(fsnotify.Write) || event.Op.Has(fsnotify.Rename) || event.Op.Has(fsnotify.Remove)) {
|
||
return nil
|
||
}
|
||
ds.invalidateTalkerCache()
|
||
return nil
|
||
})
|
||
|
||
return ds, nil
|
||
}
|
||
|
||
func (ds *DataSource) SetCallback(group string, callback func(event fsnotify.Event) error) error {
|
||
if group == "chatroom" {
|
||
group = Contact
|
||
}
|
||
return ds.dbm.AddCallback(group, callback)
|
||
}
|
||
|
||
// initMessageDbs 初始化消息数据库
|
||
func (ds *DataSource) initMessageDbs() error {
|
||
// 获取所有消息数据库文件路径
|
||
dbPaths, err := ds.dbm.GetDBPath(Message)
|
||
if err != nil {
|
||
if strings.Contains(err.Error(), "db file not found") {
|
||
ds.messageInfos = make([]MessageDBInfo, 0)
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
|
||
// 处理每个数据库文件
|
||
infos := make([]MessageDBInfo, 0)
|
||
for _, filePath := range dbPaths {
|
||
db, err := ds.dbm.OpenDB(filePath)
|
||
if err != nil {
|
||
log.Err(err).Msgf("获取数据库 %s 失败", filePath)
|
||
continue
|
||
}
|
||
|
||
// 获取 DBInfo 表中的开始时间
|
||
var startTime time.Time
|
||
|
||
rows, err := db.Query("SELECT tableIndex, tableVersion, tableDesc FROM DBInfo")
|
||
if err != nil {
|
||
log.Err(err).Msgf("查询数据库 %s 的 DBInfo 表失败", filePath)
|
||
continue
|
||
}
|
||
|
||
for rows.Next() {
|
||
var tableIndex int
|
||
var tableVersion int64
|
||
var tableDesc string
|
||
|
||
if err := rows.Scan(&tableIndex, &tableVersion, &tableDesc); err != nil {
|
||
log.Err(err).Msg("扫描 DBInfo 行失败")
|
||
continue
|
||
}
|
||
|
||
// 查找描述为 "Start Time" 的记录
|
||
if strings.Contains(tableDesc, "Start Time") {
|
||
startTime = time.Unix(tableVersion/1000, (tableVersion%1000)*1000000)
|
||
break
|
||
}
|
||
}
|
||
rows.Close()
|
||
|
||
// 组织 TalkerMap
|
||
talkerMap := make(map[string]int)
|
||
rows, err = db.Query("SELECT UsrName FROM Name2ID")
|
||
if err != nil {
|
||
log.Err(err).Msgf("查询数据库 %s 的 Name2ID 表失败", filePath)
|
||
continue
|
||
}
|
||
|
||
i := 1
|
||
for rows.Next() {
|
||
var userName string
|
||
if err := rows.Scan(&userName); err != nil {
|
||
log.Err(err).Msg("扫描 Name2ID 行失败")
|
||
continue
|
||
}
|
||
talkerMap[userName] = i
|
||
i++
|
||
}
|
||
rows.Close()
|
||
|
||
// 保存数据库信息
|
||
infos = append(infos, MessageDBInfo{
|
||
FilePath: filePath,
|
||
StartTime: startTime,
|
||
TalkerMap: talkerMap,
|
||
})
|
||
}
|
||
|
||
// 按照 StartTime 排序数据库文件
|
||
sort.Slice(infos, func(i, j int) bool {
|
||
return infos[i].StartTime.Before(infos[j].StartTime)
|
||
})
|
||
|
||
// 设置结束时间
|
||
for i := range infos {
|
||
if i == len(infos)-1 {
|
||
infos[i].EndTime = time.Now()
|
||
} else {
|
||
infos[i].EndTime = infos[i+1].StartTime
|
||
}
|
||
}
|
||
if len(ds.messageInfos) > 0 && len(infos) < len(ds.messageInfos) {
|
||
log.Warn().Msgf("message db count decreased from %d to %d, skip init", len(ds.messageInfos), len(infos))
|
||
return nil
|
||
}
|
||
ds.messageInfos = infos
|
||
ds.invalidateTalkerCache()
|
||
return nil
|
||
}
|
||
|
||
// getDBInfosForTimeRange 获取时间范围内的数据库信息
|
||
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
|
||
var dbs []MessageDBInfo
|
||
for _, info := range ds.messageInfos {
|
||
if info.StartTime.Before(endTime) && info.EndTime.After(startTime) {
|
||
dbs = append(dbs, info)
|
||
}
|
||
}
|
||
return dbs
|
||
}
|
||
|
||
func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, sender string, keyword string, limit, offset int) ([]*model.Message, error) {
|
||
if talker == "" {
|
||
return nil, errors.ErrTalkerEmpty
|
||
}
|
||
|
||
// 解析talker参数,支持多个talker(以英文逗号分隔)
|
||
talkers := util.Str2List(talker, ",")
|
||
if len(talkers) == 0 {
|
||
return nil, errors.ErrTalkerEmpty
|
||
}
|
||
|
||
// 找到时间范围内的数据库文件
|
||
dbInfos := ds.getDBInfosForTimeRange(startTime, endTime)
|
||
if len(dbInfos) == 0 {
|
||
return nil, errors.TimeRangeNotFound(startTime, endTime)
|
||
}
|
||
|
||
// 解析sender参数,支持多个发送者(以英文逗号分隔)
|
||
senders := util.Str2List(sender, ",")
|
||
|
||
// 预编译正则表达式(如果有keyword)
|
||
var regex *regexp.Regexp
|
||
if keyword != "" {
|
||
var err error
|
||
regex, err = regexp.Compile(keyword)
|
||
if err != nil {
|
||
return nil, errors.QueryFailed("invalid regex pattern", err)
|
||
}
|
||
}
|
||
|
||
// 从每个相关数据库中查询消息
|
||
filteredMessages := []*model.Message{}
|
||
|
||
for _, dbInfo := range dbInfos {
|
||
// 检查上下文是否已取消
|
||
if err := ctx.Err(); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
db, err := ds.dbm.OpenDB(dbInfo.FilePath)
|
||
if err != nil {
|
||
log.Error().Msgf("数据库 %s 未打开", dbInfo.FilePath)
|
||
continue
|
||
}
|
||
|
||
// 对每个talker进行查询
|
||
for _, talkerItem := range talkers {
|
||
// 构建查询条件
|
||
conditions := []string{"Sequence >= ? AND Sequence <= ?"}
|
||
args := []interface{}{startTime.Unix() * 1000, endTime.Unix() * 1000}
|
||
|
||
// 添加talker条件
|
||
talkerID, ok := dbInfo.TalkerMap[talkerItem]
|
||
if ok {
|
||
conditions = append(conditions, "TalkerId = ?")
|
||
args = append(args, talkerID)
|
||
} else {
|
||
conditions = append(conditions, "StrTalker = ?")
|
||
args = append(args, talkerItem)
|
||
}
|
||
|
||
query := fmt.Sprintf(`
|
||
SELECT MsgSvrID, Sequence, CreateTime, StrTalker, IsSender,
|
||
Type, SubType, StrContent, CompressContent, BytesExtra
|
||
FROM MSG
|
||
WHERE %s
|
||
ORDER BY Sequence ASC
|
||
`, strings.Join(conditions, " AND "))
|
||
|
||
// 执行查询
|
||
rows, err := db.QueryContext(ctx, query, args...)
|
||
if err != nil {
|
||
// 如果表不存在,跳过此talker
|
||
if strings.Contains(err.Error(), "no such table") {
|
||
continue
|
||
}
|
||
log.Err(err).Msgf("从数据库 %s 查询消息失败", dbInfo.FilePath)
|
||
continue
|
||
}
|
||
|
||
// 处理查询结果,在读取时进行过滤
|
||
for rows.Next() {
|
||
var msg model.MessageV3
|
||
var compressContent []byte
|
||
var bytesExtra []byte
|
||
|
||
err := rows.Scan(
|
||
&msg.MsgSvrID,
|
||
&msg.Sequence,
|
||
&msg.CreateTime,
|
||
&msg.StrTalker,
|
||
&msg.IsSender,
|
||
&msg.Type,
|
||
&msg.SubType,
|
||
&msg.StrContent,
|
||
&compressContent,
|
||
&bytesExtra,
|
||
)
|
||
if err != nil {
|
||
rows.Close()
|
||
return nil, errors.ScanRowFailed(err)
|
||
}
|
||
msg.CompressContent = compressContent
|
||
msg.BytesExtra = bytesExtra
|
||
|
||
// 将消息转换为标准格式
|
||
message := msg.Wrap()
|
||
|
||
// 应用sender过滤
|
||
if len(senders) > 0 {
|
||
senderMatch := false
|
||
for _, s := range senders {
|
||
if message.Sender == s {
|
||
senderMatch = true
|
||
break
|
||
}
|
||
}
|
||
if !senderMatch {
|
||
continue // 不匹配sender,跳过此消息
|
||
}
|
||
}
|
||
|
||
// 应用keyword过滤
|
||
if regex != nil {
|
||
plainText := message.PlainTextContent()
|
||
if !regex.MatchString(plainText) {
|
||
continue // 不匹配keyword,跳过此消息
|
||
}
|
||
}
|
||
|
||
// 通过所有过滤条件,保留此消息
|
||
filteredMessages = append(filteredMessages, message)
|
||
|
||
// 检查是否已经满足分页处理数量
|
||
if limit > 0 && len(filteredMessages) >= offset+limit {
|
||
// 已经获取了足够的消息,可以提前返回
|
||
rows.Close()
|
||
|
||
// 对所有消息按时间排序
|
||
sort.Slice(filteredMessages, func(i, j int) bool {
|
||
return filteredMessages[i].Seq < filteredMessages[j].Seq
|
||
})
|
||
|
||
// 处理分页
|
||
if offset >= len(filteredMessages) {
|
||
return []*model.Message{}, nil
|
||
}
|
||
end := offset + limit
|
||
if end > len(filteredMessages) {
|
||
end = len(filteredMessages)
|
||
}
|
||
return filteredMessages[offset:end], nil
|
||
}
|
||
}
|
||
rows.Close()
|
||
}
|
||
}
|
||
|
||
// 对所有消息按时间排序
|
||
sort.Slice(filteredMessages, func(i, j int) bool {
|
||
return filteredMessages[i].Seq < filteredMessages[j].Seq
|
||
})
|
||
|
||
// 处理分页
|
||
if limit > 0 {
|
||
if offset >= len(filteredMessages) {
|
||
return []*model.Message{}, nil
|
||
}
|
||
end := offset + limit
|
||
if end > len(filteredMessages) {
|
||
end = len(filteredMessages)
|
||
}
|
||
return filteredMessages[offset:end], nil
|
||
}
|
||
|
||
return filteredMessages, nil
|
||
}
|
||
|
||
func (ds *DataSource) GetDatasetFingerprint(context.Context) (string, error) {
|
||
return ds.dbm.FingerprintForGroups(Message)
|
||
}
|
||
|
||
func (ds *DataSource) getCachedTalkers() []string {
|
||
ds.talkerCacheMu.RLock()
|
||
if ds.talkerCacheExpiry.IsZero() || time.Now().After(ds.talkerCacheExpiry) {
|
||
ds.talkerCacheMu.RUnlock()
|
||
return nil
|
||
}
|
||
cached := append([]string(nil), ds.talkerCache...)
|
||
ds.talkerCacheMu.RUnlock()
|
||
return cached
|
||
}
|
||
|
||
func (ds *DataSource) cacheTalkers(talkers []string) {
|
||
copySlice := append([]string(nil), talkers...)
|
||
ds.talkerCacheMu.Lock()
|
||
ds.talkerCache = copySlice
|
||
ds.talkerCacheExpiry = time.Now().Add(talkerCacheTTL)
|
||
ds.talkerCacheMu.Unlock()
|
||
}
|
||
|
||
func (ds *DataSource) invalidateTalkerCache() {
|
||
ds.talkerCacheMu.Lock()
|
||
ds.talkerCache = nil
|
||
ds.talkerCacheExpiry = time.Time{}
|
||
ds.talkerCacheMu.Unlock()
|
||
}
|
||
|
||
func (ds *DataSource) collectAllTalkers(ctx context.Context) ([]string, error) {
|
||
if cached := ds.getCachedTalkers(); cached != nil {
|
||
return cached, nil
|
||
}
|
||
|
||
talkerSet := make(map[string]struct{})
|
||
talkers := make([]string, 0)
|
||
|
||
add := func(name string) {
|
||
name = strings.TrimSpace(name)
|
||
if name == "" {
|
||
return
|
||
}
|
||
if _, exists := talkerSet[name]; exists {
|
||
return
|
||
}
|
||
talkerSet[name] = struct{}{}
|
||
talkers = append(talkers, name)
|
||
}
|
||
|
||
if db, err := ds.dbm.GetDB(Contact); err == nil && db != nil {
|
||
rows, err := db.QueryContext(ctx, `SELECT strUsrName FROM Session WHERE IFNULL(strUsrName,'') <> ''`)
|
||
if err == nil {
|
||
for rows.Next() {
|
||
if err := ctx.Err(); err != nil {
|
||
rows.Close()
|
||
return nil, err
|
||
}
|
||
var username string
|
||
if err := rows.Scan(&username); err != nil {
|
||
log.Debug().Err(err).Msg("scan session talker failed")
|
||
continue
|
||
}
|
||
add(username)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
log.Debug().Err(err).Msg("iterate session talkers failed")
|
||
}
|
||
rows.Close()
|
||
} else {
|
||
log.Debug().Err(err).Msg("query session talkers failed")
|
||
}
|
||
|
||
if len(talkers) == 0 {
|
||
rows, err = db.QueryContext(ctx, `SELECT UserName FROM Contact WHERE IFNULL(UserName,'') <> ''`)
|
||
if err == nil {
|
||
for rows.Next() {
|
||
if err := ctx.Err(); err != nil {
|
||
rows.Close()
|
||
return nil, err
|
||
}
|
||
var username string
|
||
if err := rows.Scan(&username); err != nil {
|
||
log.Debug().Err(err).Msg("scan contact talker failed")
|
||
continue
|
||
}
|
||
add(username)
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
log.Debug().Err(err).Msg("iterate contact talkers failed")
|
||
}
|
||
rows.Close()
|
||
} else {
|
||
log.Debug().Err(err).Msg("query contact talkers failed")
|
||
}
|
||
}
|
||
}
|
||
|
||
if len(talkers) == 0 {
|
||
for _, info := range ds.messageInfos {
|
||
for username := range info.TalkerMap {
|
||
add(username)
|
||
}
|
||
}
|
||
}
|
||
|
||
sort.Strings(talkers)
|
||
ds.cacheTalkers(talkers)
|
||
return talkers, nil
|
||
}
|
||
|
||
// ListTalkers 返回所有对话用户名(联系人 + 群聊),供索引使用
|
||
func (ds *DataSource) ListTalkers(ctx context.Context) ([]string, error) {
|
||
return ds.collectAllTalkers(ctx)
|
||
}
|
||
|
||
// IterateMessages 按 talker 枚举消息并交给处理函数,供 Bleve 索引使用
|
||
func (ds *DataSource) IterateMessages(ctx context.Context, talkers []string, handler func(*model.Message) error) error {
|
||
if handler == nil {
|
||
return errors.InvalidArg("handler")
|
||
}
|
||
|
||
if len(talkers) == 0 {
|
||
var err error
|
||
talkers, err = ds.collectAllTalkers(ctx)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
}
|
||
if len(talkers) == 0 {
|
||
return nil
|
||
}
|
||
|
||
uniqueTalkers := make([]string, 0, len(talkers))
|
||
seen := make(map[string]struct{}, len(talkers))
|
||
for _, t := range talkers {
|
||
t = strings.TrimSpace(t)
|
||
if t == "" {
|
||
continue
|
||
}
|
||
if _, ok := seen[t]; ok {
|
||
continue
|
||
}
|
||
seen[t] = struct{}{}
|
||
uniqueTalkers = append(uniqueTalkers, t)
|
||
}
|
||
if len(uniqueTalkers) == 0 {
|
||
return nil
|
||
}
|
||
|
||
for _, info := range ds.messageInfos {
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
|
||
db, err := ds.dbm.OpenDB(info.FilePath)
|
||
if err != nil {
|
||
log.Debug().Err(err).Msgf("open message db failed: %s", info.FilePath)
|
||
continue
|
||
}
|
||
|
||
for _, talker := range uniqueTalkers {
|
||
if err := ctx.Err(); err != nil {
|
||
return err
|
||
}
|
||
|
||
conditions := []string{"StrContent IS NOT NULL"}
|
||
args := make([]interface{}, 0, 1)
|
||
if talkerID, ok := info.TalkerMap[talker]; ok {
|
||
conditions = append(conditions, "TalkerId = ?")
|
||
args = append(args, talkerID)
|
||
} else {
|
||
conditions = append(conditions, "StrTalker = ?")
|
||
args = append(args, talker)
|
||
}
|
||
|
||
query := fmt.Sprintf(`
|
||
SELECT MsgSvrID, Sequence, CreateTime, StrTalker, IsSender,
|
||
Type, SubType, StrContent, CompressContent, BytesExtra
|
||
FROM MSG
|
||
WHERE %s
|
||
ORDER BY Sequence ASC
|
||
`, strings.Join(conditions, " AND "))
|
||
|
||
rows, err := db.QueryContext(ctx, query, args...)
|
||
if err != nil {
|
||
if strings.Contains(err.Error(), "no such table") {
|
||
continue
|
||
}
|
||
return errors.QueryFailed("iterate messages", err)
|
||
}
|
||
|
||
for rows.Next() {
|
||
if err := ctx.Err(); err != nil {
|
||
rows.Close()
|
||
return err
|
||
}
|
||
var msg model.MessageV3
|
||
var compressContent []byte
|
||
var bytesExtra []byte
|
||
if scanErr := rows.Scan(
|
||
&msg.MsgSvrID,
|
||
&msg.Sequence,
|
||
&msg.CreateTime,
|
||
&msg.StrTalker,
|
||
&msg.IsSender,
|
||
&msg.Type,
|
||
&msg.SubType,
|
||
&msg.StrContent,
|
||
&compressContent,
|
||
&bytesExtra,
|
||
); scanErr != nil {
|
||
rows.Close()
|
||
return errors.ScanRowFailed(scanErr)
|
||
}
|
||
msg.CompressContent = compressContent
|
||
msg.BytesExtra = bytesExtra
|
||
|
||
wrapped := msg.Wrap()
|
||
if err := handler(wrapped); err != nil {
|
||
rows.Close()
|
||
return err
|
||
}
|
||
}
|
||
if err := rows.Err(); err != nil {
|
||
rows.Close()
|
||
return errors.QueryFailed("iterate message rows", err)
|
||
}
|
||
rows.Close()
|
||
}
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// GetContacts 实现获取联系人信息的方法
|
||
func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error) {
|
||
var query string
|
||
var args []interface{}
|
||
|
||
if key != "" {
|
||
// 按照关键字查询
|
||
query = `SELECT UserName, Alias, Remark, NickName, Reserved1 FROM Contact
|
||
WHERE UserName = ? OR Alias = ? OR Remark = ? OR NickName = ?`
|
||
args = []interface{}{key, key, key, key}
|
||
} else {
|
||
// 查询所有联系人
|
||
query = `SELECT UserName, Alias, Remark, NickName, Reserved1 FROM Contact`
|
||
}
|
||
|
||
// 添加排序、分页
|
||
query += ` ORDER BY UserName`
|
||
if limit > 0 {
|
||
query += fmt.Sprintf(" LIMIT %d", limit)
|
||
if offset > 0 {
|
||
query += fmt.Sprintf(" OFFSET %d", offset)
|
||
}
|
||
}
|
||
|
||
// 执行查询
|
||
db, err := ds.dbm.GetDB(Contact)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
rows, err := db.QueryContext(ctx, query, args...)
|
||
if err != nil {
|
||
return nil, errors.QueryFailed(query, err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
contacts := []*model.Contact{}
|
||
for rows.Next() {
|
||
var contactV3 model.ContactV3
|
||
err := rows.Scan(
|
||
&contactV3.UserName,
|
||
&contactV3.Alias,
|
||
&contactV3.Remark,
|
||
&contactV3.NickName,
|
||
&contactV3.Reserved1,
|
||
)
|
||
|
||
if err != nil {
|
||
return nil, errors.ScanRowFailed(err)
|
||
}
|
||
|
||
contacts = append(contacts, contactV3.Wrap())
|
||
}
|
||
|
||
return contacts, nil
|
||
}
|
||
|
||
// GetChatRooms 实现获取群聊信息的方法
|
||
func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error) {
|
||
var query string
|
||
var args []interface{}
|
||
|
||
if key != "" {
|
||
// 按照关键字查询
|
||
query = `SELECT ChatRoomName, Reserved2, RoomData FROM ChatRoom WHERE ChatRoomName = ?`
|
||
args = []interface{}{key}
|
||
|
||
// 执行查询
|
||
db, err := ds.dbm.GetDB(Contact)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
rows, err := db.QueryContext(ctx, query, args...)
|
||
if err != nil {
|
||
return nil, errors.QueryFailed(query, err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
chatRooms := []*model.ChatRoom{}
|
||
for rows.Next() {
|
||
var chatRoomV3 model.ChatRoomV3
|
||
err := rows.Scan(
|
||
&chatRoomV3.ChatRoomName,
|
||
&chatRoomV3.Reserved2,
|
||
&chatRoomV3.RoomData,
|
||
)
|
||
|
||
if err != nil {
|
||
return nil, errors.ScanRowFailed(err)
|
||
}
|
||
|
||
chatRooms = append(chatRooms, chatRoomV3.Wrap())
|
||
}
|
||
|
||
// 如果没有找到群聊,尝试通过联系人查找
|
||
if len(chatRooms) == 0 {
|
||
contacts, err := ds.GetContacts(ctx, key, 1, 0)
|
||
if err == nil && len(contacts) > 0 && strings.HasSuffix(contacts[0].UserName, "@chatroom") {
|
||
// 再次尝试通过用户名查找群聊
|
||
rows, err := db.QueryContext(ctx,
|
||
`SELECT ChatRoomName, Reserved2, RoomData FROM ChatRoom WHERE ChatRoomName = ?`,
|
||
contacts[0].UserName)
|
||
|
||
if err != nil {
|
||
return nil, errors.QueryFailed(query, err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
for rows.Next() {
|
||
var chatRoomV3 model.ChatRoomV3
|
||
err := rows.Scan(
|
||
&chatRoomV3.ChatRoomName,
|
||
&chatRoomV3.Reserved2,
|
||
&chatRoomV3.RoomData,
|
||
)
|
||
|
||
if err != nil {
|
||
return nil, errors.ScanRowFailed(err)
|
||
}
|
||
|
||
chatRooms = append(chatRooms, chatRoomV3.Wrap())
|
||
}
|
||
|
||
// 如果群聊记录不存在,但联系人记录存在,创建一个模拟的群聊对象
|
||
if len(chatRooms) == 0 {
|
||
chatRooms = append(chatRooms, &model.ChatRoom{
|
||
Name: contacts[0].UserName,
|
||
Users: make([]model.ChatRoomUser, 0),
|
||
User2DisplayName: make(map[string]string),
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
return chatRooms, nil
|
||
} else {
|
||
// 查询所有群聊
|
||
query = `SELECT ChatRoomName, Reserved2, RoomData FROM ChatRoom`
|
||
|
||
// 添加排序、分页
|
||
query += ` ORDER BY ChatRoomName`
|
||
if limit > 0 {
|
||
query += fmt.Sprintf(" LIMIT %d", limit)
|
||
if offset > 0 {
|
||
query += fmt.Sprintf(" OFFSET %d", offset)
|
||
}
|
||
}
|
||
|
||
// 执行查询
|
||
db, err := ds.dbm.GetDB(Contact)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
rows, err := db.QueryContext(ctx, query, args...)
|
||
if err != nil {
|
||
return nil, errors.QueryFailed(query, err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
chatRooms := []*model.ChatRoom{}
|
||
for rows.Next() {
|
||
var chatRoomV3 model.ChatRoomV3
|
||
err := rows.Scan(
|
||
&chatRoomV3.ChatRoomName,
|
||
&chatRoomV3.Reserved2,
|
||
&chatRoomV3.RoomData,
|
||
)
|
||
|
||
if err != nil {
|
||
return nil, errors.ScanRowFailed(err)
|
||
}
|
||
|
||
chatRooms = append(chatRooms, chatRoomV3.Wrap())
|
||
}
|
||
|
||
return chatRooms, nil
|
||
}
|
||
}
|
||
|
||
// GetSessions 实现获取会话信息的方法
|
||
func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error) {
|
||
var query string
|
||
var args []interface{}
|
||
|
||
if key != "" {
|
||
// 按照关键字查询
|
||
query = `SELECT strUsrName, nOrder, strNickName, strContent, nTime
|
||
FROM Session
|
||
WHERE strUsrName = ? OR strNickName = ?
|
||
ORDER BY nOrder DESC`
|
||
args = []interface{}{key, key}
|
||
} else {
|
||
// 查询所有会话
|
||
query = `SELECT strUsrName, nOrder, strNickName, strContent, nTime
|
||
FROM Session
|
||
ORDER BY nOrder DESC`
|
||
}
|
||
|
||
// 添加分页
|
||
if limit > 0 {
|
||
query += fmt.Sprintf(" LIMIT %d", limit)
|
||
if offset > 0 {
|
||
query += fmt.Sprintf(" OFFSET %d", offset)
|
||
}
|
||
}
|
||
|
||
// 执行查询
|
||
db, err := ds.dbm.GetDB(Contact)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
rows, err := db.QueryContext(ctx, query, args...)
|
||
if err != nil {
|
||
return nil, errors.QueryFailed(query, err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
sessions := []*model.Session{}
|
||
for rows.Next() {
|
||
var sessionV3 model.SessionV3
|
||
err := rows.Scan(
|
||
&sessionV3.StrUsrName,
|
||
&sessionV3.NOrder,
|
||
&sessionV3.StrNickName,
|
||
&sessionV3.StrContent,
|
||
&sessionV3.NTime,
|
||
)
|
||
|
||
if err != nil {
|
||
return nil, errors.ScanRowFailed(err)
|
||
}
|
||
|
||
sessions = append(sessions, sessionV3.Wrap())
|
||
}
|
||
|
||
return sessions, nil
|
||
}
|
||
|
||
func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*model.Media, error) {
|
||
if key == "" {
|
||
return nil, errors.ErrKeyEmpty
|
||
}
|
||
|
||
if _type == "voice" {
|
||
return ds.GetVoice(ctx, key)
|
||
}
|
||
|
||
md5key, err := hex.DecodeString(key)
|
||
if err != nil {
|
||
return nil, errors.DecodeKeyFailed(err)
|
||
}
|
||
|
||
var dbType string
|
||
var table1, table2 string
|
||
|
||
switch _type {
|
||
case "image":
|
||
dbType = Image
|
||
table1 = "HardLinkImageAttribute"
|
||
table2 = "HardLinkImageID"
|
||
case "video":
|
||
dbType = Video
|
||
table1 = "HardLinkVideoAttribute"
|
||
table2 = "HardLinkVideoID"
|
||
case "file":
|
||
dbType = File
|
||
table1 = "HardLinkFileAttribute"
|
||
table2 = "HardLinkFileID"
|
||
default:
|
||
return nil, errors.MediaTypeUnsupported(_type)
|
||
}
|
||
|
||
db, err := ds.dbm.GetDB(dbType)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
query := fmt.Sprintf(`
|
||
SELECT
|
||
a.FileName,
|
||
a.ModifyTime,
|
||
IFNULL(d1.Dir,"") AS Dir1,
|
||
IFNULL(d2.Dir,"") AS Dir2
|
||
FROM
|
||
%s a
|
||
LEFT JOIN
|
||
%s d1 ON a.DirID1 = d1.DirId
|
||
LEFT JOIN
|
||
%s d2 ON a.DirID2 = d2.DirId
|
||
WHERE
|
||
a.Md5 = ?
|
||
`, table1, table2, table2)
|
||
args := []interface{}{md5key}
|
||
|
||
rows, err := db.QueryContext(ctx, query, args...)
|
||
if err != nil {
|
||
return nil, errors.QueryFailed(query, err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
var media *model.Media
|
||
for rows.Next() {
|
||
var mediaV3 model.MediaV3
|
||
err := rows.Scan(
|
||
&mediaV3.Name,
|
||
&mediaV3.ModifyTime,
|
||
&mediaV3.Dir1,
|
||
&mediaV3.Dir2,
|
||
)
|
||
if err != nil {
|
||
return nil, errors.ScanRowFailed(err)
|
||
}
|
||
mediaV3.Type = _type
|
||
mediaV3.Key = key
|
||
media = mediaV3.Wrap()
|
||
}
|
||
|
||
if media == nil {
|
||
return nil, errors.ErrMediaNotFound
|
||
}
|
||
|
||
return media, nil
|
||
}
|
||
|
||
func (ds *DataSource) GetVoice(ctx context.Context, key string) (*model.Media, error) {
|
||
if key == "" {
|
||
return nil, errors.ErrKeyEmpty
|
||
}
|
||
|
||
query := `
|
||
SELECT Buf
|
||
FROM Media
|
||
WHERE Reserved0 = ?
|
||
`
|
||
args := []interface{}{key}
|
||
|
||
dbs, err := ds.dbm.GetDBs(Voice)
|
||
if err != nil {
|
||
return nil, errors.DBConnectFailed("", err)
|
||
}
|
||
|
||
for _, db := range dbs {
|
||
rows, err := db.QueryContext(ctx, query, args...)
|
||
if err != nil {
|
||
return nil, errors.QueryFailed(query, err)
|
||
}
|
||
defer rows.Close()
|
||
|
||
for rows.Next() {
|
||
var voiceData []byte
|
||
err := rows.Scan(
|
||
&voiceData,
|
||
)
|
||
if err != nil {
|
||
return nil, errors.ScanRowFailed(err)
|
||
}
|
||
if len(voiceData) > 0 {
|
||
return &model.Media{
|
||
Type: "voice",
|
||
Key: key,
|
||
Data: voiceData,
|
||
}, nil
|
||
}
|
||
}
|
||
}
|
||
|
||
return nil, errors.ErrMediaNotFound
|
||
}
|
||
|
||
// Close 实现 DataSource 接口的 Close 方法
|
||
func (ds *DataSource) Close() error {
|
||
return ds.dbm.Close()
|
||
}
|
||
|
||
// GetAvatar returns avatar info for a username on Windows v3 (MicroMsg.db -> ContactHeadImgUrl)
|
||
func (ds *DataSource) GetAvatar(ctx context.Context, username string, size string) (*model.Avatar, error) {
|
||
if username == "" {
|
||
return nil, errors.ErrKeyEmpty
|
||
}
|
||
db, err := ds.dbm.GetDB(Contact)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
query := `SELECT IFNULL(smallHeadImgUrl, ''), IFNULL(bigHeadImgUrl, '') FROM ContactHeadImgUrl WHERE usrName = ?`
|
||
row := db.QueryRowContext(ctx, query, username)
|
||
var small, big string
|
||
if err := row.Scan(&small, &big); err != nil {
|
||
return nil, errors.ErrAvatarNotFound
|
||
}
|
||
url := small
|
||
if strings.ToLower(size) == "big" && big != "" {
|
||
url = big
|
||
}
|
||
if url == "" {
|
||
url = big
|
||
}
|
||
if url == "" {
|
||
return nil, errors.ErrAvatarNotFound
|
||
}
|
||
return &model.Avatar{Username: username, URL: url}, nil
|
||
}
|
||
|
||
// GlobalMessageStats 聚合统计(Windows v3)
|
||
func (ds *DataSource) GlobalMessageStats(ctx context.Context) (*model.GlobalMessageStats, error) {
|
||
stats := &model.GlobalMessageStats{ByType: make(map[string]int64)}
|
||
dbs, err := ds.dbm.GetDBs(Message)
|
||
if err != nil {
|
||
return stats, nil
|
||
}
|
||
for _, db := range dbs {
|
||
// total/sent/recv/min/max
|
||
row := db.QueryRowContext(ctx, `SELECT
|
||
COUNT(*) AS total,
|
||
SUM(CASE WHEN IsSender=1 THEN 1 ELSE 0 END) AS sent,
|
||
SUM(CASE WHEN IsSender=0 THEN 1 ELSE 0 END) AS recv,
|
||
MIN(CreateTime) AS minct,
|
||
MAX(CreateTime) AS maxct
|
||
FROM MSG`)
|
||
var total, sent, recv, minct, maxct int64
|
||
if err := row.Scan(&total, &sent, &recv, &minct, &maxct); err == nil {
|
||
stats.Total += total
|
||
stats.Sent += sent
|
||
stats.Received += recv
|
||
if stats.EarliestUnix == 0 || (minct > 0 && minct < stats.EarliestUnix) {
|
||
stats.EarliestUnix = minct
|
||
}
|
||
if maxct > stats.LatestUnix {
|
||
stats.LatestUnix = maxct
|
||
}
|
||
}
|
||
|
||
// By type/subtype
|
||
rows, err := db.QueryContext(ctx, `SELECT Type, SubType, COUNT(*) FROM MSG GROUP BY Type, SubType`)
|
||
if err == nil {
|
||
for rows.Next() {
|
||
var t int64
|
||
var st int
|
||
var cnt int64
|
||
if err := rows.Scan(&t, &st, &cnt); err == nil {
|
||
label := mapV3TypeToLabel(t, int64(st))
|
||
if label != "" {
|
||
stats.ByType[label] += cnt
|
||
}
|
||
}
|
||
}
|
||
rows.Close()
|
||
}
|
||
}
|
||
return stats, nil
|
||
}
|
||
|
||
// GroupMessageCounts 统计群聊消息数量(Windows v3)
|
||
func (ds *DataSource) GroupMessageCounts(ctx context.Context) (map[string]int64, error) {
|
||
result := make(map[string]int64)
|
||
dbs, err := ds.dbm.GetDBs(Message)
|
||
if err != nil {
|
||
return result, nil
|
||
}
|
||
for _, db := range dbs {
|
||
rows, err := db.QueryContext(ctx, `SELECT StrTalker, COUNT(*) FROM MSG WHERE StrTalker LIKE '%@chatroom' GROUP BY StrTalker`)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for rows.Next() {
|
||
var talker string
|
||
var cnt int64
|
||
if err := rows.Scan(&talker, &cnt); err == nil {
|
||
result[talker] += cnt
|
||
}
|
||
}
|
||
rows.Close()
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// GroupTodayMessageCounts 统计群聊今日消息数(Windows v3):MSG 表中 StrTalker LIKE '%@chatroom' 且 CreateTime >= 今日零点
|
||
func (ds *DataSource) GroupTodayMessageCounts(ctx context.Context) (map[string]int64, error) {
|
||
result := make(map[string]int64)
|
||
dbs, err := ds.dbm.GetDBs(Message)
|
||
if err != nil {
|
||
return result, nil
|
||
}
|
||
// 今日零点(使用本地时区)
|
||
now := time.Now()
|
||
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||
since := today.Unix()
|
||
for _, db := range dbs {
|
||
rows, err := db.QueryContext(ctx, `SELECT StrTalker, COUNT(*) FROM MSG WHERE StrTalker LIKE '%@chatroom' AND CreateTime >= ? GROUP BY StrTalker`, since)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for rows.Next() {
|
||
var talker string
|
||
var cnt int64
|
||
if err := rows.Scan(&talker, &cnt); err == nil {
|
||
result[talker] += cnt
|
||
}
|
||
}
|
||
rows.Close()
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// GroupTodayHourly 统计群聊今日按小时消息数(Windows v3)
|
||
func (ds *DataSource) GroupTodayHourly(ctx context.Context) (map[string][24]int64, error) {
|
||
result := make(map[string][24]int64)
|
||
dbs, err := ds.dbm.GetDBs(Message)
|
||
if err != nil {
|
||
return result, nil
|
||
}
|
||
now := time.Now()
|
||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix()
|
||
end := start + 86400
|
||
for _, db := range dbs {
|
||
rows, err := db.QueryContext(ctx, `SELECT StrTalker, CAST(strftime('%H', datetime(CreateTime,'unixepoch')) AS INTEGER) AS h, COUNT(*) FROM MSG WHERE StrTalker LIKE '%@chatroom' AND CreateTime >= ? AND CreateTime < ? GROUP BY StrTalker, h`, start, end)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for rows.Next() {
|
||
var talker string
|
||
var hour int
|
||
var cnt int64
|
||
if rows.Scan(&talker, &hour, &cnt) == nil {
|
||
if hour >= 0 && hour < 24 {
|
||
bucket := result[talker]
|
||
bucket[hour] += cnt
|
||
result[talker] = bucket
|
||
}
|
||
}
|
||
}
|
||
rows.Close()
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// GroupWeekMessageCount 统计本周(周一00:00起)所有群聊消息总数(Windows v3)
|
||
func (ds *DataSource) GroupWeekMessageCount(ctx context.Context) (int64, error) {
|
||
var total int64
|
||
dbs, err := ds.dbm.GetDBs(Message)
|
||
if err != nil {
|
||
return 0, nil
|
||
}
|
||
now := time.Now()
|
||
wday := int(now.Weekday())
|
||
offset := wday - 1
|
||
if wday == 0 {
|
||
offset = -6
|
||
}
|
||
monday := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).AddDate(0, 0, -offset)
|
||
since := monday.Unix()
|
||
for _, db := range dbs {
|
||
row := db.QueryRowContext(ctx, `SELECT COUNT(*) FROM MSG WHERE StrTalker LIKE '%@chatroom' AND CreateTime >= ?`, since)
|
||
var cnt int64
|
||
if row.Scan(&cnt) == nil {
|
||
total += cnt
|
||
}
|
||
}
|
||
return total, nil
|
||
}
|
||
|
||
// GroupMessageTypeStats 统计群聊消息类型分布(Windows v3)
|
||
func (ds *DataSource) GroupMessageTypeStats(ctx context.Context) (map[string]int64, error) {
|
||
result := make(map[string]int64)
|
||
dbs, err := ds.dbm.GetDBs(Message)
|
||
if err != nil {
|
||
return result, nil
|
||
}
|
||
for _, db := range dbs {
|
||
rows, err := db.QueryContext(ctx, `SELECT Type, SubType, COUNT(*) FROM MSG WHERE StrTalker LIKE '%@chatroom' GROUP BY Type, SubType`)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for rows.Next() {
|
||
var t int64
|
||
var st int64
|
||
var cnt int64
|
||
if rows.Scan(&t, &st, &cnt) == nil {
|
||
label := mapV3TypeToLabel(t, st)
|
||
if label != "" {
|
||
result[label] += cnt
|
||
}
|
||
}
|
||
}
|
||
rows.Close()
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
// MonthlyTrend 返回每月 sent/received(近 months 月,若 months<=0 则返回全部)
|
||
func (ds *DataSource) MonthlyTrend(ctx context.Context, months int) ([]model.MonthlyTrend, error) {
|
||
agg := make(map[string][2]int64)
|
||
dbs, err := ds.dbm.GetDBs(Message)
|
||
if err != nil {
|
||
return []model.MonthlyTrend{}, nil
|
||
}
|
||
for _, db := range dbs {
|
||
rows, err := db.QueryContext(ctx, `SELECT strftime('%Y-%m', datetime(CreateTime, 'unixepoch')) AS ym,
|
||
SUM(CASE WHEN IsSender=1 THEN 1 ELSE 0 END) AS sent,
|
||
SUM(CASE WHEN IsSender=0 THEN 1 ELSE 0 END) AS recv
|
||
FROM MSG GROUP BY ym ORDER BY ym`)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for rows.Next() {
|
||
var ym string
|
||
var sent, recv int64
|
||
if err := rows.Scan(&ym, &sent, &recv); err == nil {
|
||
cur := agg[ym]
|
||
cur[0] += sent
|
||
cur[1] += recv
|
||
agg[ym] = cur
|
||
}
|
||
}
|
||
rows.Close()
|
||
}
|
||
trends := make([]model.MonthlyTrend, 0, len(agg))
|
||
// order is not guaranteed; we'll reconstruct sorted keys
|
||
// but to keep simple, iterate map and collect; sorting can be added later
|
||
for ym, v := range agg {
|
||
trends = append(trends, model.MonthlyTrend{Date: ym, Sent: v[0], Received: v[1]})
|
||
}
|
||
// optional: limit to recent months by trimming after sort
|
||
return trends, nil
|
||
}
|
||
|
||
// Heatmap 小时x星期(wday: 0=Sunday..6)
|
||
func (ds *DataSource) Heatmap(ctx context.Context) ([24][7]int64, error) {
|
||
var grid [24][7]int64
|
||
dbs, err := ds.dbm.GetDBs(Message)
|
||
if err != nil {
|
||
return grid, nil
|
||
}
|
||
for _, db := range dbs {
|
||
rows, err := db.QueryContext(ctx, `SELECT CAST(strftime('%H', datetime(CreateTime,'unixepoch')) AS INTEGER) AS h,
|
||
CAST(strftime('%w', datetime(CreateTime,'unixepoch')) AS INTEGER) AS d,
|
||
COUNT(*) FROM MSG GROUP BY h,d`)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for rows.Next() {
|
||
var h, d int
|
||
var cnt int64
|
||
if err := rows.Scan(&h, &d, &cnt); err == nil {
|
||
if h >= 0 && h < 24 && d >= 0 && d < 7 {
|
||
grid[h][d] += cnt
|
||
}
|
||
}
|
||
}
|
||
rows.Close()
|
||
}
|
||
return grid, nil
|
||
}
|
||
|
||
// GlobalTodayHourly 返回今日(本地时区)每小时全部消息量(含私聊+群聊)
|
||
func (ds *DataSource) GlobalTodayHourly(ctx context.Context) ([24]int64, error) {
|
||
var hours [24]int64
|
||
dbs, err := ds.dbm.GetDBs(Message)
|
||
if err != nil {
|
||
return hours, nil
|
||
}
|
||
now := time.Now()
|
||
start := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location()).Unix()
|
||
end := start + 86400
|
||
for _, db := range dbs {
|
||
rows, err := db.QueryContext(ctx, `SELECT CAST(strftime('%H', datetime(CreateTime,'unixepoch')) AS INTEGER) AS h, COUNT(*) FROM MSG WHERE CreateTime >= ? AND CreateTime < ? GROUP BY h`, start, end)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
for rows.Next() {
|
||
var h int
|
||
var cnt int64
|
||
if rows.Scan(&h, &cnt) == nil {
|
||
if h >= 0 && h < 24 {
|
||
hours[h] += cnt
|
||
}
|
||
}
|
||
}
|
||
rows.Close()
|
||
}
|
||
return hours, nil
|
||
}
|
||
|
||
// IntimacyBase 统计按联系人(非群聊)聚合的亲密度基础数据(Windows v3)
|
||
func (ds *DataSource) IntimacyBase(ctx context.Context) (map[string]*model.IntimacyBase, error) {
|
||
result := make(map[string]*model.IntimacyBase)
|
||
|
||
dbs, err := ds.dbm.GetDBs(Message)
|
||
if err != nil {
|
||
return result, nil
|
||
}
|
||
|
||
// 先获取全局最新时间戳
|
||
var maxCT int64
|
||
for _, db := range dbs {
|
||
row := db.QueryRowContext(ctx, `SELECT MAX(CreateTime) FROM MSG`)
|
||
var v sql.NullInt64
|
||
if err := row.Scan(&v); err == nil && v.Valid && v.Int64 > maxCT {
|
||
maxCT = v.Int64
|
||
}
|
||
}
|
||
if maxCT == 0 {
|
||
return result, nil
|
||
}
|
||
since90 := maxCT - 90*86400
|
||
since7 := maxCT - 7*86400
|
||
|
||
for _, db := range dbs {
|
||
// 基础计数
|
||
rows, err := db.QueryContext(ctx, `SELECT StrTalker,
|
||
COUNT(*) AS msg_count,
|
||
MIN(CreateTime) AS minct,
|
||
MAX(CreateTime) AS maxct,
|
||
SUM(CASE WHEN IsSender=1 THEN 1 ELSE 0 END) AS sent,
|
||
SUM(CASE WHEN IsSender=0 THEN 1 ELSE 0 END) AS recv
|
||
FROM MSG WHERE StrTalker NOT LIKE '%@chatroom' GROUP BY StrTalker`)
|
||
if err == nil {
|
||
for rows.Next() {
|
||
var talker string
|
||
var msgCnt, minct, maxct, sent, recv int64
|
||
if err := rows.Scan(&talker, &msgCnt, &minct, &maxct, &sent, &recv); err == nil {
|
||
base := result[talker]
|
||
if base == nil {
|
||
base = &model.IntimacyBase{UserName: talker}
|
||
result[talker] = base
|
||
}
|
||
base.MsgCount += msgCnt
|
||
base.SentCount += sent
|
||
base.ReceivedCount += recv
|
||
if base.MinCreateUnix == 0 || (minct > 0 && minct < base.MinCreateUnix) {
|
||
base.MinCreateUnix = minct
|
||
}
|
||
if maxct > base.MaxCreateUnix {
|
||
base.MaxCreateUnix = maxct
|
||
}
|
||
}
|
||
}
|
||
rows.Close()
|
||
}
|
||
|
||
// 活跃天数(全期间)
|
||
rows2, err := db.QueryContext(ctx, `SELECT StrTalker, COUNT(DISTINCT date(datetime(CreateTime,'unixepoch'))) AS days
|
||
FROM MSG WHERE StrTalker NOT LIKE '%@chatroom' GROUP BY StrTalker`)
|
||
if err == nil {
|
||
for rows2.Next() {
|
||
var talker string
|
||
var days int64
|
||
if err := rows2.Scan(&talker, &days); err == nil {
|
||
base := result[talker]
|
||
if base == nil {
|
||
base = &model.IntimacyBase{UserName: talker}
|
||
result[talker] = base
|
||
}
|
||
base.MessagingDays += days
|
||
}
|
||
}
|
||
rows2.Close()
|
||
}
|
||
|
||
// 最近90天消息数
|
||
rows3, err := db.QueryContext(ctx, `SELECT StrTalker, COUNT(*) FROM MSG WHERE CreateTime>=? AND StrTalker NOT LIKE '%@chatroom' GROUP BY StrTalker`, since90)
|
||
if err == nil {
|
||
for rows3.Next() {
|
||
var talker string
|
||
var cnt int64
|
||
if err := rows3.Scan(&talker, &cnt); err == nil {
|
||
base := result[talker]
|
||
if base == nil {
|
||
base = &model.IntimacyBase{UserName: talker}
|
||
result[talker] = base
|
||
}
|
||
base.Last90DaysMsg += cnt
|
||
}
|
||
}
|
||
rows3.Close()
|
||
}
|
||
|
||
// 过去7天发送数
|
||
rows4, err := db.QueryContext(ctx, `SELECT StrTalker, SUM(CASE WHEN IsSender=1 THEN 1 ELSE 0 END) FROM MSG WHERE CreateTime>=? AND StrTalker NOT LIKE '%@chatroom' GROUP BY StrTalker`, since7)
|
||
if err == nil {
|
||
for rows4.Next() {
|
||
var talker string
|
||
var cnt sql.NullInt64
|
||
if err := rows4.Scan(&talker, &cnt); err == nil {
|
||
base := result[talker]
|
||
if base == nil {
|
||
base = &model.IntimacyBase{UserName: talker}
|
||
result[talker] = base
|
||
}
|
||
if cnt.Valid {
|
||
base.Past7DaysSentMsg += cnt.Int64
|
||
}
|
||
}
|
||
}
|
||
rows4.Close()
|
||
}
|
||
}
|
||
|
||
return result, nil
|
||
}
|
||
|
||
// mapV3TypeToLabel 将v3 (Type,SubType) 映射为中文类别
|
||
func mapV3TypeToLabel(t int64, st int64) string {
|
||
// Windows v3:在统一文档分类基础上补充文件/链接细分(Type=49 根据 SubType 区分)
|
||
switch t {
|
||
case 1:
|
||
return "文本消息"
|
||
case 3:
|
||
return "图片消息"
|
||
case 34:
|
||
return "语音消息"
|
||
case 37:
|
||
return "好友验证消息"
|
||
case 42:
|
||
return "好友推荐消息"
|
||
case 47:
|
||
return "聊天表情"
|
||
case 48:
|
||
return "位置消息"
|
||
case 49:
|
||
if st == 6 { // 文件
|
||
return "文件消息"
|
||
}
|
||
if st == 4 || st == 5 { // 链接
|
||
return "链接消息"
|
||
}
|
||
return "XML消息" // 其他未识别子类型归类为通用 XML
|
||
case 50:
|
||
return "音视频通话"
|
||
case 51:
|
||
return "手机端操作消息"
|
||
case 10000:
|
||
return "系统通知"
|
||
case 10002:
|
||
return "撤回消息"
|
||
default:
|
||
return ""
|
||
}
|
||
}
|