Files
ops-assistant/internal/feishu/feishu.go

131 lines
3.5 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 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:]
}