package adminfinance import ( "errors" "net/http" "strconv" "strings" "time" "hfb_sys/backend/internal/auditlog" "hfb_sys/backend/internal/middleware" "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(c.Request.Context(), 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(c.Request.Context(), query) if err != nil { writeFinanceError(c, err) return } response.OK(c, result) } func (h *Handler) Disbursements(c *gin.Context) { query, ok := parseDisbursementQuery(c) if !ok { return } result, err := h.service.Disbursements(c.Request.Context(), query) if err != nil { writeFinanceError(c, err) return } response.OK(c, result) } func (h *Handler) OperatingExpenses(c *gin.Context) { query, ok := parseOperatingExpenseQuery(c) if !ok { return } result, err := h.service.OperatingExpenses(c.Request.Context(), query) if err != nil { writeFinanceError(c, err) return } response.OK(c, result) } func (h *Handler) CreateManualDisbursement(c *gin.Context) { adminID, ok := financeAdminID(c) if !ok { response.Unauthorized(c, "缺少管理员上下文") return } var req CreateManualDisbursementRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "线下出款信息不正确") return } item, err := h.service.CreateManualDisbursement(c.Request.Context(), req, adminID, financeAuditMeta(c)) if err != nil { writeFinanceError(c, err) return } response.Created(c, item) } func (h *Handler) VoidManualDisbursement(c *gin.Context) { adminID, ok := financeAdminID(c) if !ok { response.Unauthorized(c, "缺少管理员上下文") return } id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { response.BadRequest(c, "线下出款记录 ID 不正确") return } var req VoidManualDisbursementRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "请填写作废原因") return } item, err := h.service.VoidManualDisbursement(c.Request.Context(), id, req.Reason, adminID, financeAuditMeta(c)) if err != nil { writeFinanceError(c, err) return } response.OK(c, item) } func (h *Handler) CreateOperatingExpense(c *gin.Context) { adminID, ok := financeAdminID(c) if !ok { response.Unauthorized(c, "缺少管理员上下文") return } var req CreateOperatingExpenseRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "运营开支信息不正确") return } item, err := h.service.CreateOperatingExpense(c.Request.Context(), req, adminID, financeAuditMeta(c)) if err != nil { writeFinanceError(c, err) return } response.Created(c, item) } func (h *Handler) VoidOperatingExpense(c *gin.Context) { adminID, ok := financeAdminID(c) if !ok { response.Unauthorized(c, "缺少管理员上下文") return } id, err := strconv.ParseUint(c.Param("id"), 10, 64) if err != nil || id == 0 { response.BadRequest(c, "运营开支记录 ID 不正确") return } var req VoidOperatingExpenseRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "请填写作废原因") return } item, err := h.service.VoidOperatingExpense(c.Request.Context(), id, req.Reason, adminID, financeAuditMeta(c)) if err != nil { writeFinanceError(c, err) return } response.OK(c, item) } 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"), SettlementMode: c.Query("settlement_mode"), OfflineSettlementStatus: c.Query("offline_settlement_status"), DateType: c.DefaultQuery("date_type", "settled"), StartDate: start, EndDate: end, } if query.DateType != "created" && query.DateType != "settled" && query.DateType != "offline_settled" { response.BadRequest(c, "日期类型不正确") return query, false } if query.SettlementMode != "" && query.SettlementMode != "owner_wallet" && query.SettlementMode != "platform_managed" { response.BadRequest(c, "结算模式不正确") return query, false } if query.OfflineSettlementStatus != "" && query.OfflineSettlementStatus != "none" && query.OfflineSettlementStatus != "pending" && query.OfflineSettlementStatus != "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 parseDisbursementQuery(c *gin.Context) (DisbursementQuery, bool) { start, end, ok := parseDateRange(c, 29) if !ok { return DisbursementQuery{}, false } query := DisbursementQuery{ SourceType: strings.TrimSpace(c.Query("source_type")), Status: strings.TrimSpace(c.Query("status")), DateType: c.DefaultQuery("date_type", "created"), BusinessNo: strings.TrimSpace(c.Query("business_no")), Keyword: strings.TrimSpace(c.Query("keyword")), StartDate: start, EndDate: end, } if query.SourceType != "" && query.SourceType != "platform_managed" && query.SourceType != "withdrawal" && query.SourceType != "manual_offline" { response.BadRequest(c, "出款类型不正确") return query, false } if query.Status != "" && query.Status != "review_pending" && query.Status != "payment_pending" && query.Status != "paid" && query.Status != "rejected" && query.Status != "cancelled" && query.Status != "voided" { response.BadRequest(c, "出款状态不正确") return query, false } if query.DateType != "created" && query.DateType != "paid" { 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 parseOperatingExpenseQuery(c *gin.Context) (OperatingExpenseQuery, bool) { start, end, ok := parseDateRange(c, 29) if !ok { return OperatingExpenseQuery{}, false } query := OperatingExpenseQuery{ Status: strings.TrimSpace(c.Query("status")), Category: strings.TrimSpace(c.Query("category")), Keyword: strings.TrimSpace(c.Query("keyword")), DateType: c.DefaultQuery("date_type", "created"), StartDate: start, EndDate: end, } if query.Status != "" && query.Status != "paid" && query.Status != "voided" { response.BadRequest(c, "运营开支状态不正确") return query, false } if query.DateType != "created" && query.DateType != "occurred" { response.BadRequest(c, "日期类型不正确") return query, false } 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) { response.RecordError(c, err) switch { case errors.Is(err, ErrDependencyUnavailable): response.ServiceUnavailable(c, "数据库未连接") case errors.Is(err, ErrInvalidManualDisbursement): response.BadRequest(c, "线下出款信息不正确") case errors.Is(err, ErrManualDisbursementNotFound): response.NotFound(c, "线下出款记录不存在") case errors.Is(err, ErrManualDisbursementNotPaid): response.BadRequest(c, "该线下出款记录已作废") case errors.Is(err, ErrInvalidOperatingExpense): response.BadRequest(c, "运营开支信息不正确") case errors.Is(err, ErrOperatingExpenseNotFound): response.NotFound(c, "运营开支记录不存在") case errors.Is(err, ErrOperatingExpenseNotPaid): response.BadRequest(c, "该运营开支记录已作废") default: response.Error(c, http.StatusInternalServerError, "finance_error", "财务数据暂时不可用") } } func financeAdminID(c *gin.Context) (uint64, bool) { value, ok := c.Get(middleware.ContextAdminID) if !ok { return 0, false } adminID, ok := value.(uint64) return adminID, ok } func financeAuditMeta(c *gin.Context) auditlog.Meta { return auditlog.Meta{ IP: c.ClientIP(), UserAgent: c.GetHeader("User-Agent"), RequestID: middleware.GetRequestID(c), } }