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 {
|
}); err != nil {
|
||||||
return err
|
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) {
|
if err := container.Container.Provide(func() (*orders, error) {
|
||||||
obj := &orders{}
|
obj := &orders{}
|
||||||
|
|
||||||
@@ -24,19 +38,34 @@ func Provide(opts ...opt.Option) error {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
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(
|
if err := container.Container.Provide(func(
|
||||||
contents *contents,
|
contents *contents,
|
||||||
|
coupons *coupons,
|
||||||
|
creators *creators,
|
||||||
middlewares *middlewares.Middlewares,
|
middlewares *middlewares.Middlewares,
|
||||||
orders *orders,
|
orders *orders,
|
||||||
|
reports *reports,
|
||||||
tenants *tenants,
|
tenants *tenants,
|
||||||
users *users,
|
users *users,
|
||||||
|
withdrawals *withdrawals,
|
||||||
) (contracts.HttpRoute, error) {
|
) (contracts.HttpRoute, error) {
|
||||||
obj := &Routes{
|
obj := &Routes{
|
||||||
contents: contents,
|
contents: contents,
|
||||||
|
coupons: coupons,
|
||||||
|
creators: creators,
|
||||||
middlewares: middlewares,
|
middlewares: middlewares,
|
||||||
orders: orders,
|
orders: orders,
|
||||||
|
reports: reports,
|
||||||
tenants: tenants,
|
tenants: tenants,
|
||||||
users: users,
|
users: users,
|
||||||
|
withdrawals: withdrawals,
|
||||||
}
|
}
|
||||||
if err := obj.Prepare(); err != nil {
|
if err := obj.Prepare(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -60,5 +89,12 @@ func Provide(opts ...opt.Option) error {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := container.Container.Provide(func() (*withdrawals, error) {
|
||||||
|
obj := &withdrawals{}
|
||||||
|
|
||||||
|
return obj, nil
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
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)
|
||||||
|
}
|
||||||
@@ -25,9 +25,13 @@ type Routes struct {
|
|||||||
middlewares *middlewares.Middlewares
|
middlewares *middlewares.Middlewares
|
||||||
// Controller instances
|
// Controller instances
|
||||||
contents *contents
|
contents *contents
|
||||||
|
coupons *coupons
|
||||||
|
creators *creators
|
||||||
orders *orders
|
orders *orders
|
||||||
|
reports *reports
|
||||||
tenants *tenants
|
tenants *tenants
|
||||||
users *users
|
users *users
|
||||||
|
withdrawals *withdrawals
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare initializes the routes provider with logging configuration.
|
// Prepare initializes the routes provider with logging configuration.
|
||||||
@@ -71,6 +75,18 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
PathParam[int64]("id"),
|
PathParam[int64]("id"),
|
||||||
Body[dto.SuperContentReviewForm]("form"),
|
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
|
// Register routes for controller: orders
|
||||||
r.log.Debugf("Registering route: Get /super/v1/orders -> orders.List")
|
r.log.Debugf("Registering route: Get /super/v1/orders -> orders.List")
|
||||||
router.Get("/super/v1/orders"[len(r.Path()):], DataFunc1(
|
router.Get("/super/v1/orders"[len(r.Path()):], DataFunc1(
|
||||||
@@ -92,6 +108,17 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
PathParam[int64]("id"),
|
PathParam[int64]("id"),
|
||||||
Body[dto.SuperOrderRefundForm]("form"),
|
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
|
// Register routes for controller: tenants
|
||||||
r.log.Debugf("Registering route: Get /super/v1/tenants -> tenants.List")
|
r.log.Debugf("Registering route: Get /super/v1/tenants -> tenants.List")
|
||||||
router.Get("/super/v1/tenants"[len(r.Path()):], DataFunc1(
|
router.Get("/super/v1/tenants"[len(r.Path()):], DataFunc1(
|
||||||
@@ -172,6 +199,25 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
PathParam[int64]("id"),
|
PathParam[int64]("id"),
|
||||||
Body[dto.UserStatusUpdateForm]("form"),
|
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")
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"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) {
|
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)
|
tbl, q := models.OrderQuery.QueryContext(ctx)
|
||||||
q = q.Where(tbl.Type.Eq(consts.OrderTypeWithdrawal))
|
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()
|
filter.Pagination.Format()
|
||||||
total, err := q.Count()
|
total, err := q.Count()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
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 {
|
if err != nil {
|
||||||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
}
|
}
|
||||||
@@ -2226,6 +2345,436 @@ func (s *super) ListWithdrawals(ctx context.Context, filter *super_dto.SuperOrde
|
|||||||
}, nil
|
}, 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 {
|
func (s *super) ApproveWithdrawal(ctx context.Context, operatorID, id int64) error {
|
||||||
if operatorID == 0 {
|
if operatorID == 0 {
|
||||||
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
||||||
|
|||||||
@@ -175,6 +175,110 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/super/v1/coupons": {
|
||||||
|
"get": {
|
||||||
|
"description": "List coupon templates across tenants",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Coupon"
|
||||||
|
],
|
||||||
|
"summary": "List coupons",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page size",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/requests.Pager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.SuperCouponItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/super/v1/creators": {
|
||||||
|
"get": {
|
||||||
|
"description": "List creator tenants (channels) across the platform",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Creator"
|
||||||
|
],
|
||||||
|
"summary": "List creators",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page size",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/requests.Pager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.TenantItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/super/v1/orders": {
|
"/super/v1/orders": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List orders",
|
"description": "List orders",
|
||||||
@@ -325,6 +429,63 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/super/v1/reports/export": {
|
||||||
|
"post": {
|
||||||
|
"description": "Export platform report data",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Report"
|
||||||
|
],
|
||||||
|
"summary": "Export report",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Export form",
|
||||||
|
"name": "form",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.SuperReportExportForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ReportExportResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/super/v1/reports/overview": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get platform report overview",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Report"
|
||||||
|
],
|
||||||
|
"summary": "Report overview",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ReportOverviewResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/super/v1/tenants": {
|
"/super/v1/tenants": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List tenants",
|
"description": "List tenants",
|
||||||
@@ -1065,6 +1226,133 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/super/v1/withdrawals": {
|
||||||
|
"get": {
|
||||||
|
"description": "List withdrawal orders across tenants",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Finance"
|
||||||
|
],
|
||||||
|
"summary": "List withdrawals",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page size",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/requests.Pager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.SuperOrderItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/super/v1/withdrawals/{id}/approve": {
|
||||||
|
"post": {
|
||||||
|
"description": "Approve a withdrawal request",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Finance"
|
||||||
|
],
|
||||||
|
"summary": "Approve withdrawal",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Withdrawal order ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Approved",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/super/v1/withdrawals/{id}/reject": {
|
||||||
|
"post": {
|
||||||
|
"description": "Reject a withdrawal request",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Finance"
|
||||||
|
],
|
||||||
|
"summary": "Reject withdrawal",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Withdrawal order ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Reject form",
|
||||||
|
"name": "form",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.SuperWithdrawalRejectForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Rejected",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/t/{tenantCode}/v1/auth/login": {
|
"/t/{tenantCode}/v1/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Login or register user using phone number and OTP",
|
"description": "Login or register user using phone number and OTP",
|
||||||
@@ -3822,6 +4110,17 @@ const docTemplate = `{
|
|||||||
"ContentStatusBlocked"
|
"ContentStatusBlocked"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"consts.CouponType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"fix_amount",
|
||||||
|
"discount"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"CouponTypeFixAmount",
|
||||||
|
"CouponTypeDiscount"
|
||||||
|
]
|
||||||
|
},
|
||||||
"consts.Currency": {
|
"consts.Currency": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -5240,6 +5539,91 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperCouponItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"description": "CreatedAt 创建时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"description": "Description 优惠券描述。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"end_at": {
|
||||||
|
"description": "EndAt 结束时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "ID 优惠券ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"max_discount": {
|
||||||
|
"description": "MaxDiscount 最大折扣金额(折扣券)。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"min_order_amount": {
|
||||||
|
"description": "MinOrderAmount 最低订单金额门槛。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"start_at": {
|
||||||
|
"description": "StartAt 生效时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"description": "Status 状态(active/expired/upcoming)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status_description": {
|
||||||
|
"description": "StatusDescription 状态描述(用于展示)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tenant_code": {
|
||||||
|
"description": "TenantCode 租户编码。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"description": "TenantID 租户ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"tenant_name": {
|
||||||
|
"description": "TenantName 租户名称。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"description": "Title 优惠券标题。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"total_quantity": {
|
||||||
|
"description": "TotalQuantity 总发行数量(0 表示不限量)。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"description": "Type 优惠券类型。",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.CouponType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type_description": {
|
||||||
|
"description": "TypeDescription 类型描述(用于展示)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"description": "UpdatedAt 更新时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"used_quantity": {
|
||||||
|
"description": "UsedQuantity 已使用数量。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"description": "Value 优惠券面额/折扣值。",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperOrderDetail": {
|
"dto.SuperOrderDetail": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -5397,6 +5781,31 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperReportExportForm": {
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"description": "TenantID 租户ID(不传代表全平台)。",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperTenantContentStatusUpdateForm": {
|
"dto.SuperTenantContentStatusUpdateForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -5482,6 +5891,18 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperWithdrawalRejectForm": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"reason"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"reason": {
|
||||||
|
"description": "Reason 驳回原因。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.TenantAdminUserLite": {
|
"dto.TenantAdminUserLite": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -169,6 +169,110 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/super/v1/coupons": {
|
||||||
|
"get": {
|
||||||
|
"description": "List coupon templates across tenants",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Coupon"
|
||||||
|
],
|
||||||
|
"summary": "List coupons",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page size",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/requests.Pager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.SuperCouponItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/super/v1/creators": {
|
||||||
|
"get": {
|
||||||
|
"description": "List creator tenants (channels) across the platform",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Creator"
|
||||||
|
],
|
||||||
|
"summary": "List creators",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page size",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/requests.Pager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.TenantItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/super/v1/orders": {
|
"/super/v1/orders": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List orders",
|
"description": "List orders",
|
||||||
@@ -319,6 +423,63 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/super/v1/reports/export": {
|
||||||
|
"post": {
|
||||||
|
"description": "Export platform report data",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Report"
|
||||||
|
],
|
||||||
|
"summary": "Export report",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"description": "Export form",
|
||||||
|
"name": "form",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.SuperReportExportForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ReportExportResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/super/v1/reports/overview": {
|
||||||
|
"get": {
|
||||||
|
"description": "Get platform report overview",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Report"
|
||||||
|
],
|
||||||
|
"summary": "Report overview",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.ReportOverviewResponse"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/super/v1/tenants": {
|
"/super/v1/tenants": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List tenants",
|
"description": "List tenants",
|
||||||
@@ -1059,6 +1220,133 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/super/v1/withdrawals": {
|
||||||
|
"get": {
|
||||||
|
"description": "List withdrawal orders across tenants",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Finance"
|
||||||
|
],
|
||||||
|
"summary": "List withdrawals",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page number",
|
||||||
|
"name": "page",
|
||||||
|
"in": "query"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"description": "Page size",
|
||||||
|
"name": "limit",
|
||||||
|
"in": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "OK",
|
||||||
|
"schema": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/requests.Pager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"items": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/definitions/dto.SuperOrderItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/super/v1/withdrawals/{id}/approve": {
|
||||||
|
"post": {
|
||||||
|
"description": "Approve a withdrawal request",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Finance"
|
||||||
|
],
|
||||||
|
"summary": "Approve withdrawal",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Withdrawal order ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Approved",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"/super/v1/withdrawals/{id}/reject": {
|
||||||
|
"post": {
|
||||||
|
"description": "Reject a withdrawal request",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Finance"
|
||||||
|
],
|
||||||
|
"summary": "Reject withdrawal",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"type": "integer",
|
||||||
|
"format": "int64",
|
||||||
|
"description": "Withdrawal order ID",
|
||||||
|
"name": "id",
|
||||||
|
"in": "path",
|
||||||
|
"required": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Reject form",
|
||||||
|
"name": "form",
|
||||||
|
"in": "body",
|
||||||
|
"required": true,
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/definitions/dto.SuperWithdrawalRejectForm"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "Rejected",
|
||||||
|
"schema": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/t/{tenantCode}/v1/auth/login": {
|
"/t/{tenantCode}/v1/auth/login": {
|
||||||
"post": {
|
"post": {
|
||||||
"description": "Login or register user using phone number and OTP",
|
"description": "Login or register user using phone number and OTP",
|
||||||
@@ -3816,6 +4104,17 @@
|
|||||||
"ContentStatusBlocked"
|
"ContentStatusBlocked"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"consts.CouponType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"fix_amount",
|
||||||
|
"discount"
|
||||||
|
],
|
||||||
|
"x-enum-varnames": [
|
||||||
|
"CouponTypeFixAmount",
|
||||||
|
"CouponTypeDiscount"
|
||||||
|
]
|
||||||
|
},
|
||||||
"consts.Currency": {
|
"consts.Currency": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [
|
"enum": [
|
||||||
@@ -5234,6 +5533,91 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperCouponItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"created_at": {
|
||||||
|
"description": "CreatedAt 创建时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"description": {
|
||||||
|
"description": "Description 优惠券描述。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"end_at": {
|
||||||
|
"description": "EndAt 结束时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "ID 优惠券ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"max_discount": {
|
||||||
|
"description": "MaxDiscount 最大折扣金额(折扣券)。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"min_order_amount": {
|
||||||
|
"description": "MinOrderAmount 最低订单金额门槛。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"start_at": {
|
||||||
|
"description": "StartAt 生效时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"description": "Status 状态(active/expired/upcoming)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status_description": {
|
||||||
|
"description": "StatusDescription 状态描述(用于展示)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tenant_code": {
|
||||||
|
"description": "TenantCode 租户编码。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"description": "TenantID 租户ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"tenant_name": {
|
||||||
|
"description": "TenantName 租户名称。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"description": "Title 优惠券标题。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"total_quantity": {
|
||||||
|
"description": "TotalQuantity 总发行数量(0 表示不限量)。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"description": "Type 优惠券类型。",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.CouponType"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type_description": {
|
||||||
|
"description": "TypeDescription 类型描述(用于展示)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"updated_at": {
|
||||||
|
"description": "UpdatedAt 更新时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"used_quantity": {
|
||||||
|
"description": "UsedQuantity 已使用数量。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"description": "Value 优惠券面额/折扣值。",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperOrderDetail": {
|
"dto.SuperOrderDetail": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -5391,6 +5775,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperReportExportForm": {
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"tenant_id": {
|
||||||
|
"description": "TenantID 租户ID(不传代表全平台)。",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperTenantContentStatusUpdateForm": {
|
"dto.SuperTenantContentStatusUpdateForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
@@ -5476,6 +5885,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperWithdrawalRejectForm": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"reason"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"reason": {
|
||||||
|
"description": "Reason 驳回原因。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.TenantAdminUserLite": {
|
"dto.TenantAdminUserLite": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|||||||
@@ -14,6 +14,14 @@ definitions:
|
|||||||
- ContentStatusPublished
|
- ContentStatusPublished
|
||||||
- ContentStatusUnpublished
|
- ContentStatusUnpublished
|
||||||
- ContentStatusBlocked
|
- ContentStatusBlocked
|
||||||
|
consts.CouponType:
|
||||||
|
enum:
|
||||||
|
- fix_amount
|
||||||
|
- discount
|
||||||
|
type: string
|
||||||
|
x-enum-varnames:
|
||||||
|
- CouponTypeFixAmount
|
||||||
|
- CouponTypeDiscount
|
||||||
consts.Currency:
|
consts.Currency:
|
||||||
enum:
|
enum:
|
||||||
- CNY
|
- CNY
|
||||||
@@ -1021,6 +1029,67 @@ definitions:
|
|||||||
description: Name 租户名称。
|
description: Name 租户名称。
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.SuperCouponItem:
|
||||||
|
properties:
|
||||||
|
created_at:
|
||||||
|
description: CreatedAt 创建时间(RFC3339)。
|
||||||
|
type: string
|
||||||
|
description:
|
||||||
|
description: Description 优惠券描述。
|
||||||
|
type: string
|
||||||
|
end_at:
|
||||||
|
description: EndAt 结束时间(RFC3339)。
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
description: ID 优惠券ID。
|
||||||
|
type: integer
|
||||||
|
max_discount:
|
||||||
|
description: MaxDiscount 最大折扣金额(折扣券)。
|
||||||
|
type: integer
|
||||||
|
min_order_amount:
|
||||||
|
description: MinOrderAmount 最低订单金额门槛。
|
||||||
|
type: integer
|
||||||
|
start_at:
|
||||||
|
description: StartAt 生效时间(RFC3339)。
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: Status 状态(active/expired/upcoming)。
|
||||||
|
type: string
|
||||||
|
status_description:
|
||||||
|
description: StatusDescription 状态描述(用于展示)。
|
||||||
|
type: string
|
||||||
|
tenant_code:
|
||||||
|
description: TenantCode 租户编码。
|
||||||
|
type: string
|
||||||
|
tenant_id:
|
||||||
|
description: TenantID 租户ID。
|
||||||
|
type: integer
|
||||||
|
tenant_name:
|
||||||
|
description: TenantName 租户名称。
|
||||||
|
type: string
|
||||||
|
title:
|
||||||
|
description: Title 优惠券标题。
|
||||||
|
type: string
|
||||||
|
total_quantity:
|
||||||
|
description: TotalQuantity 总发行数量(0 表示不限量)。
|
||||||
|
type: integer
|
||||||
|
type:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/consts.CouponType'
|
||||||
|
description: Type 优惠券类型。
|
||||||
|
type_description:
|
||||||
|
description: TypeDescription 类型描述(用于展示)。
|
||||||
|
type: string
|
||||||
|
updated_at:
|
||||||
|
description: UpdatedAt 更新时间(RFC3339)。
|
||||||
|
type: string
|
||||||
|
used_quantity:
|
||||||
|
description: UsedQuantity 已使用数量。
|
||||||
|
type: integer
|
||||||
|
value:
|
||||||
|
description: Value 优惠券面额/折扣值。
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
dto.SuperOrderDetail:
|
dto.SuperOrderDetail:
|
||||||
properties:
|
properties:
|
||||||
buyer:
|
buyer:
|
||||||
@@ -1119,6 +1188,24 @@ definitions:
|
|||||||
description: Reason 退款原因说明。
|
description: Reason 退款原因说明。
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.SuperReportExportForm:
|
||||||
|
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
|
||||||
|
tenant_id:
|
||||||
|
description: TenantID 租户ID(不传代表全平台)。
|
||||||
|
type: integer
|
||||||
|
type: object
|
||||||
dto.SuperTenantContentStatusUpdateForm:
|
dto.SuperTenantContentStatusUpdateForm:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
@@ -1172,6 +1259,14 @@ definitions:
|
|||||||
description: VerifiedAt 实名认证时间(RFC3339)。
|
description: VerifiedAt 实名认证时间(RFC3339)。
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
dto.SuperWithdrawalRejectForm:
|
||||||
|
properties:
|
||||||
|
reason:
|
||||||
|
description: Reason 驳回原因。
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- reason
|
||||||
|
type: object
|
||||||
dto.TenantAdminUserLite:
|
dto.TenantAdminUserLite:
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
@@ -2043,6 +2138,68 @@ paths:
|
|||||||
summary: Review content
|
summary: Review content
|
||||||
tags:
|
tags:
|
||||||
- Content
|
- Content
|
||||||
|
/super/v1/coupons:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: List coupon templates across tenants
|
||||||
|
parameters:
|
||||||
|
- description: Page number
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
type: integer
|
||||||
|
- description: Page size
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/requests.Pager'
|
||||||
|
- properties:
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/dto.SuperCouponItem'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
summary: List coupons
|
||||||
|
tags:
|
||||||
|
- Coupon
|
||||||
|
/super/v1/creators:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: List creator tenants (channels) across the platform
|
||||||
|
parameters:
|
||||||
|
- description: Page number
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
type: integer
|
||||||
|
- description: Page size
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/requests.Pager'
|
||||||
|
- properties:
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/dto.TenantItem'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
summary: List creators
|
||||||
|
tags:
|
||||||
|
- Creator
|
||||||
/super/v1/orders:
|
/super/v1/orders:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -2139,6 +2296,43 @@ paths:
|
|||||||
summary: Order statistics
|
summary: Order statistics
|
||||||
tags:
|
tags:
|
||||||
- Order
|
- Order
|
||||||
|
/super/v1/reports/export:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Export platform report data
|
||||||
|
parameters:
|
||||||
|
- description: Export form
|
||||||
|
in: body
|
||||||
|
name: form
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.SuperReportExportForm'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ReportExportResponse'
|
||||||
|
summary: Export report
|
||||||
|
tags:
|
||||||
|
- Report
|
||||||
|
/super/v1/reports/overview:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Get platform report overview
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.ReportOverviewResponse'
|
||||||
|
summary: Report overview
|
||||||
|
tags:
|
||||||
|
- Report
|
||||||
/super/v1/tenants:
|
/super/v1/tenants:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
@@ -2612,6 +2806,87 @@ paths:
|
|||||||
summary: User statuses
|
summary: User statuses
|
||||||
tags:
|
tags:
|
||||||
- User
|
- User
|
||||||
|
/super/v1/withdrawals:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: List withdrawal orders across tenants
|
||||||
|
parameters:
|
||||||
|
- description: Page number
|
||||||
|
in: query
|
||||||
|
name: page
|
||||||
|
type: integer
|
||||||
|
- description: Page size
|
||||||
|
in: query
|
||||||
|
name: limit
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: OK
|
||||||
|
schema:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/requests.Pager'
|
||||||
|
- properties:
|
||||||
|
items:
|
||||||
|
items:
|
||||||
|
$ref: '#/definitions/dto.SuperOrderItem'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
summary: List withdrawals
|
||||||
|
tags:
|
||||||
|
- Finance
|
||||||
|
/super/v1/withdrawals/{id}/approve:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Approve a withdrawal request
|
||||||
|
parameters:
|
||||||
|
- description: Withdrawal order ID
|
||||||
|
format: int64
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Approved
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Approve withdrawal
|
||||||
|
tags:
|
||||||
|
- Finance
|
||||||
|
/super/v1/withdrawals/{id}/reject:
|
||||||
|
post:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: Reject a withdrawal request
|
||||||
|
parameters:
|
||||||
|
- description: Withdrawal order ID
|
||||||
|
format: int64
|
||||||
|
in: path
|
||||||
|
name: id
|
||||||
|
required: true
|
||||||
|
type: integer
|
||||||
|
- description: Reject form
|
||||||
|
in: body
|
||||||
|
name: form
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
$ref: '#/definitions/dto.SuperWithdrawalRejectForm'
|
||||||
|
produces:
|
||||||
|
- application/json
|
||||||
|
responses:
|
||||||
|
"200":
|
||||||
|
description: Rejected
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
summary: Reject withdrawal
|
||||||
|
tags:
|
||||||
|
- Finance
|
||||||
/t/{tenantCode}/v1/auth/login:
|
/t/{tenantCode}/v1/auth/login:
|
||||||
post:
|
post:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
44
frontend/superadmin/src/service/CouponService.js
Normal file
44
frontend/superadmin/src/service/CouponService.js
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { requestJson } from './apiClient';
|
||||||
|
|
||||||
|
function normalizeItems(items) {
|
||||||
|
if (Array.isArray(items)) return items;
|
||||||
|
if (items && typeof items === 'object') return [items];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CouponService = {
|
||||||
|
async listCoupons({ page, limit, id, tenant_id, tenant_code, tenant_name, keyword, type, status, created_at_from, created_at_to, sortField, sortOrder } = {}) {
|
||||||
|
const iso = (d) => {
|
||||||
|
if (!d) return undefined;
|
||||||
|
const date = d instanceof Date ? d : new Date(d);
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined;
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
tenant_code,
|
||||||
|
tenant_name,
|
||||||
|
keyword,
|
||||||
|
type,
|
||||||
|
status,
|
||||||
|
created_at_from: iso(created_at_from),
|
||||||
|
created_at_to: iso(created_at_to)
|
||||||
|
};
|
||||||
|
if (sortField && sortOrder) {
|
||||||
|
if (sortOrder === 1) query.asc = sortField;
|
||||||
|
if (sortOrder === -1) query.desc = sortField;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await requestJson('/super/v1/coupons', { query });
|
||||||
|
return {
|
||||||
|
page: data?.page ?? page ?? 1,
|
||||||
|
limit: data?.limit ?? limit ?? 10,
|
||||||
|
total: data?.total ?? 0,
|
||||||
|
items: normalizeItems(data?.items)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
42
frontend/superadmin/src/service/CreatorService.js
Normal file
42
frontend/superadmin/src/service/CreatorService.js
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { requestJson } from './apiClient';
|
||||||
|
|
||||||
|
function normalizeItems(items) {
|
||||||
|
if (Array.isArray(items)) return items;
|
||||||
|
if (items && typeof items === 'object') return [items];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CreatorService = {
|
||||||
|
async listCreators({ page, limit, id, user_id, name, code, status, created_at_from, created_at_to, sortField, sortOrder } = {}) {
|
||||||
|
const iso = (d) => {
|
||||||
|
if (!d) return undefined;
|
||||||
|
const date = d instanceof Date ? d : new Date(d);
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined;
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
id,
|
||||||
|
user_id,
|
||||||
|
name,
|
||||||
|
code,
|
||||||
|
status,
|
||||||
|
created_at_from: iso(created_at_from),
|
||||||
|
created_at_to: iso(created_at_to)
|
||||||
|
};
|
||||||
|
if (sortField && sortOrder) {
|
||||||
|
if (sortOrder === 1) query.asc = sortField;
|
||||||
|
if (sortOrder === -1) query.desc = sortField;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await requestJson('/super/v1/creators', { query });
|
||||||
|
return {
|
||||||
|
page: data?.page ?? page ?? 1,
|
||||||
|
limit: data?.limit ?? limit ?? 10,
|
||||||
|
total: data?.total ?? 0,
|
||||||
|
items: normalizeItems(data?.items)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
59
frontend/superadmin/src/service/FinanceService.js
Normal file
59
frontend/superadmin/src/service/FinanceService.js
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
import { requestJson } from './apiClient';
|
||||||
|
|
||||||
|
function normalizeItems(items) {
|
||||||
|
if (Array.isArray(items)) return items;
|
||||||
|
if (items && typeof items === 'object') return [items];
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const FinanceService = {
|
||||||
|
async listWithdrawals({ page, limit, id, tenant_id, tenant_code, tenant_name, user_id, username, status, created_at_from, created_at_to, paid_at_from, paid_at_to, amount_paid_min, amount_paid_max, sortField, sortOrder } = {}) {
|
||||||
|
const iso = (d) => {
|
||||||
|
if (!d) return undefined;
|
||||||
|
const date = d instanceof Date ? d : new Date(d);
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined;
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
page,
|
||||||
|
limit,
|
||||||
|
id,
|
||||||
|
tenant_id,
|
||||||
|
tenant_code,
|
||||||
|
tenant_name,
|
||||||
|
user_id,
|
||||||
|
username,
|
||||||
|
status,
|
||||||
|
created_at_from: iso(created_at_from),
|
||||||
|
created_at_to: iso(created_at_to),
|
||||||
|
paid_at_from: iso(paid_at_from),
|
||||||
|
paid_at_to: iso(paid_at_to),
|
||||||
|
amount_paid_min,
|
||||||
|
amount_paid_max
|
||||||
|
};
|
||||||
|
if (sortField && sortOrder) {
|
||||||
|
if (sortOrder === 1) query.asc = sortField;
|
||||||
|
if (sortOrder === -1) query.desc = sortField;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await requestJson('/super/v1/withdrawals', { query });
|
||||||
|
return {
|
||||||
|
page: data?.page ?? page ?? 1,
|
||||||
|
limit: data?.limit ?? limit ?? 10,
|
||||||
|
total: data?.total ?? 0,
|
||||||
|
items: normalizeItems(data?.items)
|
||||||
|
};
|
||||||
|
},
|
||||||
|
async approveWithdrawal(id) {
|
||||||
|
if (!id) throw new Error('id is required');
|
||||||
|
return requestJson(`/super/v1/withdrawals/${id}/approve`, { method: 'POST' });
|
||||||
|
},
|
||||||
|
async rejectWithdrawal(id, { reason } = {}) {
|
||||||
|
if (!id) throw new Error('id is required');
|
||||||
|
return requestJson(`/super/v1/withdrawals/${id}/reject`, {
|
||||||
|
method: 'POST',
|
||||||
|
body: { reason }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
39
frontend/superadmin/src/service/ReportService.js
Normal file
39
frontend/superadmin/src/service/ReportService.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { requestJson } from './apiClient';
|
||||||
|
|
||||||
|
export const ReportService = {
|
||||||
|
async getOverview({ tenant_id, start_at, end_at, granularity } = {}) {
|
||||||
|
const iso = (d) => {
|
||||||
|
if (!d) return undefined;
|
||||||
|
const date = d instanceof Date ? d : new Date(d);
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined;
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
const query = {
|
||||||
|
tenant_id,
|
||||||
|
start_at: iso(start_at),
|
||||||
|
end_at: iso(end_at),
|
||||||
|
granularity
|
||||||
|
};
|
||||||
|
return requestJson('/super/v1/reports/overview', { query });
|
||||||
|
},
|
||||||
|
async exportReport({ tenant_id, start_at, end_at, granularity, format } = {}) {
|
||||||
|
const iso = (d) => {
|
||||||
|
if (!d) return undefined;
|
||||||
|
const date = d instanceof Date ? d : new Date(d);
|
||||||
|
if (Number.isNaN(date.getTime())) return undefined;
|
||||||
|
return date.toISOString();
|
||||||
|
};
|
||||||
|
|
||||||
|
return requestJson('/super/v1/reports/export', {
|
||||||
|
method: 'POST',
|
||||||
|
body: {
|
||||||
|
tenant_id,
|
||||||
|
start_at: iso(start_at),
|
||||||
|
end_at: iso(end_at),
|
||||||
|
granularity,
|
||||||
|
format: format || 'csv'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,11 +1,251 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import PendingPanel from '@/components/PendingPanel.vue';
|
import SearchField from '@/components/SearchField.vue';
|
||||||
|
import SearchPanel from '@/components/SearchPanel.vue';
|
||||||
|
import { CouponService } from '@/service/CouponService';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
const endpoints = ['GET /super/v1/coupons', 'PATCH /super/v1/coupons/:id/status', 'GET /super/v1/coupon-grants'];
|
const toast = useToast();
|
||||||
|
|
||||||
const notes = ['Current coupon CRUD endpoints are tenant-scoped and tied to creator ownership.', 'Expose cross-tenant coupon listing before adding bulk actions.'];
|
const coupons = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const totalRecords = ref(0);
|
||||||
|
const page = ref(1);
|
||||||
|
const rows = ref(10);
|
||||||
|
|
||||||
|
const couponID = ref(null);
|
||||||
|
const tenantID = ref(null);
|
||||||
|
const tenantCode = ref('');
|
||||||
|
const tenantName = ref('');
|
||||||
|
const keyword = ref('');
|
||||||
|
const type = ref('');
|
||||||
|
const status = ref('');
|
||||||
|
const createdAtFrom = ref(null);
|
||||||
|
const createdAtTo = ref(null);
|
||||||
|
const sortField = ref('created_at');
|
||||||
|
const sortOrder = ref(-1);
|
||||||
|
|
||||||
|
const typeOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '固定金额', value: 'fix_amount' },
|
||||||
|
{ label: '折扣', value: 'discount' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '生效中', value: 'active' },
|
||||||
|
{ label: '未开始', value: 'upcoming' },
|
||||||
|
{ label: '已过期', value: 'expired' }
|
||||||
|
];
|
||||||
|
|
||||||
|
function formatDate(value) {
|
||||||
|
if (!value) return '-';
|
||||||
|
if (String(value).startsWith('0001-01-01')) return '-';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return String(value);
|
||||||
|
return date.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCny(amountInCents) {
|
||||||
|
const amount = Number(amountInCents) / 100;
|
||||||
|
if (!Number.isFinite(amount)) return '-';
|
||||||
|
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusSeverity(value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'active':
|
||||||
|
return 'success';
|
||||||
|
case 'upcoming':
|
||||||
|
return 'warn';
|
||||||
|
case 'expired':
|
||||||
|
return 'danger';
|
||||||
|
default:
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCoupons() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await CouponService.listCoupons({
|
||||||
|
page: page.value,
|
||||||
|
limit: rows.value,
|
||||||
|
id: couponID.value || undefined,
|
||||||
|
tenant_id: tenantID.value || undefined,
|
||||||
|
tenant_code: tenantCode.value,
|
||||||
|
tenant_name: tenantName.value,
|
||||||
|
keyword: keyword.value,
|
||||||
|
type: type.value,
|
||||||
|
status: status.value,
|
||||||
|
created_at_from: createdAtFrom.value || undefined,
|
||||||
|
created_at_to: createdAtTo.value || undefined,
|
||||||
|
sortField: sortField.value,
|
||||||
|
sortOrder: sortOrder.value
|
||||||
|
});
|
||||||
|
coupons.value = result.items;
|
||||||
|
totalRecords.value = result.total;
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载优惠券列表', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
page.value = 1;
|
||||||
|
loadCoupons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReset() {
|
||||||
|
couponID.value = null;
|
||||||
|
tenantID.value = null;
|
||||||
|
tenantCode.value = '';
|
||||||
|
tenantName.value = '';
|
||||||
|
keyword.value = '';
|
||||||
|
type.value = '';
|
||||||
|
status.value = '';
|
||||||
|
createdAtFrom.value = null;
|
||||||
|
createdAtTo.value = null;
|
||||||
|
sortField.value = 'created_at';
|
||||||
|
sortOrder.value = -1;
|
||||||
|
page.value = 1;
|
||||||
|
rows.value = 10;
|
||||||
|
loadCoupons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPage(event) {
|
||||||
|
page.value = (event.page ?? 0) + 1;
|
||||||
|
rows.value = event.rows ?? rows.value;
|
||||||
|
loadCoupons();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSort(event) {
|
||||||
|
sortField.value = event.sortField ?? sortField.value;
|
||||||
|
sortOrder.value = event.sortOrder ?? sortOrder.value;
|
||||||
|
loadCoupons();
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadCoupons();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PendingPanel title="Coupons" description="Coupon management needs a super admin aggregation layer." :endpoints="endpoints" :notes="notes" />
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h4 class="m-0">优惠券</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||||
|
<SearchField label="CouponID">
|
||||||
|
<InputNumber v-model="couponID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="TenantID">
|
||||||
|
<InputNumber v-model="tenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="Tenant Code">
|
||||||
|
<InputText v-model="tenantCode" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="Tenant Name">
|
||||||
|
<InputText v-model="tenantName" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="关键词">
|
||||||
|
<IconField>
|
||||||
|
<InputIcon>
|
||||||
|
<i class="pi pi-search" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText v-model="keyword" placeholder="标题/描述" class="w-full" @keyup.enter="onSearch" />
|
||||||
|
</IconField>
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="类型">
|
||||||
|
<Select v-model="type" :options="typeOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="状态">
|
||||||
|
<Select v-model="status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="创建时间 From">
|
||||||
|
<DatePicker v-model="createdAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="创建时间 To">
|
||||||
|
<DatePicker v-model="createdAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
</SearchPanel>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:value="coupons"
|
||||||
|
dataKey="id"
|
||||||
|
:loading="loading"
|
||||||
|
lazy
|
||||||
|
:paginator="true"
|
||||||
|
:rows="rows"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:first="(page - 1) * rows"
|
||||||
|
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||||
|
sortMode="single"
|
||||||
|
:sortField="sortField"
|
||||||
|
:sortOrder="sortOrder"
|
||||||
|
@page="onPage"
|
||||||
|
@sort="onSort"
|
||||||
|
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||||
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
|
scrollable
|
||||||
|
scrollHeight="flex"
|
||||||
|
responsiveLayout="scroll"
|
||||||
|
>
|
||||||
|
<Column field="id" header="ID" sortable style="min-width: 6rem" />
|
||||||
|
<Column header="租户" style="min-width: 16rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-medium">{{ data.tenant_name || '-' }}</span>
|
||||||
|
<span class="text-xs text-muted-color">Code: {{ data.tenant_code || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="title" header="标题" sortable style="min-width: 16rem" />
|
||||||
|
<Column header="类型" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ data.type_description || data.type || '-' }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="value" header="面额/折扣" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span v-if="data.type === 'discount'">{{ data.value ?? '-' }}%</span>
|
||||||
|
<span v-else>{{ formatCny(data.value) }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="min_order_amount" header="门槛" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatCny(data.min_order_amount) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="使用情况" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span v-if="data.total_quantity === 0">不限量</span>
|
||||||
|
<span v-else>{{ data.used_quantity ?? 0 }} / {{ data.total_quantity ?? 0 }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="status" header="状态" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Tag :value="data.status_description || data.status || '-'" :severity="getStatusSeverity(data.status)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="start_at" header="开始时间" sortable style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.start_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="end_at" header="结束时间" sortable style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.end_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="created_at" header="创建时间" sortable style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.created_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,18 +1,269 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import PendingPanel from '@/components/PendingPanel.vue';
|
import SearchField from '@/components/SearchField.vue';
|
||||||
|
import SearchPanel from '@/components/SearchPanel.vue';
|
||||||
|
import { CreatorService } from '@/service/CreatorService';
|
||||||
|
import { TenantService } from '@/service/TenantService';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { onMounted, ref } from 'vue';
|
||||||
|
|
||||||
const endpoints = [
|
const toast = useToast();
|
||||||
'GET /super/v1/creators',
|
|
||||||
'GET /super/v1/creator-applications',
|
|
||||||
'POST /super/v1/creator-applications/:id/review',
|
|
||||||
'GET /super/v1/creator-members',
|
|
||||||
'POST /super/v1/creator-members/:id/review',
|
|
||||||
'POST /super/v1/creator-members/invite'
|
|
||||||
];
|
|
||||||
|
|
||||||
const notes = ['Tenant-level creator endpoints require the tenant owner and are not usable from super admin today.', 'Keep creator approvals in the tenant admin portal until super admin APIs are added.'];
|
const creators = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const totalRecords = ref(0);
|
||||||
|
const page = ref(1);
|
||||||
|
const rows = ref(10);
|
||||||
|
|
||||||
|
const tenantID = ref(null);
|
||||||
|
const ownerUserID = ref(null);
|
||||||
|
const nameKeyword = ref('');
|
||||||
|
const codeKeyword = ref('');
|
||||||
|
const status = ref('');
|
||||||
|
const createdAtFrom = ref(null);
|
||||||
|
const createdAtTo = ref(null);
|
||||||
|
const sortField = ref('id');
|
||||||
|
const sortOrder = ref(-1);
|
||||||
|
|
||||||
|
const statusDialogVisible = ref(false);
|
||||||
|
const statusOptionsLoading = ref(false);
|
||||||
|
const statusOptions = ref([]);
|
||||||
|
const statusUpdating = ref(false);
|
||||||
|
const statusTenant = ref(null);
|
||||||
|
const statusValue = ref(null);
|
||||||
|
|
||||||
|
function formatDate(value) {
|
||||||
|
if (!value) return '-';
|
||||||
|
if (String(value).startsWith('0001-01-01')) return '-';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return String(value);
|
||||||
|
return date.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusSeverity(value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'verified':
|
||||||
|
return 'success';
|
||||||
|
case 'pending_verify':
|
||||||
|
return 'warn';
|
||||||
|
case 'banned':
|
||||||
|
return 'danger';
|
||||||
|
default:
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureStatusOptionsLoaded() {
|
||||||
|
if (statusOptions.value.length > 0) return;
|
||||||
|
statusOptionsLoading.value = true;
|
||||||
|
try {
|
||||||
|
const list = await TenantService.getTenantStatuses();
|
||||||
|
statusOptions.value = (list || [])
|
||||||
|
.map((kv) => ({
|
||||||
|
label: kv?.value ?? kv?.key ?? '-',
|
||||||
|
value: kv?.key ?? ''
|
||||||
|
}))
|
||||||
|
.filter((item) => item.value);
|
||||||
|
} finally {
|
||||||
|
statusOptionsLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadCreators() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await CreatorService.listCreators({
|
||||||
|
page: page.value,
|
||||||
|
limit: rows.value,
|
||||||
|
id: tenantID.value || undefined,
|
||||||
|
user_id: ownerUserID.value || undefined,
|
||||||
|
name: nameKeyword.value,
|
||||||
|
code: codeKeyword.value,
|
||||||
|
status: status.value,
|
||||||
|
created_at_from: createdAtFrom.value || undefined,
|
||||||
|
created_at_to: createdAtTo.value || undefined,
|
||||||
|
sortField: sortField.value,
|
||||||
|
sortOrder: sortOrder.value
|
||||||
|
});
|
||||||
|
creators.value = result.items;
|
||||||
|
totalRecords.value = result.total;
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载创作者列表', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
page.value = 1;
|
||||||
|
loadCreators();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReset() {
|
||||||
|
tenantID.value = null;
|
||||||
|
ownerUserID.value = null;
|
||||||
|
nameKeyword.value = '';
|
||||||
|
codeKeyword.value = '';
|
||||||
|
status.value = '';
|
||||||
|
createdAtFrom.value = null;
|
||||||
|
createdAtTo.value = null;
|
||||||
|
sortField.value = 'id';
|
||||||
|
sortOrder.value = -1;
|
||||||
|
page.value = 1;
|
||||||
|
rows.value = 10;
|
||||||
|
loadCreators();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPage(event) {
|
||||||
|
page.value = (event.page ?? 0) + 1;
|
||||||
|
rows.value = event.rows ?? rows.value;
|
||||||
|
loadCreators();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSort(event) {
|
||||||
|
sortField.value = event.sortField ?? sortField.value;
|
||||||
|
sortOrder.value = event.sortOrder ?? sortOrder.value;
|
||||||
|
loadCreators();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function openStatusDialog(tenant) {
|
||||||
|
statusTenant.value = tenant;
|
||||||
|
statusValue.value = tenant?.status ?? null;
|
||||||
|
statusDialogVisible.value = true;
|
||||||
|
try {
|
||||||
|
await ensureStatusOptionsLoaded();
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载状态选项', life: 4000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmUpdateStatus() {
|
||||||
|
const id = statusTenant.value?.id;
|
||||||
|
if (!id || !statusValue.value) return;
|
||||||
|
|
||||||
|
statusUpdating.value = true;
|
||||||
|
try {
|
||||||
|
await TenantService.updateTenantStatus({ tenantID: id, status: statusValue.value });
|
||||||
|
toast.add({ severity: 'success', summary: '更新成功', detail: `TenantID: ${id}`, life: 3000 });
|
||||||
|
statusDialogVisible.value = false;
|
||||||
|
await loadCreators();
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '更新失败', detail: error?.message || '无法更新状态', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
statusUpdating.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadCreators();
|
||||||
|
ensureStatusOptionsLoaded().catch(() => {});
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PendingPanel title="Creators" description="Super admin creator operations require cross-tenant APIs." :endpoints="endpoints" :notes="notes" />
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h4 class="m-0">创作者列表</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||||
|
<SearchField label="TenantID">
|
||||||
|
<InputNumber v-model="tenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="Owner UserID">
|
||||||
|
<InputNumber v-model="ownerUserID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="名称">
|
||||||
|
<IconField>
|
||||||
|
<InputIcon>
|
||||||
|
<i class="pi pi-search" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText v-model="nameKeyword" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||||
|
</IconField>
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="Code">
|
||||||
|
<IconField>
|
||||||
|
<InputIcon>
|
||||||
|
<i class="pi pi-search" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText v-model="codeKeyword" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||||
|
</IconField>
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="状态">
|
||||||
|
<Select v-model="status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="请选择" :loading="statusOptionsLoading" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="创建时间 From">
|
||||||
|
<DatePicker v-model="createdAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="创建时间 To">
|
||||||
|
<DatePicker v-model="createdAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
</SearchPanel>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:value="creators"
|
||||||
|
dataKey="id"
|
||||||
|
:loading="loading"
|
||||||
|
lazy
|
||||||
|
:paginator="true"
|
||||||
|
:rows="rows"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:first="(page - 1) * rows"
|
||||||
|
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||||
|
sortMode="single"
|
||||||
|
:sortField="sortField"
|
||||||
|
:sortOrder="sortOrder"
|
||||||
|
@page="onPage"
|
||||||
|
@sort="onSort"
|
||||||
|
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||||
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
|
scrollable
|
||||||
|
scrollHeight="flex"
|
||||||
|
responsiveLayout="scroll"
|
||||||
|
>
|
||||||
|
<Column field="id" header="ID" sortable style="min-width: 6rem" />
|
||||||
|
<Column field="code" header="Code" style="min-width: 10rem" />
|
||||||
|
<Column field="name" header="名称" sortable style="min-width: 16rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Button :label="data.name || '-'" icon="pi pi-external-link" text size="small" class="p-0" as="router-link" :to="`/superadmin/tenants/${data.id}`" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="owner.username" header="Owner" style="min-width: 12rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span v-if="data.owner?.username">{{ data.owner.username }}</span>
|
||||||
|
<span v-else class="text-muted-color">{{ data.user_id ?? '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="status" header="状态" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Tag :value="data.status_description || data.status || '-'" :severity="getStatusSeverity(data.status)" class="cursor-pointer" @click="openStatusDialog(data)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="user_count" header="成员数" style="min-width: 8rem" />
|
||||||
|
<Column field="created_at" header="创建时间" sortable style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.created_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="statusDialogVisible" :modal="true" :style="{ width: '420px' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium">更新创作者状态</span>
|
||||||
|
<span class="text-muted-color truncate max-w-[240px]">{{ statusTenant?.name ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">状态</label>
|
||||||
|
<Select v-model="statusValue" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="选择状态" :disabled="statusUpdating" :loading="statusOptionsLoading" fluid />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" icon="pi pi-times" text @click="statusDialogVisible = false" :disabled="statusUpdating" />
|
||||||
|
<Button label="确认" icon="pi pi-check" @click="confirmUpdateStatus" :loading="statusUpdating" :disabled="!statusValue" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,341 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import PendingPanel from '@/components/PendingPanel.vue';
|
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';
|
||||||
|
|
||||||
const endpoints = ['GET /super/v1/withdrawals', 'POST /super/v1/withdrawals/:id/approve', 'POST /super/v1/withdrawals/:id/reject', 'GET /super/v1/wallet-ledgers'];
|
const toast = useToast();
|
||||||
|
|
||||||
const notes = ['Withdrawals currently exist only in tenant creator APIs.', 'Add a super admin ledger view before exposing approvals.'];
|
const withdrawals = ref([]);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const totalRecords = ref(0);
|
||||||
|
const page = ref(1);
|
||||||
|
const rows = ref(10);
|
||||||
|
|
||||||
|
const orderID = ref(null);
|
||||||
|
const tenantID = ref(null);
|
||||||
|
const tenantCode = ref('');
|
||||||
|
const tenantName = ref('');
|
||||||
|
const userID = ref(null);
|
||||||
|
const username = ref('');
|
||||||
|
const status = ref('');
|
||||||
|
const createdAtFrom = ref(null);
|
||||||
|
const createdAtTo = ref(null);
|
||||||
|
const paidAtFrom = ref(null);
|
||||||
|
const paidAtTo = ref(null);
|
||||||
|
const amountPaidMin = ref(null);
|
||||||
|
const amountPaidMax = ref(null);
|
||||||
|
const sortField = ref('id');
|
||||||
|
const sortOrder = ref(-1);
|
||||||
|
|
||||||
|
const statusOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: 'created', value: 'created' },
|
||||||
|
{ label: 'paid', value: 'paid' },
|
||||||
|
{ label: 'failed', value: 'failed' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const approveDialogVisible = ref(false);
|
||||||
|
const approveLoading = ref(false);
|
||||||
|
const approveOrder = ref(null);
|
||||||
|
|
||||||
|
const rejectDialogVisible = ref(false);
|
||||||
|
const rejectLoading = ref(false);
|
||||||
|
const rejectOrder = ref(null);
|
||||||
|
const rejectReason = ref('');
|
||||||
|
|
||||||
|
function formatDate(value) {
|
||||||
|
if (!value) return '-';
|
||||||
|
if (String(value).startsWith('0001-01-01')) return '-';
|
||||||
|
const date = new Date(value);
|
||||||
|
if (Number.isNaN(date.getTime())) return String(value);
|
||||||
|
return date.toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCny(amountInCents) {
|
||||||
|
const amount = Number(amountInCents) / 100;
|
||||||
|
if (!Number.isFinite(amount)) return '-';
|
||||||
|
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusSeverity(value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'paid':
|
||||||
|
return 'success';
|
||||||
|
case 'created':
|
||||||
|
return 'warn';
|
||||||
|
case 'failed':
|
||||||
|
return 'danger';
|
||||||
|
default:
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadWithdrawals() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await FinanceService.listWithdrawals({
|
||||||
|
page: page.value,
|
||||||
|
limit: rows.value,
|
||||||
|
id: orderID.value || undefined,
|
||||||
|
tenant_id: tenantID.value || undefined,
|
||||||
|
tenant_code: tenantCode.value,
|
||||||
|
tenant_name: tenantName.value,
|
||||||
|
user_id: userID.value || undefined,
|
||||||
|
username: username.value,
|
||||||
|
status: status.value,
|
||||||
|
created_at_from: createdAtFrom.value || undefined,
|
||||||
|
created_at_to: createdAtTo.value || undefined,
|
||||||
|
paid_at_from: paidAtFrom.value || undefined,
|
||||||
|
paid_at_to: paidAtTo.value || undefined,
|
||||||
|
amount_paid_min: amountPaidMin.value || undefined,
|
||||||
|
amount_paid_max: amountPaidMax.value || undefined,
|
||||||
|
sortField: sortField.value,
|
||||||
|
sortOrder: sortOrder.value
|
||||||
|
});
|
||||||
|
withdrawals.value = result.items;
|
||||||
|
totalRecords.value = result.total;
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载提现列表', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
page.value = 1;
|
||||||
|
loadWithdrawals();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReset() {
|
||||||
|
orderID.value = null;
|
||||||
|
tenantID.value = null;
|
||||||
|
tenantCode.value = '';
|
||||||
|
tenantName.value = '';
|
||||||
|
userID.value = null;
|
||||||
|
username.value = '';
|
||||||
|
status.value = '';
|
||||||
|
createdAtFrom.value = null;
|
||||||
|
createdAtTo.value = null;
|
||||||
|
paidAtFrom.value = null;
|
||||||
|
paidAtTo.value = null;
|
||||||
|
amountPaidMin.value = null;
|
||||||
|
amountPaidMax.value = null;
|
||||||
|
sortField.value = 'id';
|
||||||
|
sortOrder.value = -1;
|
||||||
|
page.value = 1;
|
||||||
|
rows.value = 10;
|
||||||
|
loadWithdrawals();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPage(event) {
|
||||||
|
page.value = (event.page ?? 0) + 1;
|
||||||
|
rows.value = event.rows ?? rows.value;
|
||||||
|
loadWithdrawals();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSort(event) {
|
||||||
|
sortField.value = event.sortField ?? sortField.value;
|
||||||
|
sortOrder.value = event.sortOrder ?? sortOrder.value;
|
||||||
|
loadWithdrawals();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openApproveDialog(order) {
|
||||||
|
approveOrder.value = order;
|
||||||
|
approveDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmApprove() {
|
||||||
|
const id = approveOrder.value?.id;
|
||||||
|
if (!id) return;
|
||||||
|
approveLoading.value = true;
|
||||||
|
try {
|
||||||
|
await FinanceService.approveWithdrawal(id);
|
||||||
|
toast.add({ severity: 'success', summary: '已批准', detail: `订单ID: ${id}`, life: 3000 });
|
||||||
|
approveDialogVisible.value = false;
|
||||||
|
await loadWithdrawals();
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '操作失败', detail: error?.message || '无法批准提现', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
approveLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function openRejectDialog(order) {
|
||||||
|
rejectOrder.value = order;
|
||||||
|
rejectReason.value = '';
|
||||||
|
rejectDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function confirmReject() {
|
||||||
|
const id = rejectOrder.value?.id;
|
||||||
|
if (!id || !rejectReason.value.trim()) return;
|
||||||
|
rejectLoading.value = true;
|
||||||
|
try {
|
||||||
|
await FinanceService.rejectWithdrawal(id, { reason: rejectReason.value.trim() });
|
||||||
|
toast.add({ severity: 'success', summary: '已驳回', detail: `订单ID: ${id}`, life: 3000 });
|
||||||
|
rejectDialogVisible.value = false;
|
||||||
|
await loadWithdrawals();
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '操作失败', detail: error?.message || '无法驳回提现', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
rejectLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadWithdrawals();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PendingPanel title="Finance" description="Withdrawals and wallet visibility require super admin endpoints." :endpoints="endpoints" :notes="notes" />
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h4 class="m-0">提现审核</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||||
|
<SearchField label="OrderID">
|
||||||
|
<InputNumber v-model="orderID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="TenantID">
|
||||||
|
<InputNumber v-model="tenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="Tenant Code">
|
||||||
|
<InputText v-model="tenantCode" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="Tenant Name">
|
||||||
|
<InputText v-model="tenantName" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="UserID">
|
||||||
|
<InputNumber v-model="userID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="用户名">
|
||||||
|
<IconField>
|
||||||
|
<InputIcon>
|
||||||
|
<i class="pi pi-search" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText v-model="username" placeholder="模糊匹配" class="w-full" @keyup.enter="onSearch" />
|
||||||
|
</IconField>
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="状态">
|
||||||
|
<Select v-model="status" :options="statusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="创建时间 From">
|
||||||
|
<DatePicker v-model="createdAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="创建时间 To">
|
||||||
|
<DatePicker v-model="createdAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="支付时间 From">
|
||||||
|
<DatePicker v-model="paidAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="支付时间 To">
|
||||||
|
<DatePicker v-model="paidAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="金额 Min">
|
||||||
|
<InputNumber v-model="amountPaidMin" :min="0" placeholder=">= 0" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="金额 Max">
|
||||||
|
<InputNumber v-model="amountPaidMax" :min="0" placeholder=">= 0" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
</SearchPanel>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:value="withdrawals"
|
||||||
|
dataKey="id"
|
||||||
|
:loading="loading"
|
||||||
|
lazy
|
||||||
|
:paginator="true"
|
||||||
|
:rows="rows"
|
||||||
|
:totalRecords="totalRecords"
|
||||||
|
:first="(page - 1) * rows"
|
||||||
|
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||||
|
sortMode="single"
|
||||||
|
:sortField="sortField"
|
||||||
|
:sortOrder="sortOrder"
|
||||||
|
@page="onPage"
|
||||||
|
@sort="onSort"
|
||||||
|
currentPageReportTemplate="显示第 {first} - {last} 条,共 {totalRecords} 条"
|
||||||
|
paginatorTemplate="FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink CurrentPageReport RowsPerPageDropdown"
|
||||||
|
scrollable
|
||||||
|
scrollHeight="flex"
|
||||||
|
responsiveLayout="scroll"
|
||||||
|
>
|
||||||
|
<Column field="id" header="订单ID" sortable style="min-width: 7rem" />
|
||||||
|
<Column header="租户" style="min-width: 16rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-medium">{{ data.tenant?.name || '-' }}</span>
|
||||||
|
<span class="text-xs text-muted-color">Code: {{ data.tenant?.code || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="申请人" style="min-width: 12rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<span v-if="data.buyer?.username">{{ data.buyer.username }}</span>
|
||||||
|
<span v-else class="text-muted-color">{{ data.buyer?.id ?? '-' }}</span>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="amount_paid" header="金额" sortable style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatCny(data.amount_paid) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="status" header="状态" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Tag :value="data.status_description || data.status || '-'" :severity="getStatusSeverity(data.status)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="created_at" header="创建时间" sortable style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.created_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="paid_at" header="支付时间" sortable style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.paid_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="操作" style="min-width: 12rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<Button label="通过" icon="pi pi-check" size="small" severity="success" :disabled="data.status !== 'created'" @click="openApproveDialog(data)" />
|
||||||
|
<Button label="驳回" icon="pi pi-times" size="small" severity="danger" :disabled="data.status !== 'created'" @click="openRejectDialog(data)" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="approveDialogVisible" :modal="true" :style="{ width: '420px' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium">确认通过提现吗?</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="text-sm text-muted-color">确认后将标记为已支付,请确保外部打款已完成。</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" icon="pi pi-times" text @click="approveDialogVisible = false" :disabled="approveLoading" />
|
||||||
|
<Button label="确认通过" icon="pi pi-check" severity="success" @click="confirmApprove" :loading="approveLoading" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog v-model:visible="rejectDialogVisible" :modal="true" :style="{ width: '520px' }">
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<span class="font-medium">驳回提现申请</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div>
|
||||||
|
<label class="block font-medium mb-2">驳回原因</label>
|
||||||
|
<InputText v-model="rejectReason" placeholder="请输入驳回原因" class="w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template #footer>
|
||||||
|
<Button label="取消" icon="pi pi-times" text @click="rejectDialogVisible = false" :disabled="rejectLoading" />
|
||||||
|
<Button label="确认驳回" icon="pi pi-check" severity="danger" @click="confirmReject" :loading="rejectLoading" :disabled="!rejectReason.trim()" />
|
||||||
|
</template>
|
||||||
|
</Dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,11 +1,149 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import PendingPanel from '@/components/PendingPanel.vue';
|
import SearchField from '@/components/SearchField.vue';
|
||||||
|
import SearchPanel from '@/components/SearchPanel.vue';
|
||||||
|
import StatisticsStrip from '@/components/StatisticsStrip.vue';
|
||||||
|
import { ReportService } from '@/service/ReportService';
|
||||||
|
import { useToast } from 'primevue/usetoast';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
|
||||||
const endpoints = ['GET /super/v1/reports/overview', 'GET /super/v1/reports/series', 'POST /super/v1/reports/export'];
|
const toast = useToast();
|
||||||
|
|
||||||
const notes = ['Current report APIs are scoped to creators in tenant context.', 'Add cross-tenant aggregation before wiring charts and exports.'];
|
const overview = ref(null);
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const tenantID = ref(null);
|
||||||
|
const startAt = ref(null);
|
||||||
|
const endAt = ref(null);
|
||||||
|
const granularity = ref('day');
|
||||||
|
|
||||||
|
const granularityOptions = [{ label: '按天', value: 'day' }];
|
||||||
|
|
||||||
|
function formatPercent(value) {
|
||||||
|
const rate = Number(value);
|
||||||
|
if (!Number.isFinite(rate)) return '-';
|
||||||
|
return `${(rate * 100).toFixed(2)}%`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatCnyFromYuan(amount) {
|
||||||
|
const value = Number(amount);
|
||||||
|
if (!Number.isFinite(value)) return '-';
|
||||||
|
return new Intl.NumberFormat('zh-CN', { style: 'currency', currency: 'CNY' }).format(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
const summaryItems = computed(() => {
|
||||||
|
const summary = overview.value?.summary;
|
||||||
|
if (!summary) return [];
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ key: 'views', label: '累计曝光:', value: summary.total_views ?? 0, icon: 'pi-eye' },
|
||||||
|
{ key: 'paid-orders', label: '已支付订单:', value: summary.paid_orders ?? 0, icon: 'pi-shopping-cart' },
|
||||||
|
{ key: 'paid-amount', label: '已支付金额:', value: formatCnyFromYuan(summary.paid_amount), icon: 'pi-wallet' },
|
||||||
|
{ key: 'refund-orders', label: '退款订单:', value: summary.refund_orders ?? 0, icon: 'pi-undo' },
|
||||||
|
{ key: 'refund-amount', label: '退款金额:', value: formatCnyFromYuan(summary.refund_amount), icon: 'pi-replay' },
|
||||||
|
{ key: 'conversion', label: '转化率:', value: formatPercent(summary.conversion_rate), icon: 'pi-percentage' }
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadOverview() {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
overview.value = await ReportService.getOverview({
|
||||||
|
tenant_id: tenantID.value || undefined,
|
||||||
|
start_at: startAt.value || undefined,
|
||||||
|
end_at: endAt.value || undefined,
|
||||||
|
granularity: granularity.value
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载报表数据', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSearch() {
|
||||||
|
loadOverview();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onReset() {
|
||||||
|
tenantID.value = null;
|
||||||
|
startAt.value = null;
|
||||||
|
endAt.value = null;
|
||||||
|
granularity.value = 'day';
|
||||||
|
loadOverview();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function exportReport() {
|
||||||
|
try {
|
||||||
|
const res = await ReportService.exportReport({
|
||||||
|
tenant_id: tenantID.value || undefined,
|
||||||
|
start_at: startAt.value || undefined,
|
||||||
|
end_at: endAt.value || undefined,
|
||||||
|
granularity: granularity.value,
|
||||||
|
format: 'csv'
|
||||||
|
});
|
||||||
|
const content = res?.content ?? '';
|
||||||
|
const filename = res?.filename || 'report.csv';
|
||||||
|
const blob = new Blob([content], { type: res?.mime_type || 'text/csv' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const link = document.createElement('a');
|
||||||
|
link.href = url;
|
||||||
|
link.download = filename;
|
||||||
|
link.click();
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
toast.add({ severity: 'success', summary: '导出成功', detail: filename, life: 3000 });
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '导出失败', detail: error?.message || '无法导出报表', life: 4000 });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loadOverview();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<PendingPanel title="Reports" description="Platform reporting needs aggregated super admin APIs." :endpoints="endpoints" :notes="notes" />
|
<div>
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h4 class="m-0">运营报表</h4>
|
||||||
|
<Button label="导出 CSV" icon="pi pi-download" severity="secondary" :loading="loading" @click="exportReport" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchPanel :loading="loading" @search="onSearch" @reset="onReset">
|
||||||
|
<SearchField label="TenantID">
|
||||||
|
<InputNumber v-model="tenantID" :min="1" placeholder="不填则全平台" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="开始时间">
|
||||||
|
<DatePicker v-model="startAt" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="结束时间">
|
||||||
|
<DatePicker v-model="endAt" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="统计粒度">
|
||||||
|
<Select v-model="granularity" :options="granularityOptions" optionLabel="label" optionValue="value" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
</SearchPanel>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<StatisticsStrip v-if="summaryItems.length" :items="summaryItems" containerClass="card mb-4" />
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h4 class="m-0">趋势明细</h4>
|
||||||
|
</div>
|
||||||
|
<DataTable :value="overview?.items || []" :loading="loading" scrollable scrollHeight="420px" responsiveLayout="scroll">
|
||||||
|
<Column field="date" header="日期" style="min-width: 10rem" />
|
||||||
|
<Column field="paid_orders" header="已支付订单" style="min-width: 10rem" />
|
||||||
|
<Column field="paid_amount" header="已支付金额" style="min-width: 12rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatCnyFromYuan(data.paid_amount) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="refund_orders" header="退款订单" style="min-width: 10rem" />
|
||||||
|
<Column field="refund_amount" header="退款金额" style="min-width: 12rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatCnyFromYuan(data.refund_amount) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
Reference in New Issue
Block a user