feat: add coupon risk review

This commit is contained in:
2026-01-15 17:01:36 +08:00
parent c8ec0af07f
commit ba1d120c84
10 changed files with 1255 additions and 8 deletions

View File

@@ -45,6 +45,22 @@ func (c *coupons) ListGrants(ctx fiber.Ctx, filter *dto.SuperCouponGrantListFilt
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
//
// @Router /super/v1/coupons/:id<int>/status [patch]

View File

@@ -142,3 +142,82 @@ type SuperCouponGrantItem struct {
// CreatedAt 领取时间RFC3339
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"`
}

View File

@@ -114,6 +114,11 @@ func (r *Routes) Register(router fiber.Router) {
r.coupons.ListGrants,
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")
router.Get("/super/v1/coupons"[len(r.Path()):], DataFunc1(
r.coupons.List,

View File

@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"path/filepath"
"sort"
"strconv"
"strings"
"time"
@@ -5087,6 +5088,407 @@ func (s *super) ListCouponGrants(ctx context.Context, filter *super_dto.SuperCou
}, 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 {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
@@ -5722,6 +6124,56 @@ func (s *super) filterCouponGrantCouponIDs(ctx context.Context, filter *super_dt
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 {
if operatorID == 0 {
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")

View File

@@ -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": {
"get": {
"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": {
"type": "object",
"required": [

View File

@@ -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": {
"get": {
"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": {
"type": "object",
"required": [

View File

@@ -1345,6 +1345,68 @@ definitions:
description: Value 优惠券面额/折扣值。
type: integer
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:
properties:
status:
@@ -2975,6 +3037,37 @@ paths:
summary: List coupon grants
tags:
- 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:
get:
consumes: