From 5ecb1e1469f716c0e9517b296e16c65e5b12c3dd Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 10 Jun 2026 15:08:19 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=86=E5=88=86=E5=90=8E=E5=8F=B0=E8=B4=A2?= =?UTF-8?q?=E5=8A=A1=20Repository=20=E6=96=87=E4=BB=B6=E8=81=8C=E8=B4=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../modules/adminfinance/dashboard.go | 182 +++++++++ .../internal/modules/adminfinance/detail.go | 81 ++++ .../internal/modules/adminfinance/helper.go | 32 ++ .../modules/adminfinance/presenter.go | 77 ++++ .../modules/adminfinance/repository.go | 376 ------------------ .../internal/modules/adminfinance/subquery.go | 30 ++ 6 files changed, 402 insertions(+), 376 deletions(-) create mode 100644 backend/internal/modules/adminfinance/dashboard.go create mode 100644 backend/internal/modules/adminfinance/detail.go create mode 100644 backend/internal/modules/adminfinance/helper.go create mode 100644 backend/internal/modules/adminfinance/presenter.go create mode 100644 backend/internal/modules/adminfinance/subquery.go diff --git a/backend/internal/modules/adminfinance/dashboard.go b/backend/internal/modules/adminfinance/dashboard.go new file mode 100644 index 0000000..aef103c --- /dev/null +++ b/backend/internal/modules/adminfinance/dashboard.go @@ -0,0 +1,182 @@ +package adminfinance + +import ( + "context" + + "hfb_sys/backend/internal/timeutil" +) + +func (r *Repository) Dashboard(ctx context.Context, query DashboardQuery) (*DashboardDTO, error) { + dailyItems, err := r.dailyItems(ctx, query) + if err != nil { + return nil, err + } + summary, err := r.summary(ctx, query) + if err != nil { + return nil, err + } + return &DashboardDTO{ + Summary: *summary, + DailyItems: dailyItems, + GeneratedAt: timeutil.ShanghaiNow(), + }, nil +} + +func (r *Repository) summary(ctx context.Context, query DashboardQuery) (*FinanceSummaryDTO, error) { + db := r.db.WithContext(ctx) + var payment paymentSummaryRow + if err := db.Table("payment_orders"). + Select(`COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS total_refund_amount_cent, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent, + COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN 1 ELSE 0 END), 0) AS successful_refund_count, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count`, + refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()). + Where("created_at >= ? AND created_at <= ?", query.StartDate, query.EndDate). + Scan(&payment).Error; err != nil { + return nil, err + } + + var settlement settlementSummaryRow + if err := db.Table("rental_orders AS ro"). + Select(`COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent, + COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent, + COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent, + COUNT(ro.id) AS settled_order_count`). + Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'"). + Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(db)). + Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate). + Scan(&settlement).Error; err != nil { + return nil, err + } + + var exceptionCount int64 + if err := db.Table("(?) AS d", r.financeDetailBaseQuery(ctx, DetailQuery{ + DateType: "settled", + StartDate: query.StartDate, + EndDate: query.EndDate, + })). + Where("finance_status <> ?", "normal"). + Count(&exceptionCount).Error; err != nil { + return nil, err + } + + return &FinanceSummaryDTO{ + TotalFlowAmountCent: payment.TotalFlowAmountCent, + TotalRefundAmountCent: payment.TotalRefundAmountCent, + PendingRefundAmountCent: payment.PendingRefundAmountCent, + ChannelNetAmountCent: payment.TotalFlowAmountCent - payment.TotalRefundAmountCent, + PlatformIncomeAmountCent: settlement.PlatformIncomeAmountCent, + OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent, + OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent, + SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerWalletIncomeAmountCent, + SuccessfulPayCount: payment.SuccessfulPayCount, + SuccessfulRefundCount: payment.SuccessfulRefundCount, + PendingRefundCount: payment.PendingRefundCount, + SettledOrderCount: settlement.SettledOrderCount, + FinancialExceptionCount: exceptionCount, + }, nil +} + +func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) ([]FinanceDailyDTO, error) { + db := r.db.WithContext(ctx) + payments := make([]dailyPaymentRow, 0) + if err := db.Table("payment_orders"). + Select(`DATE(created_at) AS date, + COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS total_refund_amount_cent, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent, + COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN 1 ELSE 0 END), 0) AS successful_refund_count, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count`, + refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()). + Where("created_at >= ? AND created_at <= ?", query.StartDate, query.EndDate). + Group("DATE(created_at)"). + Scan(&payments).Error; err != nil { + return nil, err + } + + settlements := make([]dailySettlementRow, 0) + if err := db.Table("rental_orders AS ro"). + Select(`DATE(ro.settled_at) AS date, + COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent, + COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent, + COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent, + COUNT(ro.id) AS settled_order_count`). + Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'"). + Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(db)). + Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate). + Group("DATE(ro.settled_at)"). + Scan(&settlements).Error; err != nil { + return nil, err + } + + itemsByDate := make(map[string]FinanceDailyDTO) + for day := dayStart(query.StartDate); !day.After(query.EndDate); day = day.AddDate(0, 0, 1) { + date := day.Format("2006-01-02") + itemsByDate[date] = FinanceDailyDTO{Date: date} + } + for _, row := range payments { + item := itemsByDate[row.Date] + item.Date = row.Date + item.TotalFlowAmountCent = row.TotalFlowAmountCent + item.TotalRefundAmountCent = row.TotalRefundAmountCent + item.PendingRefundAmountCent = row.PendingRefundAmountCent + item.ChannelNetAmountCent = row.TotalFlowAmountCent - row.TotalRefundAmountCent + item.SuccessfulPayCount = row.SuccessfulPayCount + item.SuccessfulRefundCount = row.SuccessfulRefundCount + item.PendingRefundCount = row.PendingRefundCount + itemsByDate[row.Date] = item + } + for _, row := range settlements { + item := itemsByDate[row.Date] + item.Date = row.Date + item.PlatformIncomeAmountCent = row.PlatformIncomeAmountCent + item.OwnerShouldIncomeAmountCent = row.OwnerShouldIncomeAmountCent + item.OwnerWalletIncomeAmountCent = row.OwnerWalletIncomeAmountCent + item.SettlementDiffAmountCent = row.OwnerShouldIncomeAmountCent - row.OwnerWalletIncomeAmountCent + item.SettledOrderCount = row.SettledOrderCount + itemsByDate[row.Date] = item + } + + items := make([]FinanceDailyDTO, 0, len(itemsByDate)) + for day := dayStart(query.StartDate); !day.After(query.EndDate); day = day.AddDate(0, 0, 1) { + items = append(items, itemsByDate[day.Format("2006-01-02")]) + } + return items, nil +} + +type paymentSummaryRow struct { + TotalFlowAmountCent int64 + TotalRefundAmountCent int64 + PendingRefundAmountCent int64 + SuccessfulPayCount int64 + SuccessfulRefundCount int64 + PendingRefundCount int64 +} + +type settlementSummaryRow struct { + PlatformIncomeAmountCent int64 + OwnerShouldIncomeAmountCent int64 + OwnerWalletIncomeAmountCent int64 + SettledOrderCount int64 +} + +type dailyPaymentRow struct { + Date string + TotalFlowAmountCent int64 + TotalRefundAmountCent int64 + PendingRefundAmountCent int64 + SuccessfulPayCount int64 + SuccessfulRefundCount int64 + PendingRefundCount int64 +} + +type dailySettlementRow struct { + Date string + PlatformIncomeAmountCent int64 + OwnerShouldIncomeAmountCent int64 + OwnerWalletIncomeAmountCent int64 + SettledOrderCount int64 +} diff --git a/backend/internal/modules/adminfinance/detail.go b/backend/internal/modules/adminfinance/detail.go new file mode 100644 index 0000000..5daaaaa --- /dev/null +++ b/backend/internal/modules/adminfinance/detail.go @@ -0,0 +1,81 @@ +package adminfinance + +import ( + "context" + + "gorm.io/gorm" +) + +func (r *Repository) Details(ctx context.Context, query DetailQuery) (*PaginatedResult, error) { + baseDB := r.db.WithContext(ctx) + db := r.financeDetailBaseQuery(ctx, query) + var total int64 + countDB := baseDB.Table("(?) AS finance_rows", db) + if err := countDB.Count(&total).Error; err != nil { + return nil, err + } + + offset := (query.Page - 1) * query.PageSize + rows := make([]financeDetailRow, 0, query.PageSize) + if err := db.Order("ro.id DESC").Offset(offset).Limit(query.PageSize).Scan(&rows).Error; err != nil { + return nil, err + } + items := make([]FinanceDetailDTO, 0, len(rows)) + for _, row := range rows { + items = append(items, row.toDTO()) + } + return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil +} + +func (r *Repository) financeDetailBaseQuery(ctx context.Context, query DetailQuery) *gorm.DB { + db := r.db.WithContext(ctx).Table("rental_orders AS ro"). + Select(`ro.id AS order_id, ro.order_no, ro.status AS order_status, ro.settlement_status, ro.refund_status, + ro.renter_id, COALESCE(ru.phone, '') AS renter_phone, COALESCE(ru.nickname, '') AS renter_nickname, + ro.owner_id, COALESCE(ou.phone, '') AS owner_phone, COALESCE(ou.nickname, '') AS owner_nickname, + ro.rent_amount_cent AS order_rent_amount_cent, ro.deposit_amount_cent AS order_deposit_amount_cent, + COALESCE(oc.rent_amount_cent, 0) AS checkout_rent_amount_cent, + COALESCE(oc.renter_refund_amount_cent, 0) AS checkout_renter_refund_cent, + COALESCE(oc.owner_income_amount_cent, 0) AS checkout_owner_income_cent, + COALESCE(oc.platform_fee_cent, 0) AS checkout_platform_fee_cent, + COALESCE(w.owner_wallet_income_amount_cent, 0) AS owner_wallet_income_amount_cent, + COALESCE(p.paid_amount_cent, 0) AS paid_amount_cent, + COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent, + COALESCE(p.refunding_amount_cent, 0) AS refunding_amount_cent, + COALESCE(p.failed_refund_amount_cent, 0) AS failed_refund_amount_cent, + COALESCE(p.paid_amount_cent, 0) - COALESCE(p.refunded_amount_cent, 0) AS channel_net_amount_cent, + COALESCE(oc.platform_fee_cent, 0) + ((COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0))) AS platform_net_amount_cent, + COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0) AS settlement_diff_amount_cent, + CASE + WHEN COALESCE(p.failed_refund_amount_cent, 0) > 0 THEN 'refund_failed' + WHEN COALESCE(p.refunding_amount_cent, 0) > 0 OR ro.refund_status = 'refunding' THEN 'refund_pending' + WHEN ABS(COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0)) >= 5 THEN 'settlement_diff' + ELSE 'normal' + END AS finance_status, + ro.created_at, ro.settled_at`). + Joins("LEFT JOIN users AS ru ON ru.id = ro.renter_id"). + Joins("LEFT JOIN users AS ou ON ou.id = ro.owner_id"). + Joins("LEFT JOIN (?) AS oc ON oc.order_id = ro.id", acceptedCheckoutSubquery(r.db.WithContext(ctx))). + Joins("LEFT JOIN (?) AS p ON p.order_id = ro.id", orderPaymentSubquery(r.db.WithContext(ctx))). + Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(r.db.WithContext(ctx))) + + if query.OrderNo != "" { + db = db.Where("ro.order_no = ?", query.OrderNo) + } + if query.UserID > 0 { + db = db.Where("(ro.renter_id = ? OR ro.owner_id = ?)", query.UserID, query.UserID) + } + if query.OrderStatus != "" { + db = db.Where("ro.status = ?", query.OrderStatus) + } + if query.SettlementStatus != "" { + db = db.Where("ro.settlement_status = ?", query.SettlementStatus) + } + if !query.StartDate.IsZero() && !query.EndDate.IsZero() { + if query.DateType == "created" { + db = db.Where("ro.created_at >= ? AND ro.created_at <= ?", query.StartDate, query.EndDate) + } else { + db = db.Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate) + } + } + return db +} diff --git a/backend/internal/modules/adminfinance/helper.go b/backend/internal/modules/adminfinance/helper.go new file mode 100644 index 0000000..6eed500 --- /dev/null +++ b/backend/internal/modules/adminfinance/helper.go @@ -0,0 +1,32 @@ +package adminfinance + +import ( + "time" + + "hfb_sys/backend/internal/timeutil" +) + +func refundBizTypes() []string { + return []string{ + "cancel_refund", + "admin_close_refund", + "admin_refund", + "checkout_refund", + "deposit_refund", + "rent_refund", + "arbitration_refund", + } +} + +func dayStart(value time.Time) time.Time { + loc := timeutil.ShanghaiLocation() + local := value.In(loc) + return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, loc) +} + +func absCent(value int64) int64 { + if value < 0 { + return -value + } + return value +} diff --git a/backend/internal/modules/adminfinance/presenter.go b/backend/internal/modules/adminfinance/presenter.go new file mode 100644 index 0000000..011c3af --- /dev/null +++ b/backend/internal/modules/adminfinance/presenter.go @@ -0,0 +1,77 @@ +package adminfinance + +import ( + "time" +) + +type financeDetailRow struct { + OrderID uint64 + OrderNo string + OrderStatus string + SettlementStatus string + RefundStatus string + RenterID uint64 + RenterPhone string + RenterNickname string + OwnerID uint64 + OwnerPhone string + OwnerNickname string + OrderRentAmountCent int64 + OrderDepositAmountCent int64 + CheckoutRentAmountCent int64 + CheckoutRenterRefundCent int64 + CheckoutOwnerIncomeCent int64 + CheckoutPlatformFeeCent int64 + OwnerWalletIncomeAmountCent int64 + PaidAmountCent int64 + RefundedAmountCent int64 + RefundingAmountCent int64 + FailedRefundAmountCent int64 + ChannelNetAmountCent int64 + PlatformNetAmountCent int64 + SettlementDiffAmountCent int64 + FinanceStatus string + CreatedAt time.Time + SettledAt *time.Time +} + +func (r financeDetailRow) toDTO() FinanceDetailDTO { + diff := r.SettlementDiffAmountCent + status := r.FinanceStatus + if status == "" { + status = "normal" + } + if absCent(diff) < 5 && status == "settlement_diff" { + status = "normal" + } + return FinanceDetailDTO{ + OrderID: r.OrderID, + OrderNo: r.OrderNo, + OrderStatus: r.OrderStatus, + SettlementStatus: r.SettlementStatus, + RefundStatus: r.RefundStatus, + RenterID: r.RenterID, + RenterPhone: r.RenterPhone, + RenterNickname: r.RenterNickname, + OwnerID: r.OwnerID, + OwnerPhone: r.OwnerPhone, + OwnerNickname: r.OwnerNickname, + OrderRentAmountCent: r.OrderRentAmountCent, + OrderDepositAmountCent: r.OrderDepositAmountCent, + CheckoutRentAmountCent: r.CheckoutRentAmountCent, + CheckoutRenterRefundCent: r.CheckoutRenterRefundCent, + CheckoutOwnerIncomeCent: r.CheckoutOwnerIncomeCent, + CheckoutPlatformFeeCent: r.CheckoutPlatformFeeCent, + OwnerWalletIncomeAmountCent: r.OwnerWalletIncomeAmountCent, + PaidAmountCent: r.PaidAmountCent, + RefundedAmountCent: r.RefundedAmountCent, + RefundingAmountCent: r.RefundingAmountCent, + FailedRefundAmountCent: r.FailedRefundAmountCent, + ChannelNetAmountCent: r.ChannelNetAmountCent, + PlatformNetAmountCent: r.PlatformNetAmountCent, + SettlementDiffAmountCent: diff, + FinanceStatus: status, + CreatedAt: r.CreatedAt, + SettledAt: r.SettledAt, + } +} diff --git a/backend/internal/modules/adminfinance/repository.go b/backend/internal/modules/adminfinance/repository.go index ae7c783..fa79478 100644 --- a/backend/internal/modules/adminfinance/repository.go +++ b/backend/internal/modules/adminfinance/repository.go @@ -1,11 +1,6 @@ package adminfinance import ( - "context" - "time" - - "hfb_sys/backend/internal/timeutil" - "gorm.io/gorm" ) @@ -16,374 +11,3 @@ type Repository struct { func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } - -func (r *Repository) Dashboard(ctx context.Context, query DashboardQuery) (*DashboardDTO, error) { - dailyItems, err := r.dailyItems(ctx, query) - if err != nil { - return nil, err - } - summary, err := r.summary(ctx, query) - if err != nil { - return nil, err - } - return &DashboardDTO{ - Summary: *summary, - DailyItems: dailyItems, - GeneratedAt: timeutil.ShanghaiNow(), - }, nil -} - -func (r *Repository) Details(ctx context.Context, query DetailQuery) (*PaginatedResult, error) { - baseDB := r.db.WithContext(ctx) - db := r.financeDetailBaseQuery(ctx, query) - var total int64 - countDB := baseDB.Table("(?) AS finance_rows", db) - if err := countDB.Count(&total).Error; err != nil { - return nil, err - } - - offset := (query.Page - 1) * query.PageSize - rows := make([]financeDetailRow, 0, query.PageSize) - if err := db.Order("ro.id DESC").Offset(offset).Limit(query.PageSize).Scan(&rows).Error; err != nil { - return nil, err - } - items := make([]FinanceDetailDTO, 0, len(rows)) - for _, row := range rows { - items = append(items, row.toDTO()) - } - return &PaginatedResult{Items: items, Total: total, Page: query.Page, PageSize: query.PageSize}, nil -} - -func (r *Repository) summary(ctx context.Context, query DashboardQuery) (*FinanceSummaryDTO, error) { - db := r.db.WithContext(ctx) - var payment paymentSummaryRow - if err := db.Table("payment_orders"). - Select(`COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS total_refund_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN 1 ELSE 0 END), 0) AS successful_refund_count, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count`, - refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()). - Where("created_at >= ? AND created_at <= ?", query.StartDate, query.EndDate). - Scan(&payment).Error; err != nil { - return nil, err - } - - var settlement settlementSummaryRow - if err := db.Table("rental_orders AS ro"). - Select(`COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent, - COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent, - COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent, - COUNT(ro.id) AS settled_order_count`). - Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'"). - Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(db)). - Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate). - Scan(&settlement).Error; err != nil { - return nil, err - } - - var exceptionCount int64 - if err := db.Table("(?) AS d", r.financeDetailBaseQuery(ctx, DetailQuery{ - DateType: "settled", - StartDate: query.StartDate, - EndDate: query.EndDate, - })). - Where("finance_status <> ?", "normal"). - Count(&exceptionCount).Error; err != nil { - return nil, err - } - - return &FinanceSummaryDTO{ - TotalFlowAmountCent: payment.TotalFlowAmountCent, - TotalRefundAmountCent: payment.TotalRefundAmountCent, - PendingRefundAmountCent: payment.PendingRefundAmountCent, - ChannelNetAmountCent: payment.TotalFlowAmountCent - payment.TotalRefundAmountCent, - PlatformIncomeAmountCent: settlement.PlatformIncomeAmountCent, - OwnerShouldIncomeAmountCent: settlement.OwnerShouldIncomeAmountCent, - OwnerWalletIncomeAmountCent: settlement.OwnerWalletIncomeAmountCent, - SettlementDiffAmountCent: settlement.OwnerShouldIncomeAmountCent - settlement.OwnerWalletIncomeAmountCent, - SuccessfulPayCount: payment.SuccessfulPayCount, - SuccessfulRefundCount: payment.SuccessfulRefundCount, - PendingRefundCount: payment.PendingRefundCount, - SettledOrderCount: settlement.SettledOrderCount, - FinancialExceptionCount: exceptionCount, - }, nil -} - -func (r *Repository) dailyItems(ctx context.Context, query DashboardQuery) ([]FinanceDailyDTO, error) { - db := r.db.WithContext(ctx) - payments := make([]dailyPaymentRow, 0) - if err := db.Table("payment_orders"). - Select(`DATE(created_at) AS date, - COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS total_flow_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS total_refund_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS pending_refund_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ('order_pay', 'wallet_recharge') AND status = 'paid' THEN 1 ELSE 0 END), 0) AS successful_pay_count, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN 1 ELSE 0 END), 0) AS successful_refund_count, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN 1 ELSE 0 END), 0) AS pending_refund_count`, - refundBizTypes(), refundBizTypes(), refundBizTypes(), refundBizTypes()). - Where("created_at >= ? AND created_at <= ?", query.StartDate, query.EndDate). - Group("DATE(created_at)"). - Scan(&payments).Error; err != nil { - return nil, err - } - - settlements := make([]dailySettlementRow, 0) - if err := db.Table("rental_orders AS ro"). - Select(`DATE(ro.settled_at) AS date, - COALESCE(SUM(oc.platform_fee_cent), 0) AS platform_income_amount_cent, - COALESCE(SUM(oc.owner_income_amount_cent), 0) AS owner_should_income_amount_cent, - COALESCE(SUM(COALESCE(w.owner_wallet_income_amount_cent, 0)), 0) AS owner_wallet_income_amount_cent, - COUNT(ro.id) AS settled_order_count`). - Joins("JOIN order_checkouts AS oc ON oc.order_id = ro.id AND oc.status = 'accepted'"). - Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(db)). - Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate). - Group("DATE(ro.settled_at)"). - Scan(&settlements).Error; err != nil { - return nil, err - } - - itemsByDate := make(map[string]FinanceDailyDTO) - for day := dayStart(query.StartDate); !day.After(query.EndDate); day = day.AddDate(0, 0, 1) { - date := day.Format("2006-01-02") - itemsByDate[date] = FinanceDailyDTO{Date: date} - } - for _, row := range payments { - item := itemsByDate[row.Date] - item.Date = row.Date - item.TotalFlowAmountCent = row.TotalFlowAmountCent - item.TotalRefundAmountCent = row.TotalRefundAmountCent - item.PendingRefundAmountCent = row.PendingRefundAmountCent - item.ChannelNetAmountCent = row.TotalFlowAmountCent - row.TotalRefundAmountCent - item.SuccessfulPayCount = row.SuccessfulPayCount - item.SuccessfulRefundCount = row.SuccessfulRefundCount - item.PendingRefundCount = row.PendingRefundCount - itemsByDate[row.Date] = item - } - for _, row := range settlements { - item := itemsByDate[row.Date] - item.Date = row.Date - item.PlatformIncomeAmountCent = row.PlatformIncomeAmountCent - item.OwnerShouldIncomeAmountCent = row.OwnerShouldIncomeAmountCent - item.OwnerWalletIncomeAmountCent = row.OwnerWalletIncomeAmountCent - item.SettlementDiffAmountCent = row.OwnerShouldIncomeAmountCent - row.OwnerWalletIncomeAmountCent - item.SettledOrderCount = row.SettledOrderCount - itemsByDate[row.Date] = item - } - - items := make([]FinanceDailyDTO, 0, len(itemsByDate)) - for day := dayStart(query.StartDate); !day.After(query.EndDate); day = day.AddDate(0, 0, 1) { - items = append(items, itemsByDate[day.Format("2006-01-02")]) - } - return items, nil -} - -func (r *Repository) financeDetailBaseQuery(ctx context.Context, query DetailQuery) *gorm.DB { - db := r.db.WithContext(ctx).Table("rental_orders AS ro"). - Select(`ro.id AS order_id, ro.order_no, ro.status AS order_status, ro.settlement_status, ro.refund_status, - ro.renter_id, COALESCE(ru.phone, '') AS renter_phone, COALESCE(ru.nickname, '') AS renter_nickname, - ro.owner_id, COALESCE(ou.phone, '') AS owner_phone, COALESCE(ou.nickname, '') AS owner_nickname, - ro.rent_amount_cent AS order_rent_amount_cent, ro.deposit_amount_cent AS order_deposit_amount_cent, - COALESCE(oc.rent_amount_cent, 0) AS checkout_rent_amount_cent, - COALESCE(oc.renter_refund_amount_cent, 0) AS checkout_renter_refund_cent, - COALESCE(oc.owner_income_amount_cent, 0) AS checkout_owner_income_cent, - COALESCE(oc.platform_fee_cent, 0) AS checkout_platform_fee_cent, - COALESCE(w.owner_wallet_income_amount_cent, 0) AS owner_wallet_income_amount_cent, - COALESCE(p.paid_amount_cent, 0) AS paid_amount_cent, - COALESCE(p.refunded_amount_cent, 0) AS refunded_amount_cent, - COALESCE(p.refunding_amount_cent, 0) AS refunding_amount_cent, - COALESCE(p.failed_refund_amount_cent, 0) AS failed_refund_amount_cent, - COALESCE(p.paid_amount_cent, 0) - COALESCE(p.refunded_amount_cent, 0) AS channel_net_amount_cent, - COALESCE(oc.platform_fee_cent, 0) + ((COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0))) AS platform_net_amount_cent, - COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0) AS settlement_diff_amount_cent, - CASE - WHEN COALESCE(p.failed_refund_amount_cent, 0) > 0 THEN 'refund_failed' - WHEN COALESCE(p.refunding_amount_cent, 0) > 0 OR ro.refund_status = 'refunding' THEN 'refund_pending' - WHEN ABS(COALESCE(oc.owner_income_amount_cent, 0) - COALESCE(w.owner_wallet_income_amount_cent, 0)) >= 5 THEN 'settlement_diff' - ELSE 'normal' - END AS finance_status, - ro.created_at, ro.settled_at`). - Joins("LEFT JOIN users AS ru ON ru.id = ro.renter_id"). - Joins("LEFT JOIN users AS ou ON ou.id = ro.owner_id"). - Joins("LEFT JOIN (?) AS oc ON oc.order_id = ro.id", acceptedCheckoutSubquery(r.db.WithContext(ctx))). - Joins("LEFT JOIN (?) AS p ON p.order_id = ro.id", orderPaymentSubquery(r.db.WithContext(ctx))). - Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(r.db.WithContext(ctx))) - - if query.OrderNo != "" { - db = db.Where("ro.order_no = ?", query.OrderNo) - } - if query.UserID > 0 { - db = db.Where("(ro.renter_id = ? OR ro.owner_id = ?)", query.UserID, query.UserID) - } - if query.OrderStatus != "" { - db = db.Where("ro.status = ?", query.OrderStatus) - } - if query.SettlementStatus != "" { - db = db.Where("ro.settlement_status = ?", query.SettlementStatus) - } - if !query.StartDate.IsZero() && !query.EndDate.IsZero() { - if query.DateType == "created" { - db = db.Where("ro.created_at >= ? AND ro.created_at <= ?", query.StartDate, query.EndDate) - } else { - db = db.Where("ro.settled_at >= ? AND ro.settled_at <= ?", query.StartDate, query.EndDate) - } - } - return db -} - -func acceptedCheckoutSubquery(db *gorm.DB) *gorm.DB { - latest := db.Table("order_checkouts").Select("MAX(id) AS id").Where("status = ?", "accepted").Group("order_id") - return db.Table("order_checkouts AS oc"). - Select("oc.*"). - Joins("JOIN (?) AS latest ON latest.id = oc.id", latest) -} - -func orderPaymentSubquery(db *gorm.DB) *gorm.DB { - return db.Table("payment_orders"). - Select(`order_id, - COALESCE(SUM(CASE WHEN biz_type = 'order_pay' AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS paid_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS refunded_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS refunding_amount_cent, - COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'failed' THEN amount_cent ELSE 0 END), 0) AS failed_refund_amount_cent`, - refundBizTypes(), refundBizTypes(), refundBizTypes()). - Group("order_id") -} - -func ownerWalletIncomeSubquery(db *gorm.DB) *gorm.DB { - return db.Table("wallet_ledger"). - Select("order_id, COALESCE(SUM(amount_cent), 0) AS owner_wallet_income_amount_cent"). - Where("direction = ? AND biz_type IN ? AND order_id IS NOT NULL", "in", []string{"owner_income", "deposit_compensation"}). - Group("order_id") -} - -func refundBizTypes() []string { - return []string{ - "cancel_refund", - "admin_close_refund", - "admin_refund", - "checkout_refund", - "deposit_refund", - "rent_refund", - "arbitration_refund", - } -} - -func dayStart(value time.Time) time.Time { - loc := timeutil.ShanghaiLocation() - local := value.In(loc) - return time.Date(local.Year(), local.Month(), local.Day(), 0, 0, 0, 0, loc) -} - -type paymentSummaryRow struct { - TotalFlowAmountCent int64 - TotalRefundAmountCent int64 - PendingRefundAmountCent int64 - SuccessfulPayCount int64 - SuccessfulRefundCount int64 - PendingRefundCount int64 -} - -type settlementSummaryRow struct { - PlatformIncomeAmountCent int64 - OwnerShouldIncomeAmountCent int64 - OwnerWalletIncomeAmountCent int64 - SettledOrderCount int64 -} - -type dailyPaymentRow struct { - Date string - TotalFlowAmountCent int64 - TotalRefundAmountCent int64 - PendingRefundAmountCent int64 - SuccessfulPayCount int64 - SuccessfulRefundCount int64 - PendingRefundCount int64 -} - -type dailySettlementRow struct { - Date string - PlatformIncomeAmountCent int64 - OwnerShouldIncomeAmountCent int64 - OwnerWalletIncomeAmountCent int64 - SettledOrderCount int64 -} - -type financeDetailRow struct { - OrderID uint64 - OrderNo string - OrderStatus string - SettlementStatus string - RefundStatus string - RenterID uint64 - RenterPhone string - RenterNickname string - OwnerID uint64 - OwnerPhone string - OwnerNickname string - OrderRentAmountCent int64 - OrderDepositAmountCent int64 - CheckoutRentAmountCent int64 - CheckoutRenterRefundCent int64 - CheckoutOwnerIncomeCent int64 - CheckoutPlatformFeeCent int64 - OwnerWalletIncomeAmountCent int64 - PaidAmountCent int64 - RefundedAmountCent int64 - RefundingAmountCent int64 - FailedRefundAmountCent int64 - ChannelNetAmountCent int64 - PlatformNetAmountCent int64 - SettlementDiffAmountCent int64 - FinanceStatus string - CreatedAt time.Time - SettledAt *time.Time -} - -func (r financeDetailRow) toDTO() FinanceDetailDTO { - diff := r.SettlementDiffAmountCent - status := r.FinanceStatus - if status == "" { - status = "normal" - } - if absCent(diff) < 5 && status == "settlement_diff" { - status = "normal" - } - return FinanceDetailDTO{ - OrderID: r.OrderID, - OrderNo: r.OrderNo, - OrderStatus: r.OrderStatus, - SettlementStatus: r.SettlementStatus, - RefundStatus: r.RefundStatus, - RenterID: r.RenterID, - RenterPhone: r.RenterPhone, - RenterNickname: r.RenterNickname, - OwnerID: r.OwnerID, - OwnerPhone: r.OwnerPhone, - OwnerNickname: r.OwnerNickname, - OrderRentAmountCent: r.OrderRentAmountCent, - OrderDepositAmountCent: r.OrderDepositAmountCent, - CheckoutRentAmountCent: r.CheckoutRentAmountCent, - CheckoutRenterRefundCent: r.CheckoutRenterRefundCent, - CheckoutOwnerIncomeCent: r.CheckoutOwnerIncomeCent, - CheckoutPlatformFeeCent: r.CheckoutPlatformFeeCent, - OwnerWalletIncomeAmountCent: r.OwnerWalletIncomeAmountCent, - PaidAmountCent: r.PaidAmountCent, - RefundedAmountCent: r.RefundedAmountCent, - RefundingAmountCent: r.RefundingAmountCent, - FailedRefundAmountCent: r.FailedRefundAmountCent, - ChannelNetAmountCent: r.ChannelNetAmountCent, - PlatformNetAmountCent: r.PlatformNetAmountCent, - SettlementDiffAmountCent: diff, - FinanceStatus: status, - CreatedAt: r.CreatedAt, - SettledAt: r.SettledAt, - } -} - -func absCent(value int64) int64 { - if value < 0 { - return -value - } - return value -} diff --git a/backend/internal/modules/adminfinance/subquery.go b/backend/internal/modules/adminfinance/subquery.go new file mode 100644 index 0000000..7ef386b --- /dev/null +++ b/backend/internal/modules/adminfinance/subquery.go @@ -0,0 +1,30 @@ +package adminfinance + +import ( + "gorm.io/gorm" +) + +func acceptedCheckoutSubquery(db *gorm.DB) *gorm.DB { + latest := db.Table("order_checkouts").Select("MAX(id) AS id").Where("status = ?", "accepted").Group("order_id") + return db.Table("order_checkouts AS oc"). + Select("oc.*"). + Joins("JOIN (?) AS latest ON latest.id = oc.id", latest) +} + +func orderPaymentSubquery(db *gorm.DB) *gorm.DB { + return db.Table("payment_orders"). + Select(`order_id, + COALESCE(SUM(CASE WHEN biz_type = 'order_pay' AND status = 'paid' THEN amount_cent ELSE 0 END), 0) AS paid_amount_cent, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunded' THEN amount_cent ELSE 0 END), 0) AS refunded_amount_cent, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'refunding' THEN amount_cent ELSE 0 END), 0) AS refunding_amount_cent, + COALESCE(SUM(CASE WHEN biz_type IN ? AND status = 'failed' THEN amount_cent ELSE 0 END), 0) AS failed_refund_amount_cent`, + refundBizTypes(), refundBizTypes(), refundBizTypes()). + Group("order_id") +} + +func ownerWalletIncomeSubquery(db *gorm.DB) *gorm.DB { + return db.Table("wallet_ledger"). + Select("order_id, COALESCE(SUM(amount_cent), 0) AS owner_wallet_income_amount_cent"). + Where("direction = ? AND biz_type IN ? AND order_id IS NOT NULL", "in", []string{"owner_income", "deposit_compensation"}). + Group("order_id") +}