package feishu import ( "context" "fmt" "io" "log" "net/http" "strings" "time" "xiaji-go/internal/channel" "xiaji-go/internal/service" "xiaji-go/models" "github.com/gin-gonic/gin" "gorm.io/gorm" ) // DefaultUserID 统一用户ID,使所有平台共享同一份账本 const DefaultUserID int64 = 1 type Bot struct { db *gorm.DB finance *service.FinanceService appID string appSecret string verificationToken string encryptKey string } func NewBot(db *gorm.DB, finance *service.FinanceService, appID, appSecret, verificationToken, encryptKey string) *Bot { return &Bot{ db: db, finance: finance, appID: appID, appSecret: appSecret, verificationToken: verificationToken, encryptKey: encryptKey, } } func (b *Bot) Start(ctx context.Context) { log.Printf("🚀 Feishu Bot 已启用 app_id=%s", maskID(b.appID)) <-ctx.Done() log.Printf("⏳ Feishu Bot 已停止") } func (b *Bot) RegisterRoutes(r *gin.Engine) { r.POST("/webhook/feishu", b.handleWebhook) } func (b *Bot) handleWebhook(c *gin.Context) { body, err := io.ReadAll(io.LimitReader(c.Request.Body, 1<<20)) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } // 统一走 channel 包解析,便于后续扩展验签/解密 msg, verifyChallenge, err := channel.ParseFeishuInbound(body, b.verificationToken) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if verifyChallenge != "" { c.JSON(http.StatusOK, gin.H{"challenge": verifyChallenge}) return } if msg == nil { c.JSON(http.StatusOK, gin.H{"code": 0}) return } // 幂等去重 var existed models.MessageDedup if err := b.db.Where("platform = ? AND event_id = ?", "feishu", msg.EventID).First(&existed).Error; err == nil { c.JSON(http.StatusOK, gin.H{"code": 0}) return } _ = b.db.Create(&models.MessageDedup{Platform: "feishu", EventID: msg.EventID, ProcessedAt: time.Now()}).Error reply := b.handleText(msg.Text) if reply != "" && msg.UserID != "" { tk, err := channel.GetFeishuTenantToken(c.Request.Context(), b.appID, b.appSecret) if err == nil { _ = channel.SendFeishuText(c.Request.Context(), tk, msg.UserID, reply) } } c.JSON(http.StatusOK, gin.H{"code": 0}) } func (b *Bot) handleText(text string) string { trim := strings.TrimSpace(text) switch trim { case "帮助", "help", "/help", "菜单", "功能", "/start": return "🦞 虾记记账\n\n直接发送消费描述即可记账:\n• 午饭 25元\n• 打车 ¥30\n\n📋 命令:记录/查看、今日/今天、统计" case "查看", "记录", "列表", "最近": 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() } amount, category, err := b.finance.AddTransaction(DefaultUserID, trim) if err != nil { return "❌ 记账失败,请稍后重试" } if amount == 0 { return "📍 没看到金额,这笔花了多少钱?" } return fmt.Sprintf("✅ 已记入【%s】:%.2f元\n📝 备注:%s", category, float64(amount)/100.0, trim) } func maskID(s string) string { if len(s) <= 6 { return "***" } return s[:3] + "***" + s[len(s)-3:] }