feat: deepen report metrics
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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) != "" {
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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. **创作者提现审核**:补齐跨租户提现审核与财务联动入口。
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user