|
|
|
|
@@ -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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|