feat: add coupon risk review
This commit is contained in:
@@ -45,6 +45,22 @@ func (c *coupons) ListGrants(ctx fiber.Ctx, filter *dto.SuperCouponGrantListFilt
|
|||||||
return services.Super.ListCouponGrants(ctx, filter)
|
return services.Super.ListCouponGrants(ctx, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List coupon risks
|
||||||
|
//
|
||||||
|
// @Router /super/v1/coupon-risks [get]
|
||||||
|
// @Summary List coupon risks
|
||||||
|
// @Description List coupon risk records 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.SuperCouponRiskItem}
|
||||||
|
// @Bind filter query
|
||||||
|
func (c *coupons) ListRisks(ctx fiber.Ctx, filter *dto.SuperCouponRiskListFilter) (*requests.Pager, error) {
|
||||||
|
return services.Super.ListCouponRisks(ctx, filter)
|
||||||
|
}
|
||||||
|
|
||||||
// Update coupon status
|
// Update coupon status
|
||||||
//
|
//
|
||||||
// @Router /super/v1/coupons/:id<int>/status [patch]
|
// @Router /super/v1/coupons/:id<int>/status [patch]
|
||||||
|
|||||||
@@ -142,3 +142,82 @@ type SuperCouponGrantItem struct {
|
|||||||
// CreatedAt 领取时间(RFC3339)。
|
// CreatedAt 领取时间(RFC3339)。
|
||||||
CreatedAt string `json:"created_at"`
|
CreatedAt string `json:"created_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SuperCouponRiskListFilter 超管优惠券异常核查过滤条件。
|
||||||
|
type SuperCouponRiskListFilter struct {
|
||||||
|
requests.Pagination
|
||||||
|
// RiskType 异常类型过滤(used_without_order/order_status_mismatch/used_outside_window/unused_has_order_or_used_at/duplicate_grant)。
|
||||||
|
RiskType *string `query:"risk_type"`
|
||||||
|
// CouponID 优惠券ID过滤(精确匹配)。
|
||||||
|
CouponID *int64 `query:"coupon_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"`
|
||||||
|
// UserID 用户ID过滤(精确匹配)。
|
||||||
|
UserID *int64 `query:"user_id"`
|
||||||
|
// Username 用户名过滤(模糊匹配)。
|
||||||
|
Username *string `query:"username"`
|
||||||
|
// Status 用户券状态过滤(unused/used/expired)。
|
||||||
|
Status *consts.UserCouponStatus `query:"status"`
|
||||||
|
// OrderStatus 订单状态过滤。
|
||||||
|
OrderStatus *consts.OrderStatus `query:"order_status"`
|
||||||
|
// CreatedAtFrom 领取时间起始(RFC3339)。
|
||||||
|
CreatedAtFrom *string `query:"created_at_from"`
|
||||||
|
// CreatedAtTo 领取时间结束(RFC3339)。
|
||||||
|
CreatedAtTo *string `query:"created_at_to"`
|
||||||
|
// UsedAtFrom 使用时间起始(RFC3339)。
|
||||||
|
UsedAtFrom *string `query:"used_at_from"`
|
||||||
|
// UsedAtTo 使用时间结束(RFC3339)。
|
||||||
|
UsedAtTo *string `query:"used_at_to"`
|
||||||
|
// Asc 升序字段(id/created_at/used_at)。
|
||||||
|
Asc *string `query:"asc"`
|
||||||
|
// Desc 降序字段(id/created_at/used_at)。
|
||||||
|
Desc *string `query:"desc"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SuperCouponRiskItem 超管优惠券异常核查记录。
|
||||||
|
type SuperCouponRiskItem struct {
|
||||||
|
// ID 用户券ID。
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
// RiskType 异常类型。
|
||||||
|
RiskType string `json:"risk_type"`
|
||||||
|
// RiskReason 异常说明。
|
||||||
|
RiskReason string `json:"risk_reason"`
|
||||||
|
// CouponID 优惠券ID。
|
||||||
|
CouponID int64 `json:"coupon_id"`
|
||||||
|
// CouponTitle 优惠券标题。
|
||||||
|
CouponTitle string `json:"coupon_title"`
|
||||||
|
// TenantID 租户ID。
|
||||||
|
TenantID int64 `json:"tenant_id"`
|
||||||
|
// TenantCode 租户编码。
|
||||||
|
TenantCode string `json:"tenant_code"`
|
||||||
|
// TenantName 租户名称。
|
||||||
|
TenantName string `json:"tenant_name"`
|
||||||
|
// UserID 用户ID。
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
// Username 用户名。
|
||||||
|
Username string `json:"username"`
|
||||||
|
// Status 用户券状态。
|
||||||
|
Status consts.UserCouponStatus `json:"status"`
|
||||||
|
// StatusDescription 状态描述(用于展示)。
|
||||||
|
StatusDescription string `json:"status_description"`
|
||||||
|
// OrderID 使用订单ID。
|
||||||
|
OrderID int64 `json:"order_id"`
|
||||||
|
// OrderStatus 订单状态。
|
||||||
|
OrderStatus consts.OrderStatus `json:"order_status"`
|
||||||
|
// OrderStatusDescription 订单状态描述(用于展示)。
|
||||||
|
OrderStatusDescription string `json:"order_status_description"`
|
||||||
|
// OrderAmountPaid 订单实付金额(分)。
|
||||||
|
OrderAmountPaid int64 `json:"order_amount_paid"`
|
||||||
|
// PaidAt 订单支付时间(RFC3339)。
|
||||||
|
PaidAt string `json:"paid_at"`
|
||||||
|
// UsedAt 使用时间(RFC3339)。
|
||||||
|
UsedAt string `json:"used_at"`
|
||||||
|
// CreatedAt 领取时间(RFC3339)。
|
||||||
|
CreatedAt string `json:"created_at"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -114,6 +114,11 @@ func (r *Routes) Register(router fiber.Router) {
|
|||||||
r.coupons.ListGrants,
|
r.coupons.ListGrants,
|
||||||
Query[dto.SuperCouponGrantListFilter]("filter"),
|
Query[dto.SuperCouponGrantListFilter]("filter"),
|
||||||
))
|
))
|
||||||
|
r.log.Debugf("Registering route: Get /super/v1/coupon-risks -> coupons.ListRisks")
|
||||||
|
router.Get("/super/v1/coupon-risks"[len(r.Path()):], DataFunc1(
|
||||||
|
r.coupons.ListRisks,
|
||||||
|
Query[dto.SuperCouponRiskListFilter]("filter"),
|
||||||
|
))
|
||||||
r.log.Debugf("Registering route: Get /super/v1/coupons -> coupons.List")
|
r.log.Debugf("Registering route: Get /super/v1/coupons -> coupons.List")
|
||||||
router.Get("/super/v1/coupons"[len(r.Path()):], DataFunc1(
|
router.Get("/super/v1/coupons"[len(r.Path()):], DataFunc1(
|
||||||
r.coupons.List,
|
r.coupons.List,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -5087,6 +5088,407 @@ func (s *super) ListCouponGrants(ctx context.Context, filter *super_dto.SuperCou
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *super) ListCouponRisks(ctx context.Context, filter *super_dto.SuperCouponRiskListFilter) (*requests.Pager, error) {
|
||||||
|
if filter == nil {
|
||||||
|
filter = &super_dto.SuperCouponRiskListFilter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 风控类型校验,避免无效筛选导致误判。
|
||||||
|
targetRiskType := ""
|
||||||
|
if filter.RiskType != nil {
|
||||||
|
targetRiskType = strings.TrimSpace(*filter.RiskType)
|
||||||
|
}
|
||||||
|
if targetRiskType != "" {
|
||||||
|
switch targetRiskType {
|
||||||
|
case "used_without_order", "order_status_mismatch", "used_outside_window", "unused_has_order_or_used_at", "duplicate_grant":
|
||||||
|
default:
|
||||||
|
return nil, errorx.ErrBadRequest.WithMsg("risk_type 无效")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先做基础筛选,减少异常核查的候选数据量。
|
||||||
|
tbl, q := models.UserCouponQuery.QueryContext(ctx)
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if userFilter {
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
filter.Pagination.Format()
|
||||||
|
return &requests.Pager{
|
||||||
|
Pagination: filter.Pagination,
|
||||||
|
Total: 0,
|
||||||
|
Items: []super_dto.SuperCouponRiskItem{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
q = q.Where(tbl.UserID.In(userIDs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
couponIDs, couponFilter, err := s.filterCouponRiskCouponIDs(ctx, filter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if couponFilter {
|
||||||
|
if len(couponIDs) == 0 {
|
||||||
|
filter.Pagination.Format()
|
||||||
|
return &requests.Pager{
|
||||||
|
Pagination: filter.Pagination,
|
||||||
|
Total: 0,
|
||||||
|
Items: []super_dto.SuperCouponRiskItem{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
q = q.Where(tbl.CouponID.In(couponIDs...))
|
||||||
|
}
|
||||||
|
|
||||||
|
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.UsedAtFrom != nil {
|
||||||
|
from, err := s.parseFilterTime(filter.UsedAtFrom)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if from != nil {
|
||||||
|
q = q.Where(tbl.UsedAt.Gte(*from))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if filter.UsedAtTo != nil {
|
||||||
|
to, err := s.parseFilterTime(filter.UsedAtTo)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if to != nil {
|
||||||
|
q = q.Where(tbl.UsedAt.Lte(*to))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.OrderStatus != nil && *filter.OrderStatus != "" {
|
||||||
|
q = q.Where(tbl.OrderID.Gt(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
if targetRiskType != "" {
|
||||||
|
switch targetRiskType {
|
||||||
|
case "used_without_order":
|
||||||
|
q = q.Where(tbl.Status.Eq(consts.UserCouponStatusUsed))
|
||||||
|
case "order_status_mismatch":
|
||||||
|
q = q.Where(tbl.Status.Eq(consts.UserCouponStatusUsed)).Where(tbl.OrderID.Gt(0))
|
||||||
|
case "used_outside_window":
|
||||||
|
q = q.Where(tbl.Status.Eq(consts.UserCouponStatusUsed))
|
||||||
|
case "unused_has_order_or_used_at":
|
||||||
|
q = q.Where(tbl.Status.Eq(consts.UserCouponStatusUnused))
|
||||||
|
case "duplicate_grant":
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异常规则较为复杂,先拉取候选数据,再做细粒度判断与分页。
|
||||||
|
list, err := q.Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
if len(list) == 0 {
|
||||||
|
filter.Pagination.Format()
|
||||||
|
return &requests.Pager{
|
||||||
|
Pagination: filter.Pagination,
|
||||||
|
Total: 0,
|
||||||
|
Items: []super_dto.SuperCouponRiskItem{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
couponIDSet := make(map[int64]struct{})
|
||||||
|
userIDSet := make(map[int64]struct{})
|
||||||
|
orderIDSet := make(map[int64]struct{})
|
||||||
|
for _, uc := range list {
|
||||||
|
if uc.CouponID > 0 {
|
||||||
|
couponIDSet[uc.CouponID] = struct{}{}
|
||||||
|
}
|
||||||
|
if uc.UserID > 0 {
|
||||||
|
userIDSet[uc.UserID] = struct{}{}
|
||||||
|
}
|
||||||
|
if uc.OrderID > 0 {
|
||||||
|
orderIDSet[uc.OrderID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
couponIDs = couponIDs[:0]
|
||||||
|
for id := range couponIDSet {
|
||||||
|
couponIDs = append(couponIDs, id)
|
||||||
|
}
|
||||||
|
userIDs = userIDs[:0]
|
||||||
|
for id := range userIDSet {
|
||||||
|
userIDs = append(userIDs, id)
|
||||||
|
}
|
||||||
|
orderIDs := make([]int64, 0, len(orderIDSet))
|
||||||
|
for id := range orderIDSet {
|
||||||
|
orderIDs = append(orderIDs, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量加载关联信息,避免 N+1 查询。
|
||||||
|
couponMap := make(map[int64]*models.Coupon, len(couponIDs))
|
||||||
|
tenantMap := make(map[int64]*models.Tenant)
|
||||||
|
if len(couponIDs) > 0 {
|
||||||
|
couponTbl, couponQuery := models.CouponQuery.QueryContext(ctx)
|
||||||
|
coupons, err := couponQuery.Where(couponTbl.ID.In(couponIDs...)).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
tenantSet := make(map[int64]struct{})
|
||||||
|
for _, coupon := range coupons {
|
||||||
|
couponMap[coupon.ID] = coupon
|
||||||
|
if coupon.TenantID > 0 {
|
||||||
|
tenantSet[coupon.TenantID] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tenantIDs := make([]int64, 0, len(tenantSet))
|
||||||
|
for id := range tenantSet {
|
||||||
|
tenantIDs = append(tenantIDs, id)
|
||||||
|
}
|
||||||
|
if len(tenantIDs) > 0 {
|
||||||
|
tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx)
|
||||||
|
tenants, err := tenantQuery.Where(tenantTbl.ID.In(tenantIDs...)).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
for _, tenant := range tenants {
|
||||||
|
tenantMap[tenant.ID] = tenant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
userMap := make(map[int64]*models.User, len(userIDs))
|
||||||
|
if len(userIDs) > 0 {
|
||||||
|
userTbl, userQuery := models.UserQuery.QueryContext(ctx)
|
||||||
|
users, err := userQuery.Where(userTbl.ID.In(userIDs...)).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
for _, user := range users {
|
||||||
|
userMap[user.ID] = user
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
orderMap := make(map[int64]*models.Order, len(orderIDs))
|
||||||
|
if len(orderIDs) > 0 {
|
||||||
|
orderTbl, orderQuery := models.OrderQuery.QueryContext(ctx)
|
||||||
|
orders, err := orderQuery.Where(orderTbl.ID.In(orderIDs...)).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
for _, order := range orders {
|
||||||
|
orderMap[order.ID] = order
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type couponGrantKey struct {
|
||||||
|
userID int64
|
||||||
|
couponID int64
|
||||||
|
}
|
||||||
|
grantCounts := make(map[couponGrantKey]int, len(list))
|
||||||
|
for _, uc := range list {
|
||||||
|
if uc.UserID == 0 || uc.CouponID == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
key := couponGrantKey{userID: uc.UserID, couponID: uc.CouponID}
|
||||||
|
grantCounts[key]++
|
||||||
|
}
|
||||||
|
|
||||||
|
type couponRiskRecord struct {
|
||||||
|
item super_dto.SuperCouponRiskItem
|
||||||
|
createdAt time.Time
|
||||||
|
usedAt time.Time
|
||||||
|
}
|
||||||
|
records := make([]couponRiskRecord, 0, len(list))
|
||||||
|
|
||||||
|
matchRisk := func(target string) bool {
|
||||||
|
return targetRiskType == "" || targetRiskType == target
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, uc := range list {
|
||||||
|
order := orderMap[uc.OrderID]
|
||||||
|
if filter.OrderStatus != nil && *filter.OrderStatus != "" {
|
||||||
|
if order == nil || order.Status != *filter.OrderStatus {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
coupon := couponMap[uc.CouponID]
|
||||||
|
user := userMap[uc.UserID]
|
||||||
|
key := couponGrantKey{userID: uc.UserID, couponID: uc.CouponID}
|
||||||
|
isDuplicate := grantCounts[key] > 1
|
||||||
|
|
||||||
|
riskType := ""
|
||||||
|
riskReason := ""
|
||||||
|
|
||||||
|
if matchRisk("used_without_order") && uc.Status == consts.UserCouponStatusUsed {
|
||||||
|
if uc.OrderID == 0 {
|
||||||
|
riskType = "used_without_order"
|
||||||
|
riskReason = "已核销但未关联订单"
|
||||||
|
} else if order == nil {
|
||||||
|
riskType = "used_without_order"
|
||||||
|
riskReason = "已核销但订单不存在"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if riskType == "" && matchRisk("order_status_mismatch") && uc.Status == consts.UserCouponStatusUsed {
|
||||||
|
if uc.OrderID > 0 && order != nil && order.Status != consts.OrderStatusPaid {
|
||||||
|
riskType = "order_status_mismatch"
|
||||||
|
riskReason = "已核销但订单状态为" + order.Status.Description()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if riskType == "" && matchRisk("used_outside_window") && uc.Status == consts.UserCouponStatusUsed && coupon != nil && !uc.UsedAt.IsZero() {
|
||||||
|
if !coupon.StartAt.IsZero() && uc.UsedAt.Before(coupon.StartAt) {
|
||||||
|
riskType = "used_outside_window"
|
||||||
|
riskReason = "核销时间早于优惠券生效时间"
|
||||||
|
} else if !coupon.EndAt.IsZero() && uc.UsedAt.After(coupon.EndAt) {
|
||||||
|
riskType = "used_outside_window"
|
||||||
|
riskReason = "核销时间晚于优惠券截止时间"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if riskType == "" && matchRisk("unused_has_order_or_used_at") && uc.Status == consts.UserCouponStatusUnused {
|
||||||
|
if uc.OrderID > 0 && !uc.UsedAt.IsZero() {
|
||||||
|
riskType = "unused_has_order_or_used_at"
|
||||||
|
riskReason = "未使用但存在订单与使用时间"
|
||||||
|
} else if uc.OrderID > 0 {
|
||||||
|
riskType = "unused_has_order_or_used_at"
|
||||||
|
riskReason = "未使用但已关联订单"
|
||||||
|
} else if !uc.UsedAt.IsZero() {
|
||||||
|
riskType = "unused_has_order_or_used_at"
|
||||||
|
riskReason = "未使用但存在使用时间"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if riskType == "" && matchRisk("duplicate_grant") && isDuplicate {
|
||||||
|
riskType = "duplicate_grant"
|
||||||
|
riskReason = "同一用户重复领券"
|
||||||
|
}
|
||||||
|
if riskType == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
item := super_dto.SuperCouponRiskItem{
|
||||||
|
ID: uc.ID,
|
||||||
|
RiskType: riskType,
|
||||||
|
RiskReason: riskReason,
|
||||||
|
CouponID: uc.CouponID,
|
||||||
|
UserID: uc.UserID,
|
||||||
|
Status: uc.Status,
|
||||||
|
StatusDescription: uc.Status.Description(),
|
||||||
|
OrderID: uc.OrderID,
|
||||||
|
UsedAt: s.formatTime(uc.UsedAt),
|
||||||
|
CreatedAt: s.formatTime(uc.CreatedAt),
|
||||||
|
}
|
||||||
|
if user != nil {
|
||||||
|
item.Username = user.Username
|
||||||
|
} else if uc.UserID > 0 {
|
||||||
|
item.Username = "ID:" + strconv.FormatInt(uc.UserID, 10)
|
||||||
|
}
|
||||||
|
if coupon != nil {
|
||||||
|
item.CouponTitle = coupon.Title
|
||||||
|
item.TenantID = coupon.TenantID
|
||||||
|
if tenant := tenantMap[coupon.TenantID]; tenant != nil {
|
||||||
|
item.TenantCode = tenant.Code
|
||||||
|
item.TenantName = tenant.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if order != nil {
|
||||||
|
item.OrderStatus = order.Status
|
||||||
|
item.OrderStatusDescription = order.Status.Description()
|
||||||
|
item.OrderAmountPaid = order.AmountPaid
|
||||||
|
item.PaidAt = s.formatTime(order.PaidAt)
|
||||||
|
}
|
||||||
|
|
||||||
|
records = append(records, couponRiskRecord{
|
||||||
|
item: item,
|
||||||
|
createdAt: uc.CreatedAt,
|
||||||
|
usedAt: uc.UsedAt,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
filter.Pagination.Format()
|
||||||
|
if len(records) == 0 {
|
||||||
|
return &requests.Pager{
|
||||||
|
Pagination: filter.Pagination,
|
||||||
|
Total: 0,
|
||||||
|
Items: []super_dto.SuperCouponRiskItem{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sortField := "created_at"
|
||||||
|
desc := true
|
||||||
|
if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" {
|
||||||
|
sortField = strings.TrimSpace(*filter.Asc)
|
||||||
|
desc = false
|
||||||
|
}
|
||||||
|
if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" {
|
||||||
|
sortField = strings.TrimSpace(*filter.Desc)
|
||||||
|
desc = true
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(records, func(i, j int) bool {
|
||||||
|
left := records[i]
|
||||||
|
right := records[j]
|
||||||
|
var less bool
|
||||||
|
switch sortField {
|
||||||
|
case "id":
|
||||||
|
less = left.item.ID < right.item.ID
|
||||||
|
case "used_at":
|
||||||
|
less = left.usedAt.Before(right.usedAt)
|
||||||
|
case "created_at":
|
||||||
|
less = left.createdAt.Before(right.createdAt)
|
||||||
|
default:
|
||||||
|
less = left.createdAt.Before(right.createdAt)
|
||||||
|
}
|
||||||
|
if desc {
|
||||||
|
return !less
|
||||||
|
}
|
||||||
|
return less
|
||||||
|
})
|
||||||
|
|
||||||
|
total := int64(len(records))
|
||||||
|
start := int(filter.Pagination.Offset())
|
||||||
|
if start >= len(records) {
|
||||||
|
return &requests.Pager{
|
||||||
|
Pagination: filter.Pagination,
|
||||||
|
Total: total,
|
||||||
|
Items: []super_dto.SuperCouponRiskItem{},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
end := start + int(filter.Pagination.Limit)
|
||||||
|
if end > len(records) {
|
||||||
|
end = len(records)
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]super_dto.SuperCouponRiskItem, 0, end-start)
|
||||||
|
for _, record := range records[start:end] {
|
||||||
|
items = append(items, record.item)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &requests.Pager{
|
||||||
|
Pagination: filter.Pagination,
|
||||||
|
Total: total,
|
||||||
|
Items: items,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *super) UpdateCouponStatus(ctx context.Context, operatorID, couponID int64, form *super_dto.SuperCouponStatusUpdateForm) error {
|
func (s *super) UpdateCouponStatus(ctx context.Context, operatorID, couponID int64, form *super_dto.SuperCouponStatusUpdateForm) error {
|
||||||
if operatorID == 0 {
|
if operatorID == 0 {
|
||||||
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
||||||
@@ -5722,6 +6124,56 @@ func (s *super) filterCouponGrantCouponIDs(ctx context.Context, filter *super_dt
|
|||||||
return ids, true, nil
|
return ids, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *super) filterCouponRiskCouponIDs(ctx context.Context, filter *super_dto.SuperCouponRiskListFilter) ([]int64, bool, error) {
|
||||||
|
if filter == nil {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.CouponID != nil && *filter.CouponID > 0 {
|
||||||
|
return []int64{*filter.CouponID}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
couponTbl, couponQuery := models.CouponQuery.QueryContext(ctx)
|
||||||
|
applied := false
|
||||||
|
|
||||||
|
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||||||
|
applied = true
|
||||||
|
couponQuery = couponQuery.Where(couponTbl.TenantID.Eq(*filter.TenantID))
|
||||||
|
} else {
|
||||||
|
tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, true, err
|
||||||
|
}
|
||||||
|
if tenantFilter {
|
||||||
|
applied = true
|
||||||
|
if len(tenantIDs) == 0 {
|
||||||
|
return []int64{}, true, nil
|
||||||
|
}
|
||||||
|
couponQuery = couponQuery.Where(couponTbl.TenantID.In(tenantIDs...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" {
|
||||||
|
applied = true
|
||||||
|
keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%"
|
||||||
|
couponQuery = couponQuery.Where(field.Or(couponTbl.Title.Like(keyword), couponTbl.Description.Like(keyword)))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !applied {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
coupons, err := couponQuery.Select(couponTbl.ID).Find()
|
||||||
|
if err != nil {
|
||||||
|
return nil, true, errorx.ErrDatabaseError.WithCause(err)
|
||||||
|
}
|
||||||
|
ids := make([]int64, 0, len(coupons))
|
||||||
|
for _, coupon := range coupons {
|
||||||
|
ids = append(ids, coupon.ID)
|
||||||
|
}
|
||||||
|
return ids, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *super) RejectWithdrawal(ctx context.Context, operatorID, id int64, reason string) error {
|
func (s *super) RejectWithdrawal(ctx context.Context, operatorID, id int64, reason string) error {
|
||||||
if operatorID == 0 {
|
if operatorID == 0 {
|
||||||
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
||||||
|
|||||||
@@ -398,6 +398,58 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/super/v1/coupon-risks": {
|
||||||
|
"get": {
|
||||||
|
"description": "List coupon risk records across tenants",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Coupon"
|
||||||
|
],
|
||||||
|
"summary": "List coupon risks",
|
||||||
|
"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.SuperCouponRiskItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/super/v1/coupons": {
|
"/super/v1/coupons": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List coupon templates across tenants",
|
"description": "List coupon templates across tenants",
|
||||||
@@ -7326,6 +7378,95 @@ const docTemplate = `{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperCouponRiskItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"coupon_id": {
|
||||||
|
"description": "CouponID 优惠券ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"coupon_title": {
|
||||||
|
"description": "CouponTitle 优惠券标题。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"description": "CreatedAt 领取时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "ID 用户券ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"order_amount_paid": {
|
||||||
|
"description": "OrderAmountPaid 订单实付金额(分)。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"description": "OrderID 使用订单ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"order_status": {
|
||||||
|
"description": "OrderStatus 订单状态。",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.OrderStatus"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"order_status_description": {
|
||||||
|
"description": "OrderStatusDescription 订单状态描述(用于展示)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"paid_at": {
|
||||||
|
"description": "PaidAt 订单支付时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"risk_reason": {
|
||||||
|
"description": "RiskReason 异常说明。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"risk_type": {
|
||||||
|
"description": "RiskType 异常类型。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"description": "Status 用户券状态。",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.UserCouponStatus"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"used_at": {
|
||||||
|
"description": "UsedAt 使用时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"description": "UserID 用户ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"description": "Username 用户名。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperCouponStatusUpdateForm": {
|
"dto.SuperCouponStatusUpdateForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -392,6 +392,58 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"/super/v1/coupon-risks": {
|
||||||
|
"get": {
|
||||||
|
"description": "List coupon risk records across tenants",
|
||||||
|
"consumes": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"produces": [
|
||||||
|
"application/json"
|
||||||
|
],
|
||||||
|
"tags": [
|
||||||
|
"Coupon"
|
||||||
|
],
|
||||||
|
"summary": "List coupon risks",
|
||||||
|
"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.SuperCouponRiskItem"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"/super/v1/coupons": {
|
"/super/v1/coupons": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "List coupon templates across tenants",
|
"description": "List coupon templates across tenants",
|
||||||
@@ -7320,6 +7372,95 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"dto.SuperCouponRiskItem": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"coupon_id": {
|
||||||
|
"description": "CouponID 优惠券ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"coupon_title": {
|
||||||
|
"description": "CouponTitle 优惠券标题。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"created_at": {
|
||||||
|
"description": "CreatedAt 领取时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "ID 用户券ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"order_amount_paid": {
|
||||||
|
"description": "OrderAmountPaid 订单实付金额(分)。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"order_id": {
|
||||||
|
"description": "OrderID 使用订单ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"order_status": {
|
||||||
|
"description": "OrderStatus 订单状态。",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.OrderStatus"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"order_status_description": {
|
||||||
|
"description": "OrderStatusDescription 订单状态描述(用于展示)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"paid_at": {
|
||||||
|
"description": "PaidAt 订单支付时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"risk_reason": {
|
||||||
|
"description": "RiskReason 异常说明。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"risk_type": {
|
||||||
|
"description": "RiskType 异常类型。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"status": {
|
||||||
|
"description": "Status 用户券状态。",
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"$ref": "#/definitions/consts.UserCouponStatus"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
},
|
||||||
|
"used_at": {
|
||||||
|
"description": "UsedAt 使用时间(RFC3339)。",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"description": "UserID 用户ID。",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"description": "Username 用户名。",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"dto.SuperCouponStatusUpdateForm": {
|
"dto.SuperCouponStatusUpdateForm": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": [
|
"required": [
|
||||||
|
|||||||
@@ -1345,6 +1345,68 @@ definitions:
|
|||||||
description: Value 优惠券面额/折扣值。
|
description: Value 优惠券面额/折扣值。
|
||||||
type: integer
|
type: integer
|
||||||
type: object
|
type: object
|
||||||
|
dto.SuperCouponRiskItem:
|
||||||
|
properties:
|
||||||
|
coupon_id:
|
||||||
|
description: CouponID 优惠券ID。
|
||||||
|
type: integer
|
||||||
|
coupon_title:
|
||||||
|
description: CouponTitle 优惠券标题。
|
||||||
|
type: string
|
||||||
|
created_at:
|
||||||
|
description: CreatedAt 领取时间(RFC3339)。
|
||||||
|
type: string
|
||||||
|
id:
|
||||||
|
description: ID 用户券ID。
|
||||||
|
type: integer
|
||||||
|
order_amount_paid:
|
||||||
|
description: OrderAmountPaid 订单实付金额(分)。
|
||||||
|
type: integer
|
||||||
|
order_id:
|
||||||
|
description: OrderID 使用订单ID。
|
||||||
|
type: integer
|
||||||
|
order_status:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/consts.OrderStatus'
|
||||||
|
description: OrderStatus 订单状态。
|
||||||
|
order_status_description:
|
||||||
|
description: OrderStatusDescription 订单状态描述(用于展示)。
|
||||||
|
type: string
|
||||||
|
paid_at:
|
||||||
|
description: PaidAt 订单支付时间(RFC3339)。
|
||||||
|
type: string
|
||||||
|
risk_reason:
|
||||||
|
description: RiskReason 异常说明。
|
||||||
|
type: string
|
||||||
|
risk_type:
|
||||||
|
description: RiskType 异常类型。
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
allOf:
|
||||||
|
- $ref: '#/definitions/consts.UserCouponStatus'
|
||||||
|
description: Status 用户券状态。
|
||||||
|
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
|
||||||
|
used_at:
|
||||||
|
description: UsedAt 使用时间(RFC3339)。
|
||||||
|
type: string
|
||||||
|
user_id:
|
||||||
|
description: UserID 用户ID。
|
||||||
|
type: integer
|
||||||
|
username:
|
||||||
|
description: Username 用户名。
|
||||||
|
type: string
|
||||||
|
type: object
|
||||||
dto.SuperCouponStatusUpdateForm:
|
dto.SuperCouponStatusUpdateForm:
|
||||||
properties:
|
properties:
|
||||||
status:
|
status:
|
||||||
@@ -2975,6 +3037,37 @@ paths:
|
|||||||
summary: List coupon grants
|
summary: List coupon grants
|
||||||
tags:
|
tags:
|
||||||
- Coupon
|
- Coupon
|
||||||
|
/super/v1/coupon-risks:
|
||||||
|
get:
|
||||||
|
consumes:
|
||||||
|
- application/json
|
||||||
|
description: List coupon risk records 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.SuperCouponRiskItem'
|
||||||
|
type: array
|
||||||
|
type: object
|
||||||
|
summary: List coupon risks
|
||||||
|
tags:
|
||||||
|
- Coupon
|
||||||
/super/v1/coupons:
|
/super/v1/coupons:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## 1) 总体结论
|
## 1) 总体结论
|
||||||
|
|
||||||
- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录、互动(收藏/点赞/关注)与内容消费明细视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录、资产治理(列表/用量/清理)、通知中心(列表/群发/模板)。
|
- **已落地**:登录、租户/用户/订单/内容基础管理、内容审核(含批量)、平台概览(内容趋势/退款率/漏斗)、提现审核、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录、互动(收藏/点赞/关注)与内容消费明细视图、创作者申请/成员审核/邀请、优惠券创建/编辑/发放/冻结/发放记录/异常核查、资产治理(列表/用量/清理)、通知中心(列表/群发/模板)。
|
||||||
- **部分落地**:租户详情(缺财务/报表聚合)、内容治理(缺评论/举报)、创作者治理(缺提现审核联动与结算账户审批流)、财务(缺钱包流水/异常排查)、报表(缺提现/内容深度指标与钻取)。
|
- **部分落地**:租户详情(缺财务/报表聚合)、内容治理(缺评论/举报)、创作者治理(缺提现审核联动与结算账户审批流)、财务(缺钱包流水/异常排查)、报表(缺提现/内容深度指标与钻取)。
|
||||||
- **未落地**:审计与系统配置类能力。
|
- **未落地**:审计与系统配置类能力。
|
||||||
|
|
||||||
@@ -56,9 +56,9 @@
|
|||||||
- 缺口:结算账户审批流(若需要区分通过/驳回状态),创作者维度提现审核与财务联动入口。
|
- 缺口:结算账户审批流(若需要区分通过/驳回状态),创作者维度提现审核与财务联动入口。
|
||||||
|
|
||||||
### 2.10 优惠券 `/superadmin/coupons`
|
### 2.10 优惠券 `/superadmin/coupons`
|
||||||
- 状态:**部分完成**
|
- 状态:**已完成**
|
||||||
- 已有:跨租户优惠券列表、创建/编辑、发放、冻结、发放记录查询。
|
- 已有:跨租户优惠券列表、创建/编辑、发放、冻结、发放记录查询、异常核查。
|
||||||
- 缺口:异常核查与自动告警策略。
|
- 缺口:无显著功能缺口。
|
||||||
|
|
||||||
### 2.11 财务与钱包 `/superadmin/finance`
|
### 2.11 财务与钱包 `/superadmin/finance`
|
||||||
- 状态:**部分完成**
|
- 状态:**部分完成**
|
||||||
@@ -83,10 +83,10 @@
|
|||||||
## 3) `/super/v1` 接口覆盖度概览
|
## 3) `/super/v1` 接口覆盖度概览
|
||||||
|
|
||||||
- **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包/通知/优惠券/实名/互动/内容消费)、Contents、Orders、Withdrawals、Reports、Coupons(列表/创建/编辑/发放/冻结/记录)、Creators(列表/申请/成员审核)、Payout Accounts(列表/删除)、Assets(列表/用量/删除)、Notifications(列表/群发/模板)。
|
- **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包/通知/优惠券/实名/互动/内容消费)、Contents、Orders、Withdrawals、Reports、Coupons(列表/创建/编辑/发放/冻结/记录)、Creators(列表/申请/成员审核)、Payout Accounts(列表/删除)、Assets(列表/用量/删除)、Notifications(列表/群发/模板)。
|
||||||
- **缺失/待补**:创作者提现审核、优惠券异常核查与风控、审计与系统配置类能力。
|
- **缺失/待补**:创作者提现审核、审计与系统配置类能力。
|
||||||
|
|
||||||
## 4) 建议的下一步(按优先级)
|
## 4) 建议的下一步(按优先级)
|
||||||
|
|
||||||
1. **优惠券异常核查**:完善发放/核销异常监测与风控处理流程。
|
1. **报表深化**:补齐提现/内容维度指标与多维钻取能力。
|
||||||
2. **报表深化**:补齐提现/内容维度指标与多维钻取能力。
|
2. **审计与系统配置**:完善全量操作审计与系统级配置能力。
|
||||||
3. **审计与系统配置**:完善全量操作审计与系统级配置能力。
|
3. **创作者提现审核**:补齐跨租户提现审核与财务联动入口。
|
||||||
|
|||||||
@@ -109,6 +109,45 @@ export const CouponService = {
|
|||||||
total: data?.total ?? 0,
|
total: data?.total ?? 0,
|
||||||
items: normalizeItems(data?.items)
|
items: normalizeItems(data?.items)
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
async listCouponRisks({ page, limit, risk_type, coupon_id, tenant_id, tenant_code, tenant_name, keyword, user_id, username, status, order_status, created_at_from, created_at_to, used_at_from, used_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,
|
||||||
|
risk_type,
|
||||||
|
coupon_id,
|
||||||
|
tenant_id,
|
||||||
|
tenant_code,
|
||||||
|
tenant_name,
|
||||||
|
keyword,
|
||||||
|
user_id,
|
||||||
|
username,
|
||||||
|
status,
|
||||||
|
order_status,
|
||||||
|
created_at_from: iso(created_at_from),
|
||||||
|
created_at_to: iso(created_at_to),
|
||||||
|
used_at_from: iso(used_at_from),
|
||||||
|
used_at_to: iso(used_at_to)
|
||||||
|
};
|
||||||
|
if (sortField && sortOrder) {
|
||||||
|
if (sortOrder === 1) query.asc = sortField;
|
||||||
|
if (sortOrder === -1) query.desc = sortField;
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await requestJson('/super/v1/coupon-risks', { query });
|
||||||
|
return {
|
||||||
|
page: data?.page ?? page ?? 1,
|
||||||
|
limit: data?.limit ?? limit ?? 10,
|
||||||
|
total: data?.total ?? 0,
|
||||||
|
items: normalizeItems(data?.items)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,28 @@ const grantCreatedAtTo = ref(null);
|
|||||||
const grantUsedAtFrom = ref(null);
|
const grantUsedAtFrom = ref(null);
|
||||||
const grantUsedAtTo = ref(null);
|
const grantUsedAtTo = ref(null);
|
||||||
|
|
||||||
|
const risks = ref([]);
|
||||||
|
const risksLoading = ref(false);
|
||||||
|
const risksTotal = ref(0);
|
||||||
|
const risksPage = ref(1);
|
||||||
|
const risksRows = ref(10);
|
||||||
|
const riskType = ref('');
|
||||||
|
const riskCouponID = ref(null);
|
||||||
|
const riskTenantID = ref(null);
|
||||||
|
const riskTenantCode = ref('');
|
||||||
|
const riskTenantName = ref('');
|
||||||
|
const riskKeyword = ref('');
|
||||||
|
const riskUserID = ref(null);
|
||||||
|
const riskUsername = ref('');
|
||||||
|
const riskStatus = ref('');
|
||||||
|
const riskOrderStatus = ref('');
|
||||||
|
const riskCreatedAtFrom = ref(null);
|
||||||
|
const riskCreatedAtTo = ref(null);
|
||||||
|
const riskUsedAtFrom = ref(null);
|
||||||
|
const riskUsedAtTo = ref(null);
|
||||||
|
const riskSortField = ref('created_at');
|
||||||
|
const riskSortOrder = ref(-1);
|
||||||
|
|
||||||
const typeOptions = [
|
const typeOptions = [
|
||||||
{ label: '全部', value: '' },
|
{ label: '全部', value: '' },
|
||||||
{ label: '固定金额', value: 'fix_amount' },
|
{ label: '固定金额', value: 'fix_amount' },
|
||||||
@@ -65,6 +87,27 @@ const grantStatusOptions = [
|
|||||||
{ label: '已过期', value: 'expired' }
|
{ label: '已过期', value: 'expired' }
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const riskStatusOptions = grantStatusOptions;
|
||||||
|
|
||||||
|
const riskTypeOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '已核销无订单', value: 'used_without_order' },
|
||||||
|
{ label: '订单状态不匹配', value: 'order_status_mismatch' },
|
||||||
|
{ label: '核销超出有效期', value: 'used_outside_window' },
|
||||||
|
{ label: '未使用但有订单/时间', value: 'unused_has_order_or_used_at' },
|
||||||
|
{ label: '重复领券', value: 'duplicate_grant' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const orderStatusOptions = [
|
||||||
|
{ label: '全部', value: '' },
|
||||||
|
{ label: '已创建', value: 'created' },
|
||||||
|
{ label: '已支付', value: 'paid' },
|
||||||
|
{ label: '退款中', value: 'refunding' },
|
||||||
|
{ label: '已退款', value: 'refunded' },
|
||||||
|
{ label: '已取消', value: 'canceled' },
|
||||||
|
{ label: '失败', value: 'failed' }
|
||||||
|
];
|
||||||
|
|
||||||
const editDialogVisible = ref(false);
|
const editDialogVisible = ref(false);
|
||||||
const couponSubmitting = ref(false);
|
const couponSubmitting = ref(false);
|
||||||
const editingCoupon = ref(null);
|
const editingCoupon = ref(null);
|
||||||
@@ -135,6 +178,26 @@ function getGrantStatusSeverity(value) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getOrderStatusSeverity(value) {
|
||||||
|
switch (value) {
|
||||||
|
case 'paid':
|
||||||
|
return 'success';
|
||||||
|
case 'created':
|
||||||
|
case 'refunding':
|
||||||
|
return 'warn';
|
||||||
|
case 'failed':
|
||||||
|
case 'canceled':
|
||||||
|
return 'danger';
|
||||||
|
default:
|
||||||
|
return 'secondary';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function resolveRiskTypeLabel(value) {
|
||||||
|
const option = riskTypeOptions.find((item) => item.value === value);
|
||||||
|
return option?.label || value || '-';
|
||||||
|
}
|
||||||
|
|
||||||
function resetCouponForm() {
|
function resetCouponForm() {
|
||||||
formTenantID.value = null;
|
formTenantID.value = null;
|
||||||
formTitle.value = '';
|
formTitle.value = '';
|
||||||
@@ -315,6 +378,38 @@ async function loadGrants() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadRisks() {
|
||||||
|
risksLoading.value = true;
|
||||||
|
try {
|
||||||
|
const result = await CouponService.listCouponRisks({
|
||||||
|
page: risksPage.value,
|
||||||
|
limit: risksRows.value,
|
||||||
|
risk_type: riskType.value || undefined,
|
||||||
|
coupon_id: riskCouponID.value || undefined,
|
||||||
|
tenant_id: riskTenantID.value || undefined,
|
||||||
|
tenant_code: riskTenantCode.value,
|
||||||
|
tenant_name: riskTenantName.value,
|
||||||
|
keyword: riskKeyword.value,
|
||||||
|
user_id: riskUserID.value || undefined,
|
||||||
|
username: riskUsername.value,
|
||||||
|
status: riskStatus.value || undefined,
|
||||||
|
order_status: riskOrderStatus.value || undefined,
|
||||||
|
created_at_from: riskCreatedAtFrom.value || undefined,
|
||||||
|
created_at_to: riskCreatedAtTo.value || undefined,
|
||||||
|
used_at_from: riskUsedAtFrom.value || undefined,
|
||||||
|
used_at_to: riskUsedAtTo.value || undefined,
|
||||||
|
sortField: riskSortField.value,
|
||||||
|
sortOrder: riskSortOrder.value
|
||||||
|
});
|
||||||
|
risks.value = result.items;
|
||||||
|
risksTotal.value = result.total;
|
||||||
|
} catch (error) {
|
||||||
|
toast.add({ severity: 'error', summary: '加载失败', detail: error?.message || '无法加载异常记录', life: 4000 });
|
||||||
|
} finally {
|
||||||
|
risksLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function onSearch() {
|
function onSearch() {
|
||||||
page.value = 1;
|
page.value = 1;
|
||||||
loadCoupons();
|
loadCoupons();
|
||||||
@@ -325,6 +420,11 @@ function onGrantSearch() {
|
|||||||
loadGrants();
|
loadGrants();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onRiskSearch() {
|
||||||
|
risksPage.value = 1;
|
||||||
|
loadRisks();
|
||||||
|
}
|
||||||
|
|
||||||
function onReset() {
|
function onReset() {
|
||||||
couponID.value = null;
|
couponID.value = null;
|
||||||
tenantID.value = null;
|
tenantID.value = null;
|
||||||
@@ -359,6 +459,28 @@ function onGrantReset() {
|
|||||||
loadGrants();
|
loadGrants();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onRiskReset() {
|
||||||
|
riskType.value = '';
|
||||||
|
riskCouponID.value = null;
|
||||||
|
riskTenantID.value = null;
|
||||||
|
riskTenantCode.value = '';
|
||||||
|
riskTenantName.value = '';
|
||||||
|
riskKeyword.value = '';
|
||||||
|
riskUserID.value = null;
|
||||||
|
riskUsername.value = '';
|
||||||
|
riskStatus.value = '';
|
||||||
|
riskOrderStatus.value = '';
|
||||||
|
riskCreatedAtFrom.value = null;
|
||||||
|
riskCreatedAtTo.value = null;
|
||||||
|
riskUsedAtFrom.value = null;
|
||||||
|
riskUsedAtTo.value = null;
|
||||||
|
riskSortField.value = 'created_at';
|
||||||
|
riskSortOrder.value = -1;
|
||||||
|
risksPage.value = 1;
|
||||||
|
risksRows.value = 10;
|
||||||
|
loadRisks();
|
||||||
|
}
|
||||||
|
|
||||||
function onPage(event) {
|
function onPage(event) {
|
||||||
page.value = (event.page ?? 0) + 1;
|
page.value = (event.page ?? 0) + 1;
|
||||||
rows.value = event.rows ?? rows.value;
|
rows.value = event.rows ?? rows.value;
|
||||||
@@ -371,15 +493,28 @@ function onGrantPage(event) {
|
|||||||
loadGrants();
|
loadGrants();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onRiskPage(event) {
|
||||||
|
risksPage.value = (event.page ?? 0) + 1;
|
||||||
|
risksRows.value = event.rows ?? risksRows.value;
|
||||||
|
loadRisks();
|
||||||
|
}
|
||||||
|
|
||||||
function onSort(event) {
|
function onSort(event) {
|
||||||
sortField.value = event.sortField ?? sortField.value;
|
sortField.value = event.sortField ?? sortField.value;
|
||||||
sortOrder.value = event.sortOrder ?? sortOrder.value;
|
sortOrder.value = event.sortOrder ?? sortOrder.value;
|
||||||
loadCoupons();
|
loadCoupons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onRiskSort(event) {
|
||||||
|
riskSortField.value = event.sortField ?? riskSortField.value;
|
||||||
|
riskSortOrder.value = event.sortOrder ?? riskSortOrder.value;
|
||||||
|
loadRisks();
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loadCoupons();
|
loadCoupons();
|
||||||
loadGrants();
|
loadGrants();
|
||||||
|
loadRisks();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -389,6 +524,7 @@ onMounted(() => {
|
|||||||
<TabList>
|
<TabList>
|
||||||
<Tab value="coupons">券模板</Tab>
|
<Tab value="coupons">券模板</Tab>
|
||||||
<Tab value="grants">发放记录</Tab>
|
<Tab value="grants">发放记录</Tab>
|
||||||
|
<Tab value="risks">异常核查</Tab>
|
||||||
</TabList>
|
</TabList>
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<TabPanel value="coupons">
|
<TabPanel value="coupons">
|
||||||
@@ -625,6 +761,151 @@ onMounted(() => {
|
|||||||
</Column>
|
</Column>
|
||||||
</DataTable>
|
</DataTable>
|
||||||
</TabPanel>
|
</TabPanel>
|
||||||
|
<TabPanel value="risks">
|
||||||
|
<div class="flex items-center justify-between mb-4">
|
||||||
|
<h4 class="m-0">异常核查</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<SearchPanel :loading="risksLoading" @search="onRiskSearch" @reset="onRiskReset">
|
||||||
|
<SearchField label="异常类型">
|
||||||
|
<Select v-model="riskType" :options="riskTypeOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="CouponID">
|
||||||
|
<InputNumber v-model="riskCouponID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="TenantID">
|
||||||
|
<InputNumber v-model="riskTenantID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="Tenant Code">
|
||||||
|
<InputText v-model="riskTenantCode" placeholder="模糊匹配" class="w-full" @keyup.enter="onRiskSearch" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="Tenant Name">
|
||||||
|
<InputText v-model="riskTenantName" placeholder="模糊匹配" class="w-full" @keyup.enter="onRiskSearch" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="关键词">
|
||||||
|
<IconField>
|
||||||
|
<InputIcon>
|
||||||
|
<i class="pi pi-search" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText v-model="riskKeyword" placeholder="标题/描述" class="w-full" @keyup.enter="onRiskSearch" />
|
||||||
|
</IconField>
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="UserID">
|
||||||
|
<InputNumber v-model="riskUserID" :min="1" placeholder="精确匹配" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="Username">
|
||||||
|
<IconField>
|
||||||
|
<InputIcon>
|
||||||
|
<i class="pi pi-search" />
|
||||||
|
</InputIcon>
|
||||||
|
<InputText v-model="riskUsername" placeholder="模糊匹配" class="w-full" @keyup.enter="onRiskSearch" />
|
||||||
|
</IconField>
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="券状态">
|
||||||
|
<Select v-model="riskStatus" :options="riskStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="订单状态">
|
||||||
|
<Select v-model="riskOrderStatus" :options="orderStatusOptions" optionLabel="label" optionValue="value" placeholder="请选择" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="领取时间 From">
|
||||||
|
<DatePicker v-model="riskCreatedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="领取时间 To">
|
||||||
|
<DatePicker v-model="riskCreatedAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="使用时间 From">
|
||||||
|
<DatePicker v-model="riskUsedAtFrom" showIcon showButtonBar placeholder="开始时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
<SearchField label="使用时间 To">
|
||||||
|
<DatePicker v-model="riskUsedAtTo" showIcon showButtonBar placeholder="结束时间" class="w-full" />
|
||||||
|
</SearchField>
|
||||||
|
</SearchPanel>
|
||||||
|
|
||||||
|
<DataTable
|
||||||
|
:value="risks"
|
||||||
|
dataKey="id"
|
||||||
|
:loading="risksLoading"
|
||||||
|
lazy
|
||||||
|
:paginator="true"
|
||||||
|
:rows="risksRows"
|
||||||
|
:totalRecords="risksTotal"
|
||||||
|
:first="(risksPage - 1) * risksRows"
|
||||||
|
:rowsPerPageOptions="[10, 20, 50, 100]"
|
||||||
|
sortMode="single"
|
||||||
|
:sortField="riskSortField"
|
||||||
|
:sortOrder="riskSortOrder"
|
||||||
|
@page="onRiskPage"
|
||||||
|
@sort="onRiskSort"
|
||||||
|
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: 8rem" />
|
||||||
|
<Column header="异常类型" style="min-width: 18rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<Tag :value="resolveRiskTypeLabel(data.risk_type)" severity="danger" class="mb-1" />
|
||||||
|
<span class="text-xs text-muted-color">{{ data.risk_reason || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="优惠券" style="min-width: 16rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="font-medium">{{ data.coupon_title || '-' }}</span>
|
||||||
|
<span class="text-xs text-muted-color">CouponID: {{ data.coupon_id ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<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 || '-' }} / ID: {{ data.tenant_id ?? '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="用户" style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<router-link v-if="data.user_id" class="inline-flex items-center gap-1 font-medium text-primary hover:underline" :to="`/superadmin/users/${data.user_id}`">
|
||||||
|
<span class="truncate max-w-[200px]">{{ data.username || `ID:${data.user_id}` }}</span>
|
||||||
|
<i class="pi pi-external-link text-xs" />
|
||||||
|
</router-link>
|
||||||
|
<div class="text-xs text-muted-color">ID: {{ data.user_id ?? '-' }}</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="status" header="券状态" style="min-width: 10rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<Tag :value="data.status_description || data.status || '-'" :severity="getGrantStatusSeverity(data.status)" />
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column header="订单信息" style="min-width: 18rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
<div class="flex flex-col gap-1">
|
||||||
|
<div>
|
||||||
|
<span class="text-xs text-muted-color">OrderID:</span>
|
||||||
|
<span class="ml-1">{{ data.order_id ? data.order_id : '-' }}</span>
|
||||||
|
</div>
|
||||||
|
<Tag :value="data.order_status_description || data.order_status || '-'" :severity="getOrderStatusSeverity(data.order_status)" />
|
||||||
|
<div class="text-xs text-muted-color">实付:{{ formatCny(data.order_amount_paid) }}</div>
|
||||||
|
<div class="text-xs text-muted-color">支付时间:{{ formatDate(data.paid_at) }}</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="used_at" header="使用时间" sortable style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.used_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
<Column field="created_at" header="领取时间" sortable style="min-width: 14rem">
|
||||||
|
<template #body="{ data }">
|
||||||
|
{{ formatDate(data.created_at) }}
|
||||||
|
</template>
|
||||||
|
</Column>
|
||||||
|
</DataTable>
|
||||||
|
</TabPanel>
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user