feat: channels/audit UI unify, apply flow hardening, bump v1.1.12

This commit is contained in:
2026-03-10 03:32:40 +08:00
parent 52b0d742a7
commit 8b2557b2bf
15 changed files with 2311 additions and 262 deletions

130
internal/feishu/feishu.go Normal file
View File

@@ -0,0 +1,130 @@
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:]
}