From 87b32063f640ae74534682a24d4f43c3e684613a Mon Sep 17 00:00:00 2001 From: Rogee Date: Thu, 8 Jan 2026 10:12:18 +0800 Subject: [PATCH] feat: add super admin auth --- backend/app/http/super/v1/auth/auth.go | 2 +- backend/app/http/super/v1/dto/auth.go | 4 +- backend/app/http/super/v1/routes.manual.go | 2 +- backend/app/middlewares/middlewares.go | 35 +++ backend/app/services/super.go | 321 ++++++++++++++++++++- 5 files changed, 345 insertions(+), 19 deletions(-) diff --git a/backend/app/http/super/v1/auth/auth.go b/backend/app/http/super/v1/auth/auth.go index 91b0883..cc8b423 100644 --- a/backend/app/http/super/v1/auth/auth.go +++ b/backend/app/http/super/v1/auth/auth.go @@ -35,5 +35,5 @@ func (c *auth) Login(ctx fiber.Ctx, form *dto.LoginForm) (*dto.LoginResponse, er // @Produce json // @Success 200 {object} dto.LoginResponse func (c *auth) CheckToken(ctx fiber.Ctx) (*dto.LoginResponse, error) { - return services.Super.CheckToken(ctx) + return services.Super.CheckToken(ctx, ctx.Get("Authorization")) } diff --git a/backend/app/http/super/v1/dto/auth.go b/backend/app/http/super/v1/dto/auth.go index 667e9bf..f951e06 100644 --- a/backend/app/http/super/v1/dto/auth.go +++ b/backend/app/http/super/v1/dto/auth.go @@ -3,8 +3,8 @@ package dto import "quyun/v2/pkg/consts" type LoginForm struct { - Phone string `json:"phone"` - OTP string `json:"otp"` + Username string `json:"username"` + Password string `json:"password"` } type LoginResponse struct { diff --git a/backend/app/http/super/v1/routes.manual.go b/backend/app/http/super/v1/routes.manual.go index f151216..378b518 100644 --- a/backend/app/http/super/v1/routes.manual.go +++ b/backend/app/http/super/v1/routes.manual.go @@ -6,6 +6,6 @@ func (r *Routes) Path() string { func (r *Routes) Middlewares() []any { return []any{ - r.middlewares.Auth, + r.middlewares.SuperAuth, } } diff --git a/backend/app/middlewares/middlewares.go b/backend/app/middlewares/middlewares.go index 6ef3c39..c9836f0 100644 --- a/backend/app/middlewares/middlewares.go +++ b/backend/app/middlewares/middlewares.go @@ -3,10 +3,12 @@ package middlewares import ( "quyun/v2/app/errorx" "quyun/v2/app/services" + "quyun/v2/pkg/consts" "quyun/v2/providers/jwt" "github.com/gofiber/fiber/v3" log "github.com/sirupsen/logrus" + "go.ipao.vip/gen/types" ) // Middlewares provides reusable Fiber middlewares shared across modules. @@ -53,3 +55,36 @@ func (m *Middlewares) Auth(ctx fiber.Ctx) error { return ctx.Next() } + +func (m *Middlewares) SuperAuth(ctx fiber.Ctx) error { + authHeader := ctx.Get("Authorization") + if authHeader == "" { + return errorx.ErrUnauthorized.WithMsg("Missing token") + } + + claims, err := m.jwt.Parse(authHeader) + if err != nil { + return errorx.ErrUnauthorized.WithCause(err).WithMsg("Invalid token") + } + + user, err := services.User.GetModelByID(ctx, claims.UserID) + if err != nil { + return errorx.ErrUnauthorized.WithCause(err).WithMsg("UserNotFound") + } + + if !hasRole(user.Roles, consts.RoleSuperAdmin) { + return errorx.ErrForbidden.WithMsg("无权限访问") + } + + ctx.Locals("__ctx_user", user) + return ctx.Next() +} + +func hasRole(roles types.Array[consts.Role], role consts.Role) bool { + for _, r := range roles { + if r == role { + return true + } + } + return false +} diff --git a/backend/app/services/super.go b/backend/app/services/super.go index 0b5cd2f..cccf8f3 100644 --- a/backend/app/services/super.go +++ b/backend/app/services/super.go @@ -7,9 +7,11 @@ import ( "quyun/v2/app/errorx" super_dto "quyun/v2/app/http/super/v1/dto" + creator_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" @@ -18,15 +20,69 @@ import ( ) // @provider -type super struct{} - -func (s *super) Login(ctx context.Context, form *super_dto.LoginForm) (*super_dto.LoginResponse, error) { - // TODO: Admin specific login or reuse User service - return &super_dto.LoginResponse{}, nil +type super struct { + jwt *jwt_provider.JWT } -func (s *super) CheckToken(ctx context.Context) (*super_dto.LoginResponse, error) { - return &super_dto.LoginResponse{}, nil +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) { @@ -260,28 +316,148 @@ func (s *super) ListOrders(ctx context.Context, filter *super_dto.SuperOrderList if err != nil { return nil, errorx.ErrDatabaseError.WithCause(err) } - // TODO: Map to DTO + + items, err := s.buildSuperOrderItems(ctx, list) + if err != nil { + return nil, err + } return &requests.Pager{ Pagination: filter.Pagination, Total: total, - Items: list, + Items: items, }, nil } func (s *super) GetOrder(ctx context.Context, id int64) (*super_dto.SuperOrderDetail, error) { - return &super_dto.SuperOrderDetail{}, nil + 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 + } + + item := s.toSuperOrderItem(o, tenant, buyer) + 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 { - return nil + 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, &creator_dto.RefundForm{ + Action: "accept", + Reason: form.Reason, + }) } func (s *super) OrderStatistics(ctx context.Context) (*super_dto.OrderStatisticsResponse, error) { - return &super_dto.OrderStatisticsResponse{}, nil + 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) { - return []super_dto.UserStatistics{}, nil + 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) { @@ -292,6 +468,118 @@ 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) 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)) @@ -307,11 +595,14 @@ func (s *super) ListWithdrawals(ctx context.Context, filter *super_dto.SuperOrde return nil, errorx.ErrDatabaseError.WithCause(err) } - // TODO: Map to SuperOrderItem properly with Tenant/User lookup + items, err := s.buildSuperOrderItems(ctx, list) + if err != nil { + return nil, err + } return &requests.Pager{ Pagination: filter.Pagination, Total: total, - Items: list, + Items: items, }, nil }