Files
quyun-v2/backend/app/services/super.go

895 lines
26 KiB
Go

package services
import (
"context"
"errors"
"time"
"quyun/v2/app/errorx"
super_dto "quyun/v2/app/http/super/v1/dto"
v1_dto "quyun/v2/app/http/v1/dto"
"quyun/v2/app/requests"
"quyun/v2/database/models"
"quyun/v2/pkg/consts"
jwt_provider "quyun/v2/providers/jwt"
"github.com/google/uuid"
"github.com/spf13/cast"
"go.ipao.vip/gen/types"
"gorm.io/gorm"
)
// @provider
type super struct {
jwt *jwt_provider.JWT
}
func (s *super) Login(ctx context.Context, form *super_dto.LoginForm) (*super_dto.LoginResponse, error) {
tbl, q := models.UserQuery.QueryContext(ctx)
u, err := q.Where(tbl.Username.Eq(form.Username)).First()
if err != nil {
return nil, errorx.ErrInvalidCredentials.WithMsg("账号或密码错误")
}
if u.Password != form.Password {
return nil, errorx.ErrInvalidCredentials.WithMsg("账号或密码错误")
}
if u.Status == consts.UserStatusBanned {
return nil, errorx.ErrAccountDisabled
}
if !hasRole(u.Roles, consts.RoleSuperAdmin) {
return nil, errorx.ErrForbidden.WithMsg("无权限访问")
}
token, err := s.jwt.CreateToken(s.jwt.CreateClaims(jwt_provider.BaseClaims{
UserID: u.ID,
}))
if err != nil {
return nil, errorx.ErrInternalError.WithMsg("生成令牌失败")
}
return &super_dto.LoginResponse{
Token: token,
User: s.toSuperUserDTO(u),
}, nil
}
func (s *super) CheckToken(ctx context.Context, token string) (*super_dto.LoginResponse, error) {
if token == "" {
return nil, errorx.ErrUnauthorized.WithMsg("Missing token")
}
claims, err := s.jwt.Parse(token)
if err != nil {
return nil, errorx.ErrUnauthorized.WithCause(err)
}
tbl, q := models.UserQuery.QueryContext(ctx)
u, err := q.Where(tbl.ID.Eq(claims.UserID)).First()
if err != nil {
return nil, errorx.ErrUnauthorized.WithMsg("UserNotFound")
}
if !hasRole(u.Roles, consts.RoleSuperAdmin) {
return nil, errorx.ErrForbidden.WithMsg("无权限访问")
}
newToken, err := s.jwt.CreateTokenByOldToken(token, s.jwt.CreateClaims(jwt_provider.BaseClaims{
UserID: u.ID,
}))
if err != nil {
return nil, errorx.ErrInternalError.WithMsg("生成令牌失败")
}
return &super_dto.LoginResponse{
Token: newToken,
User: s.toSuperUserDTO(u),
}, nil
}
func (s *super) ListUsers(ctx context.Context, filter *super_dto.UserListFilter) (*requests.Pager, error) {
tbl, q := models.UserQuery.QueryContext(ctx)
if filter.Username != nil && *filter.Username != "" {
q = q.Where(tbl.Username.Like("%" + *filter.Username + "%")).Or(tbl.Nickname.Like("%" + *filter.Username + "%"))
}
filter.Pagination.Format()
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Order(tbl.ID.Desc()).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
var data []super_dto.UserItem
for _, u := range list {
data = append(data, super_dto.UserItem{
SuperUserLite: super_dto.SuperUserLite{
ID: u.ID,
Username: u.Username,
Roles: u.Roles,
Status: u.Status,
StatusDescription: u.Status.Description(),
CreatedAt: u.CreatedAt.Format(time.RFC3339),
UpdatedAt: u.UpdatedAt.Format(time.RFC3339),
},
Balance: u.Balance,
BalanceFrozen: u.BalanceFrozen,
})
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: data,
}, nil
}
func (s *super) GetUser(ctx context.Context, id int64) (*super_dto.UserItem, error) {
tbl, q := models.UserQuery.QueryContext(ctx)
u, err := q.Where(tbl.ID.Eq(id)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound
}
return nil, errorx.ErrDatabaseError.WithCause(err)
}
return &super_dto.UserItem{
SuperUserLite: super_dto.SuperUserLite{
ID: u.ID,
Username: u.Username,
Roles: u.Roles,
Status: u.Status,
StatusDescription: u.Status.Description(),
CreatedAt: u.CreatedAt.Format(time.RFC3339),
UpdatedAt: u.UpdatedAt.Format(time.RFC3339),
},
Balance: u.Balance,
BalanceFrozen: u.BalanceFrozen,
}, nil
}
func (s *super) UpdateUserStatus(ctx context.Context, id int64, form *super_dto.UserStatusUpdateForm) error {
tbl, q := models.UserQuery.QueryContext(ctx)
_, err := q.Where(tbl.ID.Eq(id)).Update(tbl.Status, consts.UserStatus(form.Status))
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
func (s *super) UpdateUserRoles(ctx context.Context, id int64, form *super_dto.UserRolesUpdateForm) error {
var roles types.Array[consts.Role]
for _, r := range form.Roles {
roles = append(roles, r)
}
tbl, q := models.UserQuery.QueryContext(ctx)
_, err := q.Where(tbl.ID.Eq(id)).Update(tbl.Roles, roles)
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
func (s *super) ListTenants(ctx context.Context, filter *super_dto.TenantListFilter) (*requests.Pager, error) {
tbl, q := models.TenantQuery.QueryContext(ctx)
if filter.Name != nil && *filter.Name != "" {
q = q.Where(tbl.Name.Like("%" + *filter.Name + "%"))
}
filter.Pagination.Format()
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Order(tbl.ID.Desc()).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
var data []super_dto.TenantItem
for _, t := range list {
data = append(data, super_dto.TenantItem{
ID: t.ID,
UUID: t.UUID.String(),
Name: t.Name,
Code: t.Code,
Status: t.Status,
StatusDescription: t.Status.Description(),
UserID: t.UserID,
CreatedAt: t.CreatedAt.Format(time.RFC3339),
UpdatedAt: t.UpdatedAt.Format(time.RFC3339),
})
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: data,
}, nil
}
func (s *super) CreateTenant(ctx context.Context, form *super_dto.TenantCreateForm) error {
uid := form.AdminUserID
if _, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(uid)).First(); err != nil {
return errorx.ErrRecordNotFound.WithMsg("用户不存在")
}
t := &models.Tenant{
UserID: uid,
Name: form.Name,
Code: form.Code,
UUID: types.UUID(uuid.New()),
Status: consts.TenantStatusVerified,
}
if err := models.TenantQuery.WithContext(ctx).Create(t); err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
func (s *super) GetTenant(ctx context.Context, id int64) (*super_dto.TenantItem, error) {
tbl, q := models.TenantQuery.QueryContext(ctx)
t, err := q.Where(tbl.ID.Eq(id)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound
}
return nil, errorx.ErrDatabaseError.WithCause(err)
}
return &super_dto.TenantItem{
ID: t.ID,
UUID: t.UUID.String(),
Name: t.Name,
Code: t.Code,
Status: t.Status,
StatusDescription: t.Status.Description(),
UserID: t.UserID,
CreatedAt: t.CreatedAt.Format(time.RFC3339),
UpdatedAt: t.UpdatedAt.Format(time.RFC3339),
}, nil
}
func (s *super) UpdateTenantStatus(ctx context.Context, id int64, form *super_dto.TenantStatusUpdateForm) error {
tbl, q := models.TenantQuery.QueryContext(ctx)
_, err := q.Where(tbl.ID.Eq(id)).Update(tbl.Status, consts.TenantStatus(form.Status))
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
func (s *super) UpdateTenantExpire(ctx context.Context, id int64, form *super_dto.TenantExpireUpdateForm) error {
expire := time.Now().AddDate(0, 0, form.Duration)
tbl, q := models.TenantQuery.QueryContext(ctx)
_, err := q.Where(tbl.ID.Eq(id)).Update(tbl.ExpiredAt, expire)
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
func (s *super) ListContents(ctx context.Context, filter *super_dto.SuperContentListFilter) (*requests.Pager, error) {
tbl, q := models.ContentQuery.QueryContext(ctx)
filter.Pagination.Format()
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
var list []*models.Content
err = q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Order(tbl.ID.Desc()).
UnderlyingDB().
Preload("Author").
Preload("ContentAssets.Asset").
Find(&list).Error
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
priceMap, err := s.contentPriceMap(ctx, list)
if err != nil {
return nil, err
}
tenantMap, err := s.contentTenantMap(ctx, list)
if err != nil {
return nil, err
}
data := make([]super_dto.AdminContentItem, 0, len(list))
for _, c := range list {
data = append(data, s.toSuperContentItem(c, priceMap[c.ID], tenantMap[c.TenantID]))
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: data,
}, nil
}
func (s *super) UpdateContentStatus(ctx context.Context, tenantID, contentID int64, form *super_dto.SuperTenantContentStatusUpdateForm) error {
tbl, q := models.ContentQuery.QueryContext(ctx)
_, err := q.Where(tbl.ID.Eq(contentID), tbl.TenantID.Eq(tenantID)).Update(tbl.Status, consts.ContentStatus(form.Status))
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
return nil
}
func (s *super) ListOrders(ctx context.Context, filter *super_dto.SuperOrderListFilter) (*requests.Pager, error) {
tbl, q := models.OrderQuery.QueryContext(ctx)
filter.Pagination.Format()
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Order(tbl.ID.Desc()).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
items, err := s.buildSuperOrderItems(ctx, list)
if err != nil {
return nil, err
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: items,
}, nil
}
func (s *super) GetOrder(ctx context.Context, id int64) (*super_dto.SuperOrderDetail, error) {
o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, errorx.ErrRecordNotFound
}
return nil, errorx.ErrDatabaseError.WithCause(err)
}
var tenant *models.Tenant
if t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(o.TenantID)).First(); err == nil {
tenant = t
}
var buyer *models.User
if u, err := models.UserQuery.WithContext(ctx).Where(models.UserQuery.ID.Eq(o.UserID)).First(); err == nil {
buyer = u
}
itemTbl, itemQ := models.OrderItemQuery.QueryContext(ctx)
orderItems, err := itemQ.Where(itemTbl.OrderID.Eq(o.ID)).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
items := make([]super_dto.SuperOrderItemLine, 0, len(orderItems))
for _, it := range orderItems {
items = append(items, s.toSuperOrderItemLine(it))
}
item := s.toSuperOrderItem(o, tenant, buyer)
item.Snapshot = o.Snapshot.Data()
item.Items = items
return &super_dto.SuperOrderDetail{
Order: &item,
Tenant: item.Tenant,
Buyer: item.Buyer,
}, nil
}
func (s *super) RefundOrder(ctx context.Context, id int64, form *super_dto.SuperOrderRefundForm) error {
o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).First()
if err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
return errorx.ErrRecordNotFound
}
return errorx.ErrDatabaseError.WithCause(err)
}
if o.Status != consts.OrderStatusRefunding {
if !form.Force {
return errorx.ErrStatusConflict.WithMsg("订单状态不是退款中")
}
_, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).Updates(&models.Order{
Status: consts.OrderStatusRefunding,
RefundReason: form.Reason,
UpdatedAt: time.Now(),
})
if err != nil {
return errorx.ErrDatabaseError.WithCause(err)
}
}
t, err := models.TenantQuery.WithContext(ctx).Where(models.TenantQuery.ID.Eq(o.TenantID)).First()
if err != nil {
return errorx.ErrRecordNotFound.WithMsg("租户不存在")
}
return Creator.ProcessRefund(ctx, t.UserID, id, &v1_dto.RefundForm{
Action: "accept",
Reason: form.Reason,
})
}
func (s *super) OrderStatistics(ctx context.Context) (*super_dto.OrderStatisticsResponse, error) {
var totals struct {
TotalCount int64 `gorm:"column:total_count"`
TotalAmountPaidSum int64 `gorm:"column:total_amount_paid_sum"`
}
err := models.OrderQuery.WithContext(ctx).
UnderlyingDB().
Model(&models.Order{}).
Select("count(*) as total_count, coalesce(sum(amount_paid), 0) as total_amount_paid_sum").
Scan(&totals).Error
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
var rows []struct {
Status consts.OrderStatus `gorm:"column:status"`
Count int64 `gorm:"column:count"`
AmountPaidSum int64 `gorm:"column:amount_paid_sum"`
}
err = models.OrderQuery.WithContext(ctx).
UnderlyingDB().
Model(&models.Order{}).
Select("status, count(*) as count, coalesce(sum(amount_paid), 0) as amount_paid_sum").
Group("status").
Scan(&rows).Error
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
stats := make([]super_dto.OrderStatisticsRow, 0, len(rows))
for _, row := range rows {
stats = append(stats, super_dto.OrderStatisticsRow{
Status: row.Status,
StatusDescription: row.Status.Description(),
Count: row.Count,
AmountPaidSum: row.AmountPaidSum,
})
}
return &super_dto.OrderStatisticsResponse{
TotalCount: totals.TotalCount,
TotalAmountPaidSum: totals.TotalAmountPaidSum,
ByStatus: stats,
}, nil
}
func (s *super) UserStatistics(ctx context.Context) ([]super_dto.UserStatistics, error) {
var rows []struct {
Status consts.UserStatus `gorm:"column:status"`
Count int64 `gorm:"column:count"`
}
err := models.UserQuery.WithContext(ctx).
UnderlyingDB().
Model(&models.User{}).
Select("status, count(*) as count").
Group("status").
Scan(&rows).Error
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
stats := make([]super_dto.UserStatistics, 0, len(rows))
for _, row := range rows {
stats = append(stats, super_dto.UserStatistics{
Status: row.Status,
StatusDescription: row.Status.Description(),
Count: row.Count,
})
}
return stats, nil
}
func (s *super) UserStatuses(ctx context.Context) ([]requests.KV, error) {
return consts.UserStatusItems(), nil
}
func (s *super) TenantStatuses(ctx context.Context) ([]requests.KV, error) {
return consts.TenantStatusItems(), nil
}
func (s *super) toSuperUserDTO(u *models.User) *super_dto.User {
return &super_dto.User{
ID: u.ID,
Phone: u.Phone,
Nickname: u.Nickname,
Avatar: u.Avatar,
Gender: u.Gender,
Bio: u.Bio,
Balance: float64(u.Balance) / 100.0,
Points: u.Points,
IsRealNameVerified: u.IsRealNameVerified,
}
}
func hasRole(roles types.Array[consts.Role], role consts.Role) bool {
for _, r := range roles {
if r == role {
return true
}
}
return false
}
func (s *super) buildSuperOrderItems(ctx context.Context, orders []*models.Order) ([]super_dto.SuperOrderItem, error) {
if len(orders) == 0 {
return []super_dto.SuperOrderItem{}, nil
}
tenantIDs := make([]int64, 0, len(orders))
userIDs := make([]int64, 0, len(orders))
tenantSet := make(map[int64]struct{})
userSet := make(map[int64]struct{})
for _, o := range orders {
if _, ok := tenantSet[o.TenantID]; !ok {
tenantSet[o.TenantID] = struct{}{}
tenantIDs = append(tenantIDs, o.TenantID)
}
if _, ok := userSet[o.UserID]; !ok {
userSet[o.UserID] = struct{}{}
userIDs = append(userIDs, o.UserID)
}
}
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
}
}
userMap := make(map[int64]*models.User, len(userIDs))
if len(userIDs) > 0 {
tbl, q := models.UserQuery.QueryContext(ctx)
users, err := q.Where(tbl.ID.In(userIDs...)).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
for _, u := range users {
userMap[u.ID] = u
}
}
items := make([]super_dto.SuperOrderItem, 0, len(orders))
for _, o := range orders {
items = append(items, s.toSuperOrderItem(o, tenantMap[o.TenantID], userMap[o.UserID]))
}
return items, nil
}
func (s *super) toSuperOrderItem(o *models.Order, tenant *models.Tenant, buyer *models.User) super_dto.SuperOrderItem {
item := super_dto.SuperOrderItem{
ID: o.ID,
Type: o.Type,
Status: o.Status,
StatusDescription: o.Status.Description(),
Currency: o.Currency,
AmountOriginal: o.AmountOriginal,
AmountDiscount: o.AmountDiscount,
AmountPaid: o.AmountPaid,
CreatedAt: o.CreatedAt.Format(time.RFC3339),
UpdatedAt: o.UpdatedAt.Format(time.RFC3339),
}
if !o.PaidAt.IsZero() {
item.PaidAt = o.PaidAt.Format(time.RFC3339)
}
if !o.RefundedAt.IsZero() {
item.RefundedAt = o.RefundedAt.Format(time.RFC3339)
}
if tenant != nil {
item.Tenant = &super_dto.OrderTenantLite{
ID: tenant.ID,
Code: tenant.Code,
Name: tenant.Name,
}
}
if buyer != nil {
item.Buyer = &super_dto.OrderBuyerLite{
ID: buyer.ID,
Username: buyer.Username,
}
}
return item
}
func (s *super) toSuperOrderItemLine(item *models.OrderItem) super_dto.SuperOrderItemLine {
return super_dto.SuperOrderItemLine{
ID: item.ID,
ContentID: item.ContentID,
AmountPaid: item.AmountPaid,
Snapshot: item.Snapshot.Data(),
}
}
func (s *super) contentPriceMap(ctx context.Context, list []*models.Content) (map[int64]*models.ContentPrice, error) {
if len(list) == 0 {
return map[int64]*models.ContentPrice{}, nil
}
ids := make([]int64, 0, len(list))
for _, item := range list {
ids = append(ids, item.ID)
}
tbl, q := models.ContentPriceQuery.QueryContext(ctx)
prices, err := q.Where(tbl.ContentID.In(ids...)).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
priceMap := make(map[int64]*models.ContentPrice, len(prices))
for _, price := range prices {
priceMap[price.ContentID] = price
}
return priceMap, nil
}
func (s *super) contentTenantMap(ctx context.Context, list []*models.Content) (map[int64]*models.Tenant, error) {
if len(list) == 0 {
return map[int64]*models.Tenant{}, nil
}
ids := make([]int64, 0, len(list))
seen := make(map[int64]struct{}, len(list))
for _, item := range list {
if _, ok := seen[item.TenantID]; ok {
continue
}
seen[item.TenantID] = struct{}{}
ids = append(ids, item.TenantID)
}
tbl, q := models.TenantQuery.QueryContext(ctx)
tenants, err := q.Where(tbl.ID.In(ids...)).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
tenantMap := make(map[int64]*models.Tenant, len(tenants))
for _, tenant := range tenants {
tenantMap[tenant.ID] = tenant
}
return tenantMap, nil
}
func (s *super) toSuperContentItem(item *models.Content, price *models.ContentPrice, tenant *models.Tenant) super_dto.AdminContentItem {
return super_dto.AdminContentItem{
Content: s.toSuperContentDTO(item, price),
Owner: s.toSuperContentOwner(item.Author),
Price: s.toSuperContentPrice(price),
StatusDescription: item.Status.Description(),
VisibilityDescription: item.Visibility.Description(),
Tenant: s.toSuperContentTenant(tenant),
}
}
func (s *super) toSuperContentOwner(author *models.User) *super_dto.AdminContentOwnerLite {
if author == nil {
return nil
}
return &super_dto.AdminContentOwnerLite{
ID: author.ID,
Username: author.Username,
Roles: author.Roles,
Status: author.Status,
}
}
func (s *super) toSuperContentTenant(tenant *models.Tenant) *super_dto.SuperContentTenantLite {
if tenant == nil {
return nil
}
return &super_dto.SuperContentTenantLite{
ID: tenant.ID,
Code: tenant.Code,
Name: tenant.Name,
}
}
func (s *super) toSuperContentDTO(item *models.Content, price *models.ContentPrice) *v1_dto.ContentItem {
dto := &v1_dto.ContentItem{
ID: item.ID,
TenantID: item.TenantID,
UserID: item.UserID,
Title: item.Title,
Genre: item.Genre,
Status: string(item.Status),
Visibility: string(item.Visibility),
AuthorID: item.UserID,
Views: int(item.Views),
Likes: int(item.Likes),
CreatedAt: item.CreatedAt.Format("2006-01-02"),
IsPurchased: false,
}
if !item.PublishedAt.IsZero() {
dto.PublishedAt = item.PublishedAt.Format("2006-01-02")
}
if price != nil {
dto.Price = float64(price.PriceAmount) / 100.0
}
if item.Author != nil {
dto.AuthorName = item.Author.Nickname
if dto.AuthorName == "" {
dto.AuthorName = item.Author.Username
}
dto.AuthorAvatar = item.Author.Avatar
}
var hasVideo, hasAudio bool
for _, asset := range item.ContentAssets {
if asset.Asset == nil {
continue
}
if asset.Role == consts.ContentAssetRoleCover {
dto.Cover = Common.GetAssetURL(asset.Asset.ObjectKey)
}
switch asset.Asset.Type {
case consts.MediaAssetTypeVideo:
hasVideo = true
case consts.MediaAssetTypeAudio:
hasAudio = true
}
}
if dto.Cover == "" && len(item.ContentAssets) > 0 {
for _, asset := range item.ContentAssets {
if asset.Asset != nil && asset.Asset.Type == consts.MediaAssetTypeImage {
dto.Cover = Common.GetAssetURL(asset.Asset.ObjectKey)
break
}
}
}
if hasVideo {
dto.Type = "video"
} else if hasAudio {
dto.Type = "audio"
} else {
dto.Type = "article"
}
return dto
}
func (s *super) toSuperContentPrice(price *models.ContentPrice) *v1_dto.ContentPrice {
if price == nil {
return nil
}
dto := &v1_dto.ContentPrice{
Currency: string(price.Currency),
PriceAmount: float64(price.PriceAmount) / 100.0,
DiscountType: string(price.DiscountType),
DiscountValue: s.toSuperDiscountValue(price),
}
if !price.DiscountStartAt.IsZero() {
dto.DiscountStartAt = price.DiscountStartAt.Format(time.RFC3339)
}
if !price.DiscountEndAt.IsZero() {
dto.DiscountEndAt = price.DiscountEndAt.Format(time.RFC3339)
}
return dto
}
func (s *super) toSuperDiscountValue(price *models.ContentPrice) float64 {
if price == nil {
return 0
}
if price.DiscountType == consts.DiscountTypeAmount {
return float64(price.DiscountValue) / 100.0
}
return float64(price.DiscountValue)
}
func (s *super) ListWithdrawals(ctx context.Context, filter *super_dto.SuperOrderListFilter) (*requests.Pager, error) {
tbl, q := models.OrderQuery.QueryContext(ctx)
q = q.Where(tbl.Type.Eq(consts.OrderTypeWithdrawal))
filter.Pagination.Format()
total, err := q.Count()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Order(tbl.ID.Desc()).Find()
if err != nil {
return nil, errorx.ErrDatabaseError.WithCause(err)
}
items, err := s.buildSuperOrderItems(ctx, list)
if err != nil {
return nil, err
}
return &requests.Pager{
Pagination: filter.Pagination,
Total: total,
Items: items,
}, nil
}
func (s *super) ApproveWithdrawal(ctx context.Context, id int64) error {
o, err := models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).First()
if err != nil {
return errorx.ErrRecordNotFound
}
if o.Status != consts.OrderStatusCreated {
return errorx.ErrStatusConflict.WithMsg("订单状态不正确")
}
// Mark as Paid (Assumes external transfer done)
_, err = models.OrderQuery.WithContext(ctx).Where(models.OrderQuery.ID.Eq(id)).Updates(&models.Order{
Status: consts.OrderStatusPaid,
PaidAt: time.Now(),
UpdatedAt: time.Now(),
})
if err == nil && Audit != nil {
Audit.Log(ctx, 0, "approve_withdrawal", cast.ToString(id), "Approved withdrawal")
}
return err
}
func (s *super) RejectWithdrawal(ctx context.Context, id int64, reason string) error {
err := models.Q.Transaction(func(tx *models.Query) error {
o, err := tx.Order.WithContext(ctx).Where(tx.Order.ID.Eq(id)).First()
if err != nil {
return errorx.ErrRecordNotFound
}
if o.Status != consts.OrderStatusCreated {
return errorx.ErrStatusConflict.WithMsg("订单状态不正确")
}
// Refund User Balance
_, err = tx.User.WithContext(ctx).Where(tx.User.ID.Eq(o.UserID)).Update(tx.User.Balance, gorm.Expr("balance + ?", o.AmountPaid))
if err != nil {
return err
}
// Update Order
_, err = tx.Order.WithContext(ctx).Where(tx.Order.ID.Eq(id)).Updates(&models.Order{
Status: consts.OrderStatusFailed, // or Canceled
RefundReason: reason,
UpdatedAt: time.Now(),
})
if err != nil {
return err
}
// Create Ledger (Adjustment/Unfreeze)
ledger := &models.TenantLedger{
TenantID: o.TenantID,
UserID: o.UserID,
OrderID: o.ID,
Type: consts.TenantLedgerTypeAdjustment,
Amount: o.AmountPaid,
Remark: "提现拒绝返还: " + reason,
OperatorUserID: 0, // System/Admin
IdempotencyKey: uuid.NewString(),
}
if err := tx.TenantLedger.WithContext(ctx).Create(ledger); err != nil {
return err
}
return nil
})
if err == nil && Audit != nil {
Audit.Log(ctx, 0, "reject_withdrawal", cast.ToString(id), "Rejected: "+reason)
}
return err
}