Files
Xiaji-go/internal/qq/qq.go

239 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package qq
import (
"context"
"fmt"
"log"
"strings"
"time"
"xiaji-go/internal/service"
"xiaji-go/models"
"github.com/tencent-connect/botgo"
"github.com/tencent-connect/botgo/dto"
"github.com/tencent-connect/botgo/dto/message"
"github.com/tencent-connect/botgo/event"
"github.com/tencent-connect/botgo/openapi"
"github.com/tencent-connect/botgo/token"
"gorm.io/gorm"
)
// DefaultUserID 统一用户ID使所有平台共享同一份账本
const DefaultUserID int64 = 1
type QQBot struct {
api openapi.OpenAPI
finance *service.FinanceService
credentials *token.QQBotCredentials
db *gorm.DB
}
func NewQQBot(db *gorm.DB, appID string, secret string, finance *service.FinanceService) *QQBot {
return &QQBot{
db: db,
finance: finance,
credentials: &token.QQBotCredentials{
AppID: appID,
AppSecret: secret,
},
}
}
func (b *QQBot) Start(ctx context.Context) {
tokenSource := token.NewQQBotTokenSource(b.credentials)
if err := token.StartRefreshAccessToken(ctx, tokenSource); err != nil {
log.Printf("❌ QQ Bot Token 刷新失败: %v", err)
return
}
b.api = botgo.NewOpenAPI(b.credentials.AppID, tokenSource).WithTimeout(5 * time.Second)
_ = event.RegisterHandlers(
b.groupATMessageHandler(),
b.c2cMessageHandler(),
b.channelATMessageHandler(),
)
wsInfo, err := b.api.WS(ctx, nil, "")
if err != nil {
log.Printf("❌ QQ Bot 获取 WS 信息失败: %v", err)
return
}
intent := dto.Intent(1<<25 | 1<<30)
log.Printf("🚀 QQ Bot 已启动 (WebSocket, shards=%d)", wsInfo.Shards)
if err := botgo.NewSessionManager().Start(wsInfo, tokenSource, &intent); err != nil {
log.Printf("❌ QQ Bot WebSocket 断开: %v", err)
}
}
func isCommand(text string, keywords ...string) bool {
for _, kw := range keywords {
if text == kw {
return true
}
}
return false
}
func (b *QQBot) isDuplicate(eventID string) bool {
if b.db == nil || strings.TrimSpace(eventID) == "" {
return false
}
var existed models.MessageDedup
if err := b.db.Where("platform = ? AND event_id = ?", "qqbot_official", eventID).First(&existed).Error; err == nil {
return true
}
_ = b.db.Create(&models.MessageDedup{Platform: "qqbot_official", EventID: eventID, ProcessedAt: time.Now()}).Error
return false
}
func (b *QQBot) processAndReply(userID string, content string) string {
text := strings.TrimSpace(message.ETLInput(content))
if text == "" {
return ""
}
today := time.Now().Format("2006-01-02")
switch {
case isCommand(text, "帮助", "help", "/help", "/start", "菜单", "功能"):
return "🦞 虾记记账\n\n" +
"直接发送消费描述即可记账:\n" +
"• 午饭 25元\n" +
"• 打车 ¥30\n" +
"• 买咖啡15块\n\n" +
"📋 命令列表:\n" +
"• 记录/查看 — 最近10条\n" +
"• 今日/今天 — 今日汇总\n" +
"• 统计/报表 — 本月分类统计\n" +
"• 帮助 — 本帮助信息"
case isCommand(text, "查看", "记录", "列表", "list", "/list", "最近"):
items, err := b.finance.GetTransactions(DefaultUserID, 10)
if err != nil {
return "❌ 查询失败"
}
if len(items) == 0 {
return "📭 暂无记录"
}
var sb strings.Builder
sb.WriteString("📋 最近记录:\n\n")
for _, item := range items {
sb.WriteString(fmt.Sprintf("• [%s] %s%.2f元\n", item.Date, item.Category, item.AmountYuan()))
}
return sb.String()
case isCommand(text, "今日", "今天", "today"):
items, err := b.finance.GetTransactionsByDate(DefaultUserID, today)
if err != nil {
return "❌ 查询失败"
}
if len(items) == 0 {
return fmt.Sprintf("📭 %s 暂无消费记录", today)
}
var sb strings.Builder
var total int64
sb.WriteString(fmt.Sprintf("📊 今日(%s消费\n\n", today))
for _, item := range items {
sb.WriteString(fmt.Sprintf("• %s%.2f元\n", item.Category, item.AmountYuan()))
total += item.Amount
}
sb.WriteString(fmt.Sprintf("\n💰 共 %d 笔,合计 %.2f 元", len(items), float64(total)/100.0))
return sb.String()
case isCommand(text, "统计", "报表", "图表", "chart", "/chart"):
now := time.Now()
dateFrom := now.Format("2006-01") + "-01"
dateTo := now.Format("2006-01-02")
stats, err := b.finance.GetCategoryStats(DefaultUserID, dateFrom, dateTo)
if err != nil || len(stats) == 0 {
return fmt.Sprintf("📭 %d年%d月暂无消费数据", now.Year(), now.Month())
}
var sb strings.Builder
var grandTotal int64
var grandCount int
sb.WriteString(fmt.Sprintf("📊 %d年%d月消费统计\n\n", now.Year(), now.Month()))
for _, s := range stats {
yuan := float64(s.Total) / 100.0
sb.WriteString(fmt.Sprintf("• %s%.2f元(%d笔\n", s.Category, yuan, s.Count))
grandTotal += s.Total
grandCount += s.Count
}
sb.WriteString(fmt.Sprintf("\n💰 共 %d 笔,合计 %.2f 元", grandCount, float64(grandTotal)/100.0))
return sb.String()
}
amount, category, err := b.finance.AddTransaction(DefaultUserID, text)
if err != nil {
log.Printf("QQ记账失败 user=%s: %v", userID, err)
return "❌ 记账失败,请稍后重试"
}
if amount == 0 {
return "📍 没看到金额,这笔花了多少钱?"
}
amountYuan := float64(amount) / 100.0
return fmt.Sprintf("✅ 已记入【%s】%.2f元\n📝 备注:%s", category, amountYuan, text)
}
func (b *QQBot) channelATMessageHandler() event.ATMessageEventHandler {
return func(ev *dto.WSPayload, data *dto.WSATMessageData) error {
eventID := "qq:channel:" + strings.TrimSpace(data.ID)
if b.isDuplicate(eventID) {
return nil
}
log.Printf("📩 inbound platform=qqbot_official event=%s chat=%s user=%s text=%q", eventID, data.ChannelID, data.Author.ID, strings.TrimSpace(message.ETLInput(data.Content)))
reply := b.processAndReply(data.Author.ID, data.Content)
if reply == "" {
return nil
}
_, err := b.api.PostMessage(context.Background(), data.ChannelID, &dto.MessageToCreate{MsgID: data.ID, Content: reply})
if err != nil {
log.Printf("QQ频道消息发送失败: %v", err)
}
return nil
}
}
func (b *QQBot) groupATMessageHandler() event.GroupATMessageEventHandler {
return func(ev *dto.WSPayload, data *dto.WSGroupATMessageData) error {
eventID := "qq:group:" + strings.TrimSpace(data.ID)
if b.isDuplicate(eventID) {
return nil
}
log.Printf("📩 inbound platform=qqbot_official event=%s chat=%s user=%s text=%q", eventID, data.GroupID, data.Author.ID, strings.TrimSpace(message.ETLInput(data.Content)))
reply := b.processAndReply(data.Author.ID, data.Content)
if reply == "" {
return nil
}
_, err := b.api.PostGroupMessage(context.Background(), data.GroupID, dto.MessageToCreate{MsgID: data.ID, Content: reply})
if err != nil {
log.Printf("QQ群消息发送失败: %v", err)
}
return nil
}
}
func (b *QQBot) c2cMessageHandler() event.C2CMessageEventHandler {
return func(ev *dto.WSPayload, data *dto.WSC2CMessageData) error {
eventID := "qq:c2c:" + strings.TrimSpace(data.ID)
if b.isDuplicate(eventID) {
return nil
}
log.Printf("📩 inbound platform=qqbot_official event=%s chat=%s user=%s text=%q", eventID, data.Author.ID, data.Author.ID, strings.TrimSpace(message.ETLInput(data.Content)))
reply := b.processAndReply(data.Author.ID, data.Content)
if reply == "" {
return nil
}
_, err := b.api.PostC2CMessage(context.Background(), data.Author.ID, dto.MessageToCreate{MsgID: data.ID, Content: reply})
if err != nil {
log.Printf("QQ私聊消息发送失败: %v", err)
}
return nil
}
}