When a refund row in an uploaded Alipay bill has no matching expense row in the same batch (because the original purchase was uploaded in a prior batch), the refund is now reconciled against the stored record in bills_cleaned rather than being silently discarded. Changes: - analyzer/cleaners/base.py: add unresolved_refunds list to BaseCleaner - analyzer/cleaners/alipay.py: _aggregate_refunds stores full refund metadata (dict); _process_expenses tracks matched keys and populates self.unresolved_refunds for unmatched refunds - analyzer/server.py: thread unresolved_refunds through do_clean, CleanResponse, and both /clean endpoints - server/adapter/adapter.go: add UnresolvedRefund type and field to CleanResult - server/adapter/http/cleaner.go: deserialize unresolved_refunds from Python response and populate CleanResult - server/repository/repository.go: add ReconcileRefund to BillRepository interface - server/repository/mongo/repository.go: implement ReconcileRefund — full refund soft-deletes the bill, partial refund reduces amount and appends remark with original amount and refund order number - server/handler/upload.go: capture clean result and call ReconcileRefund for each unresolved refund after saving cleaned bills - server/model/response.go: add ReconciledRefundCount to UploadData Also: add CLAUDE.md (@AGENTS.md), update AGENTS.md, fix DailyTrendChart missing-date gap by filling zero-expense dates in daily map. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
66 lines
2.8 KiB
Go
66 lines
2.8 KiB
Go
// Package repository 定义数据存储层接口
|
||
// 负责所有数据持久化操作的抽象
|
||
package repository
|
||
|
||
import "billai-server/model"
|
||
|
||
// BillRepository 账单存储接口
|
||
type BillRepository interface {
|
||
// Connect 建立连接
|
||
Connect() error
|
||
|
||
// Disconnect 断开连接
|
||
Disconnect() error
|
||
|
||
// SaveRawBills 保存原始账单数据
|
||
SaveRawBills(bills []model.RawBill) (int, error)
|
||
|
||
// SaveCleanedBills 保存清洗后的账单数据
|
||
// 返回: 保存数量、重复数量、错误
|
||
SaveCleanedBills(bills []model.CleanedBill) (saved int, duplicates int, err error)
|
||
|
||
// CheckRawDuplicate 检查原始数据是否重复
|
||
CheckRawDuplicate(fieldName, value string) (bool, error)
|
||
|
||
// CheckCleanedDuplicate 检查清洗后数据是否重复
|
||
CheckCleanedDuplicate(bill *model.CleanedBill) (bool, error)
|
||
|
||
// GetCleanedBills 获取清洗后的账单列表
|
||
GetCleanedBills(filter map[string]interface{}) ([]model.CleanedBill, error)
|
||
|
||
// GetCleanedBillsPaged 获取清洗后的账单列表(带分页)
|
||
// 返回: 账单列表、总数、错误
|
||
GetCleanedBillsPaged(filter map[string]interface{}, page, pageSize int) ([]model.CleanedBill, int64, error)
|
||
|
||
// GetBillsAggregate 获取账单聚合统计(总收入、总支出)
|
||
// 返回: 总支出、总收入、错误
|
||
GetBillsAggregate(filter map[string]interface{}) (totalExpense float64, totalIncome float64, err error)
|
||
|
||
// GetMonthlyStats 获取月度统计(全部数据,不受筛选条件影响)
|
||
// 返回: 月度统计列表、错误
|
||
GetMonthlyStats() ([]model.MonthlyStat, error)
|
||
|
||
// GetBillsNeedReview 获取需要复核的账单
|
||
GetBillsNeedReview() ([]model.CleanedBill, error)
|
||
|
||
// 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)
|
||
|
||
// SoftDeleteJDRelatedBills 软删除描述中包含"京东-订单编号"的非京东账单
|
||
// 用于避免京东账单与其他来源(微信、支付宝)账单重复计算
|
||
// 返回: 删除数量、错误
|
||
SoftDeleteJDRelatedBills() (int64, error)
|
||
|
||
// ReconcileRefund 将跨批次退款核销到已存储的清洗后账单
|
||
// 按 bill_type + (transaction_id == orderNo 或 merchant_order_no == merchantOrderNo) 查找未删除记录
|
||
// 全额退款(剩余金额 <= 0.005)则软删除;部分退款则扣减 amount 并追加备注
|
||
// 返回: 是否找到并核销了匹配记录、错误(未找到匹配记录不算错误,返回 matched=false)
|
||
ReconcileRefund(billType, orderNo, merchantOrderNo string, refundAmount float64, refundTime, merchant, description, refundOrderNo string) (matched bool, err error)
|
||
}
|