feat: deepen report metrics

This commit is contained in:
2026-01-15 17:50:37 +08:00
parent ba1d120c84
commit 914df9edf2
10 changed files with 1163 additions and 52 deletions

View File

@@ -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 {

View File

@@ -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) != "" {

View File

@@ -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")
})
}

View File

@@ -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)
}

View File

@@ -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"
}
}
},

View File

@@ -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"
}
}
},

View File

@@ -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:

View File

@@ -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. **创作者提现审核**:补齐跨租户提现审核与财务联动入口

View File

@@ -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 }
);
</script>
<template>

View File

@@ -24,6 +24,12 @@ const orderChartData = ref(null);
const amountChartData = ref(null);
const orderChartOptions = ref(null);
const amountChartOptions = ref(null);
const withdrawOrderChartData = ref(null);
const withdrawAmountChartData = ref(null);
const withdrawOrderChartOptions = ref(null);
const withdrawAmountChartOptions = ref(null);
const interactionChartData = ref(null);
const interactionChartOptions = ref(null);
const granularityOptions = [{ label: '按天', value: 'day' }];
@@ -63,9 +69,40 @@ function goToOrders(status, usePaidAt) {
router.push({ name: 'superadmin-orders', query: buildOrdersQuery({ status, usePaidAt }) });
}
function goToContents() {
function buildWithdrawalsQuery({ status, usePaidAt } = {}) {
const query = {};
if (tenantID.value) query.tenant_id = tenantID.value;
if (status) query.status = status;
const start = formatDateParam(startAt.value);
const end = formatDateParam(endAt.value);
if (usePaidAt) {
if (start) query.paid_at_from = start;
if (end) query.paid_at_to = end;
} else {
if (start) query.created_at_from = start;
if (end) query.created_at_to = end;
}
return query;
}
function goToWithdrawals(status, usePaidAt) {
router.push({ name: 'superadmin-finance', query: buildWithdrawalsQuery({ status, usePaidAt }) });
}
function buildContentsQuery({ useCreatedAt } = {}) {
const query = {};
if (tenantID.value) query.tenant_id = tenantID.value;
if (useCreatedAt) {
const start = formatDateParam(startAt.value);
const end = formatDateParam(endAt.value);
if (start) query.created_at_from = start;
if (end) query.created_at_to = end;
}
return query;
}
function goToContents(extraQuery = {}) {
const query = { ...buildContentsQuery(), ...extraQuery };
router.push({ name: 'superadmin-contents', query });
}
@@ -127,6 +164,129 @@ function buildAmountChartData(items) {
};
}
function buildWithdrawOrderChartData(items) {
const documentStyle = getComputedStyle(document.documentElement);
const labels = items.map((item) => item.date);
const applyOrders = items.map((item) => Number(item.withdrawal_apply_orders ?? 0));
const paidOrders = items.map((item) => Number(item.withdrawal_paid_orders ?? 0));
const failedOrders = items.map((item) => Number(item.withdrawal_failed_orders ?? 0));
return {
labels,
datasets: [
{
label: '提现申请',
data: applyOrders,
fill: true,
backgroundColor: documentStyle.getPropertyValue('--p-primary-200'),
borderColor: documentStyle.getPropertyValue('--p-primary-500'),
tension: 0.35
},
{
label: '提现完成',
data: paidOrders,
fill: true,
backgroundColor: documentStyle.getPropertyValue('--p-emerald-200'),
borderColor: documentStyle.getPropertyValue('--p-emerald-500'),
tension: 0.35
},
{
label: '提现失败',
data: failedOrders,
fill: true,
backgroundColor: documentStyle.getPropertyValue('--p-rose-200'),
borderColor: documentStyle.getPropertyValue('--p-rose-500'),
tension: 0.35
}
]
};
}
function buildWithdrawAmountChartData(items) {
const documentStyle = getComputedStyle(document.documentElement);
const labels = items.map((item) => item.date);
const applyAmounts = items.map((item) => Number(item.withdrawal_apply_amount ?? 0));
const paidAmounts = items.map((item) => Number(item.withdrawal_paid_amount ?? 0));
const failedAmounts = items.map((item) => Number(item.withdrawal_failed_amount ?? 0));
return {
labels,
datasets: [
{
label: '申请金额',
data: applyAmounts,
fill: true,
backgroundColor: documentStyle.getPropertyValue('--p-primary-200'),
borderColor: documentStyle.getPropertyValue('--p-primary-500'),
tension: 0.35
},
{
label: '完成金额',
data: paidAmounts,
fill: true,
backgroundColor: documentStyle.getPropertyValue('--p-emerald-200'),
borderColor: documentStyle.getPropertyValue('--p-emerald-500'),
tension: 0.35
},
{
label: '失败金额',
data: failedAmounts,
fill: true,
backgroundColor: documentStyle.getPropertyValue('--p-rose-200'),
borderColor: documentStyle.getPropertyValue('--p-rose-500'),
tension: 0.35
}
]
};
}
function buildInteractionChartData(items) {
const documentStyle = getComputedStyle(document.documentElement);
const labels = items.map((item) => item.date);
const contentCreated = items.map((item) => Number(item.content_created ?? 0));
const likeActions = items.map((item) => Number(item.like_actions ?? 0));
const favoriteActions = items.map((item) => Number(item.favorite_actions ?? 0));
const commentCount = items.map((item) => Number(item.comment_count ?? 0));
return {
labels,
datasets: [
{
label: '新增内容',
data: contentCreated,
fill: true,
backgroundColor: documentStyle.getPropertyValue('--p-primary-200'),
borderColor: documentStyle.getPropertyValue('--p-primary-500'),
tension: 0.35
},
{
label: '新增点赞',
data: likeActions,
fill: true,
backgroundColor: documentStyle.getPropertyValue('--p-emerald-200'),
borderColor: documentStyle.getPropertyValue('--p-emerald-500'),
tension: 0.35
},
{
label: '新增收藏',
data: favoriteActions,
fill: true,
backgroundColor: documentStyle.getPropertyValue('--p-orange-200'),
borderColor: documentStyle.getPropertyValue('--p-orange-500'),
tension: 0.35
},
{
label: '新增评论',
data: commentCount,
fill: true,
backgroundColor: documentStyle.getPropertyValue('--p-rose-200'),
borderColor: documentStyle.getPropertyValue('--p-rose-500'),
tension: 0.35
}
]
};
}
function buildChartOptions({ amountMode } = {}) {
const documentStyle = getComputedStyle(document.documentElement);
const textColor = documentStyle.getPropertyValue('--text-color');
@@ -186,6 +346,12 @@ function updateCharts() {
amountChartData.value = buildAmountChartData(items);
orderChartOptions.value = buildChartOptions({ amountMode: false });
amountChartOptions.value = buildChartOptions({ amountMode: true });
withdrawOrderChartData.value = buildWithdrawOrderChartData(items);
withdrawAmountChartData.value = buildWithdrawAmountChartData(items);
withdrawOrderChartOptions.value = buildChartOptions({ amountMode: false });
withdrawAmountChartOptions.value = buildChartOptions({ amountMode: true });
interactionChartData.value = buildInteractionChartData(items);
interactionChartOptions.value = buildChartOptions({ amountMode: false });
}
const summaryItems = computed(() => {
@@ -202,6 +368,31 @@ const summaryItems = computed(() => {
];
});
const withdrawSummaryItems = computed(() => {
const summary = overview.value?.summary;
if (!summary) return [];
return [
{ key: 'withdraw-apply', label: '提现申请:', value: summary.withdrawal_apply_orders ?? 0, icon: 'pi-inbox', onClick: () => goToWithdrawals('created', false) },
{ key: 'withdraw-paid', label: '提现完成:', value: summary.withdrawal_paid_orders ?? 0, icon: 'pi-check-circle', onClick: () => goToWithdrawals('paid', true) },
{ key: 'withdraw-failed', label: '提现失败:', value: summary.withdrawal_failed_orders ?? 0, icon: 'pi-times-circle', onClick: () => goToWithdrawals('failed', false) }
];
});
const contentSummaryItems = computed(() => {
const summary = overview.value?.summary;
if (!summary) return [];
const rangeQuery = buildContentsQuery({ useCreatedAt: true });
return [
{ key: 'content-count', label: '内容总量:', value: summary.content_count ?? 0, icon: 'pi-book', onClick: () => goToContents() },
{ key: 'content-created', label: '新增内容:', value: summary.content_created ?? 0, icon: 'pi-plus', onClick: () => goToContents(rangeQuery) },
{ key: 'like-actions', label: '新增点赞:', value: summary.like_actions ?? 0, icon: 'pi-thumbs-up', onClick: () => goToContents(rangeQuery) },
{ key: 'favorite-actions', label: '新增收藏:', value: summary.favorite_actions ?? 0, icon: 'pi-star', onClick: () => goToContents(rangeQuery) },
{ key: 'comment-count', label: '新增评论:', value: summary.comment_count ?? 0, icon: 'pi-comments', onClick: () => goToContents(rangeQuery) }
];
});
async function loadOverview() {
loading.value = true;
try {
@@ -287,6 +478,8 @@ watch([getPrimary, getSurface, isDarkTheme], () => {
</div>
<StatisticsStrip v-if="summaryItems.length" :items="summaryItems" containerClass="card mb-4" />
<StatisticsStrip v-if="withdrawSummaryItems.length" :items="withdrawSummaryItems" containerClass="card mb-4" />
<StatisticsStrip v-if="contentSummaryItems.length" :items="contentSummaryItems" containerClass="card mb-4" />
<div class="card mb-4">
<div class="flex items-center justify-between mb-4">
@@ -313,6 +506,41 @@ watch([getPrimary, getSurface, isDarkTheme], () => {
</div>
</div>
<div class="card mb-4">
<div class="flex items-center justify-between mb-4">
<div class="flex flex-col">
<h4 class="m-0">提现趋势</h4>
<span class="text-muted-color">申请 / 完成 / 失败</span>
</div>
</div>
<div class="grid grid-cols-1 xl:grid-cols-2 gap-6">
<div class="flex flex-col gap-3">
<div class="flex items-center justify-between">
<span class="font-medium">订单趋势</span>
<span class="text-muted-color text-sm">申请 vs 完成 vs 失败</span>
</div>
<Chart type="line" :data="withdrawOrderChartData" :options="withdrawOrderChartOptions" class="h-72" />
</div>
<div class="flex flex-col gap-3">
<div class="flex items-center justify-between">
<span class="font-medium">金额趋势</span>
<span class="text-muted-color text-sm">单位</span>
</div>
<Chart type="line" :data="withdrawAmountChartData" :options="withdrawAmountChartOptions" class="h-72" />
</div>
</div>
</div>
<div class="card mb-4">
<div class="flex items-center justify-between mb-4">
<div class="flex flex-col">
<h4 class="m-0">内容互动趋势</h4>
<span class="text-muted-color">新增内容 / 点赞 / 收藏 / 评论</span>
</div>
</div>
<Chart type="line" :data="interactionChartData" :options="interactionChartOptions" class="h-72" />
</div>
<div class="card">
<div class="flex items-center justify-between mb-4">
<h4 class="m-0">趋势明细</h4>
@@ -331,6 +559,28 @@ watch([getPrimary, getSurface, isDarkTheme], () => {
{{ formatCnyFromYuan(data.refund_amount) }}
</template>
</Column>
<Column field="withdrawal_apply_orders" header="提现申请" style="min-width: 10rem" />
<Column field="withdrawal_apply_amount" header="申请金额" style="min-width: 12rem">
<template #body="{ data }">
{{ formatCnyFromYuan(data.withdrawal_apply_amount) }}
</template>
</Column>
<Column field="withdrawal_paid_orders" header="提现完成" style="min-width: 10rem" />
<Column field="withdrawal_paid_amount" header="完成金额" style="min-width: 12rem">
<template #body="{ data }">
{{ formatCnyFromYuan(data.withdrawal_paid_amount) }}
</template>
</Column>
<Column field="withdrawal_failed_orders" header="提现失败" style="min-width: 10rem" />
<Column field="withdrawal_failed_amount" header="失败金额" style="min-width: 12rem">
<template #body="{ data }">
{{ formatCnyFromYuan(data.withdrawal_failed_amount) }}
</template>
</Column>
<Column field="content_created" header="新增内容" style="min-width: 10rem" />
<Column field="like_actions" header="新增点赞" style="min-width: 10rem" />
<Column field="favorite_actions" header="新增收藏" style="min-width: 10rem" />
<Column field="comment_count" header="新增评论" style="min-width: 10rem" />
</DataTable>
</div>
</div>