diff --git a/backend/app/http/v1/creator.go b/backend/app/http/v1/creator.go index 2eaea47..7092579 100644 --- a/backend/app/http/v1/creator.go +++ b/backend/app/http/v1/creator.go @@ -69,6 +69,50 @@ func (c *Creator) CreateMemberInvite( return services.Tenant.CreateInvite(ctx, tenantID, user.ID, form) } +// Get report overview +// +// @Router /t/:tenantCode/v1/creator/reports/overview [get] +// @Summary Report overview +// @Description Get creator report overview +// @Tags CreatorCenter +// @Accept json +// @Produce json +// @Param start_at query string false "Start time (RFC3339)" +// @Param end_at query string false "End time (RFC3339)" +// @Param granularity query string false "Granularity (day)" +// @Success 200 {object} dto.ReportOverviewResponse +// @Bind user local key(__ctx_user) +// @Bind filter query +func (c *Creator) ReportOverview( + ctx fiber.Ctx, + user *models.User, + filter *dto.ReportOverviewFilter, +) (*dto.ReportOverviewResponse, error) { + tenantID := getTenantID(ctx) + return services.Creator.ReportOverview(ctx, tenantID, user.ID, filter) +} + +// Export report overview +// +// @Router /t/:tenantCode/v1/creator/reports/export [post] +// @Summary Export report overview +// @Description Export creator report overview +// @Tags CreatorCenter +// @Accept json +// @Produce json +// @Param form body dto.ReportExportForm true "Export form" +// @Success 200 {object} dto.ReportExportResponse +// @Bind user local key(__ctx_user) +// @Bind form body +func (c *Creator) ExportReport( + ctx fiber.Ctx, + user *models.User, + form *dto.ReportExportForm, +) (*dto.ReportExportResponse, error) { + tenantID := getTenantID(ctx) + return services.Creator.ExportReport(ctx, tenantID, user.ID, form) +} + // Get creator dashboard stats // // @Router /t/:tenantCode/v1/creator/dashboard [get] diff --git a/backend/app/http/v1/dto/creator_report.go b/backend/app/http/v1/dto/creator_report.go new file mode 100644 index 0000000..248da5c --- /dev/null +++ b/backend/app/http/v1/dto/creator_report.go @@ -0,0 +1,65 @@ +package dto + +type ReportOverviewFilter struct { + // StartAt 统计开始时间(RFC3339,可选;默认当前时间往前 7 天)。 + StartAt *string `query:"start_at"` + // EndAt 统计结束时间(RFC3339,可选;默认当前时间)。 + EndAt *string `query:"end_at"` + // Granularity 统计粒度(day;目前仅支持 day)。 + Granularity *string `query:"granularity"` +} + +type ReportOverviewResponse struct { + // Summary 汇总指标。 + Summary ReportSummary `json:"summary"` + // Items 按日期拆分的趋势数据。 + Items []ReportOverviewItem `json:"items"` +} + +type ReportSummary struct { + // TotalViews 内容累计曝光(全量累计值,用于粗略换算)。 + TotalViews int64 `json:"total_views"` + // PaidOrders 统计区间内已支付订单数。 + PaidOrders int64 `json:"paid_orders"` + // PaidAmount 统计区间内已支付金额(单位元)。 + PaidAmount float64 `json:"paid_amount"` + // RefundOrders 统计区间内退款订单数。 + RefundOrders int64 `json:"refund_orders"` + // RefundAmount 统计区间内退款金额(单位元)。 + RefundAmount float64 `json:"refund_amount"` + // ConversionRate 转化率(已支付订单数 / 累计曝光)。 + ConversionRate float64 `json:"conversion_rate"` +} + +type ReportOverviewItem struct { + // Date 日期(YYYY-MM-DD)。 + Date string `json:"date"` + // PaidOrders 当日已支付订单数。 + PaidOrders int64 `json:"paid_orders"` + // PaidAmount 当日已支付金额(单位元)。 + PaidAmount float64 `json:"paid_amount"` + // RefundOrders 当日退款订单数。 + RefundOrders int64 `json:"refund_orders"` + // RefundAmount 当日退款金额(单位元)。 + RefundAmount float64 `json:"refund_amount"` +} + +type ReportExportForm struct { + // StartAt 统计开始时间(RFC3339,可选;默认当前时间往前 7 天)。 + StartAt *string `json:"start_at"` + // EndAt 统计结束时间(RFC3339,可选;默认当前时间)。 + EndAt *string `json:"end_at"` + // Granularity 统计粒度(day;目前仅支持 day)。 + Granularity *string `json:"granularity"` + // Format 导出格式(仅支持 csv)。 + Format string `json:"format"` +} + +type ReportExportResponse struct { + // Filename 导出文件名。 + Filename string `json:"filename"` + // MimeType 导出内容类型。 + MimeType string `json:"mime_type"` + // Content 导出内容(CSV 文本)。 + Content string `json:"content"` +} diff --git a/backend/app/http/v1/routes.gen.go b/backend/app/http/v1/routes.gen.go index c7f656b..f4071d4 100644 --- a/backend/app/http/v1/routes.gen.go +++ b/backend/app/http/v1/routes.gen.go @@ -191,6 +191,12 @@ func (r *Routes) Register(router fiber.Router) { r.creator.ListPayoutAccounts, Local[*models.User]("__ctx_user"), )) + r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/reports/overview -> creator.ReportOverview") + router.Get("/t/:tenantCode/v1/creator/reports/overview"[len(r.Path()):], DataFunc2( + r.creator.ReportOverview, + Local[*models.User]("__ctx_user"), + Query[dto.ReportOverviewFilter]("filter"), + )) r.log.Debugf("Registering route: Get /t/:tenantCode/v1/creator/settings -> creator.GetSettings") router.Get("/t/:tenantCode/v1/creator/settings"[len(r.Path()):], DataFunc1( r.creator.GetSettings, @@ -234,6 +240,12 @@ func (r *Routes) Register(router fiber.Router) { Local[*models.User]("__ctx_user"), Body[dto.PayoutAccount]("form"), )) + r.log.Debugf("Registering route: Post /t/:tenantCode/v1/creator/reports/export -> creator.ExportReport") + router.Post("/t/:tenantCode/v1/creator/reports/export"[len(r.Path()):], DataFunc2( + r.creator.ExportReport, + Local[*models.User]("__ctx_user"), + Body[dto.ReportExportForm]("form"), + )) r.log.Debugf("Registering route: Post /t/:tenantCode/v1/creator/withdraw -> creator.Withdraw") router.Post("/t/:tenantCode/v1/creator/withdraw"[len(r.Path()):], Func2( r.creator.Withdraw, diff --git a/backend/app/services/creator_report.go b/backend/app/services/creator_report.go new file mode 100644 index 0000000..df063ba --- /dev/null +++ b/backend/app/services/creator_report.go @@ -0,0 +1,256 @@ +package services + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "quyun/v2/app/errorx" + creator_dto "quyun/v2/app/http/v1/dto" + "quyun/v2/database/models" + "quyun/v2/pkg/consts" +) + +type reportRange struct { + startDay time.Time + endDay time.Time + endNext time.Time +} + +func (s *creator) ReportOverview( + ctx context.Context, + tenantID int64, + userID int64, + filter *creator_dto.ReportOverviewFilter, +) (*creator_dto.ReportOverviewResponse, error) { + // 校验租户归属(仅租户主可查看统计)。 + tid, err := s.getTenantID(ctx, tenantID, userID) + if err != nil { + return nil, err + } + + // 统一统计时间范围与粒度。 + rg, err := s.normalizeReportRange(filter) + if err != nil { + return nil, err + } + + // 统计累计曝光(全量累计值,暂无按时间拆分的曝光记录)。 + var totalViews int64 + err = models.ContentQuery.WithContext(ctx). + UnderlyingDB(). + Model(&models.Content{}). + Select("coalesce(sum(views), 0)"). + Where("tenant_id = ?", tid). + Scan(&totalViews).Error + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + // 订单仅统计内容购买类型,并按状态划分已支付/已退款。 + paidCount, paidAmount, err := s.orderAggregate(ctx, tid, consts.OrderStatusPaid, "paid_at", rg) + if err != nil { + return nil, err + } + refundCount, refundAmount, err := s.orderAggregate(ctx, tid, consts.OrderStatusRefunded, "updated_at", rg) + if err != nil { + return nil, err + } + + conversionRate := 0.0 + if totalViews > 0 { + conversionRate = float64(paidCount) / float64(totalViews) + } + + // 生成按日趋势序列。 + paidSeries, err := s.orderSeries(ctx, tid, consts.OrderStatusPaid, "paid_at", rg) + if err != nil { + return nil, err + } + refundSeries, err := s.orderSeries(ctx, tid, consts.OrderStatusRefunded, "updated_at", rg) + if err != nil { + return nil, err + } + + items := make([]creator_dto.ReportOverviewItem, 0) + for day := rg.startDay; !day.After(rg.endDay); day = day.AddDate(0, 0, 1) { + key := day.Format("2006-01-02") + paidItem := paidSeries[key] + refundItem := refundSeries[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, + }) + } + + 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, + }, + Items: items, + }, nil +} + +func (s *creator) ExportReport( + ctx context.Context, + tenantID int64, + userID int64, + form *creator_dto.ReportExportForm, +) (*creator_dto.ReportExportResponse, error) { + if form == nil { + return nil, errorx.ErrBadRequest.WithMsg("导出参数不能为空") + } + format := strings.ToLower(strings.TrimSpace(form.Format)) + if format == "" { + format = "csv" + } + if format != "csv" { + return nil, errorx.ErrBadRequest.WithMsg("仅支持 CSV 导出") + } + + // 导出逻辑复用概览统计,保证口径一致。 + overview, err := s.ReportOverview(ctx, tenantID, userID, &creator_dto.ReportOverviewFilter{ + StartAt: form.StartAt, + EndAt: form.EndAt, + Granularity: form.Granularity, + }) + if err != nil { + return nil, err + } + + builder := &strings.Builder{} + builder.WriteString("date,paid_orders,paid_amount,refund_orders,refund_amount\n") + for _, item := range overview.Items { + builder.WriteString(item.Date) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.PaidOrders, 10)) + builder.WriteString(",") + builder.WriteString(formatAmount(item.PaidAmount)) + builder.WriteString(",") + builder.WriteString(strconv.FormatInt(item.RefundOrders, 10)) + builder.WriteString(",") + builder.WriteString(formatAmount(item.RefundAmount)) + builder.WriteString("\n") + } + + filename := fmt.Sprintf("report_overview_%s.csv", time.Now().Format("20060102_150405")) + return &creator_dto.ReportExportResponse{ + Filename: filename, + MimeType: "text/csv", + Content: builder.String(), + }, nil +} + +type reportAggRow struct { + Day time.Time `gorm:"column:day"` + Count int64 `gorm:"column:count"` + Amount int64 `gorm:"column:amount"` +} + +func (s *creator) orderAggregate( + ctx context.Context, + tenantID int64, + status consts.OrderStatus, + timeField string, + rg reportRange, +) (int64, int64, error) { + var total struct { + Count int64 `gorm:"column:count"` + Amount int64 `gorm:"column:amount"` + } + err := models.OrderQuery.WithContext(ctx). + UnderlyingDB(). + 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). + Scan(&total).Error + if err != nil { + return 0, 0, errorx.ErrDatabaseError.WithCause(err) + } + return total.Count, total.Amount, nil +} + +func (s *creator) orderSeries( + ctx context.Context, + tenantID int64, + status consts.OrderStatus, + timeField string, + rg reportRange, +) (map[string]reportAggRow, error) { + rows := make([]reportAggRow, 0) + err := models.OrderQuery.WithContext(ctx). + UnderlyingDB(). + 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). + Group("day"). + Scan(&rows).Error + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + result := make(map[string]reportAggRow, len(rows)) + for _, row := range rows { + key := row.Day.Format("2006-01-02") + result[key] = row + } + return result, nil +} + +func (s *creator) normalizeReportRange(filter *creator_dto.ReportOverviewFilter) (reportRange, error) { + granularity := "day" + if filter != nil && filter.Granularity != nil && strings.TrimSpace(*filter.Granularity) != "" { + granularity = strings.ToLower(strings.TrimSpace(*filter.Granularity)) + } + if granularity != "day" { + return reportRange{}, errorx.ErrBadRequest.WithMsg("仅支持按天统计") + } + + now := time.Now() + endAt := now + if filter != nil && filter.EndAt != nil && strings.TrimSpace(*filter.EndAt) != "" { + parsed, err := time.Parse(time.RFC3339, strings.TrimSpace(*filter.EndAt)) + if err != nil { + return reportRange{}, errorx.ErrBadRequest.WithMsg("结束时间格式错误") + } + endAt = parsed + } + + startAt := endAt.AddDate(0, 0, -6) + if filter != nil && filter.StartAt != nil && strings.TrimSpace(*filter.StartAt) != "" { + parsed, err := time.Parse(time.RFC3339, strings.TrimSpace(*filter.StartAt)) + if err != nil { + return reportRange{}, errorx.ErrBadRequest.WithMsg("开始时间格式错误") + } + startAt = parsed + } + + startDay := time.Date(startAt.Year(), startAt.Month(), startAt.Day(), 0, 0, 0, 0, startAt.Location()) + endDay := time.Date(endAt.Year(), endAt.Month(), endAt.Day(), 0, 0, 0, 0, endAt.Location()) + if endDay.Before(startDay) { + return reportRange{}, errorx.ErrBadRequest.WithMsg("结束时间不能早于开始时间") + } + + endNext := endDay.AddDate(0, 0, 1) + return reportRange{ + startDay: startDay, + endDay: endDay, + endNext: endNext, + }, nil +} + +func formatAmount(amount float64) string { + return strconv.FormatFloat(amount, 'f', 2, 64) +} diff --git a/backend/app/services/creator_test.go b/backend/app/services/creator_test.go index 11f1237..4f2da02 100644 --- a/backend/app/services/creator_test.go +++ b/backend/app/services/creator_test.go @@ -4,6 +4,7 @@ import ( "context" "database/sql" "testing" + "time" "quyun/v2/app/commands/testx" creator_dto "quyun/v2/app/http/v1/dto" @@ -384,3 +385,128 @@ func (s *CreatorTestSuite) Test_Refund() { }) }) } + +func (s *CreatorTestSuite) Test_ReportOverview() { + Convey("ReportOverview", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, + models.TableNameTenant, + models.TableNameUser, + models.TableNameContent, + models.TableNameOrder, + ) + + owner := &models.User{Username: "owner_r", Phone: "13900001011"} + models.UserQuery.WithContext(ctx).Create(owner) + tenant := &models.Tenant{ + Name: "Tenant Report", + UserID: owner.ID, + Status: consts.TenantStatusVerified, + } + models.TenantQuery.WithContext(ctx).Create(tenant) + + models.ContentQuery.WithContext(ctx).Create(&models.Content{ + TenantID: tenant.ID, + UserID: owner.ID, + Title: "Content A", + Status: consts.ContentStatusPublished, + Views: 100, + }) + + now := time.Now() + inRangePaidAt := now.Add(-12 * time.Hour) + outRangePaidAt := now.Add(-10 * 24 * time.Hour) + + models.OrderQuery.WithContext(ctx).Create( + &models.Order{ + TenantID: tenant.ID, + UserID: owner.ID, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusPaid, + AmountPaid: 1000, + PaidAt: inRangePaidAt, + }, + &models.Order{ + TenantID: tenant.ID, + UserID: owner.ID, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusPaid, + AmountPaid: 2000, + PaidAt: outRangePaidAt, + }, + &models.Order{ + TenantID: tenant.ID, + UserID: owner.ID, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusRefunded, + AmountPaid: 500, + UpdatedAt: now.Add(-6 * time.Hour), + }, + ) + + 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{ + StartAt: &start, + EndAt: &end, + }) + So(err, ShouldBeNil) + So(report.Summary.TotalViews, ShouldEqual, 100) + 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) + + var paidSum, refundSum int64 + for _, item := range report.Items { + paidSum += item.PaidOrders + refundSum += item.RefundOrders + } + So(paidSum, ShouldEqual, report.Summary.PaidOrders) + So(refundSum, ShouldEqual, report.Summary.RefundOrders) + }) +} + +func (s *CreatorTestSuite) Test_ExportReport() { + Convey("ExportReport", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, + models.TableNameTenant, + models.TableNameUser, + models.TableNameContent, + models.TableNameOrder, + ) + + owner := &models.User{Username: "owner_e", Phone: "13900001012"} + models.UserQuery.WithContext(ctx).Create(owner) + tenant := &models.Tenant{ + Name: "Tenant Export", + UserID: owner.ID, + Status: consts.TenantStatusVerified, + } + models.TenantQuery.WithContext(ctx).Create(tenant) + + models.ContentQuery.WithContext(ctx).Create(&models.Content{ + TenantID: tenant.ID, + UserID: owner.ID, + Title: "Content Export", + Status: consts.ContentStatusPublished, + Views: 10, + }) + + models.OrderQuery.WithContext(ctx).Create(&models.Order{ + TenantID: tenant.ID, + UserID: owner.ID, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusPaid, + AmountPaid: 1200, + PaidAt: time.Now().Add(-2 * time.Hour), + }) + + form := &creator_dto.ReportExportForm{Format: "csv"} + 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") + }) +} diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 3ea2e7f..a9410ff 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -1884,6 +1884,83 @@ const docTemplate = `{ } } }, + "/t/{tenantCode}/v1/creator/reports/export": { + "post": { + "description": "Export creator report overview", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Export report overview", + "parameters": [ + { + "description": "Export form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ReportExportForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ReportExportResponse" + } + } + } + } + }, + "/t/{tenantCode}/v1/creator/reports/overview": { + "get": { + "description": "Get creator report overview", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Report overview", + "parameters": [ + { + "type": "string", + "description": "Start time (RFC3339)", + "name": "start_at", + "in": "query" + }, + { + "type": "string", + "description": "End time (RFC3339)", + "name": "end_at", + "in": "query" + }, + { + "type": "string", + "description": "Granularity (day)", + "name": "granularity", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ReportOverviewResponse" + } + } + } + } + }, "/t/{tenantCode}/v1/creator/settings": { "get": { "description": "Get channel settings", @@ -4442,6 +4519,118 @@ const docTemplate = `{ } } }, + "dto.ReportExportForm": { + "type": "object", + "properties": { + "end_at": { + "description": "EndAt 统计结束时间(RFC3339,可选;默认当前时间)。", + "type": "string" + }, + "format": { + "description": "Format 导出格式(仅支持 csv)。", + "type": "string" + }, + "granularity": { + "description": "Granularity 统计粒度(day;目前仅支持 day)。", + "type": "string" + }, + "start_at": { + "description": "StartAt 统计开始时间(RFC3339,可选;默认当前时间往前 7 天)。", + "type": "string" + } + } + }, + "dto.ReportExportResponse": { + "type": "object", + "properties": { + "content": { + "description": "Content 导出内容(CSV 文本)。", + "type": "string" + }, + "filename": { + "description": "Filename 导出文件名。", + "type": "string" + }, + "mime_type": { + "description": "MimeType 导出内容类型。", + "type": "string" + } + } + }, + "dto.ReportOverviewItem": { + "type": "object", + "properties": { + "date": { + "description": "Date 日期(YYYY-MM-DD)。", + "type": "string" + }, + "paid_amount": { + "description": "PaidAmount 当日已支付金额(单位元)。", + "type": "number" + }, + "paid_orders": { + "description": "PaidOrders 当日已支付订单数。", + "type": "integer" + }, + "refund_amount": { + "description": "RefundAmount 当日退款金额(单位元)。", + "type": "number" + }, + "refund_orders": { + "description": "RefundOrders 当日退款订单数。", + "type": "integer" + } + } + }, + "dto.ReportOverviewResponse": { + "type": "object", + "properties": { + "items": { + "description": "Items 按日期拆分的趋势数据。", + "type": "array", + "items": { + "$ref": "#/definitions/dto.ReportOverviewItem" + } + }, + "summary": { + "description": "Summary 汇总指标。", + "allOf": [ + { + "$ref": "#/definitions/dto.ReportSummary" + } + ] + } + } + }, + "dto.ReportSummary": { + "type": "object", + "properties": { + "conversion_rate": { + "description": "ConversionRate 转化率(已支付订单数 / 累计曝光)。", + "type": "number" + }, + "paid_amount": { + "description": "PaidAmount 统计区间内已支付金额(单位元)。", + "type": "number" + }, + "paid_orders": { + "description": "PaidOrders 统计区间内已支付订单数。", + "type": "integer" + }, + "refund_amount": { + "description": "RefundAmount 统计区间内退款金额(单位元)。", + "type": "number" + }, + "refund_orders": { + "description": "RefundOrders 统计区间内退款订单数。", + "type": "integer" + }, + "total_views": { + "description": "TotalViews 内容累计曝光(全量累计值,用于粗略换算)。", + "type": "integer" + } + } + }, "dto.Settings": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 4a5062e..58fec52 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -1878,6 +1878,83 @@ } } }, + "/t/{tenantCode}/v1/creator/reports/export": { + "post": { + "description": "Export creator report overview", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Export report overview", + "parameters": [ + { + "description": "Export form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ReportExportForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ReportExportResponse" + } + } + } + } + }, + "/t/{tenantCode}/v1/creator/reports/overview": { + "get": { + "description": "Get creator report overview", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Report overview", + "parameters": [ + { + "type": "string", + "description": "Start time (RFC3339)", + "name": "start_at", + "in": "query" + }, + { + "type": "string", + "description": "End time (RFC3339)", + "name": "end_at", + "in": "query" + }, + { + "type": "string", + "description": "Granularity (day)", + "name": "granularity", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ReportOverviewResponse" + } + } + } + } + }, "/t/{tenantCode}/v1/creator/settings": { "get": { "description": "Get channel settings", @@ -4436,6 +4513,118 @@ } } }, + "dto.ReportExportForm": { + "type": "object", + "properties": { + "end_at": { + "description": "EndAt 统计结束时间(RFC3339,可选;默认当前时间)。", + "type": "string" + }, + "format": { + "description": "Format 导出格式(仅支持 csv)。", + "type": "string" + }, + "granularity": { + "description": "Granularity 统计粒度(day;目前仅支持 day)。", + "type": "string" + }, + "start_at": { + "description": "StartAt 统计开始时间(RFC3339,可选;默认当前时间往前 7 天)。", + "type": "string" + } + } + }, + "dto.ReportExportResponse": { + "type": "object", + "properties": { + "content": { + "description": "Content 导出内容(CSV 文本)。", + "type": "string" + }, + "filename": { + "description": "Filename 导出文件名。", + "type": "string" + }, + "mime_type": { + "description": "MimeType 导出内容类型。", + "type": "string" + } + } + }, + "dto.ReportOverviewItem": { + "type": "object", + "properties": { + "date": { + "description": "Date 日期(YYYY-MM-DD)。", + "type": "string" + }, + "paid_amount": { + "description": "PaidAmount 当日已支付金额(单位元)。", + "type": "number" + }, + "paid_orders": { + "description": "PaidOrders 当日已支付订单数。", + "type": "integer" + }, + "refund_amount": { + "description": "RefundAmount 当日退款金额(单位元)。", + "type": "number" + }, + "refund_orders": { + "description": "RefundOrders 当日退款订单数。", + "type": "integer" + } + } + }, + "dto.ReportOverviewResponse": { + "type": "object", + "properties": { + "items": { + "description": "Items 按日期拆分的趋势数据。", + "type": "array", + "items": { + "$ref": "#/definitions/dto.ReportOverviewItem" + } + }, + "summary": { + "description": "Summary 汇总指标。", + "allOf": [ + { + "$ref": "#/definitions/dto.ReportSummary" + } + ] + } + } + }, + "dto.ReportSummary": { + "type": "object", + "properties": { + "conversion_rate": { + "description": "ConversionRate 转化率(已支付订单数 / 累计曝光)。", + "type": "number" + }, + "paid_amount": { + "description": "PaidAmount 统计区间内已支付金额(单位元)。", + "type": "number" + }, + "paid_orders": { + "description": "PaidOrders 统计区间内已支付订单数。", + "type": "integer" + }, + "refund_amount": { + "description": "RefundAmount 统计区间内退款金额(单位元)。", + "type": "number" + }, + "refund_orders": { + "description": "RefundOrders 统计区间内退款订单数。", + "type": "integer" + }, + "total_views": { + "description": "TotalViews 内容累计曝光(全量累计值,用于粗略换算)。", + "type": "integer" + } + } + }, "dto.Settings": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 81d36db..2fdf9f8 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -768,6 +768,84 @@ definitions: description: Reason 退款原因/备注。 type: string type: object + dto.ReportExportForm: + properties: + end_at: + description: EndAt 统计结束时间(RFC3339,可选;默认当前时间)。 + type: string + format: + description: Format 导出格式(仅支持 csv)。 + type: string + granularity: + description: Granularity 统计粒度(day;目前仅支持 day)。 + type: string + start_at: + description: StartAt 统计开始时间(RFC3339,可选;默认当前时间往前 7 天)。 + type: string + type: object + dto.ReportExportResponse: + properties: + content: + description: Content 导出内容(CSV 文本)。 + type: string + filename: + description: Filename 导出文件名。 + type: string + mime_type: + description: MimeType 导出内容类型。 + type: string + type: object + dto.ReportOverviewItem: + properties: + date: + description: Date 日期(YYYY-MM-DD)。 + type: string + paid_amount: + description: PaidAmount 当日已支付金额(单位元)。 + type: number + paid_orders: + description: PaidOrders 当日已支付订单数。 + type: integer + refund_amount: + description: RefundAmount 当日退款金额(单位元)。 + type: number + refund_orders: + description: RefundOrders 当日退款订单数。 + type: integer + type: object + dto.ReportOverviewResponse: + properties: + items: + description: Items 按日期拆分的趋势数据。 + items: + $ref: '#/definitions/dto.ReportOverviewItem' + type: array + summary: + allOf: + - $ref: '#/definitions/dto.ReportSummary' + description: Summary 汇总指标。 + type: object + dto.ReportSummary: + properties: + conversion_rate: + description: ConversionRate 转化率(已支付订单数 / 累计曝光)。 + type: number + paid_amount: + description: PaidAmount 统计区间内已支付金额(单位元)。 + type: number + paid_orders: + description: PaidOrders 统计区间内已支付订单数。 + type: integer + refund_amount: + description: RefundAmount 统计区间内退款金额(单位元)。 + type: number + refund_orders: + description: RefundOrders 统计区间内退款订单数。 + type: integer + total_views: + description: TotalViews 内容累计曝光(全量累计值,用于粗略换算)。 + type: integer + type: object dto.Settings: properties: avatar: @@ -2885,6 +2963,56 @@ paths: summary: Add payout account tags: - CreatorCenter + /t/{tenantCode}/v1/creator/reports/export: + post: + consumes: + - application/json + description: Export creator report overview + parameters: + - description: Export form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.ReportExportForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ReportExportResponse' + summary: Export report overview + tags: + - CreatorCenter + /t/{tenantCode}/v1/creator/reports/overview: + get: + consumes: + - application/json + description: Get creator report overview + parameters: + - description: Start time (RFC3339) + in: query + name: start_at + type: string + - description: End time (RFC3339) + in: query + name: end_at + type: string + - description: Granularity (day) + in: query + name: granularity + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ReportOverviewResponse' + summary: Report overview + tags: + - CreatorCenter /t/{tenantCode}/v1/creator/settings: get: consumes: diff --git a/docs/todo_list.md b/docs/todo_list.md index 7b4a268..cd831c5 100644 --- a/docs/todo_list.md +++ b/docs/todo_list.md @@ -196,6 +196,7 @@ - ID 类型已统一为 int64(仅保留 upload_id/external_id/uuid 等非数字标识)。 - 内容资源权限与预览差异化(未购预览、已购/管理员/成员全量)。 - 审计操作显式传入操作者信息(服务层不再依赖 ctx 读取)。 +- 运营统计报表(overview + CSV 导出基础版)。 ## 里程碑建议 - M1:完成 P0