新增财务仪表盘并统一金额到角

This commit is contained in:
yml
2026-06-09 00:13:03 +08:00
parent a2a0489158
commit 2dfa2611fd
40 changed files with 1324 additions and 70 deletions
@@ -0,0 +1,96 @@
package adminfinance
import "time"
type DashboardQuery struct {
StartDate time.Time
EndDate time.Time
}
type DetailQuery struct {
OrderNo string
UserID uint64
OrderStatus string
SettlementStatus string
DateType string
StartDate time.Time
EndDate time.Time
Page int
PageSize int
}
type DashboardDTO struct {
Summary FinanceSummaryDTO `json:"summary"`
DailyItems []FinanceDailyDTO `json:"daily_items"`
GeneratedAt time.Time `json:"generated_at"`
}
type FinanceSummaryDTO struct {
TotalFlowAmountCent int64 `json:"total_flow_amount_cent"`
TotalRefundAmountCent int64 `json:"total_refund_amount_cent"`
PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformIncomeAmount float64 `json:"platform_income_amount"`
OwnerShouldIncomeAmount float64 `json:"owner_should_income_amount"`
OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"`
SettlementDiffAmount float64 `json:"settlement_diff_amount"`
SuccessfulPayCount int64 `json:"successful_pay_count"`
SuccessfulRefundCount int64 `json:"successful_refund_count"`
PendingRefundCount int64 `json:"pending_refund_count"`
SettledOrderCount int64 `json:"settled_order_count"`
FinancialExceptionCount int64 `json:"financial_exception_count"`
}
type FinanceDailyDTO struct {
Date string `json:"date"`
TotalFlowAmountCent int64 `json:"total_flow_amount_cent"`
TotalRefundAmountCent int64 `json:"total_refund_amount_cent"`
PendingRefundAmountCent int64 `json:"pending_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformIncomeAmount float64 `json:"platform_income_amount"`
OwnerShouldIncomeAmount float64 `json:"owner_should_income_amount"`
OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"`
SettlementDiffAmount float64 `json:"settlement_diff_amount"`
SuccessfulPayCount int64 `json:"successful_pay_count"`
SuccessfulRefundCount int64 `json:"successful_refund_count"`
PendingRefundCount int64 `json:"pending_refund_count"`
SettledOrderCount int64 `json:"settled_order_count"`
}
type PaginatedResult struct {
Items interface{} `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
PageSize int `json:"page_size"`
}
type FinanceDetailDTO struct {
OrderID uint64 `json:"order_id"`
OrderNo string `json:"order_no"`
OrderStatus string `json:"order_status"`
SettlementStatus string `json:"settlement_status"`
RefundStatus string `json:"refund_status"`
RenterID uint64 `json:"renter_id"`
RenterPhone string `json:"renter_phone"`
RenterNickname string `json:"renter_nickname"`
OwnerID uint64 `json:"owner_id"`
OwnerPhone string `json:"owner_phone"`
OwnerNickname string `json:"owner_nickname"`
OrderRentAmount float64 `json:"order_rent_amount"`
OrderDepositAmount float64 `json:"order_deposit_amount"`
CheckoutRentAmount float64 `json:"checkout_rent_amount"`
CheckoutRenterRefund float64 `json:"checkout_renter_refund"`
CheckoutOwnerIncome float64 `json:"checkout_owner_income"`
CheckoutPlatformFee float64 `json:"checkout_platform_fee"`
OwnerWalletIncomeAmount float64 `json:"owner_wallet_income_amount"`
PaidAmountCent int64 `json:"paid_amount_cent"`
RefundedAmountCent int64 `json:"refunded_amount_cent"`
RefundingAmountCent int64 `json:"refunding_amount_cent"`
FailedRefundAmountCent int64 `json:"failed_refund_amount_cent"`
ChannelNetAmountCent int64 `json:"channel_net_amount_cent"`
PlatformNetAmount float64 `json:"platform_net_amount"`
SettlementDiffAmount float64 `json:"settlement_diff_amount"`
FinanceStatus string `json:"finance_status"`
CreatedAt time.Time `json:"created_at"`
SettledAt *time.Time `json:"settled_at,omitempty"`
}
@@ -0,0 +1,127 @@
package adminfinance
import (
"errors"
"net/http"
"strconv"
"time"
"hfb_sys/backend/internal/timeutil"
"hfb_sys/backend/pkg/response"
"github.com/gin-gonic/gin"
)
type Handler struct {
service *Service
}
func NewHandler(service *Service) *Handler {
return &Handler{service: service}
}
func (h *Handler) Dashboard(c *gin.Context) {
query, ok := parseDashboardQuery(c)
if !ok {
return
}
item, err := h.service.Dashboard(query)
if err != nil {
writeFinanceError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) Details(c *gin.Context) {
query, ok := parseDetailQuery(c)
if !ok {
return
}
result, err := h.service.Details(query)
if err != nil {
writeFinanceError(c, err)
return
}
response.OK(c, result)
}
func parseDashboardQuery(c *gin.Context) (DashboardQuery, bool) {
start, end, ok := parseDateRange(c, 6)
if !ok {
return DashboardQuery{}, false
}
return DashboardQuery{StartDate: start, EndDate: end}, true
}
func parseDetailQuery(c *gin.Context) (DetailQuery, bool) {
start, end, ok := parseDateRange(c, 29)
if !ok {
return DetailQuery{}, false
}
query := DetailQuery{
OrderNo: c.Query("order_no"),
OrderStatus: c.Query("order_status"),
SettlementStatus: c.Query("settlement_status"),
DateType: c.DefaultQuery("date_type", "settled"),
StartDate: start,
EndDate: end,
}
if query.DateType != "created" && query.DateType != "settled" {
response.BadRequest(c, "日期类型不正确")
return query, false
}
if raw := c.Query("user_id"); raw != "" {
value, err := strconv.ParseUint(raw, 10, 64)
if err != nil || value == 0 {
response.BadRequest(c, "用户 ID 不正确")
return query, false
}
query.UserID = value
}
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
query.Page = page
query.PageSize = pageSize
return query, true
}
func parseDateRange(c *gin.Context, defaultLookbackDays int) (time.Time, time.Time, bool) {
loc := timeutil.ShanghaiLocation()
today := timeutil.ShanghaiNow()
defaultStart := time.Date(today.Year(), today.Month(), today.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, -defaultLookbackDays)
defaultEnd := time.Date(today.Year(), today.Month(), today.Day(), 23, 59, 59, int(time.Second-time.Nanosecond), loc)
start := defaultStart
end := defaultEnd
if raw := c.Query("start_date"); raw != "" {
parsed, err := time.ParseInLocation("2006-01-02", raw, loc)
if err != nil {
response.BadRequest(c, "开始日期不正确")
return start, end, false
}
start = parsed
}
if raw := c.Query("end_date"); raw != "" {
parsed, err := time.ParseInLocation("2006-01-02", raw, loc)
if err != nil {
response.BadRequest(c, "结束日期不正确")
return start, end, false
}
end = parsed.AddDate(0, 0, 1).Add(-time.Nanosecond)
}
if start.After(end) {
response.BadRequest(c, "开始日期不能晚于结束日期")
return start, end, false
}
return start, end, true
}
func writeFinanceError(c *gin.Context, err error) {
switch {
case errors.Is(err, ErrDependencyUnavailable):
response.ServiceUnavailable(c, "数据库未连接")
default:
response.Error(c, http.StatusInternalServerError, "finance_error", "财务数据暂时不可用")
}
}
@@ -0,0 +1,380 @@
package adminfinance
import (
"math"
"time"
"hfb_sys/backend/internal/timeutil"
"hfb_sys/backend/pkg/money"
"gorm.io/gorm"
)
type Repository struct {
db *gorm.DB
}
func NewRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
func (r *Repository) Dashboard(query DashboardQuery) (*DashboardDTO, error) {
dailyItems, err := r.dailyItems(query)
if err != nil {
return nil, err
}
summary, err := r.summary(query)
if err != nil {
return nil, err
}
return &DashboardDTO{
Summary: *summary,
DailyItems: dailyItems,
GeneratedAt: timeutil.ShanghaiNow(),
}, nil
}
func (r *Repository) Details(query DetailQuery) (*PaginatedResult, error) {
db := r.financeDetailBaseQuery(query)
var total int64
countDB := r.db.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(query DashboardQuery) (*FinanceSummaryDTO, error) {
var payment paymentSummaryRow
if err := r.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 := r.db.Table("rental_orders AS ro").
Select(`COALESCE(SUM(oc.platform_fee), 0) AS platform_income_amount,
COALESCE(SUM(oc.owner_income_amount), 0) AS owner_should_income_amount,
COALESCE(SUM(COALESCE(w.owner_wallet_income_amount, 0)), 0) AS owner_wallet_income_amount,
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(r.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 := r.db.Table("(?) AS d", r.financeDetailBaseQuery(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,
PlatformIncomeAmount: money.Round(settlement.PlatformIncomeAmount),
OwnerShouldIncomeAmount: money.Round(settlement.OwnerShouldIncomeAmount),
OwnerWalletIncomeAmount: money.Round(settlement.OwnerWalletIncomeAmount),
SettlementDiffAmount: money.Round(settlement.OwnerShouldIncomeAmount - settlement.OwnerWalletIncomeAmount),
SuccessfulPayCount: payment.SuccessfulPayCount,
SuccessfulRefundCount: payment.SuccessfulRefundCount,
PendingRefundCount: payment.PendingRefundCount,
SettledOrderCount: settlement.SettledOrderCount,
FinancialExceptionCount: exceptionCount,
}, nil
}
func (r *Repository) dailyItems(query DashboardQuery) ([]FinanceDailyDTO, error) {
payments := make([]dailyPaymentRow, 0)
if err := r.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 := r.db.Table("rental_orders AS ro").
Select(`DATE(ro.settled_at) AS date,
COALESCE(SUM(oc.platform_fee), 0) AS platform_income_amount,
COALESCE(SUM(oc.owner_income_amount), 0) AS owner_should_income_amount,
COALESCE(SUM(COALESCE(w.owner_wallet_income_amount, 0)), 0) AS owner_wallet_income_amount,
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(r.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.PlatformIncomeAmount = money.Round(row.PlatformIncomeAmount)
item.OwnerShouldIncomeAmount = money.Round(row.OwnerShouldIncomeAmount)
item.OwnerWalletIncomeAmount = money.Round(row.OwnerWalletIncomeAmount)
item.SettlementDiffAmount = money.Round(row.OwnerShouldIncomeAmount - row.OwnerWalletIncomeAmount)
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(query DetailQuery) *gorm.DB {
db := r.db.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 AS order_rent_amount, ro.deposit_amount AS order_deposit_amount,
COALESCE(oc.rent_amount, 0) AS checkout_rent_amount,
COALESCE(oc.renter_refund_amount, 0) AS checkout_renter_refund,
COALESCE(oc.owner_income_amount, 0) AS checkout_owner_income,
COALESCE(oc.platform_fee, 0) AS checkout_platform_fee,
COALESCE(w.owner_wallet_income_amount, 0) AS owner_wallet_income_amount,
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, 0) + ((COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0))) AS platform_net_amount,
COALESCE(oc.owner_income_amount, 0) - COALESCE(w.owner_wallet_income_amount, 0) AS settlement_diff_amount,
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, 0) - COALESCE(w.owner_wallet_income_amount, 0)) >= 0.05 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)).
Joins("LEFT JOIN (?) AS p ON p.order_id = ro.id", orderPaymentSubquery(r.db)).
Joins("LEFT JOIN (?) AS w ON w.order_id = ro.id", ownerWalletIncomeSubquery(r.db))
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), 0) AS owner_wallet_income_amount").
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 {
PlatformIncomeAmount float64
OwnerShouldIncomeAmount float64
OwnerWalletIncomeAmount float64
SettledOrderCount int64
}
type dailyPaymentRow struct {
Date string
TotalFlowAmountCent int64
TotalRefundAmountCent int64
PendingRefundAmountCent int64
SuccessfulPayCount int64
SuccessfulRefundCount int64
PendingRefundCount int64
}
type dailySettlementRow struct {
Date string
PlatformIncomeAmount float64
OwnerShouldIncomeAmount float64
OwnerWalletIncomeAmount float64
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
OrderRentAmount float64
OrderDepositAmount float64
CheckoutRentAmount float64
CheckoutRenterRefund float64
CheckoutOwnerIncome float64
CheckoutPlatformFee float64
OwnerWalletIncomeAmount float64
PaidAmountCent int64
RefundedAmountCent int64
RefundingAmountCent int64
FailedRefundAmountCent int64
ChannelNetAmountCent int64
PlatformNetAmount float64
SettlementDiffAmount float64
FinanceStatus string
CreatedAt time.Time
SettledAt *time.Time
}
func (r financeDetailRow) toDTO() FinanceDetailDTO {
diff := money.Round(r.SettlementDiffAmount)
status := r.FinanceStatus
if status == "" {
status = "normal"
}
if math.Abs(diff) < 0.05 && 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,
OrderRentAmount: money.Round(r.OrderRentAmount),
OrderDepositAmount: money.Round(r.OrderDepositAmount),
CheckoutRentAmount: money.Round(r.CheckoutRentAmount),
CheckoutRenterRefund: money.Round(r.CheckoutRenterRefund),
CheckoutOwnerIncome: money.Round(r.CheckoutOwnerIncome),
CheckoutPlatformFee: money.Round(r.CheckoutPlatformFee),
OwnerWalletIncomeAmount: money.Round(r.OwnerWalletIncomeAmount),
PaidAmountCent: r.PaidAmountCent,
RefundedAmountCent: r.RefundedAmountCent,
RefundingAmountCent: r.RefundingAmountCent,
FailedRefundAmountCent: r.FailedRefundAmountCent,
ChannelNetAmountCent: r.ChannelNetAmountCent,
PlatformNetAmount: money.Round(r.PlatformNetAmount),
SettlementDiffAmount: diff,
FinanceStatus: status,
CreatedAt: r.CreatedAt,
SettledAt: r.SettledAt,
}
}
@@ -0,0 +1,36 @@
package adminfinance
import "errors"
var ErrDependencyUnavailable = errors.New("dependency unavailable")
type Service struct {
repo *Repository
}
func NewService(repo *Repository) *Service {
return &Service{repo: repo}
}
func (s *Service) Dashboard(query DashboardQuery) (*DashboardDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.Dashboard(query)
}
func (s *Service) Details(query DetailQuery) (*PaginatedResult, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if query.Page < 1 {
query.Page = 1
}
if query.PageSize < 1 {
query.PageSize = 20
}
if query.PageSize > 100 {
query.PageSize = 100
}
return s.repo.Details(query)
}
+9
View File
@@ -11,6 +11,7 @@ import (
"hfb_sys/backend/internal/modules/adminaudit"
"hfb_sys/backend/internal/modules/adminauth"
"hfb_sys/backend/internal/modules/admindashboard"
"hfb_sys/backend/internal/modules/adminfinance"
"hfb_sys/backend/internal/modules/adminmgr"
"hfb_sys/backend/internal/modules/adminrole"
"hfb_sys/backend/internal/modules/adminuser"
@@ -78,6 +79,12 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
}
adminDashboardService := admindashboard.NewService(adminDashboardRepo)
adminDashboardHandler := admindashboard.NewHandler(adminDashboardService)
var adminFinanceRepo *adminfinance.Repository
if deps.DB != nil {
adminFinanceRepo = adminfinance.NewRepository(deps.DB)
}
adminFinanceService := adminfinance.NewService(adminFinanceRepo)
adminFinanceHandler := adminfinance.NewHandler(adminFinanceService)
var adminUserRepo *adminuser.Repository
if deps.DB != nil {
adminUserRepo = adminuser.NewRepository(deps.DB)
@@ -424,6 +431,8 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
adminRoutes.POST("/listings/:id/mark-abnormal", requirePerm("listing:offline"), listingHandler.AdminMarkAbnormal)
adminRoutes.GET("/disputes", requirePerm("dispute:view"), disputeHandler.AdminList)
adminRoutes.POST("/disputes/:id/arbitrate", requirePerm("dispute:arbitrate"), disputeHandler.AdminArbitrate)
adminRoutes.GET("/finance/dashboard", requirePerm("wallet:view"), adminFinanceHandler.Dashboard)
adminRoutes.GET("/finance/details", requirePerm("wallet:view"), adminFinanceHandler.Details)
adminRoutes.GET("/wallet/ledger", requirePerm("wallet:view"), walletHandler.AdminLedger)
adminRoutes.GET("/payments", requirePerm("wallet:view"), paymentHandler.AdminList)