package services import ( "context" "errors" "strings" "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) if filter.ID != nil && *filter.ID > 0 { q = q.Where(tbl.ID.Eq(*filter.ID)) } if filter.TenantID != nil && *filter.TenantID > 0 { q = q.Where(tbl.TenantID.Eq(*filter.TenantID)) } if filter.UserID != nil && *filter.UserID > 0 { q = q.Where(tbl.UserID.Eq(*filter.UserID)) } if filter.Status != nil && *filter.Status != "" { q = q.Where(tbl.Status.Eq(*filter.Status)) } if filter.Visibility != nil && *filter.Visibility != "" { q = q.Where(tbl.Visibility.Eq(*filter.Visibility)) } if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" { keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%" q = q.Where(tbl.Title.Like(keyword)).Or(tbl.Description.Like(keyword)).Or(tbl.Summary.Like(keyword)) } tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName) if err != nil { return nil, err } if tenantFilter { if len(tenantIDs) == 0 { q = q.Where(tbl.ID.Eq(-1)) } else { q = q.Where(tbl.TenantID.In(tenantIDs...)) } } userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username) if err != nil { return nil, err } if userFilter { if len(userIDs) == 0 { q = q.Where(tbl.ID.Eq(-1)) } else { q = q.Where(tbl.UserID.In(userIDs...)) } } if filter.PublishedAtFrom != nil { from, err := s.parseFilterTime(filter.PublishedAtFrom) if err != nil { return nil, err } if from != nil { q = q.Where(tbl.PublishedAt.Gte(*from)) } } if filter.PublishedAtTo != nil { to, err := s.parseFilterTime(filter.PublishedAtTo) if err != nil { return nil, err } if to != nil { q = q.Where(tbl.PublishedAt.Lte(*to)) } } 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.PriceAmountMin != nil || filter.PriceAmountMax != nil { pTbl, pQ := models.ContentPriceQuery.QueryContext(ctx) pq := pQ if filter.PriceAmountMin != nil { pq = pq.Where(pTbl.PriceAmount.Gte(*filter.PriceAmountMin)) } if filter.PriceAmountMax != nil { pq = pq.Where(pTbl.PriceAmount.Lte(*filter.PriceAmountMax)) } prices, err := pq.Select(pTbl.ContentID).Find() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } ids := make([]int64, 0, len(prices)) for _, price := range prices { ids = append(ids, price.ContentID) } if len(ids) == 0 { q = q.Where(tbl.ID.Eq(-1)) } else { q = q.Where(tbl.ID.In(ids...)) } } orderApplied := false if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" { switch strings.TrimSpace(*filter.Desc) { case "id": q = q.Order(tbl.ID.Desc()) case "title": q = q.Order(tbl.Title.Desc()) case "tenant_id": q = q.Order(tbl.TenantID.Desc()) case "user_id": q = q.Order(tbl.UserID.Desc()) case "status": q = q.Order(tbl.Status.Desc()) case "visibility": q = q.Order(tbl.Visibility.Desc()) case "published_at": q = q.Order(tbl.PublishedAt.Desc()) case "created_at": q = q.Order(tbl.CreatedAt.Desc()) } orderApplied = true } else if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" { switch strings.TrimSpace(*filter.Asc) { case "id": q = q.Order(tbl.ID) case "title": q = q.Order(tbl.Title) case "tenant_id": q = q.Order(tbl.TenantID) case "user_id": q = q.Order(tbl.UserID) case "status": q = q.Order(tbl.Status) case "visibility": q = q.Order(tbl.Visibility) case "published_at": q = q.Order(tbl.PublishedAt) case "created_at": q = q.Order(tbl.CreatedAt) } orderApplied = true } if !orderApplied { q = q.Order(tbl.ID.Desc()) } filter.Pagination.Format() total, err := q.Count() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } var list []*models.Content err = q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)). 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) if filter.ID != nil && *filter.ID > 0 { q = q.Where(tbl.ID.Eq(*filter.ID)) } if filter.TenantID != nil && *filter.TenantID > 0 { q = q.Where(tbl.TenantID.Eq(*filter.TenantID)) } if filter.UserID != nil && *filter.UserID > 0 { q = q.Where(tbl.UserID.Eq(*filter.UserID)) } if filter.Type != nil && *filter.Type != "" { q = q.Where(tbl.Type.Eq(*filter.Type)) } if filter.Status != nil && *filter.Status != "" { q = q.Where(tbl.Status.Eq(*filter.Status)) } if filter.AmountPaidMin != nil { q = q.Where(tbl.AmountPaid.Gte(*filter.AmountPaidMin)) } if filter.AmountPaidMax != nil { q = q.Where(tbl.AmountPaid.Lte(*filter.AmountPaidMax)) } tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName) if err != nil { return nil, err } if tenantFilter { if len(tenantIDs) == 0 { q = q.Where(tbl.ID.Eq(-1)) } else { q = q.Where(tbl.TenantID.In(tenantIDs...)) } } userIDs, userFilter, err := s.lookupUserIDs(ctx, filter.Username) if err != nil { return nil, err } if userFilter { if len(userIDs) == 0 { q = q.Where(tbl.ID.Eq(-1)) } else { q = q.Where(tbl.UserID.In(userIDs...)) } } orderIDs, contentFilter, err := s.lookupOrderIDsByContent(ctx, filter.ContentID, filter.ContentTitle) if err != nil { return nil, err } if contentFilter { if len(orderIDs) == 0 { q = q.Where(tbl.ID.Eq(-1)) } else { q = q.Where(tbl.ID.In(orderIDs...)) } } if filter.CreatedAtFrom != nil { from, err := s.parseFilterTime(filter.CreatedAtFrom) if err != nil { return nil, err } if from != nil { q = q.Where(tbl.CreatedAt.Gte(*from)) } } if filter.CreatedAtTo != nil { to, err := s.parseFilterTime(filter.CreatedAtTo) if err != nil { return nil, err } if to != nil { q = q.Where(tbl.CreatedAt.Lte(*to)) } } if filter.PaidAtFrom != nil { from, err := s.parseFilterTime(filter.PaidAtFrom) if err != nil { return nil, err } if from != nil { q = q.Where(tbl.PaidAt.Gte(*from)) } } if filter.PaidAtTo != nil { to, err := s.parseFilterTime(filter.PaidAtTo) if err != nil { return nil, err } if to != nil { q = q.Where(tbl.PaidAt.Lte(*to)) } } orderApplied := false if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" { switch strings.TrimSpace(*filter.Desc) { case "id": q = q.Order(tbl.ID.Desc()) case "created_at": q = q.Order(tbl.CreatedAt.Desc()) case "paid_at": q = q.Order(tbl.PaidAt.Desc()) case "amount_paid": q = q.Order(tbl.AmountPaid.Desc()) case "amount_original": q = q.Order(tbl.AmountOriginal.Desc()) case "amount_discount": q = q.Order(tbl.AmountDiscount.Desc()) case "status": q = q.Order(tbl.Status.Desc()) case "type": q = q.Order(tbl.Type.Desc()) } orderApplied = true } else if filter.Asc != nil && strings.TrimSpace(*filter.Asc) != "" { switch strings.TrimSpace(*filter.Asc) { case "id": q = q.Order(tbl.ID) case "created_at": q = q.Order(tbl.CreatedAt) case "paid_at": q = q.Order(tbl.PaidAt) case "amount_paid": q = q.Order(tbl.AmountPaid) case "amount_original": q = q.Order(tbl.AmountOriginal) case "amount_discount": q = q.Order(tbl.AmountDiscount) case "status": q = q.Order(tbl.Status) case "type": q = q.Order(tbl.Type) } orderApplied = true } if !orderApplied { q = q.Order(tbl.ID.Desc()) } filter.Pagination.Format() total, err := q.Count() if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).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) parseFilterTime(value *string) (*time.Time, error) { if value == nil { return nil, nil } text := strings.TrimSpace(*value) if text == "" { return nil, nil } if t, err := time.Parse(time.RFC3339, text); err == nil { return &t, nil } t, err := time.Parse("2006-01-02", text) if err != nil { return nil, errorx.ErrInvalidFormat.WithCause(err) } return &t, nil } func (s *super) lookupTenantIDs(ctx context.Context, code, name *string) ([]int64, bool, error) { codeText := "" if code != nil { codeText = strings.TrimSpace(*code) } nameText := "" if name != nil { nameText = strings.TrimSpace(*name) } if codeText == "" && nameText == "" { return nil, false, nil } tbl, q := models.TenantQuery.QueryContext(ctx) if codeText != "" { q = q.Where(tbl.Code.Like("%" + codeText + "%")) } if nameText != "" { q = q.Where(tbl.Name.Like("%" + nameText + "%")) } tenants, err := q.Select(tbl.ID).Find() if err != nil { return nil, true, errorx.ErrDatabaseError.WithCause(err) } ids := make([]int64, 0, len(tenants)) for _, tenant := range tenants { ids = append(ids, tenant.ID) } return ids, true, nil } func (s *super) lookupUserIDs(ctx context.Context, username *string) ([]int64, bool, error) { text := "" if username != nil { text = strings.TrimSpace(*username) } if text == "" { return nil, false, nil } tbl, q := models.UserQuery.QueryContext(ctx) keyword := "%" + text + "%" q = q.Where(tbl.Username.Like(keyword)).Or(tbl.Nickname.Like(keyword)) users, err := q.Select(tbl.ID).Find() if err != nil { return nil, true, errorx.ErrDatabaseError.WithCause(err) } ids := make([]int64, 0, len(users)) for _, user := range users { ids = append(ids, user.ID) } return ids, true, nil } func (s *super) lookupOrderIDsByContent(ctx context.Context, contentID *int64, contentTitle *string) ([]int64, bool, error) { var id int64 if contentID != nil { id = *contentID } title := "" if contentTitle != nil { title = strings.TrimSpace(*contentTitle) } if id <= 0 && title == "" { return nil, false, nil } tbl, q := models.OrderItemQuery.QueryContext(ctx) if id > 0 { q = q.Where(tbl.ContentID.Eq(id)) } var items []*models.OrderItem if title != "" { // JSONB 字段需要使用 UnderlyingDB 做模糊查询。 keyword := "%" + title + "%" err := q.UnderlyingDB(). Where("snapshot ->> 'content_title' ILIKE ?", keyword). Select("order_id"). Find(&items).Error if err != nil { return nil, true, errorx.ErrDatabaseError.WithCause(err) } } else { list, err := q.Select(tbl.OrderID).Find() if err != nil { return nil, true, errorx.ErrDatabaseError.WithCause(err) } items = list } idMap := make(map[int64]struct{}, len(items)) for _, item := range items { idMap[item.OrderID] = struct{}{} } ids := make([]int64, 0, len(idMap)) for orderID := range idMap { ids = append(ids, orderID) } return ids, true, nil } 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 }