feat: 添加账单软删除功能
- 新增删除按钮(带二次确认)到账单详情抽屉 - 后端实现软删除(设置 is_deleted 标记) - 所有查询方法自动过滤已删除记录 - 账单列表和复核页面都支持删除 - 版本更新至 1.2.0
This commit is contained in:
42
server/handler/delete_bill.go
Normal file
42
server/handler/delete_bill.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"billai-server/repository"
|
||||
)
|
||||
|
||||
type DeleteBillResponse struct {
|
||||
Result bool `json:"result"`
|
||||
Message string `json:"message,omitempty"`
|
||||
}
|
||||
|
||||
// DeleteBill DELETE /api/bills/:id 删除清洗后的账单记录
|
||||
func DeleteBill(c *gin.Context) {
|
||||
id := strings.TrimSpace(c.Param("id"))
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, DeleteBillResponse{Result: false, Message: "缺少账单 ID"})
|
||||
return
|
||||
}
|
||||
|
||||
repo := repository.GetRepository()
|
||||
if repo == nil {
|
||||
c.JSON(http.StatusInternalServerError, DeleteBillResponse{Result: false, Message: "数据库未连接"})
|
||||
return
|
||||
}
|
||||
|
||||
err := repo.DeleteCleanedBillByID(id)
|
||||
if err != nil {
|
||||
if err == repository.ErrNotFound {
|
||||
c.JSON(http.StatusNotFound, DeleteBillResponse{Result: false, Message: "账单不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, DeleteBillResponse{Result: false, Message: "删除失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, DeleteBillResponse{Result: true, Message: "删除成功"})
|
||||
}
|
||||
@@ -23,6 +23,7 @@ type UpdateBillRequest struct {
|
||||
PayMethod *string `json:"pay_method,omitempty"`
|
||||
Status *string `json:"status,omitempty"`
|
||||
Remark *string `json:"remark,omitempty"`
|
||||
ReviewLevel *string `json:"review_level,omitempty"`
|
||||
}
|
||||
|
||||
type UpdateBillResponse struct {
|
||||
@@ -119,6 +120,16 @@ func UpdateBill(c *gin.Context) {
|
||||
updates["remark"] = strings.TrimSpace(*req.Remark)
|
||||
}
|
||||
|
||||
if req.ReviewLevel != nil {
|
||||
// 允许设置为空字符串(清除复核等级)或 HIGH/LOW
|
||||
v := strings.TrimSpace(*req.ReviewLevel)
|
||||
if v != "" && v != "HIGH" && v != "LOW" {
|
||||
c.JSON(http.StatusBadRequest, UpdateBillResponse{Result: false, Message: "review_level 只能是空、HIGH 或 LOW"})
|
||||
return
|
||||
}
|
||||
updates["review_level"] = v
|
||||
}
|
||||
|
||||
if len(updates) == 0 {
|
||||
c.JSON(http.StatusBadRequest, UpdateBillResponse{Result: false, Message: "没有可更新的字段"})
|
||||
return
|
||||
|
||||
@@ -94,6 +94,7 @@ type CleanedBill struct {
|
||||
Status string `bson:"status" json:"status"` // 交易状态
|
||||
Remark string `bson:"remark" json:"remark"` // 备注
|
||||
ReviewLevel string `bson:"review_level" json:"review_level"` // 复核等级: HIGH/LOW/空
|
||||
IsDeleted bool `bson:"is_deleted" json:"is_deleted"` // 是否已删除(软删除)
|
||||
CreatedAt time.Time `bson:"created_at" json:"created_at"` // 创建时间
|
||||
UpdatedAt time.Time `bson:"updated_at" json:"updated_at"` // 更新时间
|
||||
SourceFile string `bson:"source_file" json:"source_file"` // 来源文件名
|
||||
|
||||
@@ -192,6 +192,9 @@ func (r *Repository) GetCleanedBills(filter map[string]interface{}) ([]model.Cle
|
||||
bsonFilter[k] = v
|
||||
}
|
||||
|
||||
// 排除已删除的记录
|
||||
bsonFilter["is_deleted"] = bson.M{"$ne": true}
|
||||
|
||||
// 按时间倒序排列
|
||||
opts := options.Find().SetSort(bson.D{{Key: "time", Value: -1}})
|
||||
|
||||
@@ -220,6 +223,9 @@ func (r *Repository) GetCleanedBillsPaged(filter map[string]interface{}, page, p
|
||||
bsonFilter[k] = v
|
||||
}
|
||||
|
||||
// 排除已删除的记录
|
||||
bsonFilter["is_deleted"] = bson.M{"$ne": true}
|
||||
|
||||
// 计算总数
|
||||
total, err := r.cleanedCollection.CountDocuments(ctx, bsonFilter)
|
||||
if err != nil {
|
||||
@@ -260,6 +266,9 @@ func (r *Repository) GetBillsAggregate(filter map[string]interface{}) (totalExpe
|
||||
bsonFilter[k] = v
|
||||
}
|
||||
|
||||
// 排除已删除的记录
|
||||
bsonFilter["is_deleted"] = bson.M{"$ne": true}
|
||||
|
||||
// 使用聚合管道按 income_expense 分组统计金额
|
||||
pipeline := mongo.Pipeline{
|
||||
{{Key: "$match", Value: bsonFilter}},
|
||||
@@ -300,6 +309,7 @@ func (r *Repository) GetBillsAggregate(filter map[string]interface{}) (totalExpe
|
||||
func (r *Repository) GetBillsNeedReview() ([]model.CleanedBill, error) {
|
||||
filter := map[string]interface{}{
|
||||
"review_level": bson.M{"$in": []string{"HIGH", "LOW"}},
|
||||
"is_deleted": bson.M{"$ne": true},
|
||||
}
|
||||
return r.GetCleanedBills(filter)
|
||||
}
|
||||
@@ -312,6 +322,8 @@ func (r *Repository) GetMonthlyStats() ([]model.MonthlyStat, error) {
|
||||
// 使用聚合管道按月份分组统计
|
||||
// 先按月份和收支类型分组,再汇总
|
||||
pipeline := mongo.Pipeline{
|
||||
// 排除已删除的记录
|
||||
{{Key: "$match", Value: bson.M{"is_deleted": bson.M{"$ne": true}}}},
|
||||
// 添加月份字段
|
||||
{{Key: "$addFields", Value: bson.D{
|
||||
{Key: "month", Value: bson.D{
|
||||
@@ -418,6 +430,34 @@ func (r *Repository) UpdateCleanedBillByID(id string, updates map[string]interfa
|
||||
return &updated, nil
|
||||
}
|
||||
|
||||
// DeleteCleanedBillByID 按 ID 软删除清洗后的账单(设置 is_deleted = true)
|
||||
func (r *Repository) DeleteCleanedBillByID(id string) error {
|
||||
if r.cleanedCollection == nil {
|
||||
return fmt.Errorf("cleaned collection not initialized")
|
||||
}
|
||||
|
||||
oid, err := primitive.ObjectIDFromHex(id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid id: %w", err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
filter := bson.M{"_id": oid}
|
||||
update := bson.M{"$set": bson.M{"is_deleted": true}}
|
||||
result, err := r.cleanedCollection.UpdateOne(ctx, filter, update)
|
||||
if err != nil {
|
||||
return fmt.Errorf("soft delete bill failed: %w", err)
|
||||
}
|
||||
|
||||
if result.MatchedCount == 0 {
|
||||
return repository.ErrNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetClient 获取 MongoDB 客户端(用于兼容旧代码)
|
||||
func (r *Repository) GetClient() *mongo.Client {
|
||||
return r.client
|
||||
|
||||
@@ -46,6 +46,9 @@ type BillRepository interface {
|
||||
// UpdateCleanedBillByID 按 ID 更新清洗后的账单,并返回更新后的记录
|
||||
UpdateCleanedBillByID(id string, updates map[string]interface{}) (*model.CleanedBill, error)
|
||||
|
||||
// DeleteCleanedBillByID 按 ID 删除清洗后的账单
|
||||
DeleteCleanedBillByID(id string) error
|
||||
|
||||
// CountRawByField 按字段统计原始数据数量
|
||||
CountRawByField(fieldName, value string) (int64, error)
|
||||
}
|
||||
|
||||
@@ -62,6 +62,9 @@ func setupAPIRoutes(r *gin.Engine) {
|
||||
// 编辑账单
|
||||
authed.POST("/bills/:id", handler.UpdateBill)
|
||||
|
||||
// 删除账单(软删除)
|
||||
authed.DELETE("/bills/:id", handler.DeleteBill)
|
||||
|
||||
// 手动创建账单
|
||||
authed.POST("/bills/manual", handler.CreateManualBills)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user