Files
billai/server/handler/export.go
clz 02de11caac feat: 新增账单导出 Excel 功能
- 后端新增 /api/bills/export 接口,支持当前筛选条件导出全部记录
- 使用 excelize 库生成 xlsx 格式文件
- 前端账单管理页面添加导出按钮
- 更新 Go 版本到 1.24 以支持 excelize 依赖
2026-03-23 19:16:54 +08:00

135 lines
3.7 KiB
Go

package handler
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/xuri/excelize/v2"
"billai-server/repository"
)
type ExportBillsRequest struct {
StartDate string `form:"start_date"`
EndDate string `form:"end_date"`
Category string `form:"category"`
Type string `form:"type"`
IncomeExpense string `form:"income_expense"`
}
func ExportBills(c *gin.Context) {
var req ExportBillsRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"result": false,
"message": "参数解析失败: " + err.Error(),
})
return
}
filter := buildFilterFromRequest(req)
repo := repository.GetRepository()
if repo == nil {
c.JSON(http.StatusInternalServerError, gin.H{
"result": false,
"message": "数据库未连接",
})
return
}
bills, err := repo.GetCleanedBills(filter)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"result": false,
"message": "查询失败: " + err.Error(),
})
return
}
f := excelize.NewFile()
sheet := "账单"
f.SetSheetName("Sheet1", sheet)
headers := []string{"时间", "来源", "分类", "交易对方", "商品说明", "收/支", "金额", "支付方式", "状态", "备注"}
for i, header := range headers {
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
f.SetCellValue(sheet, cell, header)
}
for idx, bill := range bills {
row := idx + 2
f.SetCellValue(sheet, fmt.Sprintf("A%d", row), bill.Time.Time().Format("2006-01-02 15:04:05"))
f.SetCellValue(sheet, fmt.Sprintf("B%d", row), bill.BillType)
f.SetCellValue(sheet, fmt.Sprintf("C%d", row), bill.Category)
f.SetCellValue(sheet, fmt.Sprintf("D%d", row), bill.Merchant)
f.SetCellValue(sheet, fmt.Sprintf("E%d", row), bill.Description)
f.SetCellValue(sheet, fmt.Sprintf("F%d", row), bill.IncomeExpense)
f.SetCellValue(sheet, fmt.Sprintf("G%d", row), bill.Amount)
f.SetCellValue(sheet, fmt.Sprintf("H%d", row), bill.PayMethod)
f.SetCellValue(sheet, fmt.Sprintf("I%d", row), bill.Status)
f.SetCellValue(sheet, fmt.Sprintf("J%d", row), bill.Remark)
}
f.SetColWidth(sheet, "A", "A", 20)
f.SetColWidth(sheet, "B", "B", 8)
f.SetColWidth(sheet, "C", "C", 12)
f.SetColWidth(sheet, "D", "D", 20)
f.SetColWidth(sheet, "E", "E", 30)
f.SetColWidth(sheet, "F", "F", 8)
f.SetColWidth(sheet, "G", "G", 12)
f.SetColWidth(sheet, "H", "H", 15)
f.SetColWidth(sheet, "I", "I", 10)
f.SetColWidth(sheet, "J", "J", 20)
filename := fmt.Sprintf("bills_%s.xlsx", time.Now().Format("20060102_150405"))
c.Header("Content-Type", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet")
c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filename))
c.Header("Access-Control-Expose-Headers", "Content-Disposition")
if err := f.Write(c.Writer); err != nil {
fmt.Printf("导出 Excel 失败: %v\n", err)
}
}
func buildFilterFromRequest(req ExportBillsRequest) map[string]interface{} {
filter := make(map[string]interface{})
if req.StartDate != "" || req.EndDate != "" {
timeFilter := make(map[string]interface{})
if req.StartDate != "" {
startTime, err := time.ParseInLocation("2006-01-02", req.StartDate, time.Local)
if err == nil {
timeFilter["$gte"] = startTime
}
}
if req.EndDate != "" {
endTime, err := time.ParseInLocation("2006-01-02", req.EndDate, time.Local)
if err == nil {
endTime = endTime.Add(24 * time.Hour)
timeFilter["$lt"] = endTime
}
}
if len(timeFilter) > 0 {
filter["time"] = timeFilter
}
}
if req.Category != "" {
filter["category"] = req.Category
}
if req.Type != "" {
filter["bill_type"] = req.Type
}
if req.IncomeExpense != "" {
filter["income_expense"] = req.IncomeExpense
}
filter["is_deleted"] = false
return filter
}