feat: 手动账单输入功能及时区修复
- 新增手动账单输入功能 - 创建 ManualBillInput 组件,支持批量添加账单 - 添加服务器端 API /api/bills/manual 处理手动账单创建 - 支持时间选择器,默认当前时间 - 交易对方字段设为可选 - 实时显示待提交账单列表 - 提交成功后显示成功/失败/重复统计 - 修复时区问题 - 后端使用 time.ParseInLocation 解析本地时间,避免 UTC 时区错误 - 确保手动输入的时间按本地时区正确存储 - UI 优化 - 账单管理页面添加标签页切换(列表/手动添加) - 主页添加快捷按钮跳转至手动添加页面 - 手动账单来源正确显示为"手动输入" - 使用 shadcn-svelte 组件统一 UI 风格 - 提交成功后保持日期筛选并重新加载数据
This commit is contained in:
133
server/handler/manual_bills.go
Normal file
133
server/handler/manual_bills.go
Normal file
@@ -0,0 +1,133 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"billai-server/model"
|
||||
"billai-server/repository"
|
||||
)
|
||||
|
||||
// ManualBillInput 手动输入的账单
|
||||
type ManualBillInput struct {
|
||||
Time string `json:"time" binding:"required"` // 交易时间
|
||||
Category string `json:"category" binding:"required"` // 交易分类
|
||||
Merchant string `json:"merchant" binding:"required"` // 交易对方
|
||||
Description string `json:"description"` // 商品说明
|
||||
IncomeExpense string `json:"income_expense" binding:"required"` // 收/支
|
||||
Amount float64 `json:"amount" binding:"required"` // 金额
|
||||
PayMethod string `json:"pay_method"` // 支付方式
|
||||
Status string `json:"status"` // 交易状态
|
||||
Remark string `json:"remark"` // 备注
|
||||
}
|
||||
|
||||
// CreateManualBillsRequest 批量创建手动账单请求
|
||||
type CreateManualBillsRequest struct {
|
||||
Bills []ManualBillInput `json:"bills" binding:"required,min=1"`
|
||||
}
|
||||
|
||||
// CreateManualBillsResponse 批量创建手动账单响应
|
||||
type CreateManualBillsResponse struct {
|
||||
Result bool `json:"result"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Data *CreateManualBillsData `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
// CreateManualBillsData 批量创建手动账单数据
|
||||
type CreateManualBillsData struct {
|
||||
Success int `json:"success"` // 成功创建数量
|
||||
Failed int `json:"failed"` // 失败数量
|
||||
Duplicates int `json:"duplicates"` // 重复数量
|
||||
}
|
||||
|
||||
// CreateManualBills 批量创建手动输入的账单
|
||||
func CreateManualBills(c *gin.Context) {
|
||||
var req CreateManualBillsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, CreateManualBillsResponse{
|
||||
Result: false,
|
||||
Message: "参数解析失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 获取 repository
|
||||
repo := repository.GetRepository()
|
||||
if repo == nil {
|
||||
c.JSON(http.StatusInternalServerError, CreateManualBillsResponse{
|
||||
Result: false,
|
||||
Message: "数据库未连接",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 转换为 CleanedBill
|
||||
bills := make([]model.CleanedBill, 0, len(req.Bills))
|
||||
for _, input := range req.Bills {
|
||||
// 解析时间(使用本地时区,避免 UTC 时区问题)
|
||||
t, err := time.ParseInLocation("2006-01-02 15:04:05", input.Time, time.Local)
|
||||
if err != nil {
|
||||
// 尝试另一种格式
|
||||
t, err = time.ParseInLocation("2006-01-02T15:04:05Z07:00", input.Time, time.Local)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, CreateManualBillsResponse{
|
||||
Result: false,
|
||||
Message: "时间格式错误: " + input.Time,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
status := input.Status
|
||||
if status == "" {
|
||||
status = "交易成功"
|
||||
}
|
||||
|
||||
bill := model.CleanedBill{
|
||||
BillType: "manual",
|
||||
TransactionID: "", // 手动账单不设置交易ID
|
||||
MerchantOrderNo: "",
|
||||
Time: t,
|
||||
Category: input.Category,
|
||||
Merchant: input.Merchant,
|
||||
Description: input.Description,
|
||||
IncomeExpense: input.IncomeExpense,
|
||||
Amount: input.Amount,
|
||||
PayMethod: input.PayMethod,
|
||||
Status: status,
|
||||
Remark: input.Remark,
|
||||
ReviewLevel: "", // 手动账单不需要复核
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
SourceFile: "manual_input",
|
||||
UploadBatch: time.Now().Format("20060102_150405"),
|
||||
}
|
||||
|
||||
bills = append(bills, bill)
|
||||
}
|
||||
|
||||
// 保存账单(带去重)
|
||||
saved, duplicates, err := repo.SaveCleanedBills(bills)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, CreateManualBillsResponse{
|
||||
Result: false,
|
||||
Message: "保存失败: " + err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
failed := len(bills) - saved - duplicates
|
||||
|
||||
c.JSON(http.StatusOK, CreateManualBillsResponse{
|
||||
Result: true,
|
||||
Message: "创建成功",
|
||||
Data: &CreateManualBillsData{
|
||||
Success: saved,
|
||||
Failed: failed,
|
||||
Duplicates: duplicates,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -50,6 +50,9 @@ func setupAPIRoutes(r *gin.Engine) {
|
||||
// 账单查询
|
||||
api.GET("/bills", handler.ListBills)
|
||||
|
||||
// 手动创建账单
|
||||
api.POST("/bills/manual", handler.CreateManualBills)
|
||||
|
||||
// 月度统计(全部数据)
|
||||
api.GET("/monthly-stats", handler.MonthlyStats)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user