package adminfinance import ( "errors" "fmt" "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")), Operator: strings.TrimSpace(c.Query("operator")), StartDate: start, EndDate: end, } if query.Operator == "" { query.Operator = strings.TrimSpace(c.Query("operator_name")) } if query.SourceType != "" && query.SourceType != "platform_managed" && query.SourceType != "pickup" && query.SourceType != "pickup_adjustment" && 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("operator_id"); raw != "" { value, err := strconv.ParseUint(raw, 10, 64) if err != nil || value == 0 { response.BadRequest(c, "操作人 ID 不正确") return query, false } query.OperatorID = value } 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 parseFlexibleDateTime(raw string, loc *time.Location, isEnd bool) (time.Time, error) { raw = strings.TrimSpace(raw) if raw == "" { return time.Time{}, errors.New("empty time string") } formats := []string{ "2006-01-02 15:04:05", "2006-01-02 15:04", "2006-01-02T15:04:05", "2006-01-02T15:04", time.RFC3339, "2006-01-02", } for _, fmtStr := range formats { if t, err := time.ParseInLocation(fmtStr, raw, loc); err == nil { switch fmtStr { case "2006-01-02": if isEnd { return t.AddDate(0, 0, 1).Add(-time.Nanosecond), nil } return t, nil case "2006-01-02 15:04", "2006-01-02T15:04": if isEnd { return t.Add(time.Minute).Add(-time.Nanosecond), nil } return t, nil case "2006-01-02 15:04:05", "2006-01-02T15:04:05": if isEnd { return t.Add(time.Second - time.Nanosecond), nil } return t, nil default: return t, nil } } } return time.Time{}, fmt.Errorf("invalid time format: %s", raw) } 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 rawStart := strings.TrimSpace(c.Query("start_time")) usingStartTime := rawStart != "" if rawStart == "" { rawStart = strings.TrimSpace(c.Query("start_date")) } if rawStart != "" { parsed, err := parseFlexibleDateTime(rawStart, loc, false) if err != nil { if usingStartTime { response.BadRequest(c, "开始时间不正确") } else { response.BadRequest(c, "开始日期不正确") } return start, end, false } start = parsed } rawEnd := strings.TrimSpace(c.Query("end_time")) usingEndTime := rawEnd != "" if rawEnd == "" { rawEnd = strings.TrimSpace(c.Query("end_date")) } if rawEnd != "" { parsed, err := parseFlexibleDateTime(rawEnd, loc, true) if err != nil { if usingEndTime { response.BadRequest(c, "结束时间不正确") } else { response.BadRequest(c, "结束日期不正确") } return start, end, false } end = parsed } if start.After(end) { if usingStartTime || usingEndTime { response.BadRequest(c, "开始时间不能晚于结束时间") } else { 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), } }