feat: 新增账单导出 Excel 功能
- 后端新增 /api/bills/export 接口,支持当前筛选条件导出全部记录 - 使用 excelize 库生成 xlsx 格式文件 - 前端账单管理页面添加导出按钮 - 更新 Go 版本到 1.24 以支持 excelize 依赖
This commit is contained in:
134
server/handler/export.go
Normal file
134
server/handler/export.go
Normal file
@@ -0,0 +1,134 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user