2014 lines
54 KiB
Go
2014 lines
54 KiB
Go
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/field"
|
||
"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 && strings.TrimSpace(*filter.Username) != "" {
|
||
keyword := "%" + strings.TrimSpace(*filter.Username) + "%"
|
||
q = q.Where(field.Or(tbl.Username.Like(keyword), tbl.Nickname.Like(keyword)))
|
||
}
|
||
if filter.ID != nil && *filter.ID > 0 {
|
||
q = q.Where(tbl.ID.Eq(*filter.ID))
|
||
}
|
||
if filter.Status != nil && *filter.Status != "" {
|
||
q = q.Where(tbl.Status.Eq(*filter.Status))
|
||
}
|
||
if filter.Role != nil && *filter.Role != "" {
|
||
q = q.Where(tbl.Roles.Contains(types.Array[consts.Role]{*filter.Role}))
|
||
}
|
||
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||
// 按租户成员过滤用户,需要先定位租户成员关系。
|
||
tblTu, qTu := models.TenantUserQuery.QueryContext(ctx)
|
||
userIDs, err := qTu.Where(tblTu.TenantID.Eq(*filter.TenantID)).Select(tblTu.UserID).Find()
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
ids := make([]int64, 0, len(userIDs))
|
||
for _, row := range userIDs {
|
||
ids = append(ids, row.UserID)
|
||
}
|
||
if len(ids) == 0 {
|
||
q = q.Where(tbl.ID.Eq(-1))
|
||
} else {
|
||
q = q.Where(tbl.ID.In(ids...))
|
||
}
|
||
}
|
||
|
||
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.VerifiedAtFrom != nil {
|
||
from, err := s.parseFilterTime(filter.VerifiedAtFrom)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if from != nil {
|
||
q = q.Where(tbl.VerifiedAt.Gte(*from))
|
||
}
|
||
}
|
||
if filter.VerifiedAtTo != nil {
|
||
to, err := s.parseFilterTime(filter.VerifiedAtTo)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if to != nil {
|
||
q = q.Where(tbl.VerifiedAt.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 "username":
|
||
q = q.Order(tbl.Username.Desc())
|
||
case "status":
|
||
q = q.Order(tbl.Status.Desc())
|
||
case "verified_at":
|
||
q = q.Order(tbl.VerifiedAt.Desc())
|
||
case "created_at":
|
||
q = q.Order(tbl.CreatedAt.Desc())
|
||
case "updated_at":
|
||
q = q.Order(tbl.UpdatedAt.Desc())
|
||
case "balance":
|
||
q = q.Order(tbl.Balance.Desc())
|
||
case "balance_frozen":
|
||
q = q.Order(tbl.BalanceFrozen.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 "username":
|
||
q = q.Order(tbl.Username)
|
||
case "status":
|
||
q = q.Order(tbl.Status)
|
||
case "verified_at":
|
||
q = q.Order(tbl.VerifiedAt)
|
||
case "created_at":
|
||
q = q.Order(tbl.CreatedAt)
|
||
case "updated_at":
|
||
q = q.Order(tbl.UpdatedAt)
|
||
case "balance":
|
||
q = q.Order(tbl.Balance)
|
||
case "balance_frozen":
|
||
q = q.Order(tbl.BalanceFrozen)
|
||
}
|
||
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)).Order(tbl.ID.Desc()).Find()
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
|
||
userIDs := make([]int64, 0, len(list))
|
||
for _, u := range list {
|
||
userIDs = append(userIDs, u.ID)
|
||
}
|
||
|
||
ownedCountMap, err := s.userOwnedTenantCount(ctx, userIDs)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
joinedCountMap, err := s.userJoinedTenantCount(ctx, userIDs)
|
||
if err != nil {
|
||
return nil, 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(),
|
||
VerifiedAt: s.formatTime(u.VerifiedAt),
|
||
CreatedAt: s.formatTime(u.CreatedAt),
|
||
UpdatedAt: s.formatTime(u.UpdatedAt),
|
||
},
|
||
Balance: u.Balance,
|
||
BalanceFrozen: u.BalanceFrozen,
|
||
OwnedTenantCount: ownedCountMap[u.ID],
|
||
JoinedTenantCount: joinedCountMap[u.ID],
|
||
})
|
||
}
|
||
|
||
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.ID != nil && *filter.ID > 0 {
|
||
q = q.Where(tbl.ID.Eq(*filter.ID))
|
||
}
|
||
if filter.UserID != nil && *filter.UserID > 0 {
|
||
q = q.Where(tbl.UserID.Eq(*filter.UserID))
|
||
}
|
||
if filter.Name != nil && strings.TrimSpace(*filter.Name) != "" {
|
||
q = q.Where(tbl.Name.Like("%" + strings.TrimSpace(*filter.Name) + "%"))
|
||
}
|
||
if filter.Code != nil && strings.TrimSpace(*filter.Code) != "" {
|
||
q = q.Where(tbl.Code.Like("%" + strings.TrimSpace(*filter.Code) + "%"))
|
||
}
|
||
if filter.Status != nil && *filter.Status != "" {
|
||
q = q.Where(tbl.Status.Eq(*filter.Status))
|
||
}
|
||
if filter.ExpiredAtFrom != nil {
|
||
from, err := s.parseFilterTime(filter.ExpiredAtFrom)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if from != nil {
|
||
q = q.Where(tbl.ExpiredAt.Gte(*from))
|
||
}
|
||
}
|
||
if filter.ExpiredAtTo != nil {
|
||
to, err := s.parseFilterTime(filter.ExpiredAtTo)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if to != nil {
|
||
q = q.Where(tbl.ExpiredAt.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))
|
||
}
|
||
}
|
||
|
||
orderApplied := false
|
||
if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" {
|
||
switch strings.TrimSpace(*filter.Desc) {
|
||
case "id":
|
||
q = q.Order(tbl.ID.Desc())
|
||
case "name":
|
||
q = q.Order(tbl.Name.Desc())
|
||
case "code":
|
||
q = q.Order(tbl.Code.Desc())
|
||
case "status":
|
||
q = q.Order(tbl.Status.Desc())
|
||
case "expired_at":
|
||
q = q.Order(tbl.ExpiredAt.Desc())
|
||
case "created_at":
|
||
q = q.Order(tbl.CreatedAt.Desc())
|
||
case "updated_at":
|
||
q = q.Order(tbl.UpdatedAt.Desc())
|
||
case "user_id":
|
||
q = q.Order(tbl.UserID.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 "name":
|
||
q = q.Order(tbl.Name)
|
||
case "code":
|
||
q = q.Order(tbl.Code)
|
||
case "status":
|
||
q = q.Order(tbl.Status)
|
||
case "expired_at":
|
||
q = q.Order(tbl.ExpiredAt)
|
||
case "created_at":
|
||
q = q.Order(tbl.CreatedAt)
|
||
case "updated_at":
|
||
q = q.Order(tbl.UpdatedAt)
|
||
case "user_id":
|
||
q = q.Order(tbl.UserID)
|
||
}
|
||
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)
|
||
}
|
||
|
||
data, err := s.buildTenantItems(ctx, list)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
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)
|
||
}
|
||
items, err := s.buildTenantItems(ctx, []*models.Tenant{t})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if len(items) == 0 {
|
||
return nil, errorx.ErrRecordNotFound
|
||
}
|
||
return &items[0], 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) ListTenantUsers(ctx context.Context, tenantID int64, filter *super_dto.SuperTenantUserListFilter) (*requests.Pager, error) {
|
||
tbl, q := models.TenantUserQuery.QueryContext(ctx)
|
||
q = q.Where(tbl.TenantID.Eq(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.Role != nil && *filter.Role != "" {
|
||
q = q.Where(tbl.Role.Contains(types.Array[consts.TenantUserRole]{*filter.Role}))
|
||
}
|
||
|
||
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...))
|
||
}
|
||
}
|
||
|
||
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.CreatedAt.Desc()).Find()
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
|
||
userMap, err := s.userMapByTenantUsers(ctx, list)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
items := make([]super_dto.SuperTenantUserItem, 0, len(list))
|
||
for _, tu := range list {
|
||
items = append(items, super_dto.SuperTenantUserItem{
|
||
User: s.toSuperUserLite(userMap[tu.UserID]),
|
||
TenantUser: s.toSuperTenantUserDTO(tu),
|
||
})
|
||
}
|
||
|
||
return &requests.Pager{
|
||
Pagination: filter.Pagination,
|
||
Total: total,
|
||
Items: items,
|
||
}, nil
|
||
}
|
||
|
||
func (s *super) ListUserTenants(ctx context.Context, userID int64, filter *super_dto.SuperUserTenantListFilter) (*requests.Pager, error) {
|
||
tbl, q := models.TenantUserQuery.QueryContext(ctx)
|
||
q = q.Where(tbl.UserID.Eq(userID))
|
||
|
||
if filter.TenantID != nil && *filter.TenantID > 0 {
|
||
q = q.Where(tbl.TenantID.Eq(*filter.TenantID))
|
||
}
|
||
if filter.Status != nil && *filter.Status != "" {
|
||
q = q.Where(tbl.Status.Eq(*filter.Status))
|
||
}
|
||
if filter.Role != nil && *filter.Role != "" {
|
||
q = q.Where(tbl.Role.Contains(types.Array[consts.TenantUserRole]{*filter.Role}))
|
||
}
|
||
|
||
tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.Code, filter.Name)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if tenantFilter {
|
||
if len(tenantIDs) == 0 {
|
||
q = q.Where(tbl.ID.Eq(-1))
|
||
} else {
|
||
q = q.Where(tbl.TenantID.In(tenantIDs...))
|
||
}
|
||
}
|
||
|
||
if filter.CreatedAtFrom != nil {
|
||
from, err := s.parseFilterTime(filter.CreatedAtFrom)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if from != nil {
|
||
q = q.Where(tbl.CreatedAt.Gte(*from))
|
||
}
|
||
}
|
||
if filter.CreatedAtTo != nil {
|
||
to, err := s.parseFilterTime(filter.CreatedAtTo)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
if to != nil {
|
||
q = q.Where(tbl.CreatedAt.Lte(*to))
|
||
}
|
||
}
|
||
|
||
orderApplied := false
|
||
if filter.Desc != nil && strings.TrimSpace(*filter.Desc) != "" {
|
||
switch strings.TrimSpace(*filter.Desc) {
|
||
case "tenant_id":
|
||
q = q.Order(tbl.TenantID.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 "tenant_id":
|
||
q = q.Order(tbl.TenantID)
|
||
case "created_at":
|
||
q = q.Order(tbl.CreatedAt)
|
||
}
|
||
orderApplied = true
|
||
}
|
||
if !orderApplied {
|
||
q = q.Order(tbl.CreatedAt.Desc())
|
||
}
|
||
|
||
filter.Pagination.Format()
|
||
total, err := q.Count()
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
list, err := q.Offset(int(filter.Pagination.Offset())).Limit(int(filter.Pagination.Limit)).Find()
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
|
||
tenantMap, ownerMap, err := s.tenantMapsForTenantUsers(ctx, list)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
items := make([]super_dto.UserTenantItem, 0, len(list))
|
||
for _, tu := range list {
|
||
tenant := tenantMap[tu.TenantID]
|
||
owner := ownerMap[tu.TenantID]
|
||
item := super_dto.UserTenantItem{
|
||
TenantID: tu.TenantID,
|
||
Role: tu.Role,
|
||
MemberStatus: tu.Status,
|
||
MemberStatusDescription: tu.Status.Description(),
|
||
JoinedAt: s.formatTime(tu.CreatedAt),
|
||
}
|
||
if tenant != nil {
|
||
item.TenantStatus = tenant.Status
|
||
item.TenantStatusDescription = tenant.Status.Description()
|
||
item.Name = tenant.Name
|
||
item.Code = tenant.Code
|
||
item.ExpiredAt = s.formatTime(tenant.ExpiredAt)
|
||
}
|
||
if owner != nil {
|
||
item.Owner = &super_dto.TenantOwnerUserLite{
|
||
ID: owner.ID,
|
||
Username: owner.Username,
|
||
}
|
||
}
|
||
items = append(items, item)
|
||
}
|
||
|
||
return &requests.Pager{
|
||
Pagination: filter.Pagination,
|
||
Total: total,
|
||
Items: items,
|
||
}, nil
|
||
}
|
||
|
||
func (s *super) ListContents(ctx context.Context, filter *super_dto.SuperContentListFilter) (*requests.Pager, error) {
|
||
tbl, q := models.ContentQuery.QueryContext(ctx)
|
||
|
||
if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" {
|
||
keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%"
|
||
q = q.Where(field.Or(
|
||
tbl.Title.Like(keyword),
|
||
tbl.Description.Like(keyword),
|
||
tbl.Summary.Like(keyword),
|
||
))
|
||
}
|
||
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))
|
||
}
|
||
|
||
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.ID, 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 (s *super) toSuperUserLite(u *models.User) *super_dto.SuperUserLite {
|
||
if u == nil {
|
||
return nil
|
||
}
|
||
return &super_dto.SuperUserLite{
|
||
ID: u.ID,
|
||
Username: u.Username,
|
||
Roles: u.Roles,
|
||
Status: u.Status,
|
||
StatusDescription: u.Status.Description(),
|
||
VerifiedAt: s.formatTime(u.VerifiedAt),
|
||
CreatedAt: s.formatTime(u.CreatedAt),
|
||
UpdatedAt: s.formatTime(u.UpdatedAt),
|
||
}
|
||
}
|
||
|
||
func hasRole(roles types.Array[consts.Role], role consts.Role) bool {
|
||
for _, r := range roles {
|
||
if r == role {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func hasTenantRole(roles types.Array[consts.TenantUserRole], role consts.TenantUserRole) 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(field.Or(tbl.Username.Like(keyword), 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) buildTenantItems(ctx context.Context, list []*models.Tenant) ([]super_dto.TenantItem, error) {
|
||
if len(list) == 0 {
|
||
return []super_dto.TenantItem{}, nil
|
||
}
|
||
|
||
tenantIDs := make([]int64, 0, len(list))
|
||
tenantOwnerIDs := make(map[int64]struct{}, len(list))
|
||
for _, t := range list {
|
||
tenantIDs = append(tenantIDs, t.ID)
|
||
tenantOwnerIDs[t.UserID] = struct{}{}
|
||
}
|
||
|
||
// 统计租户成员数与管理员列表(基于 tenant_users)。
|
||
userCountMap := make(map[int64]int64, len(list))
|
||
adminSet := make(map[int64]map[int64]struct{}, len(list))
|
||
tblTu, qTu := models.TenantUserQuery.QueryContext(ctx)
|
||
tenantUsers, err := qTu.Where(tblTu.TenantID.In(tenantIDs...)).Find()
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
for _, tu := range tenantUsers {
|
||
userCountMap[tu.TenantID]++
|
||
if hasTenantRole(tu.Role, consts.TenantUserRoleTenantAdmin) {
|
||
if _, ok := adminSet[tu.TenantID]; !ok {
|
||
adminSet[tu.TenantID] = make(map[int64]struct{})
|
||
}
|
||
adminSet[tu.TenantID][tu.UserID] = struct{}{}
|
||
}
|
||
}
|
||
|
||
adminIDs := make(map[int64]struct{})
|
||
for _, ids := range adminSet {
|
||
for id := range ids {
|
||
adminIDs[id] = struct{}{}
|
||
}
|
||
}
|
||
|
||
userIDs := make([]int64, 0, len(adminIDs)+len(tenantOwnerIDs))
|
||
seen := make(map[int64]struct{})
|
||
for id := range adminIDs {
|
||
seen[id] = struct{}{}
|
||
userIDs = append(userIDs, id)
|
||
}
|
||
for id := range tenantOwnerIDs {
|
||
if _, ok := seen[id]; ok {
|
||
continue
|
||
}
|
||
seen[id] = struct{}{}
|
||
userIDs = append(userIDs, id)
|
||
}
|
||
|
||
userMap := make(map[int64]*models.User, len(userIDs))
|
||
if len(userIDs) > 0 {
|
||
tblUser, qUser := models.UserQuery.QueryContext(ctx)
|
||
users, err := qUser.Where(tblUser.ID.In(userIDs...)).Find()
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
for _, u := range users {
|
||
userMap[u.ID] = u
|
||
}
|
||
}
|
||
|
||
// 汇总租户收入(按已支付/退款中/已退款订单统计实付金额)。
|
||
incomeMap := make(map[int64]int64, len(list))
|
||
if len(tenantIDs) > 0 {
|
||
var rows []struct {
|
||
TenantID int64 `gorm:"column:tenant_id"`
|
||
AmountPaidSum int64 `gorm:"column:amount_paid_sum"`
|
||
}
|
||
err := models.OrderQuery.WithContext(ctx).
|
||
UnderlyingDB().
|
||
Model(&models.Order{}).
|
||
Select("tenant_id, coalesce(sum(amount_paid), 0) as amount_paid_sum").
|
||
Where("tenant_id IN ?", tenantIDs).
|
||
Where("status IN ?", []consts.OrderStatus{
|
||
consts.OrderStatusPaid,
|
||
consts.OrderStatusRefunding,
|
||
consts.OrderStatusRefunded,
|
||
}).
|
||
Group("tenant_id").
|
||
Scan(&rows).Error
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
for _, row := range rows {
|
||
incomeMap[row.TenantID] = row.AmountPaidSum
|
||
}
|
||
}
|
||
|
||
items := make([]super_dto.TenantItem, 0, len(list))
|
||
for _, t := range list {
|
||
item := 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,
|
||
UserCount: userCountMap[t.ID],
|
||
IncomeAmountPaidSum: incomeMap[t.ID],
|
||
ExpiredAt: s.formatTime(t.ExpiredAt),
|
||
CreatedAt: s.formatTime(t.CreatedAt),
|
||
UpdatedAt: s.formatTime(t.UpdatedAt),
|
||
}
|
||
|
||
if owner := userMap[t.UserID]; owner != nil {
|
||
item.Owner = &super_dto.TenantOwnerUserLite{
|
||
ID: owner.ID,
|
||
Username: owner.Username,
|
||
}
|
||
}
|
||
|
||
if adminSet[t.ID] != nil {
|
||
admins := make([]*super_dto.TenantAdminUserLite, 0, len(adminSet[t.ID]))
|
||
for adminID := range adminSet[t.ID] {
|
||
if u := userMap[adminID]; u != nil {
|
||
admins = append(admins, &super_dto.TenantAdminUserLite{
|
||
ID: u.ID,
|
||
Username: u.Username,
|
||
})
|
||
}
|
||
}
|
||
item.AdminUsers = admins
|
||
}
|
||
|
||
items = append(items, item)
|
||
}
|
||
|
||
return items, nil
|
||
}
|
||
|
||
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, operatorID, id int64) error {
|
||
if operatorID == 0 {
|
||
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
||
}
|
||
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, operatorID, "approve_withdrawal", cast.ToString(id), "Approved withdrawal")
|
||
}
|
||
return err
|
||
}
|
||
|
||
func (s *super) userOwnedTenantCount(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
|
||
result := make(map[int64]int64, len(userIDs))
|
||
if len(userIDs) == 0 {
|
||
return result, nil
|
||
}
|
||
var rows []struct {
|
||
UserID int64 `gorm:"column:user_id"`
|
||
Count int64 `gorm:"column:count"`
|
||
}
|
||
err := models.TenantQuery.WithContext(ctx).
|
||
UnderlyingDB().
|
||
Model(&models.Tenant{}).
|
||
Select("user_id, count(*) as count").
|
||
Where("user_id IN ?", userIDs).
|
||
Group("user_id").
|
||
Scan(&rows).Error
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
for _, row := range rows {
|
||
result[row.UserID] = row.Count
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (s *super) userJoinedTenantCount(ctx context.Context, userIDs []int64) (map[int64]int64, error) {
|
||
result := make(map[int64]int64, len(userIDs))
|
||
if len(userIDs) == 0 {
|
||
return result, nil
|
||
}
|
||
var rows []struct {
|
||
UserID int64 `gorm:"column:user_id"`
|
||
Count int64 `gorm:"column:count"`
|
||
}
|
||
err := models.TenantUserQuery.WithContext(ctx).
|
||
UnderlyingDB().
|
||
Model(&models.TenantUser{}).
|
||
Select("user_id, count(*) as count").
|
||
Where("user_id IN ?", userIDs).
|
||
Group("user_id").
|
||
Scan(&rows).Error
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
for _, row := range rows {
|
||
result[row.UserID] = row.Count
|
||
}
|
||
return result, nil
|
||
}
|
||
|
||
func (s *super) userMapByTenantUsers(ctx context.Context, list []*models.TenantUser) (map[int64]*models.User, error) {
|
||
userIDs := make([]int64, 0, len(list))
|
||
seen := make(map[int64]struct{}, len(list))
|
||
for _, tu := range list {
|
||
if _, ok := seen[tu.UserID]; ok {
|
||
continue
|
||
}
|
||
seen[tu.UserID] = struct{}{}
|
||
userIDs = append(userIDs, tu.UserID)
|
||
}
|
||
userMap := make(map[int64]*models.User, len(userIDs))
|
||
if len(userIDs) == 0 {
|
||
return userMap, nil
|
||
}
|
||
tblUser, qUser := models.UserQuery.QueryContext(ctx)
|
||
users, err := qUser.Where(tblUser.ID.In(userIDs...)).Find()
|
||
if err != nil {
|
||
return nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
for _, u := range users {
|
||
userMap[u.ID] = u
|
||
}
|
||
return userMap, nil
|
||
}
|
||
|
||
func (s *super) toSuperTenantUserDTO(tu *models.TenantUser) *super_dto.TenantUser {
|
||
if tu == nil {
|
||
return nil
|
||
}
|
||
return &super_dto.TenantUser{
|
||
ID: tu.ID,
|
||
TenantID: tu.TenantID,
|
||
UserID: tu.UserID,
|
||
Role: tu.Role,
|
||
Status: tu.Status,
|
||
CreatedAt: s.formatTime(tu.CreatedAt),
|
||
UpdatedAt: s.formatTime(tu.UpdatedAt),
|
||
}
|
||
}
|
||
|
||
func (s *super) tenantMapsForTenantUsers(ctx context.Context, list []*models.TenantUser) (map[int64]*models.Tenant, map[int64]*models.User, error) {
|
||
tenantIDs := make([]int64, 0, len(list))
|
||
seen := make(map[int64]struct{}, len(list))
|
||
for _, tu := range list {
|
||
if _, ok := seen[tu.TenantID]; ok {
|
||
continue
|
||
}
|
||
seen[tu.TenantID] = struct{}{}
|
||
tenantIDs = append(tenantIDs, tu.TenantID)
|
||
}
|
||
|
||
tenantMap := make(map[int64]*models.Tenant, len(tenantIDs))
|
||
ownerMap := make(map[int64]*models.User, len(tenantIDs))
|
||
if len(tenantIDs) == 0 {
|
||
return tenantMap, ownerMap, nil
|
||
}
|
||
|
||
tblTenant, qTenant := models.TenantQuery.QueryContext(ctx)
|
||
tenants, err := qTenant.Where(tblTenant.ID.In(tenantIDs...)).Find()
|
||
if err != nil {
|
||
return nil, nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
|
||
ownerIDs := make([]int64, 0, len(tenants))
|
||
ownerSeen := make(map[int64]struct{}, len(tenants))
|
||
for _, t := range tenants {
|
||
tenantMap[t.ID] = t
|
||
if _, ok := ownerSeen[t.UserID]; ok {
|
||
continue
|
||
}
|
||
ownerSeen[t.UserID] = struct{}{}
|
||
ownerIDs = append(ownerIDs, t.UserID)
|
||
}
|
||
|
||
userMap := make(map[int64]*models.User, len(ownerIDs))
|
||
if len(ownerIDs) > 0 {
|
||
tblUser, qUser := models.UserQuery.QueryContext(ctx)
|
||
users, err := qUser.Where(tblUser.ID.In(ownerIDs...)).Find()
|
||
if err != nil {
|
||
return nil, nil, errorx.ErrDatabaseError.WithCause(err)
|
||
}
|
||
for _, u := range users {
|
||
userMap[u.ID] = u
|
||
}
|
||
}
|
||
|
||
for tenantID, tenant := range tenantMap {
|
||
if tenant == nil {
|
||
continue
|
||
}
|
||
if owner := userMap[tenant.UserID]; owner != nil {
|
||
ownerMap[tenantID] = owner
|
||
}
|
||
}
|
||
|
||
return tenantMap, ownerMap, nil
|
||
}
|
||
|
||
func (s *super) formatTime(t time.Time) string {
|
||
if t.IsZero() {
|
||
return ""
|
||
}
|
||
return t.Format(time.RFC3339)
|
||
}
|
||
|
||
func (s *super) RejectWithdrawal(ctx context.Context, operatorID, id int64, reason string) error {
|
||
if operatorID == 0 {
|
||
return errorx.ErrUnauthorized.WithMsg("缺少操作者信息")
|
||
}
|
||
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: operatorID,
|
||
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, operatorID, "reject_withdrawal", cast.ToString(id), "Rejected: "+reason)
|
||
}
|
||
return err
|
||
}
|