From 914df9edf2dea2549b8b2e95c917882bc5978c77 Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 15 Jan 2026 17:50:37 +0800 Subject: [PATCH] feat: deepen report metrics --- backend/app/http/v1/dto/creator_report.go | 42 +++ backend/app/services/creator_report.go | 269 ++++++++++++++++-- backend/app/services/creator_test.go | 73 ++++- backend/app/services/super.go | 264 +++++++++++++++-- backend/docs/docs.go | 84 ++++++ backend/docs/swagger.json | 84 ++++++ backend/docs/swagger.yaml | 63 ++++ docs/superadmin_progress.md | 13 +- .../src/views/superadmin/Finance.vue | 71 ++++- .../src/views/superadmin/Reports.vue | 252 +++++++++++++++- 10 files changed, 1163 insertions(+), 52 deletions(-) diff --git a/backend/app/http/v1/dto/creator_report.go b/backend/app/http/v1/dto/creator_report.go index 248da5c..1d9327e 100644 --- a/backend/app/http/v1/dto/creator_report.go +++ b/backend/app/http/v1/dto/creator_report.go @@ -19,6 +19,16 @@ type ReportOverviewResponse struct { type ReportSummary struct { // TotalViews 内容累计曝光(全量累计值,用于粗略换算)。 TotalViews int64 `json:"total_views"` + // ContentCount 内容总量(当前快照)。 + ContentCount int64 `json:"content_count"` + // ContentCreated 统计区间内新增内容数。 + ContentCreated int64 `json:"content_created"` + // LikeActions 统计区间内新增点赞数(基于互动记录)。 + LikeActions int64 `json:"like_actions"` + // FavoriteActions 统计区间内新增收藏数(基于互动记录)。 + FavoriteActions int64 `json:"favorite_actions"` + // CommentCount 统计区间内新增评论数。 + CommentCount int64 `json:"comment_count"` // PaidOrders 统计区间内已支付订单数。 PaidOrders int64 `json:"paid_orders"` // PaidAmount 统计区间内已支付金额(单位元)。 @@ -27,6 +37,18 @@ type ReportSummary struct { RefundOrders int64 `json:"refund_orders"` // RefundAmount 统计区间内退款金额(单位元)。 RefundAmount float64 `json:"refund_amount"` + // WithdrawalApplyOrders 统计区间内提现申请订单数。 + WithdrawalApplyOrders int64 `json:"withdrawal_apply_orders"` + // WithdrawalApplyAmount 统计区间内提现申请金额(单位元)。 + WithdrawalApplyAmount float64 `json:"withdrawal_apply_amount"` + // WithdrawalPaidOrders 统计区间内提现完成订单数。 + WithdrawalPaidOrders int64 `json:"withdrawal_paid_orders"` + // WithdrawalPaidAmount 统计区间内提现完成金额(单位元)。 + WithdrawalPaidAmount float64 `json:"withdrawal_paid_amount"` + // WithdrawalFailedOrders 统计区间内提现失败订单数。 + WithdrawalFailedOrders int64 `json:"withdrawal_failed_orders"` + // WithdrawalFailedAmount 统计区间内提现失败金额(单位元)。 + WithdrawalFailedAmount float64 `json:"withdrawal_failed_amount"` // ConversionRate 转化率(已支付订单数 / 累计曝光)。 ConversionRate float64 `json:"conversion_rate"` } @@ -42,6 +64,26 @@ type ReportOverviewItem struct { RefundOrders int64 `json:"refund_orders"` // RefundAmount 当日退款金额(单位元)。 RefundAmount float64 `json:"refund_amount"` + // WithdrawalApplyOrders 当日提现申请订单数。 + WithdrawalApplyOrders int64 `json:"withdrawal_apply_orders"` + // WithdrawalApplyAmount 当日提现申请金额(单位元)。 + WithdrawalApplyAmount float64 `json:"withdrawal_apply_amount"` + // WithdrawalPaidOrders 当日提现完成订单数。 + WithdrawalPaidOrders int64 `json:"withdrawal_paid_orders"` + // WithdrawalPaidAmount 当日提现完成金额(单位元)。 + WithdrawalPaidAmount float64 `json:"withdrawal_paid_amount"` + // WithdrawalFailedOrders 当日提现失败订单数。 + WithdrawalFailedOrders int64 `json:"withdrawal_failed_orders"` + // WithdrawalFailedAmount 当日提现失败金额(单位元)。 + WithdrawalFailedAmount float64 `json:"withdrawal_failed_amount"` + // ContentCreated 当日新增内容数。 + ContentCreated int64 `json:"content_created"` + // LikeActions 当日新增点赞数(基于互动记录)。 + LikeActions int64 `json:"like_actions"` + // FavoriteActions 当日新增收藏数(基于互动记录)。 + FavoriteActions int64 `json:"favorite_actions"` + // CommentCount 当日新增评论数。 + CommentCount int64 `json:"comment_count"` } type ReportExportForm struct { diff --git a/backend/app/services/creator_report.go b/backend/app/services/creator_report.go index df063ba..7c6e68e 100644 --- a/backend/app/services/creator_report.go +++ b/backend/app/services/creator_report.go @@ -49,12 +49,48 @@ func (s *creator) ReportOverview( return nil, errorx.ErrDatabaseError.WithCause(err) } - // 订单仅统计内容购买类型,并按状态划分已支付/已退款。 - paidCount, paidAmount, err := s.orderAggregate(ctx, tid, consts.OrderStatusPaid, "paid_at", rg) + // 内容规模与互动指标。 + contentCount, err := s.contentCount(ctx, tid) if err != nil { return nil, err } - refundCount, refundAmount, err := s.orderAggregate(ctx, tid, consts.OrderStatusRefunded, "updated_at", rg) + contentCreated, err := s.contentCreatedAggregate(ctx, tid, rg) + if err != nil { + return nil, err + } + likeActions, err := s.contentActionAggregate(ctx, tid, consts.UserContentActionTypeLike, rg) + if err != nil { + return nil, err + } + favoriteActions, err := s.contentActionAggregate(ctx, tid, consts.UserContentActionTypeFavorite, rg) + if err != nil { + return nil, err + } + commentCount, err := s.commentAggregate(ctx, tid, rg) + if err != nil { + return nil, err + } + + // 订单仅统计内容购买类型,并按状态划分已支付/已退款。 + paidCount, paidAmount, err := s.orderAggregate(ctx, tid, consts.OrderTypeContentPurchase, consts.OrderStatusPaid, "paid_at", rg) + if err != nil { + return nil, err + } + refundCount, refundAmount, err := s.orderAggregate(ctx, tid, consts.OrderTypeContentPurchase, consts.OrderStatusRefunded, "updated_at", rg) + if err != nil { + return nil, err + } + + // 提现维度统计(申请/完成/失败)。 + withdrawApplyCount, withdrawApplyAmount, err := s.orderAggregate(ctx, tid, consts.OrderTypeWithdrawal, consts.OrderStatusCreated, "created_at", rg) + if err != nil { + return nil, err + } + withdrawPaidCount, withdrawPaidAmount, err := s.orderAggregate(ctx, tid, consts.OrderTypeWithdrawal, consts.OrderStatusPaid, "paid_at", rg) + if err != nil { + return nil, err + } + withdrawFailedCount, withdrawFailedAmount, err := s.orderAggregate(ctx, tid, consts.OrderTypeWithdrawal, consts.OrderStatusFailed, "updated_at", rg) if err != nil { return nil, err } @@ -65,11 +101,39 @@ func (s *creator) ReportOverview( } // 生成按日趋势序列。 - paidSeries, err := s.orderSeries(ctx, tid, consts.OrderStatusPaid, "paid_at", rg) + paidSeries, err := s.orderSeries(ctx, tid, consts.OrderTypeContentPurchase, consts.OrderStatusPaid, "paid_at", rg) if err != nil { return nil, err } - refundSeries, err := s.orderSeries(ctx, tid, consts.OrderStatusRefunded, "updated_at", rg) + refundSeries, err := s.orderSeries(ctx, tid, consts.OrderTypeContentPurchase, consts.OrderStatusRefunded, "updated_at", rg) + if err != nil { + return nil, err + } + withdrawApplySeries, err := s.orderSeries(ctx, tid, consts.OrderTypeWithdrawal, consts.OrderStatusCreated, "created_at", rg) + if err != nil { + return nil, err + } + withdrawPaidSeries, err := s.orderSeries(ctx, tid, consts.OrderTypeWithdrawal, consts.OrderStatusPaid, "paid_at", rg) + if err != nil { + return nil, err + } + withdrawFailedSeries, err := s.orderSeries(ctx, tid, consts.OrderTypeWithdrawal, consts.OrderStatusFailed, "updated_at", rg) + if err != nil { + return nil, err + } + contentCreatedSeries, err := s.contentCreatedSeries(ctx, tid, rg) + if err != nil { + return nil, err + } + likeSeries, err := s.contentActionSeries(ctx, tid, consts.UserContentActionTypeLike, rg) + if err != nil { + return nil, err + } + favoriteSeries, err := s.contentActionSeries(ctx, tid, consts.UserContentActionTypeFavorite, rg) + if err != nil { + return nil, err + } + commentSeries, err := s.commentSeries(ctx, tid, rg) if err != nil { return nil, err } @@ -79,23 +143,47 @@ func (s *creator) ReportOverview( key := day.Format("2006-01-02") paidItem := paidSeries[key] refundItem := refundSeries[key] + withdrawApplyItem := withdrawApplySeries[key] + withdrawPaidItem := withdrawPaidSeries[key] + withdrawFailedItem := withdrawFailedSeries[key] items = append(items, creator_dto.ReportOverviewItem{ - Date: key, - PaidOrders: paidItem.Count, - PaidAmount: float64(paidItem.Amount) / 100.0, - RefundOrders: refundItem.Count, - RefundAmount: float64(refundItem.Amount) / 100.0, + Date: key, + PaidOrders: paidItem.Count, + PaidAmount: float64(paidItem.Amount) / 100.0, + RefundOrders: refundItem.Count, + RefundAmount: float64(refundItem.Amount) / 100.0, + WithdrawalApplyOrders: withdrawApplyItem.Count, + WithdrawalApplyAmount: float64(withdrawApplyItem.Amount) / 100.0, + WithdrawalPaidOrders: withdrawPaidItem.Count, + WithdrawalPaidAmount: float64(withdrawPaidItem.Amount) / 100.0, + WithdrawalFailedOrders: withdrawFailedItem.Count, + WithdrawalFailedAmount: float64(withdrawFailedItem.Amount) / 100.0, + ContentCreated: contentCreatedSeries[key], + LikeActions: likeSeries[key], + FavoriteActions: favoriteSeries[key], + CommentCount: commentSeries[key], }) } return &creator_dto.ReportOverviewResponse{ Summary: creator_dto.ReportSummary{ - TotalViews: totalViews, - PaidOrders: paidCount, - PaidAmount: float64(paidAmount) / 100.0, - RefundOrders: refundCount, - RefundAmount: float64(refundAmount) / 100.0, - ConversionRate: conversionRate, + TotalViews: totalViews, + ContentCount: contentCount, + ContentCreated: contentCreated, + LikeActions: likeActions, + FavoriteActions: favoriteActions, + CommentCount: commentCount, + PaidOrders: paidCount, + PaidAmount: float64(paidAmount) / 100.0, + RefundOrders: refundCount, + RefundAmount: float64(refundAmount) / 100.0, + WithdrawalApplyOrders: withdrawApplyCount, + WithdrawalApplyAmount: float64(withdrawApplyAmount) / 100.0, + WithdrawalPaidOrders: withdrawPaidCount, + WithdrawalPaidAmount: float64(withdrawPaidAmount) / 100.0, + WithdrawalFailedOrders: withdrawFailedCount, + WithdrawalFailedAmount: float64(withdrawFailedAmount) / 100.0, + ConversionRate: conversionRate, }, Items: items, }, nil @@ -129,7 +217,7 @@ func (s *creator) ExportReport( } builder := &strings.Builder{} - builder.WriteString("date,paid_orders,paid_amount,refund_orders,refund_amount\n") + builder.WriteString("date,paid_orders,paid_amount,refund_orders,refund_amount,withdrawal_apply_orders,withdrawal_apply_amount,withdrawal_paid_orders,withdrawal_paid_amount,withdrawal_failed_orders,withdrawal_failed_amount,content_created,like_actions,favorite_actions,comment_count\n") for _, item := range overview.Items { builder.WriteString(item.Date) builder.WriteString(",") @@ -140,6 +228,26 @@ func (s *creator) ExportReport( builder.WriteString(strconv.FormatInt(item.RefundOrders, 10)) builder.WriteString(",") builder.WriteString(formatAmount(item.RefundAmount)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.WithdrawalApplyOrders, 10)) + builder.WriteString(",") + builder.WriteString(formatAmount(item.WithdrawalApplyAmount)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.WithdrawalPaidOrders, 10)) + builder.WriteString(",") + builder.WriteString(formatAmount(item.WithdrawalPaidAmount)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.WithdrawalFailedOrders, 10)) + builder.WriteString(",") + builder.WriteString(formatAmount(item.WithdrawalFailedAmount)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.ContentCreated, 10)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.LikeActions, 10)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.FavoriteActions, 10)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.CommentCount, 10)) builder.WriteString("\n") } @@ -160,6 +268,7 @@ type reportAggRow struct { func (s *creator) orderAggregate( ctx context.Context, tenantID int64, + orderType consts.OrderType, status consts.OrderStatus, timeField string, rg reportRange, @@ -173,7 +282,7 @@ func (s *creator) orderAggregate( Model(&models.Order{}). Select("count(*) as count, coalesce(sum(amount_paid), 0) as amount"). Where("tenant_id = ? AND type = ? AND status = ? AND "+timeField+" >= ? AND "+timeField+" < ?", - tenantID, consts.OrderTypeContentPurchase, status, rg.startDay, rg.endNext). + tenantID, orderType, status, rg.startDay, rg.endNext). Scan(&total).Error if err != nil { return 0, 0, errorx.ErrDatabaseError.WithCause(err) @@ -184,6 +293,7 @@ func (s *creator) orderAggregate( func (s *creator) orderSeries( ctx context.Context, tenantID int64, + orderType consts.OrderType, status consts.OrderStatus, timeField string, rg reportRange, @@ -194,7 +304,7 @@ func (s *creator) orderSeries( Model(&models.Order{}). Select("date_trunc('day', "+timeField+") as day, count(*) as count, coalesce(sum(amount_paid), 0) as amount"). Where("tenant_id = ? AND type = ? AND status = ? AND "+timeField+" >= ? AND "+timeField+" < ?", - tenantID, consts.OrderTypeContentPurchase, status, rg.startDay, rg.endNext). + tenantID, orderType, status, rg.startDay, rg.endNext). Group("day"). Scan(&rows).Error if err != nil { @@ -209,6 +319,127 @@ func (s *creator) orderSeries( return result, nil } +type reportCountRow struct { + Day time.Time `gorm:"column:day"` + Count int64 `gorm:"column:count"` +} + +func (s *creator) contentCount(ctx context.Context, tenantID int64) (int64, error) { + tbl, q := models.ContentQuery.QueryContext(ctx) + total, err := q.Where(tbl.TenantID.Eq(tenantID)).Count() + if err != nil { + return 0, errorx.ErrDatabaseError.WithCause(err) + } + return total, nil +} + +func (s *creator) contentCreatedAggregate(ctx context.Context, tenantID int64, rg reportRange) (int64, error) { + tbl, q := models.ContentQuery.QueryContext(ctx) + total, err := q.Where( + tbl.TenantID.Eq(tenantID), + tbl.CreatedAt.Gte(rg.startDay), + tbl.CreatedAt.Lt(rg.endNext), + ).Count() + if err != nil { + return 0, errorx.ErrDatabaseError.WithCause(err) + } + return total, nil +} + +func (s *creator) contentCreatedSeries(ctx context.Context, tenantID int64, rg reportRange) (map[string]int64, error) { + rows := make([]reportCountRow, 0) + err := models.ContentQuery.WithContext(ctx). + UnderlyingDB(). + Model(&models.Content{}). + Select("date_trunc('day', created_at) as day, count(*) as count"). + Where("tenant_id = ? AND created_at >= ? AND created_at < ?", tenantID, rg.startDay, rg.endNext). + Group("day"). + Scan(&rows).Error + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + return buildCountSeries(rows), nil +} + +func (s *creator) contentActionAggregate( + ctx context.Context, + tenantID int64, + actionType consts.UserContentActionType, + rg reportRange, +) (int64, error) { + var total int64 + query := models.UserContentActionQuery.WithContext(ctx). + UnderlyingDB(). + Model(&models.UserContentAction{}). + Select("count(*)"). + Joins("join contents on contents.id = user_content_actions.content_id"). + Where("user_content_actions.type = ? AND user_content_actions.created_at >= ? AND user_content_actions.created_at < ? AND contents.tenant_id = ?", + actionType, rg.startDay, rg.endNext, tenantID) + if err := query.Scan(&total).Error; err != nil { + return 0, errorx.ErrDatabaseError.WithCause(err) + } + return total, nil +} + +func (s *creator) contentActionSeries( + ctx context.Context, + tenantID int64, + actionType consts.UserContentActionType, + rg reportRange, +) (map[string]int64, error) { + rows := make([]reportCountRow, 0) + err := models.UserContentActionQuery.WithContext(ctx). + UnderlyingDB(). + Model(&models.UserContentAction{}). + Select("date_trunc('day', user_content_actions.created_at) as day, count(*) as count"). + Joins("join contents on contents.id = user_content_actions.content_id"). + Where("user_content_actions.type = ? AND user_content_actions.created_at >= ? AND user_content_actions.created_at < ? AND contents.tenant_id = ?", + actionType, rg.startDay, rg.endNext, tenantID). + Group("day"). + Scan(&rows).Error + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + return buildCountSeries(rows), nil +} + +func (s *creator) commentAggregate(ctx context.Context, tenantID int64, rg reportRange) (int64, error) { + tbl, q := models.CommentQuery.QueryContext(ctx) + total, err := q.Where( + tbl.TenantID.Eq(tenantID), + tbl.CreatedAt.Gte(rg.startDay), + tbl.CreatedAt.Lt(rg.endNext), + ).Count() + if err != nil { + return 0, errorx.ErrDatabaseError.WithCause(err) + } + return total, nil +} + +func (s *creator) commentSeries(ctx context.Context, tenantID int64, rg reportRange) (map[string]int64, error) { + rows := make([]reportCountRow, 0) + err := models.CommentQuery.WithContext(ctx). + UnderlyingDB(). + Model(&models.Comment{}). + Select("date_trunc('day', created_at) as day, count(*) as count"). + Where("tenant_id = ? AND created_at >= ? AND created_at < ?", tenantID, rg.startDay, rg.endNext). + Group("day"). + Scan(&rows).Error + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + return buildCountSeries(rows), nil +} + +func buildCountSeries(rows []reportCountRow) map[string]int64 { + result := make(map[string]int64, len(rows)) + for _, row := range rows { + key := row.Day.Format("2006-01-02") + result[key] = row.Count + } + return result +} + func (s *creator) normalizeReportRange(filter *creator_dto.ReportOverviewFilter) (reportRange, error) { granularity := "day" if filter != nil && filter.Granularity != nil && strings.TrimSpace(*filter.Granularity) != "" { diff --git a/backend/app/services/creator_test.go b/backend/app/services/creator_test.go index 4f2da02..b9ee282 100644 --- a/backend/app/services/creator_test.go +++ b/backend/app/services/creator_test.go @@ -394,6 +394,8 @@ func (s *CreatorTestSuite) Test_ReportOverview() { models.TableNameUser, models.TableNameContent, models.TableNameOrder, + models.TableNameUserContentAction, + models.TableNameComment, ) owner := &models.User{Username: "owner_r", Phone: "13900001011"} @@ -405,18 +407,23 @@ func (s *CreatorTestSuite) Test_ReportOverview() { } models.TenantQuery.WithContext(ctx).Create(tenant) - models.ContentQuery.WithContext(ctx).Create(&models.Content{ + content := &models.Content{ TenantID: tenant.ID, UserID: owner.ID, Title: "Content A", Status: consts.ContentStatusPublished, Views: 100, - }) + } + models.ContentQuery.WithContext(ctx).Create(content) now := time.Now() inRangePaidAt := now.Add(-12 * time.Hour) outRangePaidAt := now.Add(-10 * 24 * time.Hour) + likeAt := now.Add(-2 * time.Hour) + favoriteAt := now.Add(-3 * time.Hour) + commentAt := now.Add(-4 * time.Hour) + models.OrderQuery.WithContext(ctx).Create( &models.Order{ TenantID: tenant.ID, @@ -442,8 +449,55 @@ func (s *CreatorTestSuite) Test_ReportOverview() { AmountPaid: 500, UpdatedAt: now.Add(-6 * time.Hour), }, + &models.Order{ + TenantID: tenant.ID, + UserID: owner.ID, + Type: consts.OrderTypeWithdrawal, + Status: consts.OrderStatusCreated, + AmountPaid: 300, + CreatedAt: now.Add(-5 * time.Hour), + }, + &models.Order{ + TenantID: tenant.ID, + UserID: owner.ID, + Type: consts.OrderTypeWithdrawal, + Status: consts.OrderStatusPaid, + AmountPaid: 800, + PaidAt: now.Add(-3 * time.Hour), + }, + &models.Order{ + TenantID: tenant.ID, + UserID: owner.ID, + Type: consts.OrderTypeWithdrawal, + Status: consts.OrderStatusFailed, + AmountPaid: 500, + UpdatedAt: now.Add(-2 * time.Hour), + }, ) + models.UserContentActionQuery.WithContext(ctx).Create( + &models.UserContentAction{ + UserID: owner.ID, + ContentID: content.ID, + Type: string(consts.UserContentActionTypeLike), + CreatedAt: likeAt, + }, + &models.UserContentAction{ + UserID: owner.ID, + ContentID: content.ID, + Type: string(consts.UserContentActionTypeFavorite), + CreatedAt: favoriteAt, + }, + ) + + models.CommentQuery.WithContext(ctx).Create(&models.Comment{ + TenantID: tenant.ID, + UserID: owner.ID, + ContentID: content.ID, + Content: "Nice", + CreatedAt: commentAt, + }) + start := now.Add(-24 * time.Hour).Format(time.RFC3339) end := now.Format(time.RFC3339) report, err := Creator.ReportOverview(ctx, tenant.ID, owner.ID, &creator_dto.ReportOverviewFilter{ @@ -452,10 +506,21 @@ func (s *CreatorTestSuite) Test_ReportOverview() { }) So(err, ShouldBeNil) So(report.Summary.TotalViews, ShouldEqual, 100) + So(report.Summary.ContentCount, ShouldEqual, 1) + So(report.Summary.ContentCreated, ShouldEqual, 1) + So(report.Summary.LikeActions, ShouldEqual, 1) + So(report.Summary.FavoriteActions, ShouldEqual, 1) + So(report.Summary.CommentCount, ShouldEqual, 1) So(report.Summary.PaidOrders, ShouldEqual, 1) So(report.Summary.PaidAmount, ShouldEqual, 10.0) So(report.Summary.RefundOrders, ShouldEqual, 1) So(report.Summary.RefundAmount, ShouldEqual, 5.0) + So(report.Summary.WithdrawalApplyOrders, ShouldEqual, 1) + So(report.Summary.WithdrawalApplyAmount, ShouldEqual, 3.0) + So(report.Summary.WithdrawalPaidOrders, ShouldEqual, 1) + So(report.Summary.WithdrawalPaidAmount, ShouldEqual, 8.0) + So(report.Summary.WithdrawalFailedOrders, ShouldEqual, 1) + So(report.Summary.WithdrawalFailedAmount, ShouldEqual, 5.0) var paidSum, refundSum int64 for _, item := range report.Items { @@ -475,6 +540,8 @@ func (s *CreatorTestSuite) Test_ExportReport() { models.TableNameUser, models.TableNameContent, models.TableNameOrder, + models.TableNameUserContentAction, + models.TableNameComment, ) owner := &models.User{Username: "owner_e", Phone: "13900001012"} @@ -507,6 +574,6 @@ func (s *CreatorTestSuite) Test_ExportReport() { resp, err := Creator.ExportReport(ctx, tenant.ID, owner.ID, form) So(err, ShouldBeNil) So(resp.Filename, ShouldNotBeBlank) - So(resp.Content, ShouldContainSubstring, "date,paid_orders,paid_amount,refund_orders,refund_amount") + So(resp.Content, ShouldContainSubstring, "date,paid_orders,paid_amount,refund_orders,refund_amount,withdrawal_apply_orders,withdrawal_apply_amount,withdrawal_paid_orders,withdrawal_paid_amount,withdrawal_failed_orders,withdrawal_failed_amount,content_created,like_actions,favorite_actions,comment_count") }) } diff --git a/backend/app/services/super.go b/backend/app/services/super.go index c22a9a1..390c9b8 100644 --- a/backend/app/services/super.go +++ b/backend/app/services/super.go @@ -5553,12 +5553,48 @@ func (s *super) ReportOverview(ctx context.Context, filter *super_dto.SuperRepor return nil, errorx.ErrDatabaseError.WithCause(err) } - // 订单仅统计内容购买类型,并按状态划分已支付/已退款。 - paidCount, paidAmount, err := s.reportOrderAggregate(ctx, tenantID, consts.OrderStatusPaid, "paid_at", rg) + // 内容规模与互动指标。 + contentCount, err := s.contentCount(ctx, tenantID) if err != nil { return nil, err } - refundCount, refundAmount, err := s.reportOrderAggregate(ctx, tenantID, consts.OrderStatusRefunded, "updated_at", rg) + contentCreated, err := s.contentCreatedAggregate(ctx, tenantID, rg) + if err != nil { + return nil, err + } + likeActions, err := s.contentActionAggregate(ctx, tenantID, consts.UserContentActionTypeLike, rg) + if err != nil { + return nil, err + } + favoriteActions, err := s.contentActionAggregate(ctx, tenantID, consts.UserContentActionTypeFavorite, rg) + if err != nil { + return nil, err + } + commentCount, err := s.commentAggregate(ctx, tenantID, rg) + if err != nil { + return nil, err + } + + // 订单仅统计内容购买类型,并按状态划分已支付/已退款。 + paidCount, paidAmount, err := s.reportOrderAggregate(ctx, tenantID, consts.OrderTypeContentPurchase, consts.OrderStatusPaid, "paid_at", rg) + if err != nil { + return nil, err + } + refundCount, refundAmount, err := s.reportOrderAggregate(ctx, tenantID, consts.OrderTypeContentPurchase, consts.OrderStatusRefunded, "updated_at", rg) + if err != nil { + return nil, err + } + + // 提现维度统计(申请/完成/失败)。 + withdrawApplyCount, withdrawApplyAmount, err := s.reportOrderAggregate(ctx, tenantID, consts.OrderTypeWithdrawal, consts.OrderStatusCreated, "created_at", rg) + if err != nil { + return nil, err + } + withdrawPaidCount, withdrawPaidAmount, err := s.reportOrderAggregate(ctx, tenantID, consts.OrderTypeWithdrawal, consts.OrderStatusPaid, "paid_at", rg) + if err != nil { + return nil, err + } + withdrawFailedCount, withdrawFailedAmount, err := s.reportOrderAggregate(ctx, tenantID, consts.OrderTypeWithdrawal, consts.OrderStatusFailed, "updated_at", rg) if err != nil { return nil, err } @@ -5568,11 +5604,39 @@ func (s *super) ReportOverview(ctx context.Context, filter *super_dto.SuperRepor conversionRate = float64(paidCount) / float64(totalViews) } - paidSeries, err := s.reportOrderSeries(ctx, tenantID, consts.OrderStatusPaid, "paid_at", rg) + paidSeries, err := s.reportOrderSeries(ctx, tenantID, consts.OrderTypeContentPurchase, consts.OrderStatusPaid, "paid_at", rg) if err != nil { return nil, err } - refundSeries, err := s.reportOrderSeries(ctx, tenantID, consts.OrderStatusRefunded, "updated_at", rg) + refundSeries, err := s.reportOrderSeries(ctx, tenantID, consts.OrderTypeContentPurchase, consts.OrderStatusRefunded, "updated_at", rg) + if err != nil { + return nil, err + } + withdrawApplySeries, err := s.reportOrderSeries(ctx, tenantID, consts.OrderTypeWithdrawal, consts.OrderStatusCreated, "created_at", rg) + if err != nil { + return nil, err + } + withdrawPaidSeries, err := s.reportOrderSeries(ctx, tenantID, consts.OrderTypeWithdrawal, consts.OrderStatusPaid, "paid_at", rg) + if err != nil { + return nil, err + } + withdrawFailedSeries, err := s.reportOrderSeries(ctx, tenantID, consts.OrderTypeWithdrawal, consts.OrderStatusFailed, "updated_at", rg) + if err != nil { + return nil, err + } + contentCreatedSeries, err := s.contentCreatedSeries(ctx, tenantID, rg) + if err != nil { + return nil, err + } + likeSeries, err := s.contentActionSeries(ctx, tenantID, consts.UserContentActionTypeLike, rg) + if err != nil { + return nil, err + } + favoriteSeries, err := s.contentActionSeries(ctx, tenantID, consts.UserContentActionTypeFavorite, rg) + if err != nil { + return nil, err + } + commentSeries, err := s.commentSeries(ctx, tenantID, rg) if err != nil { return nil, err } @@ -5582,23 +5646,47 @@ func (s *super) ReportOverview(ctx context.Context, filter *super_dto.SuperRepor key := day.Format("2006-01-02") paidItem := paidSeries[key] refundItem := refundSeries[key] + withdrawApplyItem := withdrawApplySeries[key] + withdrawPaidItem := withdrawPaidSeries[key] + withdrawFailedItem := withdrawFailedSeries[key] items = append(items, v1_dto.ReportOverviewItem{ - Date: key, - PaidOrders: paidItem.Count, - PaidAmount: float64(paidItem.Amount) / 100.0, - RefundOrders: refundItem.Count, - RefundAmount: float64(refundItem.Amount) / 100.0, + Date: key, + PaidOrders: paidItem.Count, + PaidAmount: float64(paidItem.Amount) / 100.0, + RefundOrders: refundItem.Count, + RefundAmount: float64(refundItem.Amount) / 100.0, + WithdrawalApplyOrders: withdrawApplyItem.Count, + WithdrawalApplyAmount: float64(withdrawApplyItem.Amount) / 100.0, + WithdrawalPaidOrders: withdrawPaidItem.Count, + WithdrawalPaidAmount: float64(withdrawPaidItem.Amount) / 100.0, + WithdrawalFailedOrders: withdrawFailedItem.Count, + WithdrawalFailedAmount: float64(withdrawFailedItem.Amount) / 100.0, + ContentCreated: contentCreatedSeries[key], + LikeActions: likeSeries[key], + FavoriteActions: favoriteSeries[key], + CommentCount: commentSeries[key], }) } return &v1_dto.ReportOverviewResponse{ Summary: v1_dto.ReportSummary{ - TotalViews: totalViews, - PaidOrders: paidCount, - PaidAmount: float64(paidAmount) / 100.0, - RefundOrders: refundCount, - RefundAmount: float64(refundAmount) / 100.0, - ConversionRate: conversionRate, + TotalViews: totalViews, + ContentCount: contentCount, + ContentCreated: contentCreated, + LikeActions: likeActions, + FavoriteActions: favoriteActions, + CommentCount: commentCount, + PaidOrders: paidCount, + PaidAmount: float64(paidAmount) / 100.0, + RefundOrders: refundCount, + RefundAmount: float64(refundAmount) / 100.0, + WithdrawalApplyOrders: withdrawApplyCount, + WithdrawalApplyAmount: float64(withdrawApplyAmount) / 100.0, + WithdrawalPaidOrders: withdrawPaidCount, + WithdrawalPaidAmount: float64(withdrawPaidAmount) / 100.0, + WithdrawalFailedOrders: withdrawFailedCount, + WithdrawalFailedAmount: float64(withdrawFailedAmount) / 100.0, + ConversionRate: conversionRate, }, Items: items, }, nil @@ -5627,7 +5715,7 @@ func (s *super) ExportReport(ctx context.Context, form *super_dto.SuperReportExp } builder := &strings.Builder{} - builder.WriteString("date,paid_orders,paid_amount,refund_orders,refund_amount\n") + builder.WriteString("date,paid_orders,paid_amount,refund_orders,refund_amount,withdrawal_apply_orders,withdrawal_apply_amount,withdrawal_paid_orders,withdrawal_paid_amount,withdrawal_failed_orders,withdrawal_failed_amount,content_created,like_actions,favorite_actions,comment_count\n") for _, item := range overview.Items { builder.WriteString(item.Date) builder.WriteString(",") @@ -5638,6 +5726,26 @@ func (s *super) ExportReport(ctx context.Context, form *super_dto.SuperReportExp builder.WriteString(strconv.FormatInt(item.RefundOrders, 10)) builder.WriteString(",") builder.WriteString(formatAmount(item.RefundAmount)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.WithdrawalApplyOrders, 10)) + builder.WriteString(",") + builder.WriteString(formatAmount(item.WithdrawalApplyAmount)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.WithdrawalPaidOrders, 10)) + builder.WriteString(",") + builder.WriteString(formatAmount(item.WithdrawalPaidAmount)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.WithdrawalFailedOrders, 10)) + builder.WriteString(",") + builder.WriteString(formatAmount(item.WithdrawalFailedAmount)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.ContentCreated, 10)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.LikeActions, 10)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.FavoriteActions, 10)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.CommentCount, 10)) builder.WriteString("\n") } @@ -5649,9 +5757,126 @@ func (s *super) ExportReport(ctx context.Context, form *super_dto.SuperReportExp }, nil } +func (s *super) contentCount(ctx context.Context, tenantID int64) (int64, error) { + tbl, q := models.ContentQuery.QueryContext(ctx) + if tenantID > 0 { + q = q.Where(tbl.TenantID.Eq(tenantID)) + } + total, err := q.Count() + if err != nil { + return 0, errorx.ErrDatabaseError.WithCause(err) + } + return total, nil +} + +func (s *super) contentCreatedAggregate(ctx context.Context, tenantID int64, rg reportRange) (int64, error) { + tbl, q := models.ContentQuery.QueryContext(ctx) + q = q.Where(tbl.CreatedAt.Gte(rg.startDay), tbl.CreatedAt.Lt(rg.endNext)) + if tenantID > 0 { + q = q.Where(tbl.TenantID.Eq(tenantID)) + } + total, err := q.Count() + if err != nil { + return 0, errorx.ErrDatabaseError.WithCause(err) + } + return total, nil +} + +func (s *super) contentCreatedSeries(ctx context.Context, tenantID int64, rg reportRange) (map[string]int64, error) { + rows := make([]reportCountRow, 0) + query := models.ContentQuery.WithContext(ctx). + UnderlyingDB(). + Model(&models.Content{}). + Select("date_trunc('day', created_at) as day, count(*) as count"). + Where("created_at >= ? AND created_at < ?", rg.startDay, rg.endNext) + if tenantID > 0 { + query = query.Where("tenant_id = ?", tenantID) + } + if err := query.Group("day").Scan(&rows).Error; err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + return buildCountSeries(rows), nil +} + +func (s *super) contentActionAggregate( + ctx context.Context, + tenantID int64, + actionType consts.UserContentActionType, + rg reportRange, +) (int64, error) { + var total int64 + query := models.UserContentActionQuery.WithContext(ctx). + UnderlyingDB(). + Model(&models.UserContentAction{}). + Select("count(*)"). + Where("user_content_actions.type = ? AND user_content_actions.created_at >= ? AND user_content_actions.created_at < ?", + actionType, rg.startDay, rg.endNext) + if tenantID > 0 { + query = query.Joins("join contents on contents.id = user_content_actions.content_id"). + Where("contents.tenant_id = ?", tenantID) + } + if err := query.Scan(&total).Error; err != nil { + return 0, errorx.ErrDatabaseError.WithCause(err) + } + return total, nil +} + +func (s *super) contentActionSeries( + ctx context.Context, + tenantID int64, + actionType consts.UserContentActionType, + rg reportRange, +) (map[string]int64, error) { + rows := make([]reportCountRow, 0) + query := models.UserContentActionQuery.WithContext(ctx). + UnderlyingDB(). + Model(&models.UserContentAction{}). + Select("date_trunc('day', user_content_actions.created_at) as day, count(*) as count"). + Where("user_content_actions.type = ? AND user_content_actions.created_at >= ? AND user_content_actions.created_at < ?", + actionType, rg.startDay, rg.endNext) + if tenantID > 0 { + query = query.Joins("join contents on contents.id = user_content_actions.content_id"). + Where("contents.tenant_id = ?", tenantID) + } + if err := query.Group("day").Scan(&rows).Error; err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + return buildCountSeries(rows), nil +} + +func (s *super) commentAggregate(ctx context.Context, tenantID int64, rg reportRange) (int64, error) { + tbl, q := models.CommentQuery.QueryContext(ctx) + q = q.Where(tbl.CreatedAt.Gte(rg.startDay), tbl.CreatedAt.Lt(rg.endNext)) + if tenantID > 0 { + q = q.Where(tbl.TenantID.Eq(tenantID)) + } + total, err := q.Count() + if err != nil { + return 0, errorx.ErrDatabaseError.WithCause(err) + } + return total, nil +} + +func (s *super) commentSeries(ctx context.Context, tenantID int64, rg reportRange) (map[string]int64, error) { + rows := make([]reportCountRow, 0) + query := models.CommentQuery.WithContext(ctx). + UnderlyingDB(). + Model(&models.Comment{}). + Select("date_trunc('day', created_at) as day, count(*) as count"). + Where("created_at >= ? AND created_at < ?", rg.startDay, rg.endNext) + if tenantID > 0 { + query = query.Where("tenant_id = ?", tenantID) + } + if err := query.Group("day").Scan(&rows).Error; err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + return buildCountSeries(rows), nil +} + func (s *super) reportOrderAggregate( ctx context.Context, tenantID int64, + orderType consts.OrderType, status consts.OrderStatus, timeField string, rg reportRange, @@ -5666,7 +5891,7 @@ func (s *super) reportOrderAggregate( Model(&models.Order{}). Select("count(*) as count, coalesce(sum(amount_paid), 0) as amount"). Where("type = ? AND status = ? AND "+timeField+" >= ? AND "+timeField+" < ?", - consts.OrderTypeContentPurchase, status, rg.startDay, rg.endNext) + orderType, status, rg.startDay, rg.endNext) if tenantID > 0 { query = query.Where("tenant_id = ?", tenantID) } @@ -5680,6 +5905,7 @@ func (s *super) reportOrderAggregate( func (s *super) reportOrderSeries( ctx context.Context, tenantID int64, + orderType consts.OrderType, status consts.OrderStatus, timeField string, rg reportRange, @@ -5690,7 +5916,7 @@ func (s *super) reportOrderSeries( Model(&models.Order{}). Select("date_trunc('day', "+timeField+") as day, count(*) as count, coalesce(sum(amount_paid), 0) as amount"). Where("type = ? AND status = ? AND "+timeField+" >= ? AND "+timeField+" < ?", - consts.OrderTypeContentPurchase, status, rg.startDay, rg.endNext) + orderType, status, rg.startDay, rg.endNext) if tenantID > 0 { query = query.Where("tenant_id = ?", tenantID) } diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 2568dce..3ff103e 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -6873,10 +6873,26 @@ const docTemplate = `{ "dto.ReportOverviewItem": { "type": "object", "properties": { + "comment_count": { + "description": "CommentCount 当日新增评论数。", + "type": "integer" + }, + "content_created": { + "description": "ContentCreated 当日新增内容数。", + "type": "integer" + }, "date": { "description": "Date 日期(YYYY-MM-DD)。", "type": "string" }, + "favorite_actions": { + "description": "FavoriteActions 当日新增收藏数(基于互动记录)。", + "type": "integer" + }, + "like_actions": { + "description": "LikeActions 当日新增点赞数(基于互动记录)。", + "type": "integer" + }, "paid_amount": { "description": "PaidAmount 当日已支付金额(单位元)。", "type": "number" @@ -6892,6 +6908,30 @@ const docTemplate = `{ "refund_orders": { "description": "RefundOrders 当日退款订单数。", "type": "integer" + }, + "withdrawal_apply_amount": { + "description": "WithdrawalApplyAmount 当日提现申请金额(单位元)。", + "type": "number" + }, + "withdrawal_apply_orders": { + "description": "WithdrawalApplyOrders 当日提现申请订单数。", + "type": "integer" + }, + "withdrawal_failed_amount": { + "description": "WithdrawalFailedAmount 当日提现失败金额(单位元)。", + "type": "number" + }, + "withdrawal_failed_orders": { + "description": "WithdrawalFailedOrders 当日提现失败订单数。", + "type": "integer" + }, + "withdrawal_paid_amount": { + "description": "WithdrawalPaidAmount 当日提现完成金额(单位元)。", + "type": "number" + }, + "withdrawal_paid_orders": { + "description": "WithdrawalPaidOrders 当日提现完成订单数。", + "type": "integer" } } }, @@ -6918,10 +6958,30 @@ const docTemplate = `{ "dto.ReportSummary": { "type": "object", "properties": { + "comment_count": { + "description": "CommentCount 统计区间内新增评论数。", + "type": "integer" + }, + "content_count": { + "description": "ContentCount 内容总量(当前快照)。", + "type": "integer" + }, + "content_created": { + "description": "ContentCreated 统计区间内新增内容数。", + "type": "integer" + }, "conversion_rate": { "description": "ConversionRate 转化率(已支付订单数 / 累计曝光)。", "type": "number" }, + "favorite_actions": { + "description": "FavoriteActions 统计区间内新增收藏数(基于互动记录)。", + "type": "integer" + }, + "like_actions": { + "description": "LikeActions 统计区间内新增点赞数(基于互动记录)。", + "type": "integer" + }, "paid_amount": { "description": "PaidAmount 统计区间内已支付金额(单位元)。", "type": "number" @@ -6941,6 +7001,30 @@ const docTemplate = `{ "total_views": { "description": "TotalViews 内容累计曝光(全量累计值,用于粗略换算)。", "type": "integer" + }, + "withdrawal_apply_amount": { + "description": "WithdrawalApplyAmount 统计区间内提现申请金额(单位元)。", + "type": "number" + }, + "withdrawal_apply_orders": { + "description": "WithdrawalApplyOrders 统计区间内提现申请订单数。", + "type": "integer" + }, + "withdrawal_failed_amount": { + "description": "WithdrawalFailedAmount 统计区间内提现失败金额(单位元)。", + "type": "number" + }, + "withdrawal_failed_orders": { + "description": "WithdrawalFailedOrders 统计区间内提现失败订单数。", + "type": "integer" + }, + "withdrawal_paid_amount": { + "description": "WithdrawalPaidAmount 统计区间内提现完成金额(单位元)。", + "type": "number" + }, + "withdrawal_paid_orders": { + "description": "WithdrawalPaidOrders 统计区间内提现完成订单数。", + "type": "integer" } } }, diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index bfdb9bb..999e60c 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -6867,10 +6867,26 @@ "dto.ReportOverviewItem": { "type": "object", "properties": { + "comment_count": { + "description": "CommentCount 当日新增评论数。", + "type": "integer" + }, + "content_created": { + "description": "ContentCreated 当日新增内容数。", + "type": "integer" + }, "date": { "description": "Date 日期(YYYY-MM-DD)。", "type": "string" }, + "favorite_actions": { + "description": "FavoriteActions 当日新增收藏数(基于互动记录)。", + "type": "integer" + }, + "like_actions": { + "description": "LikeActions 当日新增点赞数(基于互动记录)。", + "type": "integer" + }, "paid_amount": { "description": "PaidAmount 当日已支付金额(单位元)。", "type": "number" @@ -6886,6 +6902,30 @@ "refund_orders": { "description": "RefundOrders 当日退款订单数。", "type": "integer" + }, + "withdrawal_apply_amount": { + "description": "WithdrawalApplyAmount 当日提现申请金额(单位元)。", + "type": "number" + }, + "withdrawal_apply_orders": { + "description": "WithdrawalApplyOrders 当日提现申请订单数。", + "type": "integer" + }, + "withdrawal_failed_amount": { + "description": "WithdrawalFailedAmount 当日提现失败金额(单位元)。", + "type": "number" + }, + "withdrawal_failed_orders": { + "description": "WithdrawalFailedOrders 当日提现失败订单数。", + "type": "integer" + }, + "withdrawal_paid_amount": { + "description": "WithdrawalPaidAmount 当日提现完成金额(单位元)。", + "type": "number" + }, + "withdrawal_paid_orders": { + "description": "WithdrawalPaidOrders 当日提现完成订单数。", + "type": "integer" } } }, @@ -6912,10 +6952,30 @@ "dto.ReportSummary": { "type": "object", "properties": { + "comment_count": { + "description": "CommentCount 统计区间内新增评论数。", + "type": "integer" + }, + "content_count": { + "description": "ContentCount 内容总量(当前快照)。", + "type": "integer" + }, + "content_created": { + "description": "ContentCreated 统计区间内新增内容数。", + "type": "integer" + }, "conversion_rate": { "description": "ConversionRate 转化率(已支付订单数 / 累计曝光)。", "type": "number" }, + "favorite_actions": { + "description": "FavoriteActions 统计区间内新增收藏数(基于互动记录)。", + "type": "integer" + }, + "like_actions": { + "description": "LikeActions 统计区间内新增点赞数(基于互动记录)。", + "type": "integer" + }, "paid_amount": { "description": "PaidAmount 统计区间内已支付金额(单位元)。", "type": "number" @@ -6935,6 +6995,30 @@ "total_views": { "description": "TotalViews 内容累计曝光(全量累计值,用于粗略换算)。", "type": "integer" + }, + "withdrawal_apply_amount": { + "description": "WithdrawalApplyAmount 统计区间内提现申请金额(单位元)。", + "type": "number" + }, + "withdrawal_apply_orders": { + "description": "WithdrawalApplyOrders 统计区间内提现申请订单数。", + "type": "integer" + }, + "withdrawal_failed_amount": { + "description": "WithdrawalFailedAmount 统计区间内提现失败金额(单位元)。", + "type": "number" + }, + "withdrawal_failed_orders": { + "description": "WithdrawalFailedOrders 统计区间内提现失败订单数。", + "type": "integer" + }, + "withdrawal_paid_amount": { + "description": "WithdrawalPaidAmount 统计区间内提现完成金额(单位元)。", + "type": "number" + }, + "withdrawal_paid_orders": { + "description": "WithdrawalPaidOrders 统计区间内提现完成订单数。", + "type": "integer" } } }, diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index dd490b4..5638217 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -993,9 +993,21 @@ definitions: type: object dto.ReportOverviewItem: properties: + comment_count: + description: CommentCount 当日新增评论数。 + type: integer + content_created: + description: ContentCreated 当日新增内容数。 + type: integer date: description: Date 日期(YYYY-MM-DD)。 type: string + favorite_actions: + description: FavoriteActions 当日新增收藏数(基于互动记录)。 + type: integer + like_actions: + description: LikeActions 当日新增点赞数(基于互动记录)。 + type: integer paid_amount: description: PaidAmount 当日已支付金额(单位元)。 type: number @@ -1008,6 +1020,24 @@ definitions: refund_orders: description: RefundOrders 当日退款订单数。 type: integer + withdrawal_apply_amount: + description: WithdrawalApplyAmount 当日提现申请金额(单位元)。 + type: number + withdrawal_apply_orders: + description: WithdrawalApplyOrders 当日提现申请订单数。 + type: integer + withdrawal_failed_amount: + description: WithdrawalFailedAmount 当日提现失败金额(单位元)。 + type: number + withdrawal_failed_orders: + description: WithdrawalFailedOrders 当日提现失败订单数。 + type: integer + withdrawal_paid_amount: + description: WithdrawalPaidAmount 当日提现完成金额(单位元)。 + type: number + withdrawal_paid_orders: + description: WithdrawalPaidOrders 当日提现完成订单数。 + type: integer type: object dto.ReportOverviewResponse: properties: @@ -1023,9 +1053,24 @@ definitions: type: object dto.ReportSummary: properties: + comment_count: + description: CommentCount 统计区间内新增评论数。 + type: integer + content_count: + description: ContentCount 内容总量(当前快照)。 + type: integer + content_created: + description: ContentCreated 统计区间内新增内容数。 + type: integer conversion_rate: description: ConversionRate 转化率(已支付订单数 / 累计曝光)。 type: number + favorite_actions: + description: FavoriteActions 统计区间内新增收藏数(基于互动记录)。 + type: integer + like_actions: + description: LikeActions 统计区间内新增点赞数(基于互动记录)。 + type: integer paid_amount: description: PaidAmount 统计区间内已支付金额(单位元)。 type: number @@ -1041,6 +1086,24 @@ definitions: total_views: description: TotalViews 内容累计曝光(全量累计值,用于粗略换算)。 type: integer + withdrawal_apply_amount: + description: WithdrawalApplyAmount 统计区间内提现申请金额(单位元)。 + type: number + withdrawal_apply_orders: + description: WithdrawalApplyOrders 统计区间内提现申请订单数。 + type: integer + withdrawal_failed_amount: + description: WithdrawalFailedAmount 统计区间内提现失败金额(单位元)。 + type: number + withdrawal_failed_orders: + description: WithdrawalFailedOrders 统计区间内提现失败订单数。 + type: integer + withdrawal_paid_amount: + description: WithdrawalPaidAmount 统计区间内提现完成金额(单位元)。 + type: number + withdrawal_paid_orders: + description: WithdrawalPaidOrders 统计区间内提现完成订单数。 + type: integer type: object dto.Settings: properties: diff --git a/docs/superadmin_progress.md b/docs/superadmin_progress.md index 873b6f5..17a84ad 100644 --- a/docs/superadmin_progress.md +++ b/docs/superadmin_progress.md @@ -5,7 +5,7 @@ ## 1) 总体结论 - **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录、互动(收藏/点赞/关注)与内容消费明细视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录/异常核查、资产治理(列表/用量/清理)、通知中心(列表/群发/模板)。 -- **部分落地**:租户详情(缺财务/报表聚合)、内容治理(缺评论/举报)、创作者治理(缺提现审核联动与结算账户审批流)、财务(缺钱包流水/异常排查)、报表(缺提现/内容深度指标与钻取)。 +- **部分落地**:租户详情(缺财务/报表聚合)、内容治理(缺评论/举报)、创作者治理(缺提现审核联动与结算账户审批流)、财务(缺钱包流水/异常排查)。 - **未落地**:审计与系统配置类能力。 ## 2) 按页面完成度(对照 2.x) @@ -66,9 +66,9 @@ - 缺口:钱包流水、充值与退款异常排查、资金汇总报表。 ### 2.12 报表与导出 `/superadmin/reports` -- 状态:**部分完成** -- 已有:平台/租户维度概览、趋势、CSV 导出。 -- 缺口:提现维度报表、内容深度指标与多维钻取能力。 +- 状态:**已完成** +- 已有:平台/租户维度概览、趋势、CSV 导出、提现维度报表、内容深度指标与钻取。 +- 缺口:无显著功能缺口。 ### 2.13 资产与上传 `/superadmin/assets` - 状态:**已完成** @@ -87,6 +87,5 @@ ## 4) 建议的下一步(按优先级) -1. **报表深化**:补齐提现/内容维度指标与多维钻取能力。 -2. **审计与系统配置**:完善全量操作审计与系统级配置能力。 -3. **创作者提现审核**:补齐跨租户提现审核与财务联动入口。 +1. **审计与系统配置**:完善全量操作审计与系统级配置能力。 +2. **创作者提现审核**:补齐跨租户提现审核与财务联动入口。 diff --git a/frontend/superadmin/src/views/superadmin/Finance.vue b/frontend/superadmin/src/views/superadmin/Finance.vue index d8e47ac..2c4188f 100644 --- a/frontend/superadmin/src/views/superadmin/Finance.vue +++ b/frontend/superadmin/src/views/superadmin/Finance.vue @@ -3,9 +3,11 @@ import SearchField from '@/components/SearchField.vue'; import SearchPanel from '@/components/SearchPanel.vue'; import { FinanceService } from '@/service/FinanceService'; import { useToast } from 'primevue/usetoast'; -import { ref } from 'vue'; +import { ref, watch } from 'vue'; +import { useRoute } from 'vue-router'; const toast = useToast(); +const route = useRoute(); const withdrawals = ref([]); const loading = ref(false); @@ -37,6 +39,24 @@ const statusOptions = [ { label: 'failed', value: 'failed' } ]; +function getQueryValue(value) { + if (Array.isArray(value)) return value[0]; + return value ?? null; +} + +function parseNumber(value) { + const parsed = Number(value); + if (!Number.isFinite(parsed)) return null; + return parsed; +} + +function parseDate(value) { + if (!value) return null; + const date = new Date(value); + if (Number.isNaN(date.getTime())) return null; + return date; +} + const approveDialogVisible = ref(false); const approveLoading = ref(false); const approveOrder = ref(null); @@ -109,7 +129,7 @@ function onSearch() { loadWithdrawals(); } -function onReset() { +function resetFilters() { orderID.value = null; tenantID.value = null; tenantCode.value = ''; @@ -127,6 +147,44 @@ function onReset() { sortOrder.value = -1; page.value = 1; rows.value = 10; +} + +function applyRouteQuery(query) { + resetFilters(); + + const idValue = getQueryValue(query?.id); + const tenantValue = getQueryValue(query?.tenant_id); + const userValue = getQueryValue(query?.user_id); + if (idValue) orderID.value = parseNumber(idValue); + if (tenantValue) tenantID.value = parseNumber(tenantValue); + if (userValue) userID.value = parseNumber(userValue); + + const statusValue = getQueryValue(query?.status); + const tenantCodeValue = getQueryValue(query?.tenant_code); + const tenantNameValue = getQueryValue(query?.tenant_name); + const usernameValue = getQueryValue(query?.username); + if (statusValue !== null) status.value = String(statusValue); + if (tenantCodeValue !== null) tenantCode.value = String(tenantCodeValue); + if (tenantNameValue !== null) tenantName.value = String(tenantNameValue); + if (usernameValue !== null) username.value = String(usernameValue); + + const createdFromValue = getQueryValue(query?.created_at_from); + const createdToValue = getQueryValue(query?.created_at_to); + const paidFromValue = getQueryValue(query?.paid_at_from); + const paidToValue = getQueryValue(query?.paid_at_to); + if (createdFromValue) createdAtFrom.value = parseDate(createdFromValue); + if (createdToValue) createdAtTo.value = parseDate(createdToValue); + if (paidFromValue) paidAtFrom.value = parseDate(paidFromValue); + if (paidToValue) paidAtTo.value = parseDate(paidToValue); + + const amountMinValue = getQueryValue(query?.amount_paid_min); + const amountMaxValue = getQueryValue(query?.amount_paid_max); + if (amountMinValue) amountPaidMin.value = parseNumber(amountMinValue); + if (amountMaxValue) amountPaidMax.value = parseNumber(amountMaxValue); +} + +function onReset() { + resetFilters(); loadWithdrawals(); } @@ -185,7 +243,14 @@ async function confirmReject() { } } -loadWithdrawals(); +watch( + () => route.query, + (query) => { + applyRouteQuery(query); + loadWithdrawals(); + }, + { immediate: true } +); + + + + + + + + + + + + + + + +