feat: xiaji-go v1.0.0 - 智能记账机器人
- Telegram Bot + QQ Bot (WebSocket) 双平台支持 - 150+ 预设分类关键词,jieba 智能分词 - Web 管理后台(记录查看/删除/CSV导出) - 金额精确存储(分/int64) - 版本信息嵌入(编译时注入) - Docker 支持 - 优雅关闭(context + signal)
This commit is contained in:
116
internal/web/server.go
Normal file
116
internal/web/server.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"xiaji-go/models"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type WebServer struct {
|
||||
db *gorm.DB
|
||||
port int
|
||||
username string
|
||||
password string
|
||||
}
|
||||
|
||||
func NewWebServer(db *gorm.DB, port int, username, password string) *WebServer {
|
||||
return &WebServer{db: db, port: port, username: username, password: password}
|
||||
}
|
||||
|
||||
func (s *WebServer) Start() {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
r := gin.Default()
|
||||
r.LoadHTMLGlob("templates/*")
|
||||
|
||||
// 页面
|
||||
r.GET("/", s.handleIndex)
|
||||
r.GET("/api/records", s.handleRecords)
|
||||
r.POST("/delete/:id", s.handleDelete)
|
||||
r.GET("/export", s.handleExport)
|
||||
|
||||
// 健康检查
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
logAddr := fmt.Sprintf(":%d", s.port)
|
||||
fmt.Printf("🌐 Web后台运行在 http://127.0.0.1%s\n", logAddr)
|
||||
if err := r.Run(logAddr); err != nil {
|
||||
fmt.Printf("❌ Web服务启动失败: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *WebServer) handleIndex(c *gin.Context) {
|
||||
c.HTML(http.StatusOK, "index.html", nil)
|
||||
}
|
||||
|
||||
func (s *WebServer) handleRecords(c *gin.Context) {
|
||||
var items []models.Transaction
|
||||
s.db.Where("is_deleted = ?", false).Order("id desc").Limit(50).Find(&items)
|
||||
|
||||
type txResponse struct {
|
||||
ID uint `json:"id"`
|
||||
UserID int64 `json:"user_id"`
|
||||
Amount float64 `json:"amount"`
|
||||
Category string `json:"category"`
|
||||
Note string `json:"note"`
|
||||
Date string `json:"date"`
|
||||
}
|
||||
|
||||
resp := make([]txResponse, len(items))
|
||||
for i, item := range items {
|
||||
resp[i] = txResponse{
|
||||
ID: item.ID,
|
||||
UserID: item.UserID,
|
||||
Amount: item.AmountYuan(),
|
||||
Category: item.Category,
|
||||
Note: item.Note,
|
||||
Date: item.Date,
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (s *WebServer) handleDelete(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "无效的ID"})
|
||||
return
|
||||
}
|
||||
|
||||
result := s.db.Model(&models.Transaction{}).Where("id = ? AND is_deleted = ?", id, false).Update("is_deleted", true)
|
||||
if result.Error != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除失败"})
|
||||
return
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "记录不存在或已删除"})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": "success"})
|
||||
}
|
||||
|
||||
func (s *WebServer) handleExport(c *gin.Context) {
|
||||
var items []models.Transaction
|
||||
s.db.Where("is_deleted = ?", false).Order("date asc, id asc").Find(&items)
|
||||
|
||||
c.Header("Content-Type", "text/csv; charset=utf-8")
|
||||
c.Header("Content-Disposition", "attachment; filename=transactions.csv")
|
||||
|
||||
// BOM for Excel
|
||||
c.Writer.Write([]byte{0xEF, 0xBB, 0xBF})
|
||||
c.Writer.WriteString("ID,日期,分类,金额(元),备注\n")
|
||||
|
||||
for _, item := range items {
|
||||
line := fmt.Sprintf("%d,%s,%s,%.2f,\"%s\"\n", item.ID, item.Date, item.Category, item.AmountYuan(), item.Note)
|
||||
c.Writer.WriteString(line)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user