feat: wire superadmin p1 data
This commit is contained in:
28
backend/app/http/super/v1/coupons.go
Normal file
28
backend/app/http/super/v1/coupons.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
dto "quyun/v2/app/http/super/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type coupons struct{}
|
||||
|
||||
// List coupons
|
||||
//
|
||||
// @Router /super/v1/coupons [get]
|
||||
// @Summary List coupons
|
||||
// @Description List coupon templates across tenants
|
||||
// @Tags Coupon
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number"
|
||||
// @Param limit query int false "Page size"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.SuperCouponItem}
|
||||
// @Bind filter query
|
||||
func (c *coupons) List(ctx fiber.Ctx, filter *dto.SuperCouponListFilter) (*requests.Pager, error) {
|
||||
return services.Super.ListCoupons(ctx, filter)
|
||||
}
|
||||
28
backend/app/http/super/v1/creators.go
Normal file
28
backend/app/http/super/v1/creators.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
dto "quyun/v2/app/http/super/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type creators struct{}
|
||||
|
||||
// List creators
|
||||
//
|
||||
// @Router /super/v1/creators [get]
|
||||
// @Summary List creators
|
||||
// @Description List creator tenants (channels) across the platform
|
||||
// @Tags Creator
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number"
|
||||
// @Param limit query int false "Page size"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.TenantItem}
|
||||
// @Bind filter query
|
||||
func (c *creators) List(ctx fiber.Ctx, filter *dto.TenantListFilter) (*requests.Pager, error) {
|
||||
return services.Super.ListTenants(ctx, filter)
|
||||
}
|
||||
75
backend/app/http/super/v1/dto/super_coupon.go
Normal file
75
backend/app/http/super/v1/dto/super_coupon.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/pkg/consts"
|
||||
)
|
||||
|
||||
// SuperCouponListFilter 超管优惠券列表过滤条件。
|
||||
type SuperCouponListFilter struct {
|
||||
requests.Pagination
|
||||
// ID 优惠券ID,精确匹配。
|
||||
ID *int64 `query:"id"`
|
||||
// TenantID 租户ID,精确匹配。
|
||||
TenantID *int64 `query:"tenant_id"`
|
||||
// TenantCode 租户编码,模糊匹配。
|
||||
TenantCode *string `query:"tenant_code"`
|
||||
// TenantName 租户名称,模糊匹配。
|
||||
TenantName *string `query:"tenant_name"`
|
||||
// Keyword 标题或描述关键词,模糊匹配。
|
||||
Keyword *string `query:"keyword"`
|
||||
// Type 优惠券类型过滤(fix_amount/discount)。
|
||||
Type *string `query:"type"`
|
||||
// Status 状态过滤(active/expired/upcoming)。
|
||||
Status *string `query:"status"`
|
||||
// CreatedAtFrom 创建时间起始(RFC3339)。
|
||||
CreatedAtFrom *string `query:"created_at_from"`
|
||||
// CreatedAtTo 创建时间结束(RFC3339)。
|
||||
CreatedAtTo *string `query:"created_at_to"`
|
||||
// Asc 升序字段(id/created_at/start_at/end_at)。
|
||||
Asc *string `query:"asc"`
|
||||
// Desc 降序字段(id/created_at/start_at/end_at)。
|
||||
Desc *string `query:"desc"`
|
||||
}
|
||||
|
||||
// SuperCouponItem 超管优惠券列表项。
|
||||
type SuperCouponItem struct {
|
||||
// ID 优惠券ID。
|
||||
ID int64 `json:"id"`
|
||||
// TenantID 租户ID。
|
||||
TenantID int64 `json:"tenant_id"`
|
||||
// TenantCode 租户编码。
|
||||
TenantCode string `json:"tenant_code"`
|
||||
// TenantName 租户名称。
|
||||
TenantName string `json:"tenant_name"`
|
||||
// Title 优惠券标题。
|
||||
Title string `json:"title"`
|
||||
// Description 优惠券描述。
|
||||
Description string `json:"description"`
|
||||
// Type 优惠券类型。
|
||||
Type consts.CouponType `json:"type"`
|
||||
// TypeDescription 类型描述(用于展示)。
|
||||
TypeDescription string `json:"type_description"`
|
||||
// Value 优惠券面额/折扣值。
|
||||
Value int64 `json:"value"`
|
||||
// MinOrderAmount 最低订单金额门槛。
|
||||
MinOrderAmount int64 `json:"min_order_amount"`
|
||||
// MaxDiscount 最大折扣金额(折扣券)。
|
||||
MaxDiscount int64 `json:"max_discount"`
|
||||
// TotalQuantity 总发行数量(0 表示不限量)。
|
||||
TotalQuantity int32 `json:"total_quantity"`
|
||||
// UsedQuantity 已使用数量。
|
||||
UsedQuantity int32 `json:"used_quantity"`
|
||||
// Status 状态(active/expired/upcoming)。
|
||||
Status string `json:"status"`
|
||||
// StatusDescription 状态描述(用于展示)。
|
||||
StatusDescription string `json:"status_description"`
|
||||
// StartAt 生效时间(RFC3339)。
|
||||
StartAt string `json:"start_at"`
|
||||
// EndAt 结束时间(RFC3339)。
|
||||
EndAt string `json:"end_at"`
|
||||
// CreatedAt 创建时间(RFC3339)。
|
||||
CreatedAt string `json:"created_at"`
|
||||
// UpdatedAt 更新时间(RFC3339)。
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
27
backend/app/http/super/v1/dto/super_report.go
Normal file
27
backend/app/http/super/v1/dto/super_report.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package dto
|
||||
|
||||
// SuperReportOverviewFilter 超管报表查询条件。
|
||||
type SuperReportOverviewFilter struct {
|
||||
// TenantID 租户ID(不传代表全平台)。
|
||||
TenantID *int64 `query:"tenant_id"`
|
||||
// StartAt 统计开始时间(RFC3339,可选;默认当前时间往前 7 天)。
|
||||
StartAt *string `query:"start_at"`
|
||||
// EndAt 统计结束时间(RFC3339,可选;默认当前时间)。
|
||||
EndAt *string `query:"end_at"`
|
||||
// Granularity 统计粒度(day;目前仅支持 day)。
|
||||
Granularity *string `query:"granularity"`
|
||||
}
|
||||
|
||||
// SuperReportExportForm 超管报表导出参数。
|
||||
type SuperReportExportForm struct {
|
||||
// TenantID 租户ID(不传代表全平台)。
|
||||
TenantID *int64 `json:"tenant_id"`
|
||||
// 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"`
|
||||
}
|
||||
7
backend/app/http/super/v1/dto/super_withdrawal.go
Normal file
7
backend/app/http/super/v1/dto/super_withdrawal.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package dto
|
||||
|
||||
// SuperWithdrawalRejectForm 超管驳回提现表单。
|
||||
type SuperWithdrawalRejectForm struct {
|
||||
// Reason 驳回原因。
|
||||
Reason string `json:"reason" validate:"required"`
|
||||
}
|
||||
@@ -17,6 +17,20 @@ func Provide(opts ...opt.Option) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*coupons, error) {
|
||||
obj := &coupons{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*creators, error) {
|
||||
obj := &creators{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*orders, error) {
|
||||
obj := &orders{}
|
||||
|
||||
@@ -24,19 +38,34 @@ func Provide(opts ...opt.Option) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*reports, error) {
|
||||
obj := &reports{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func(
|
||||
contents *contents,
|
||||
coupons *coupons,
|
||||
creators *creators,
|
||||
middlewares *middlewares.Middlewares,
|
||||
orders *orders,
|
||||
reports *reports,
|
||||
tenants *tenants,
|
||||
users *users,
|
||||
withdrawals *withdrawals,
|
||||
) (contracts.HttpRoute, error) {
|
||||
obj := &Routes{
|
||||
contents: contents,
|
||||
coupons: coupons,
|
||||
creators: creators,
|
||||
middlewares: middlewares,
|
||||
orders: orders,
|
||||
reports: reports,
|
||||
tenants: tenants,
|
||||
users: users,
|
||||
withdrawals: withdrawals,
|
||||
}
|
||||
if err := obj.Prepare(); err != nil {
|
||||
return nil, err
|
||||
@@ -60,5 +89,12 @@ func Provide(opts ...opt.Option) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := container.Container.Provide(func() (*withdrawals, error) {
|
||||
obj := &withdrawals{}
|
||||
|
||||
return obj, nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
41
backend/app/http/super/v1/reports.go
Normal file
41
backend/app/http/super/v1/reports.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
dto "quyun/v2/app/http/super/v1/dto"
|
||||
v1_dto "quyun/v2/app/http/v1/dto"
|
||||
"quyun/v2/app/services"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type reports struct{}
|
||||
|
||||
// Report overview
|
||||
//
|
||||
// @Router /super/v1/reports/overview [get]
|
||||
// @Summary Report overview
|
||||
// @Description Get platform report overview
|
||||
// @Tags Report
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} v1_dto.ReportOverviewResponse
|
||||
// @Bind filter query
|
||||
func (c *reports) Overview(ctx fiber.Ctx, filter *dto.SuperReportOverviewFilter) (*v1_dto.ReportOverviewResponse, error) {
|
||||
return services.Super.ReportOverview(ctx, filter)
|
||||
}
|
||||
|
||||
// Export report
|
||||
//
|
||||
// @Router /super/v1/reports/export [post]
|
||||
// @Summary Export report
|
||||
// @Description Export platform report data
|
||||
// @Tags Report
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param form body dto.SuperReportExportForm true "Export form"
|
||||
// @Success 200 {object} v1_dto.ReportExportResponse
|
||||
// @Bind form body
|
||||
func (c *reports) Export(ctx fiber.Ctx, form *dto.SuperReportExportForm) (*v1_dto.ReportExportResponse, error) {
|
||||
return services.Super.ExportReport(ctx, form)
|
||||
}
|
||||
@@ -24,10 +24,14 @@ type Routes struct {
|
||||
log *log.Entry `inject:"false"`
|
||||
middlewares *middlewares.Middlewares
|
||||
// Controller instances
|
||||
contents *contents
|
||||
orders *orders
|
||||
tenants *tenants
|
||||
users *users
|
||||
contents *contents
|
||||
coupons *coupons
|
||||
creators *creators
|
||||
orders *orders
|
||||
reports *reports
|
||||
tenants *tenants
|
||||
users *users
|
||||
withdrawals *withdrawals
|
||||
}
|
||||
|
||||
// Prepare initializes the routes provider with logging configuration.
|
||||
@@ -71,6 +75,18 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.SuperContentReviewForm]("form"),
|
||||
))
|
||||
// Register routes for controller: coupons
|
||||
r.log.Debugf("Registering route: Get /super/v1/coupons -> coupons.List")
|
||||
router.Get("/super/v1/coupons"[len(r.Path()):], DataFunc1(
|
||||
r.coupons.List,
|
||||
Query[dto.SuperCouponListFilter]("filter"),
|
||||
))
|
||||
// Register routes for controller: creators
|
||||
r.log.Debugf("Registering route: Get /super/v1/creators -> creators.List")
|
||||
router.Get("/super/v1/creators"[len(r.Path()):], DataFunc1(
|
||||
r.creators.List,
|
||||
Query[dto.TenantListFilter]("filter"),
|
||||
))
|
||||
// Register routes for controller: orders
|
||||
r.log.Debugf("Registering route: Get /super/v1/orders -> orders.List")
|
||||
router.Get("/super/v1/orders"[len(r.Path()):], DataFunc1(
|
||||
@@ -92,6 +108,17 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.SuperOrderRefundForm]("form"),
|
||||
))
|
||||
// Register routes for controller: reports
|
||||
r.log.Debugf("Registering route: Get /super/v1/reports/overview -> reports.Overview")
|
||||
router.Get("/super/v1/reports/overview"[len(r.Path()):], DataFunc1(
|
||||
r.reports.Overview,
|
||||
Query[dto.SuperReportOverviewFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /super/v1/reports/export -> reports.Export")
|
||||
router.Post("/super/v1/reports/export"[len(r.Path()):], DataFunc1(
|
||||
r.reports.Export,
|
||||
Body[dto.SuperReportExportForm]("form"),
|
||||
))
|
||||
// Register routes for controller: tenants
|
||||
r.log.Debugf("Registering route: Get /super/v1/tenants -> tenants.List")
|
||||
router.Get("/super/v1/tenants"[len(r.Path()):], DataFunc1(
|
||||
@@ -172,6 +199,25 @@ func (r *Routes) Register(router fiber.Router) {
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.UserStatusUpdateForm]("form"),
|
||||
))
|
||||
// Register routes for controller: withdrawals
|
||||
r.log.Debugf("Registering route: Get /super/v1/withdrawals -> withdrawals.List")
|
||||
router.Get("/super/v1/withdrawals"[len(r.Path()):], DataFunc1(
|
||||
r.withdrawals.List,
|
||||
Query[dto.SuperOrderListFilter]("filter"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /super/v1/withdrawals/:id<int>/approve -> withdrawals.Approve")
|
||||
router.Post("/super/v1/withdrawals/:id<int>/approve"[len(r.Path()):], Func2(
|
||||
r.withdrawals.Approve,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
))
|
||||
r.log.Debugf("Registering route: Post /super/v1/withdrawals/:id<int>/reject -> withdrawals.Reject")
|
||||
router.Post("/super/v1/withdrawals/:id<int>/reject"[len(r.Path()):], Func3(
|
||||
r.withdrawals.Reject,
|
||||
Local[*models.User]("__ctx_user"),
|
||||
PathParam[int64]("id"),
|
||||
Body[dto.SuperWithdrawalRejectForm]("form"),
|
||||
))
|
||||
|
||||
r.log.Info("Successfully registered all routes")
|
||||
}
|
||||
|
||||
63
backend/app/http/super/v1/withdrawals.go
Normal file
63
backend/app/http/super/v1/withdrawals.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
dto "quyun/v2/app/http/super/v1/dto"
|
||||
"quyun/v2/app/requests"
|
||||
"quyun/v2/app/services"
|
||||
"quyun/v2/database/models"
|
||||
|
||||
"github.com/gofiber/fiber/v3"
|
||||
)
|
||||
|
||||
// @provider
|
||||
type withdrawals struct{}
|
||||
|
||||
// List withdrawals
|
||||
//
|
||||
// @Router /super/v1/withdrawals [get]
|
||||
// @Summary List withdrawals
|
||||
// @Description List withdrawal orders across tenants
|
||||
// @Tags Finance
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "Page number"
|
||||
// @Param limit query int false "Page size"
|
||||
// @Success 200 {object} requests.Pager{items=[]dto.SuperOrderItem}
|
||||
// @Bind filter query
|
||||
func (c *withdrawals) List(ctx fiber.Ctx, filter *dto.SuperOrderListFilter) (*requests.Pager, error) {
|
||||
return services.Super.ListWithdrawals(ctx, filter)
|
||||
}
|
||||
|
||||
// Approve withdrawal
|
||||
//
|
||||
// @Router /super/v1/withdrawals/:id<int>/approve [post]
|
||||
// @Summary Approve withdrawal
|
||||
// @Description Approve a withdrawal request
|
||||
// @Tags Finance
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Withdrawal order ID"
|
||||
// @Success 200 {string} string "Approved"
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id path
|
||||
func (c *withdrawals) Approve(ctx fiber.Ctx, user *models.User, id int64) error {
|
||||
return services.Super.ApproveWithdrawal(ctx, user.ID, id)
|
||||
}
|
||||
|
||||
// Reject withdrawal
|
||||
//
|
||||
// @Router /super/v1/withdrawals/:id<int>/reject [post]
|
||||
// @Summary Reject withdrawal
|
||||
// @Description Reject a withdrawal request
|
||||
// @Tags Finance
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int64 true "Withdrawal order ID"
|
||||
// @Param form body dto.SuperWithdrawalRejectForm true "Reject form"
|
||||
// @Success 200 {string} string "Rejected"
|
||||
// @Bind user local key(__ctx_user)
|
||||
// @Bind id path
|
||||
// @Bind form body
|
||||
func (c *withdrawals) Reject(ctx fiber.Ctx, user *models.User, id int64, form *dto.SuperWithdrawalRejectForm) error {
|
||||
return services.Super.RejectWithdrawal(ctx, user.ID, id, form.Reason)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package services
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -2201,16 +2202,134 @@ func (s *super) evaluateTenantHealth(tenant *models.Tenant, metrics tenantHealth
|
||||
}
|
||||
|
||||
func (s *super) ListWithdrawals(ctx context.Context, filter *super_dto.SuperOrderListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &super_dto.SuperOrderListFilter{}
|
||||
}
|
||||
|
||||
tbl, q := models.OrderQuery.QueryContext(ctx)
|
||||
q = q.Where(tbl.Type.Eq(consts.OrderTypeWithdrawal))
|
||||
|
||||
if filter.ID != nil && *filter.ID > 0 {
|
||||
q = q.Where(tbl.ID.Eq(*filter.ID))
|
||||
}
|
||||
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
|
||||
}
|
||||
if filter.UserID != nil && *filter.UserID > 0 {
|
||||
q = q.Where(tbl.UserID.Eq(*filter.UserID))
|
||||
}
|
||||
if filter.Status != nil && *filter.Status != "" {
|
||||
q = q.Where(tbl.Status.Eq(*filter.Status))
|
||||
}
|
||||
if filter.AmountPaidMin != nil {
|
||||
q = q.Where(tbl.AmountPaid.Gte(*filter.AmountPaidMin))
|
||||
}
|
||||
if filter.AmountPaidMax != nil {
|
||||
q = q.Where(tbl.AmountPaid.Lte(*filter.AmountPaidMax))
|
||||
}
|
||||
|
||||
tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantFilter {
|
||||
if len(tenantIDs) == 0 {
|
||||
q = q.Where(tbl.ID.Eq(-1))
|
||||
} else {
|
||||
q = q.Where(tbl.TenantID.In(tenantIDs...))
|
||||
}
|
||||
}
|
||||
|
||||
userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if userFilter {
|
||||
if len(userIDs) == 0 {
|
||||
q = q.Where(tbl.ID.Eq(-1))
|
||||
} else {
|
||||
q = q.Where(tbl.UserID.In(userIDs...))
|
||||
}
|
||||
}
|
||||
|
||||
if filter.CreatedAtFrom != nil {
|
||||
from, err := s.parseFilterTime(filter.CreatedAtFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if from != nil {
|
||||
q = q.Where(tbl.CreatedAt.Gte(*from))
|
||||
}
|
||||
}
|
||||
if filter.CreatedAtTo != nil {
|
||||
to, err := s.parseFilterTime(filter.CreatedAtTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if to != nil {
|
||||
q = q.Where(tbl.CreatedAt.Lte(*to))
|
||||
}
|
||||
}
|
||||
if filter.PaidAtFrom != nil {
|
||||
from, err := s.parseFilterTime(filter.PaidAtFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if from != nil {
|
||||
q = q.Where(tbl.PaidAt.Gte(*from))
|
||||
}
|
||||
}
|
||||
if filter.PaidAtTo != nil {
|
||||
to, err := s.parseFilterTime(filter.PaidAtTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if to != nil {
|
||||
q = q.Where(tbl.PaidAt.Lte(*to))
|
||||
}
|
||||
}
|
||||
|
||||
orderApplied := false
|
||||
if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" {
|
||||
switch strings.TrimSpace(*filter.Desc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID.Desc())
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
case "paid_at":
|
||||
q = q.Order(tbl.PaidAt.Desc())
|
||||
case "amount_paid":
|
||||
q = q.Order(tbl.AmountPaid.Desc())
|
||||
case "status":
|
||||
q = q.Order(tbl.Status.Desc())
|
||||
}
|
||||
orderApplied = true
|
||||
} else if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" {
|
||||
switch strings.TrimSpace(*filter.Asc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID)
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt)
|
||||
case "paid_at":
|
||||
q = q.Order(tbl.PaidAt)
|
||||
case "amount_paid":
|
||||
q = q.Order(tbl.AmountPaid)
|
||||
case "status":
|
||||
q = q.Order(tbl.Status)
|
||||
}
|
||||
orderApplied = true
|
||||
}
|
||||
if !orderApplied {
|
||||
q = q.Order(tbl.ID.Desc())
|
||||
}
|
||||
|
||||
filter.Pagination.Format()
|
||||
total, err := q.Count()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Order(tbl.ID.Desc()).Find()
|
||||
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
@@ -2226,6 +2345,436 @@ func (s *super) ListWithdrawals(ctx context.Context, filter *super_dto.SuperOrde
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) ListCoupons(ctx context.Context, filter *super_dto.SuperCouponListFilter) (*requests.Pager, error) {
|
||||
if filter == nil {
|
||||
filter = &super_dto.SuperCouponListFilter{}
|
||||
}
|
||||
|
||||
tbl, q := models.CouponQuery.QueryContext(ctx)
|
||||
if filter.ID != nil && *filter.ID > 0 {
|
||||
q = q.Where(tbl.ID.Eq(*filter.ID))
|
||||
}
|
||||
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||||
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
|
||||
}
|
||||
|
||||
tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if tenantFilter {
|
||||
if len(tenantIDs) == 0 {
|
||||
q = q.Where(tbl.ID.Eq(-1))
|
||||
} else {
|
||||
q = q.Where(tbl.TenantID.In(tenantIDs...))
|
||||
}
|
||||
}
|
||||
|
||||
if filter.Type != nil && strings.TrimSpace(*filter.Type) != "" {
|
||||
parsed, err := consts.ParseCouponType(strings.TrimSpace(*filter.Type))
|
||||
if err != nil {
|
||||
return nil, errorx.ErrInvalidParameter.WithCause(err).WithMsg("优惠券类型无效")
|
||||
}
|
||||
q = q.Where(tbl.Type.Eq(parsed))
|
||||
}
|
||||
if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" {
|
||||
keyword := strings.TrimSpace(*filter.Keyword)
|
||||
q = q.Where(field.Or(
|
||||
tbl.Title.Like("%"+keyword+"%"),
|
||||
tbl.Description.Like("%"+keyword+"%"),
|
||||
))
|
||||
}
|
||||
if filter.Status != nil && strings.TrimSpace(*filter.Status) != "" {
|
||||
status := strings.ToLower(strings.TrimSpace(*filter.Status))
|
||||
now := time.Now()
|
||||
switch status {
|
||||
case "active":
|
||||
q = q.Where(field.Or(tbl.StartAt.Lte(now), tbl.StartAt.IsNull()))
|
||||
q = q.Where(field.Or(tbl.EndAt.Gte(now), tbl.EndAt.IsNull()))
|
||||
case "expired":
|
||||
q = q.Where(tbl.EndAt.IsNotNull(), tbl.EndAt.Lt(now))
|
||||
case "upcoming":
|
||||
q = q.Where(tbl.StartAt.IsNotNull(), tbl.StartAt.Gt(now))
|
||||
default:
|
||||
return nil, errorx.ErrInvalidParameter.WithMsg("状态参数无效")
|
||||
}
|
||||
}
|
||||
|
||||
if filter.CreatedAtFrom != nil {
|
||||
from, err := s.parseFilterTime(filter.CreatedAtFrom)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if from != nil {
|
||||
q = q.Where(tbl.CreatedAt.Gte(*from))
|
||||
}
|
||||
}
|
||||
if filter.CreatedAtTo != nil {
|
||||
to, err := s.parseFilterTime(filter.CreatedAtTo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if to != nil {
|
||||
q = q.Where(tbl.CreatedAt.Lte(*to))
|
||||
}
|
||||
}
|
||||
|
||||
orderApplied := false
|
||||
if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" {
|
||||
switch strings.TrimSpace(*filter.Desc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID.Desc())
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
case "start_at":
|
||||
q = q.Order(tbl.StartAt.Desc())
|
||||
case "end_at":
|
||||
q = q.Order(tbl.EndAt.Desc())
|
||||
}
|
||||
orderApplied = true
|
||||
} else if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" {
|
||||
switch strings.TrimSpace(*filter.Asc) {
|
||||
case "id":
|
||||
q = q.Order(tbl.ID)
|
||||
case "created_at":
|
||||
q = q.Order(tbl.CreatedAt)
|
||||
case "start_at":
|
||||
q = q.Order(tbl.StartAt)
|
||||
case "end_at":
|
||||
q = q.Order(tbl.EndAt)
|
||||
}
|
||||
orderApplied = true
|
||||
}
|
||||
if !orderApplied {
|
||||
q = q.Order(tbl.CreatedAt.Desc())
|
||||
}
|
||||
|
||||
filter.Pagination.Format()
|
||||
total, err := q.Count()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
items, err := s.buildSuperCouponItems(ctx, list)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &requests.Pager{
|
||||
Pagination: filter.Pagination,
|
||||
Total: total,
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) ReportOverview(ctx context.Context, filter *super_dto.SuperReportOverviewFilter) (*v1_dto.ReportOverviewResponse, error) {
|
||||
// 统一统计时间范围与粒度。
|
||||
rg, err := s.normalizeReportRange(filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tenantID := int64(0)
|
||||
if filter != nil && filter.TenantID != nil {
|
||||
tenantID = *filter.TenantID
|
||||
}
|
||||
|
||||
// 统计累计曝光(全量累计值,暂无按时间拆分的曝光记录)。
|
||||
var totalViews int64
|
||||
contentQuery := models.ContentQuery.WithContext(ctx).
|
||||
UnderlyingDB().
|
||||
Model(&models.Content{}).
|
||||
Select("coalesce(sum(views), 0)")
|
||||
if tenantID > 0 {
|
||||
contentQuery = contentQuery.Where("tenant_id = ?", tenantID)
|
||||
}
|
||||
if err := contentQuery.Scan(&totalViews).Error; err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
|
||||
// 订单仅统计内容购买类型,并按状态划分已支付/已退款。
|
||||
paidCount, paidAmount, err := s.reportOrderAggregate(ctx, tenantID, consts.OrderStatusPaid, "paid_at", rg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refundCount, refundAmount, err := s.reportOrderAggregate(ctx, tenantID, 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.reportOrderSeries(ctx, tenantID, consts.OrderStatusPaid, "paid_at", rg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refundSeries, err := s.reportOrderSeries(ctx, tenantID, consts.OrderStatusRefunded, "updated_at", rg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]v1_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, v1_dto.ReportOverviewItem{
|
||||
Date: key,
|
||||
PaidOrders: paidItem.Count,
|
||||
PaidAmount: float64(paidItem.Amount) / 100.0,
|
||||
RefundOrders: refundItem.Count,
|
||||
RefundAmount: float64(refundItem.Amount) / 100.0,
|
||||
})
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
Items: items,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) ExportReport(ctx context.Context, form *super_dto.SuperReportExportForm) (*v1_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, &super_dto.SuperReportOverviewFilter{
|
||||
TenantID: form.TenantID,
|
||||
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 := "report_overview_" + time.Now().Format("20060102_150405") + ".csv"
|
||||
return &v1_dto.ReportExportResponse{
|
||||
Filename: filename,
|
||||
MimeType: "text/csv",
|
||||
Content: builder.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *super) reportOrderAggregate(
|
||||
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"`
|
||||
}
|
||||
|
||||
query := models.OrderQuery.WithContext(ctx).
|
||||
UnderlyingDB().
|
||||
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)
|
||||
if tenantID > 0 {
|
||||
query = query.Where("tenant_id = ?", tenantID)
|
||||
}
|
||||
|
||||
if err := query.Scan(&total).Error; err != nil {
|
||||
return 0, 0, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
return total.Count, total.Amount, nil
|
||||
}
|
||||
|
||||
func (s *super) reportOrderSeries(
|
||||
ctx context.Context,
|
||||
tenantID int64,
|
||||
status consts.OrderStatus,
|
||||
timeField string,
|
||||
rg reportRange,
|
||||
) (map[string]reportAggRow, error) {
|
||||
rows := make([]reportAggRow, 0)
|
||||
query := 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("type = ? AND status = ? AND "+timeField+" >= ? AND "+timeField+" < ?",
|
||||
consts.OrderTypeContentPurchase, status, 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)
|
||||
}
|
||||
|
||||
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 *super) normalizeReportRange(filter *super_dto.SuperReportOverviewFilter) (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 (s *super) buildSuperCouponItems(ctx context.Context, list []*models.Coupon) ([]super_dto.SuperCouponItem, error) {
|
||||
if len(list) == 0 {
|
||||
return []super_dto.SuperCouponItem{}, nil
|
||||
}
|
||||
|
||||
tenantIDs := make([]int64, 0, len(list))
|
||||
seen := make(map[int64]struct{}, len(list))
|
||||
for _, c := range list {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[c.TenantID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[c.TenantID] = struct{}{}
|
||||
tenantIDs = append(tenantIDs, c.TenantID)
|
||||
}
|
||||
|
||||
tenantMap := make(map[int64]*models.Tenant, len(tenantIDs))
|
||||
if len(tenantIDs) > 0 {
|
||||
tbl, q := models.TenantQuery.QueryContext(ctx)
|
||||
tenants, err := q.Where(tbl.ID.In(tenantIDs...)).Find()
|
||||
if err != nil {
|
||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||
}
|
||||
for _, t := range tenants {
|
||||
tenantMap[t.ID] = t
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]super_dto.SuperCouponItem, 0, len(list))
|
||||
for _, c := range list {
|
||||
if c == nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, s.toSuperCouponItem(c, tenantMap[c.TenantID]))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *super) toSuperCouponItem(c *models.Coupon, tenant *models.Tenant) super_dto.SuperCouponItem {
|
||||
status, statusDescription := s.resolveCouponStatus(c)
|
||||
item := super_dto.SuperCouponItem{
|
||||
ID: c.ID,
|
||||
TenantID: c.TenantID,
|
||||
Title: c.Title,
|
||||
Description: c.Description,
|
||||
Type: c.Type,
|
||||
TypeDescription: c.Type.Description(),
|
||||
Value: c.Value,
|
||||
MinOrderAmount: c.MinOrderAmount,
|
||||
MaxDiscount: c.MaxDiscount,
|
||||
TotalQuantity: c.TotalQuantity,
|
||||
UsedQuantity: c.UsedQuantity,
|
||||
Status: status,
|
||||
StatusDescription: statusDescription,
|
||||
CreatedAt: s.formatTime(c.CreatedAt),
|
||||
UpdatedAt: s.formatTime(c.UpdatedAt),
|
||||
}
|
||||
if tenant != nil {
|
||||
item.TenantCode = tenant.Code
|
||||
item.TenantName = tenant.Name
|
||||
}
|
||||
if !c.StartAt.IsZero() {
|
||||
item.StartAt = s.formatTime(c.StartAt)
|
||||
}
|
||||
if !c.EndAt.IsZero() {
|
||||
item.EndAt = s.formatTime(c.EndAt)
|
||||
}
|
||||
return item
|
||||
}
|
||||
|
||||
func (s *super) resolveCouponStatus(c *models.Coupon) (string, string) {
|
||||
now := time.Now()
|
||||
if !c.EndAt.IsZero() && c.EndAt.Before(now) {
|
||||
return "expired", "已过期"
|
||||
}
|
||||
if !c.StartAt.IsZero() && c.StartAt.After(now) {
|
||||
return "upcoming", "未开始"
|
||||
}
|
||||
return "active", "生效中"
|
||||
}
|
||||
|
||||
func (s *super) ApproveWithdrawal(ctx context.Context, operatorID, id int64) error {
|
||||
if operatorID == 0 {
|
||||
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
||||
|
||||
Reference in New Issue
Block a user