diff --git a/backend/app/http/super/v1/dto/super_user.go b/backend/app/http/super/v1/dto/super_user.go new file mode 100644 index 0000000..195ba5a --- /dev/null +++ b/backend/app/http/super/v1/dto/super_user.go @@ -0,0 +1,118 @@ +package dto + +import ( + "quyun/v2/app/requests" + "quyun/v2/pkg/consts" +) + +// SuperUserNotificationListFilter 超管用户通知列表过滤条件。 +type SuperUserNotificationListFilter struct { + requests.Pagination + // TenantID 租户ID过滤(为空表示全部)。 + TenantID *int64 `query:"tenant_id"` + // Type 通知类型过滤(system/order/interaction)。 + Type *string `query:"type"` + // Read 是否已读过滤。 + Read *bool `query:"read"` + // CreatedAtFrom 创建时间起始(RFC3339)。 + CreatedAtFrom *string `query:"created_at_from"` + // CreatedAtTo 创建时间结束(RFC3339)。 + CreatedAtTo *string `query:"created_at_to"` +} + +// SuperUserNotificationItem 超管用户通知列表项。 +type SuperUserNotificationItem struct { + // ID 通知ID。 + ID int64 `json:"id"` + // TenantID 通知所属租户ID。 + TenantID int64 `json:"tenant_id"` + // TenantCode 通知所属租户编码。 + TenantCode string `json:"tenant_code"` + // TenantName 通知所属租户名称。 + TenantName string `json:"tenant_name"` + // Type 通知类型。 + Type string `json:"type"` + // Title 通知标题。 + Title string `json:"title"` + // Content 通知内容。 + Content string `json:"content"` + // Read 是否已读。 + Read bool `json:"read"` + // CreatedAt 发送时间(RFC3339)。 + CreatedAt string `json:"created_at"` +} + +// SuperUserCouponListFilter 超管用户优惠券列表过滤条件。 +type SuperUserCouponListFilter struct { + requests.Pagination + // TenantID 租户ID过滤(为空表示全部)。 + TenantID *int64 `query:"tenant_id"` + // TenantCode 租户编码(模糊匹配)。 + TenantCode *string `query:"tenant_code"` + // TenantName 租户名称(模糊匹配)。 + TenantName *string `query:"tenant_name"` + // Status 用户券状态过滤(unused/used/expired)。 + Status *consts.UserCouponStatus `query:"status"` + // Type 券模板类型过滤(fix_amount/discount)。 + Type *consts.CouponType `query:"type"` + // Keyword 标题或描述关键词(模糊匹配)。 + Keyword *string `query:"keyword"` + // CreatedAtFrom 领取时间起始(RFC3339)。 + CreatedAtFrom *string `query:"created_at_from"` + // CreatedAtTo 领取时间结束(RFC3339)。 + CreatedAtTo *string `query:"created_at_to"` +} + +// SuperUserCouponItem 超管用户优惠券列表项。 +type SuperUserCouponItem struct { + // ID 用户券ID。 + ID int64 `json:"id"` + // CouponID 券模板ID。 + CouponID int64 `json:"coupon_id"` + // TenantID 券所属租户ID。 + TenantID int64 `json:"tenant_id"` + // TenantCode 券所属租户编码。 + TenantCode string `json:"tenant_code"` + // TenantName 券所属租户名称。 + TenantName string `json:"tenant_name"` + // Title 券标题。 + Title string `json:"title"` + // Description 券描述。 + Description string `json:"description"` + // Type 券类型。 + Type consts.CouponType `json:"type"` + // TypeDescription 券类型描述(用于展示)。 + TypeDescription string `json:"type_description"` + // Value 券面值/折扣值。 + Value int64 `json:"value"` + // MinOrderAmount 使用门槛金额(分)。 + MinOrderAmount int64 `json:"min_order_amount"` + // MaxDiscount 折扣券最高抵扣金额(分)。 + MaxDiscount int64 `json:"max_discount"` + // StartAt 生效时间(RFC3339)。 + StartAt string `json:"start_at"` + // EndAt 过期时间(RFC3339)。 + EndAt string `json:"end_at"` + // Status 用户券状态。 + Status consts.UserCouponStatus `json:"status"` + // StatusDescription 用户券状态描述(用于展示)。 + StatusDescription string `json:"status_description"` + // OrderID 使用订单ID(未使用为0)。 + OrderID int64 `json:"order_id"` + // UsedAt 使用时间(RFC3339)。 + UsedAt string `json:"used_at"` + // CreatedAt 领取时间(RFC3339)。 + CreatedAt string `json:"created_at"` +} + +// SuperUserRealNameResponse 超管实名认证详情。 +type SuperUserRealNameResponse struct { + // IsRealNameVerified 是否已实名认证。 + IsRealNameVerified bool `json:"is_real_name_verified"` + // VerifiedAt 实名认证时间(RFC3339)。 + VerifiedAt string `json:"verified_at"` + // RealName 真实姓名(来自用户元数据)。 + RealName string `json:"real_name"` + // IDCardMasked 身份证号脱敏展示。 + IDCardMasked string `json:"id_card_masked"` +} diff --git a/backend/app/http/super/v1/routes.gen.go b/backend/app/http/super/v1/routes.gen.go index 780b708..65dc564 100644 --- a/backend/app/http/super/v1/routes.gen.go +++ b/backend/app/http/super/v1/routes.gen.go @@ -226,6 +226,23 @@ func (r *Routes) Register(router fiber.Router) { r.users.Get, PathParam[int64]("id"), )) + r.log.Debugf("Registering route: Get /super/v1/users/:id/coupons -> users.ListCoupons") + router.Get("/super/v1/users/:id/coupons"[len(r.Path()):], DataFunc2( + r.users.ListCoupons, + PathParam[int64]("id"), + Query[dto.SuperUserCouponListFilter]("filter"), + )) + r.log.Debugf("Registering route: Get /super/v1/users/:id/notifications -> users.ListNotifications") + router.Get("/super/v1/users/:id/notifications"[len(r.Path()):], DataFunc2( + r.users.ListNotifications, + PathParam[int64]("id"), + Query[dto.SuperUserNotificationListFilter]("filter"), + )) + r.log.Debugf("Registering route: Get /super/v1/users/:id/realname -> users.RealName") + router.Get("/super/v1/users/:id/realname"[len(r.Path()):], DataFunc1( + r.users.RealName, + PathParam[int64]("id"), + )) r.log.Debugf("Registering route: Get /super/v1/users/:id/tenants -> users.ListTenants") router.Get("/super/v1/users/:id/tenants"[len(r.Path()):], DataFunc2( r.users.ListTenants, diff --git a/backend/app/http/super/v1/users.go b/backend/app/http/super/v1/users.go index 4263497..76d75ab 100644 --- a/backend/app/http/super/v1/users.go +++ b/backend/app/http/super/v1/users.go @@ -58,6 +58,57 @@ func (c *users) Wallet(ctx fiber.Ctx, id int64) (*dto.SuperWalletResponse, error return services.Super.GetUserWallet(ctx, id) } +// List user notifications +// +// @Router /super/v1/users/:id/notifications [get] +// @Summary List user notifications +// @Description List notifications of a user +// @Tags User +// @Accept json +// @Produce json +// @Param id path int64 true "User ID" +// @Param page query int false "Page number" +// @Param limit query int false "Page size" +// @Success 200 {object} requests.Pager{items=[]dto.SuperUserNotificationItem} +// @Bind id path +// @Bind filter query +func (c *users) ListNotifications(ctx fiber.Ctx, id int64, filter *dto.SuperUserNotificationListFilter) (*requests.Pager, error) { + return services.Super.ListUserNotifications(ctx, id, filter) +} + +// List user coupons +// +// @Router /super/v1/users/:id/coupons [get] +// @Summary List user coupons +// @Description List coupons of a user +// @Tags User +// @Accept json +// @Produce json +// @Param id path int64 true "User ID" +// @Param page query int false "Page number" +// @Param limit query int false "Page size" +// @Success 200 {object} requests.Pager{items=[]dto.SuperUserCouponItem} +// @Bind id path +// @Bind filter query +func (c *users) ListCoupons(ctx fiber.Ctx, id int64, filter *dto.SuperUserCouponListFilter) (*requests.Pager, error) { + return services.Super.ListUserCoupons(ctx, id, filter) +} + +// Get user real-name verification detail +// +// @Router /super/v1/users/:id/realname [get] +// @Summary Get user real-name verification detail +// @Description Get real-name verification detail of a user +// @Tags User +// @Accept json +// @Produce json +// @Param id path int64 true "User ID" +// @Success 200 {object} dto.SuperUserRealNameResponse +// @Bind id path +func (c *users) RealName(ctx fiber.Ctx, id int64) (*dto.SuperUserRealNameResponse, error) { + return services.Super.GetUserRealName(ctx, id) +} + // List user tenants // // @Router /super/v1/users/:id/tenants [get] diff --git a/backend/app/services/super.go b/backend/app/services/super.go index bc6b2c3..f420da3 100644 --- a/backend/app/services/super.go +++ b/backend/app/services/super.go @@ -2,6 +2,7 @@ package services import ( "context" + "encoding/json" "errors" "strconv" "strings" @@ -286,6 +287,7 @@ func (s *super) GetUser(ctx context.Context, id int64) (*super_dto.UserItem, err Roles: u.Roles, Status: u.Status, StatusDescription: u.Status.Description(), + VerifiedAt: s.formatTime(u.VerifiedAt), CreatedAt: u.CreatedAt.Format(time.RFC3339), UpdatedAt: u.UpdatedAt.Format(time.RFC3339), }, @@ -382,6 +384,301 @@ func (s *super) GetUserWallet(ctx context.Context, userID int64) (*super_dto.Sup }, nil } +func (s *super) GetUserRealName(ctx context.Context, userID int64) (*super_dto.SuperUserRealNameResponse, error) { + if userID == 0 { + return nil, errorx.ErrBadRequest.WithMsg("用户ID不能为空") + } + + tbl, q := models.UserQuery.QueryContext(ctx) + u, err := q.Where(tbl.ID.Eq(userID)).First() + if err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + return nil, errorx.ErrRecordNotFound + } + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + // 从用户元数据中读取实名字段,便于超管展示。 + meta := make(map[string]interface{}) + if len(u.Metas) > 0 { + if err := json.Unmarshal(u.Metas, &meta); err != nil { + return nil, errorx.ErrInternalError.WithCause(err).WithMsg("解析实名认证信息失败") + } + } + + realName := "" + if value, ok := meta["real_name"].(string); ok { + realName = value + } + idCard := "" + if value, ok := meta["id_card"].(string); ok { + idCard = value + } + + return &super_dto.SuperUserRealNameResponse{ + IsRealNameVerified: u.IsRealNameVerified, + VerifiedAt: s.formatTime(u.VerifiedAt), + RealName: realName, + IDCardMasked: s.maskIDCard(idCard), + }, nil +} + +func (s *super) ListUserNotifications(ctx context.Context, userID int64, filter *super_dto.SuperUserNotificationListFilter) (*requests.Pager, error) { + if userID == 0 { + return nil, errorx.ErrBadRequest.WithMsg("用户ID不能为空") + } + if filter == nil { + filter = &super_dto.SuperUserNotificationListFilter{} + } + + tbl, q := models.NotificationQuery.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.Type != nil && strings.TrimSpace(*filter.Type) != "" { + q = q.Where(tbl.Type.Eq(strings.TrimSpace(*filter.Type))) + } + if filter.Read != nil { + q = q.Where(tbl.IsRead.Is(*filter.Read)) + } + 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)) + } + } + + filter.Pagination.Format() + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + list, err := q.Order(tbl.CreatedAt.Desc()). + Offset(int(filter.Pagination.Offset())). + Limit(int(filter.Pagination.Limit)). + Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + + // 补齐租户信息,便于跨租户展示。 + tenantIDs := make([]int64, 0, len(list)) + tenantSet := make(map[int64]struct{}) + for _, n := range list { + if n.TenantID > 0 { + if _, ok := tenantSet[n.TenantID]; !ok { + tenantSet[n.TenantID] = struct{}{} + tenantIDs = append(tenantIDs, n.TenantID) + } + } + } + tenantMap := make(map[int64]*models.Tenant, len(tenantIDs)) + if len(tenantIDs) > 0 { + tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx) + tenants, err := tenantQuery.Where(tenantTbl.ID.In(tenantIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, tenant := range tenants { + tenantMap[tenant.ID] = tenant + } + } + + items := make([]super_dto.SuperUserNotificationItem, 0, len(list)) + for _, n := range list { + tenant := tenantMap[n.TenantID] + tenantCode := "" + tenantName := "" + if tenant != nil { + tenantCode = tenant.Code + tenantName = tenant.Name + } + items = append(items, super_dto.SuperUserNotificationItem{ + ID: n.ID, + TenantID: n.TenantID, + TenantCode: tenantCode, + TenantName: tenantName, + Type: n.Type, + Title: n.Title, + Content: n.Content, + Read: n.IsRead, + CreatedAt: s.formatTime(n.CreatedAt), + }) + } + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: items, + }, nil +} + +func (s *super) ListUserCoupons(ctx context.Context, userID int64, filter *super_dto.SuperUserCouponListFilter) (*requests.Pager, error) { + if userID == 0 { + return nil, errorx.ErrBadRequest.WithMsg("用户ID不能为空") + } + if filter == nil { + filter = &super_dto.SuperUserCouponListFilter{} + } + + couponIDs, couponFilter, err := s.filterCouponIDs(ctx, filter) + if err != nil { + return nil, err + } + + tbl, q := models.UserCouponQuery.QueryContext(ctx) + q = q.Where(tbl.UserID.Eq(userID)) + if filter.Status != nil && *filter.Status != "" { + q = q.Where(tbl.Status.Eq(*filter.Status)) + } + 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 couponFilter { + if len(couponIDs) == 0 { + filter.Pagination.Format() + return &requests.Pager{ + Pagination: filter.Pagination, + Total: 0, + Items: []super_dto.SuperUserCouponItem{}, + }, nil + } + q = q.Where(tbl.CouponID.In(couponIDs...)) + } + + filter.Pagination.Format() + total, err := q.Count() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + list, err := q.Order(tbl.CreatedAt.Desc()). + Offset(int(filter.Pagination.Offset())). + Limit(int(filter.Pagination.Limit)). + Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + if len(list) == 0 { + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: []super_dto.SuperUserCouponItem{}, + }, nil + } + + // 读取券模板与租户信息,便于聚合展示。 + couponIDSet := make(map[int64]struct{}) + couponIDs = make([]int64, 0, len(list)) + for _, uc := range list { + if uc.CouponID > 0 { + if _, ok := couponIDSet[uc.CouponID]; !ok { + couponIDSet[uc.CouponID] = struct{}{} + couponIDs = append(couponIDs, uc.CouponID) + } + } + } + + couponMap := make(map[int64]*models.Coupon, len(couponIDs)) + tenantIDs := make([]int64, 0) + tenantSet := make(map[int64]struct{}) + if len(couponIDs) > 0 { + couponTbl, couponQuery := models.CouponQuery.QueryContext(ctx) + coupons, err := couponQuery.Where(couponTbl.ID.In(couponIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, coupon := range coupons { + couponMap[coupon.ID] = coupon + if coupon.TenantID > 0 { + if _, ok := tenantSet[coupon.TenantID]; !ok { + tenantSet[coupon.TenantID] = struct{}{} + tenantIDs = append(tenantIDs, coupon.TenantID) + } + } + } + } + + tenantMap := make(map[int64]*models.Tenant, len(tenantIDs)) + if len(tenantIDs) > 0 { + tenantTbl, tenantQuery := models.TenantQuery.QueryContext(ctx) + tenants, err := tenantQuery.Where(tenantTbl.ID.In(tenantIDs...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, tenant := range tenants { + tenantMap[tenant.ID] = tenant + } + } + + items := make([]super_dto.SuperUserCouponItem, 0, len(list)) + for _, uc := range list { + item := super_dto.SuperUserCouponItem{ + ID: uc.ID, + CouponID: uc.CouponID, + Status: uc.Status, + StatusDescription: uc.Status.Description(), + OrderID: uc.OrderID, + UsedAt: s.formatTime(uc.UsedAt), + CreatedAt: s.formatTime(uc.CreatedAt), + } + + coupon := couponMap[uc.CouponID] + if coupon != nil { + item.TenantID = coupon.TenantID + item.Title = coupon.Title + item.Description = coupon.Description + item.Type = coupon.Type + item.TypeDescription = coupon.Type.Description() + item.Value = coupon.Value + item.MinOrderAmount = coupon.MinOrderAmount + item.MaxDiscount = coupon.MaxDiscount + item.StartAt = s.formatTime(coupon.StartAt) + item.EndAt = s.formatTime(coupon.EndAt) + + if tenant := tenantMap[coupon.TenantID]; tenant != nil { + item.TenantCode = tenant.Code + item.TenantName = tenant.Name + } + } + + items = append(items, item) + } + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: items, + }, 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)) @@ -3400,6 +3697,75 @@ func (s *super) formatTime(t time.Time) string { return t.Format(time.RFC3339) } +func (s *super) maskIDCard(raw string) string { + text := strings.TrimSpace(raw) + if text == "" { + return "" + } + text = strings.TrimPrefix(text, "ENC:") + if text == "" { + return "" + } + length := len(text) + if length <= 4 { + return strings.Repeat("*", length) + } + if length <= 8 { + return text[:2] + strings.Repeat("*", length-4) + text[length-2:] + } + return text[:3] + strings.Repeat("*", length-7) + text[length-4:] +} + +func (s *super) filterCouponIDs(ctx context.Context, filter *super_dto.SuperUserCouponListFilter) ([]int64, bool, error) { + if filter == nil { + return nil, false, nil + } + + couponTbl, couponQuery := models.CouponQuery.QueryContext(ctx) + applied := false + + if filter.TenantID != nil && *filter.TenantID > 0 { + applied = true + couponQuery = couponQuery.Where(couponTbl.TenantID.Eq(*filter.TenantID)) + } else { + tenantIDs, tenantFilter, err := s.lookupTenantIDs(ctx, filter.TenantCode, filter.TenantName) + if err != nil { + return nil, true, err + } + if tenantFilter { + applied = true + if len(tenantIDs) == 0 { + return []int64{}, true, nil + } + couponQuery = couponQuery.Where(couponTbl.TenantID.In(tenantIDs...)) + } + } + + if filter.Type != nil && *filter.Type != "" { + applied = true + couponQuery = couponQuery.Where(couponTbl.Type.Eq(*filter.Type)) + } + if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" { + applied = true + keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%" + couponQuery = couponQuery.Where(field.Or(couponTbl.Title.Like(keyword), couponTbl.Description.Like(keyword))) + } + + if !applied { + return nil, false, nil + } + + coupons, err := couponQuery.Select(couponTbl.ID).Find() + if err != nil { + return nil, true, errorx.ErrDatabaseError.WithCause(err) + } + ids := make([]int64, 0, len(coupons)) + for _, coupon := range coupons { + ids = append(ids, coupon.ID) + } + return ids, true, nil +} + func (s *super) RejectWithdrawal(ctx context.Context, operatorID, id int64, reason string) error { if operatorID == 0 { return errorx.ErrUnauthorized.WithMsg("缺少操作者信息") diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 2855383..ad60097 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -520,6 +520,100 @@ const docTemplate = `{ } } }, + "/super/v1/tenant-join-requests": { + "get": { + "description": "List tenant join requests across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "List tenant join requests", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperTenantJoinRequestItem" + } + } + } + } + ] + } + } + } + } + }, + "/super/v1/tenant-join-requests/{id}/review": { + "post": { + "description": "Approve or reject a tenant join request", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "Review tenant join request", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Join request ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Review form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TenantJoinReviewForm" + } + } + ], + "responses": { + "200": { + "description": "Reviewed", + "schema": { + "type": "string" + } + } + } + } + }, "/super/v1/tenants": { "get": { "description": "List tenants", @@ -913,6 +1007,229 @@ const docTemplate = `{ } } }, + "/super/v1/tenants/{tenantID}/coupons": { + "post": { + "description": "Create coupon for tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "Create coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "tenantID", + "in": "path", + "required": true + }, + { + "description": "Create form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CouponCreateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + } + }, + "/super/v1/tenants/{tenantID}/coupons/{id}": { + "get": { + "description": "Get coupon detail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "Get coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "tenantID", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + }, + "put": { + "description": "Update coupon for tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "Update coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "tenantID", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CouponUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + } + }, + "/super/v1/tenants/{tenantID}/coupons/{id}/grant": { + "post": { + "description": "Grant coupon to users", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "Grant coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "tenantID", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Grant form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CouponGrantForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuperCouponGrantResponse" + } + } + } + } + }, + "/super/v1/tenants/{tenantID}/invites": { + "post": { + "description": "Create tenant invite code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "Create tenant invite", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "tenantID", + "in": "path", + "required": true + }, + { + "description": "Invite form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TenantInviteCreateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.TenantInviteItem" + } + } + } + } + }, "/super/v1/tenants/{tenantID}/users": { "get": { "description": "List tenant users", @@ -1116,6 +1433,159 @@ const docTemplate = `{ } } }, + "/super/v1/users/{id}/coupons": { + "get": { + "description": "List coupons of a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List user coupons", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperUserCouponItem" + } + } + } + } + ] + } + } + } + } + }, + "/super/v1/users/{id}/notifications": { + "get": { + "description": "List notifications of a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List user notifications", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperUserNotificationItem" + } + } + } + } + ] + } + } + } + } + }, + "/super/v1/users/{id}/realname": { + "get": { + "description": "Get real-name verification detail of a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get user real-name verification detail", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuperUserRealNameResponse" + } + } + } + } + }, "/super/v1/users/{id}/roles": { "patch": { "description": "Update user roles", @@ -4279,6 +4749,19 @@ const docTemplate = `{ "TenantUserRoleTenantAdmin" ] }, + "consts.UserCouponStatus": { + "type": "string", + "enum": [ + "unused", + "used", + "expired" + ], + "x-enum-varnames": [ + "UserCouponStatusUnused", + "UserCouponStatusUsed", + "UserCouponStatusExpired" + ] + }, "consts.UserStatus": { "type": "string", "enum": [ @@ -5635,6 +6118,15 @@ const docTemplate = `{ } } }, + "dto.SuperCouponGrantResponse": { + "type": "object", + "properties": { + "granted": { + "description": "Granted 实际发放数量。", + "type": "integer" + } + } + }, "dto.SuperCouponItem": { "type": "object", "properties": { @@ -5922,6 +6414,67 @@ const docTemplate = `{ } } }, + "dto.SuperTenantJoinRequestItem": { + "type": "object", + "properties": { + "created_at": { + "description": "CreatedAt 申请时间(RFC3339)。", + "type": "string" + }, + "decided_at": { + "description": "DecidedAt 审核时间(RFC3339)。", + "type": "string" + }, + "decided_operator_user_id": { + "description": "DecidedOperatorUserID 审核操作者ID。", + "type": "integer" + }, + "decided_reason": { + "description": "DecidedReason 审核备注/原因。", + "type": "string" + }, + "id": { + "description": "ID 申请记录ID。", + "type": "integer" + }, + "reason": { + "description": "Reason 申请说明。", + "type": "string" + }, + "status": { + "description": "Status 申请状态。", + "type": "string" + }, + "status_description": { + "description": "StatusDescription 状态描述(用于展示)。", + "type": "string" + }, + "tenant_code": { + "description": "TenantCode 租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 租户名称。", + "type": "string" + }, + "updated_at": { + "description": "UpdatedAt 更新时间(RFC3339)。", + "type": "string" + }, + "user_id": { + "description": "UserID 申请用户ID。", + "type": "integer" + }, + "username": { + "description": "Username 申请用户名称。", + "type": "string" + } + } + }, "dto.SuperTenantUserItem": { "type": "object", "properties": { @@ -5943,6 +6496,95 @@ const docTemplate = `{ } } }, + "dto.SuperUserCouponItem": { + "type": "object", + "properties": { + "coupon_id": { + "description": "CouponID 券模板ID。", + "type": "integer" + }, + "created_at": { + "description": "CreatedAt 领取时间(RFC3339)。", + "type": "string" + }, + "description": { + "description": "Description 券描述。", + "type": "string" + }, + "end_at": { + "description": "EndAt 过期时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 用户券ID。", + "type": "integer" + }, + "max_discount": { + "description": "MaxDiscount 折扣券最高抵扣金额(分)。", + "type": "integer" + }, + "min_order_amount": { + "description": "MinOrderAmount 使用门槛金额(分)。", + "type": "integer" + }, + "order_id": { + "description": "OrderID 使用订单ID(未使用为0)。", + "type": "integer" + }, + "start_at": { + "description": "StartAt 生效时间(RFC3339)。", + "type": "string" + }, + "status": { + "description": "Status 用户券状态。", + "allOf": [ + { + "$ref": "#/definitions/consts.UserCouponStatus" + } + ] + }, + "status_description": { + "description": "StatusDescription 用户券状态描述(用于展示)。", + "type": "string" + }, + "tenant_code": { + "description": "TenantCode 券所属租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 券所属租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 券所属租户名称。", + "type": "string" + }, + "title": { + "description": "Title 券标题。", + "type": "string" + }, + "type": { + "description": "Type 券类型。", + "allOf": [ + { + "$ref": "#/definitions/consts.CouponType" + } + ] + }, + "type_description": { + "description": "TypeDescription 券类型描述(用于展示)。", + "type": "string" + }, + "used_at": { + "description": "UsedAt 使用时间(RFC3339)。", + "type": "string" + }, + "value": { + "description": "Value 券面值/折扣值。", + "type": "integer" + } + } + }, "dto.SuperUserLite": { "type": "object", "properties": { @@ -5987,6 +6629,68 @@ const docTemplate = `{ } } }, + "dto.SuperUserNotificationItem": { + "type": "object", + "properties": { + "content": { + "description": "Content 通知内容。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 发送时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 通知ID。", + "type": "integer" + }, + "read": { + "description": "Read 是否已读。", + "type": "boolean" + }, + "tenant_code": { + "description": "TenantCode 通知所属租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 通知所属租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 通知所属租户名称。", + "type": "string" + }, + "title": { + "description": "Title 通知标题。", + "type": "string" + }, + "type": { + "description": "Type 通知类型。", + "type": "string" + } + } + }, + "dto.SuperUserRealNameResponse": { + "type": "object", + "properties": { + "id_card_masked": { + "description": "IDCardMasked 身份证号脱敏展示。", + "type": "string" + }, + "is_real_name_verified": { + "description": "IsRealNameVerified 是否已实名认证。", + "type": "boolean" + }, + "real_name": { + "description": "RealName 真实姓名(来自用户元数据)。", + "type": "string" + }, + "verified_at": { + "description": "VerifiedAt 实名认证时间(RFC3339)。", + "type": "string" + } + } + }, "dto.SuperWalletResponse": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index e5de6eb..c2ed9ad 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -514,6 +514,100 @@ } } }, + "/super/v1/tenant-join-requests": { + "get": { + "description": "List tenant join requests across tenants", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "List tenant join requests", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperTenantJoinRequestItem" + } + } + } + } + ] + } + } + } + } + }, + "/super/v1/tenant-join-requests/{id}/review": { + "post": { + "description": "Approve or reject a tenant join request", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "Review tenant join request", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Join request ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Review form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TenantJoinReviewForm" + } + } + ], + "responses": { + "200": { + "description": "Reviewed", + "schema": { + "type": "string" + } + } + } + } + }, "/super/v1/tenants": { "get": { "description": "List tenants", @@ -907,6 +1001,229 @@ } } }, + "/super/v1/tenants/{tenantID}/coupons": { + "post": { + "description": "Create coupon for tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "Create coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "tenantID", + "in": "path", + "required": true + }, + { + "description": "Create form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CouponCreateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + } + }, + "/super/v1/tenants/{tenantID}/coupons/{id}": { + "get": { + "description": "Get coupon detail", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "Get coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "tenantID", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + }, + "put": { + "description": "Update coupon for tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "Update coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "tenantID", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CouponUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + } + }, + "/super/v1/tenants/{tenantID}/coupons/{id}/grant": { + "post": { + "description": "Grant coupon to users", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Coupon" + ], + "summary": "Grant coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "tenantID", + "in": "path", + "required": true + }, + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Grant form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CouponGrantForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuperCouponGrantResponse" + } + } + } + } + }, + "/super/v1/tenants/{tenantID}/invites": { + "post": { + "description": "Create tenant invite code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "Tenant" + ], + "summary": "Create tenant invite", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "tenantID", + "in": "path", + "required": true + }, + { + "description": "Invite form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TenantInviteCreateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.TenantInviteItem" + } + } + } + } + }, "/super/v1/tenants/{tenantID}/users": { "get": { "description": "List tenant users", @@ -1110,6 +1427,159 @@ } } }, + "/super/v1/users/{id}/coupons": { + "get": { + "description": "List coupons of a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List user coupons", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperUserCouponItem" + } + } + } + } + ] + } + } + } + } + }, + "/super/v1/users/{id}/notifications": { + "get": { + "description": "List notifications of a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "List user notifications", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + }, + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.SuperUserNotificationItem" + } + } + } + } + ] + } + } + } + } + }, + "/super/v1/users/{id}/realname": { + "get": { + "description": "Get real-name verification detail of a user", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "User" + ], + "summary": "Get user real-name verification detail", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "User ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.SuperUserRealNameResponse" + } + } + } + } + }, "/super/v1/users/{id}/roles": { "patch": { "description": "Update user roles", @@ -4273,6 +4743,19 @@ "TenantUserRoleTenantAdmin" ] }, + "consts.UserCouponStatus": { + "type": "string", + "enum": [ + "unused", + "used", + "expired" + ], + "x-enum-varnames": [ + "UserCouponStatusUnused", + "UserCouponStatusUsed", + "UserCouponStatusExpired" + ] + }, "consts.UserStatus": { "type": "string", "enum": [ @@ -5629,6 +6112,15 @@ } } }, + "dto.SuperCouponGrantResponse": { + "type": "object", + "properties": { + "granted": { + "description": "Granted 实际发放数量。", + "type": "integer" + } + } + }, "dto.SuperCouponItem": { "type": "object", "properties": { @@ -5916,6 +6408,67 @@ } } }, + "dto.SuperTenantJoinRequestItem": { + "type": "object", + "properties": { + "created_at": { + "description": "CreatedAt 申请时间(RFC3339)。", + "type": "string" + }, + "decided_at": { + "description": "DecidedAt 审核时间(RFC3339)。", + "type": "string" + }, + "decided_operator_user_id": { + "description": "DecidedOperatorUserID 审核操作者ID。", + "type": "integer" + }, + "decided_reason": { + "description": "DecidedReason 审核备注/原因。", + "type": "string" + }, + "id": { + "description": "ID 申请记录ID。", + "type": "integer" + }, + "reason": { + "description": "Reason 申请说明。", + "type": "string" + }, + "status": { + "description": "Status 申请状态。", + "type": "string" + }, + "status_description": { + "description": "StatusDescription 状态描述(用于展示)。", + "type": "string" + }, + "tenant_code": { + "description": "TenantCode 租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 租户名称。", + "type": "string" + }, + "updated_at": { + "description": "UpdatedAt 更新时间(RFC3339)。", + "type": "string" + }, + "user_id": { + "description": "UserID 申请用户ID。", + "type": "integer" + }, + "username": { + "description": "Username 申请用户名称。", + "type": "string" + } + } + }, "dto.SuperTenantUserItem": { "type": "object", "properties": { @@ -5937,6 +6490,95 @@ } } }, + "dto.SuperUserCouponItem": { + "type": "object", + "properties": { + "coupon_id": { + "description": "CouponID 券模板ID。", + "type": "integer" + }, + "created_at": { + "description": "CreatedAt 领取时间(RFC3339)。", + "type": "string" + }, + "description": { + "description": "Description 券描述。", + "type": "string" + }, + "end_at": { + "description": "EndAt 过期时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 用户券ID。", + "type": "integer" + }, + "max_discount": { + "description": "MaxDiscount 折扣券最高抵扣金额(分)。", + "type": "integer" + }, + "min_order_amount": { + "description": "MinOrderAmount 使用门槛金额(分)。", + "type": "integer" + }, + "order_id": { + "description": "OrderID 使用订单ID(未使用为0)。", + "type": "integer" + }, + "start_at": { + "description": "StartAt 生效时间(RFC3339)。", + "type": "string" + }, + "status": { + "description": "Status 用户券状态。", + "allOf": [ + { + "$ref": "#/definitions/consts.UserCouponStatus" + } + ] + }, + "status_description": { + "description": "StatusDescription 用户券状态描述(用于展示)。", + "type": "string" + }, + "tenant_code": { + "description": "TenantCode 券所属租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 券所属租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 券所属租户名称。", + "type": "string" + }, + "title": { + "description": "Title 券标题。", + "type": "string" + }, + "type": { + "description": "Type 券类型。", + "allOf": [ + { + "$ref": "#/definitions/consts.CouponType" + } + ] + }, + "type_description": { + "description": "TypeDescription 券类型描述(用于展示)。", + "type": "string" + }, + "used_at": { + "description": "UsedAt 使用时间(RFC3339)。", + "type": "string" + }, + "value": { + "description": "Value 券面值/折扣值。", + "type": "integer" + } + } + }, "dto.SuperUserLite": { "type": "object", "properties": { @@ -5981,6 +6623,68 @@ } } }, + "dto.SuperUserNotificationItem": { + "type": "object", + "properties": { + "content": { + "description": "Content 通知内容。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 发送时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 通知ID。", + "type": "integer" + }, + "read": { + "description": "Read 是否已读。", + "type": "boolean" + }, + "tenant_code": { + "description": "TenantCode 通知所属租户编码。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 通知所属租户ID。", + "type": "integer" + }, + "tenant_name": { + "description": "TenantName 通知所属租户名称。", + "type": "string" + }, + "title": { + "description": "Title 通知标题。", + "type": "string" + }, + "type": { + "description": "Type 通知类型。", + "type": "string" + } + } + }, + "dto.SuperUserRealNameResponse": { + "type": "object", + "properties": { + "id_card_masked": { + "description": "IDCardMasked 身份证号脱敏展示。", + "type": "string" + }, + "is_real_name_verified": { + "description": "IsRealNameVerified 是否已实名认证。", + "type": "boolean" + }, + "real_name": { + "description": "RealName 真实姓名(来自用户元数据)。", + "type": "string" + }, + "verified_at": { + "description": "VerifiedAt 实名认证时间(RFC3339)。", + "type": "string" + } + } + }, "dto.SuperWalletResponse": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 06c241b..7d3f1cf 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -92,6 +92,16 @@ definitions: x-enum-varnames: - TenantUserRoleMember - TenantUserRoleTenantAdmin + consts.UserCouponStatus: + enum: + - unused + - used + - expired + type: string + x-enum-varnames: + - UserCouponStatusUnused + - UserCouponStatusUsed + - UserCouponStatusExpired consts.UserStatus: enum: - active @@ -1050,6 +1060,12 @@ definitions: description: Name 租户名称。 type: string type: object + dto.SuperCouponGrantResponse: + properties: + granted: + description: Granted 实际发放数量。 + type: integer + type: object dto.SuperCouponItem: properties: created_at: @@ -1239,6 +1255,51 @@ definitions: required: - status type: object + dto.SuperTenantJoinRequestItem: + properties: + created_at: + description: CreatedAt 申请时间(RFC3339)。 + type: string + decided_at: + description: DecidedAt 审核时间(RFC3339)。 + type: string + decided_operator_user_id: + description: DecidedOperatorUserID 审核操作者ID。 + type: integer + decided_reason: + description: DecidedReason 审核备注/原因。 + type: string + id: + description: ID 申请记录ID。 + type: integer + reason: + description: Reason 申请说明。 + type: string + status: + description: Status 申请状态。 + type: string + status_description: + description: StatusDescription 状态描述(用于展示)。 + type: string + tenant_code: + description: TenantCode 租户编码。 + type: string + tenant_id: + description: TenantID 租户ID。 + type: integer + tenant_name: + description: TenantName 租户名称。 + type: string + updated_at: + description: UpdatedAt 更新时间(RFC3339)。 + type: string + user_id: + description: UserID 申请用户ID。 + type: integer + username: + description: Username 申请用户名称。 + type: string + type: object dto.SuperTenantUserItem: properties: tenant_user: @@ -1250,6 +1311,68 @@ definitions: - $ref: '#/definitions/dto.SuperUserLite' description: User 用户信息。 type: object + dto.SuperUserCouponItem: + properties: + coupon_id: + description: CouponID 券模板ID。 + type: integer + created_at: + description: CreatedAt 领取时间(RFC3339)。 + type: string + description: + description: Description 券描述。 + type: string + end_at: + description: EndAt 过期时间(RFC3339)。 + type: string + id: + description: ID 用户券ID。 + type: integer + max_discount: + description: MaxDiscount 折扣券最高抵扣金额(分)。 + type: integer + min_order_amount: + description: MinOrderAmount 使用门槛金额(分)。 + type: integer + order_id: + description: OrderID 使用订单ID(未使用为0)。 + type: integer + start_at: + description: StartAt 生效时间(RFC3339)。 + type: string + status: + allOf: + - $ref: '#/definitions/consts.UserCouponStatus' + description: Status 用户券状态。 + status_description: + description: StatusDescription 用户券状态描述(用于展示)。 + type: string + tenant_code: + description: TenantCode 券所属租户编码。 + type: string + tenant_id: + description: TenantID 券所属租户ID。 + type: integer + tenant_name: + description: TenantName 券所属租户名称。 + type: string + title: + description: Title 券标题。 + type: string + type: + allOf: + - $ref: '#/definitions/consts.CouponType' + description: Type 券类型。 + type_description: + description: TypeDescription 券类型描述(用于展示)。 + type: string + used_at: + description: UsedAt 使用时间(RFC3339)。 + type: string + value: + description: Value 券面值/折扣值。 + type: integer + type: object dto.SuperUserLite: properties: created_at: @@ -1280,6 +1403,51 @@ definitions: description: VerifiedAt 实名认证时间(RFC3339)。 type: string type: object + dto.SuperUserNotificationItem: + properties: + content: + description: Content 通知内容。 + type: string + created_at: + description: CreatedAt 发送时间(RFC3339)。 + type: string + id: + description: ID 通知ID。 + type: integer + read: + description: Read 是否已读。 + type: boolean + tenant_code: + description: TenantCode 通知所属租户编码。 + type: string + tenant_id: + description: TenantID 通知所属租户ID。 + type: integer + tenant_name: + description: TenantName 通知所属租户名称。 + type: string + title: + description: Title 通知标题。 + type: string + type: + description: Type 通知类型。 + type: string + type: object + dto.SuperUserRealNameResponse: + properties: + id_card_masked: + description: IDCardMasked 身份证号脱敏展示。 + type: string + is_real_name_verified: + description: IsRealNameVerified 是否已实名认证。 + type: boolean + real_name: + description: RealName 真实姓名(来自用户元数据)。 + type: string + verified_at: + description: VerifiedAt 实名认证时间(RFC3339)。 + type: string + type: object dto.SuperWalletResponse: properties: balance: @@ -2421,6 +2589,65 @@ paths: summary: Report overview tags: - Report + /super/v1/tenant-join-requests: + get: + consumes: + - application/json + description: List tenant join requests across tenants + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.SuperTenantJoinRequestItem' + type: array + type: object + summary: List tenant join requests + tags: + - Tenant + /super/v1/tenant-join-requests/{id}/review: + post: + consumes: + - application/json + description: Approve or reject a tenant join request + parameters: + - description: Join request ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Review form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.TenantJoinReviewForm' + produces: + - application/json + responses: + "200": + description: Reviewed + schema: + type: string + summary: Review tenant join request + tags: + - Tenant /super/v1/tenants: get: consumes: @@ -2625,6 +2852,157 @@ paths: summary: Update content status tags: - Content + /super/v1/tenants/{tenantID}/coupons: + post: + consumes: + - application/json + description: Create coupon for tenant + parameters: + - description: Tenant ID + format: int64 + in: path + name: tenantID + required: true + type: integer + - description: Create form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.CouponCreateForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.CouponItem' + summary: Create coupon + tags: + - Coupon + /super/v1/tenants/{tenantID}/coupons/{id}: + get: + consumes: + - application/json + description: Get coupon detail + parameters: + - description: Tenant ID + format: int64 + in: path + name: tenantID + required: true + type: integer + - description: Coupon ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.CouponItem' + summary: Get coupon + tags: + - Coupon + put: + consumes: + - application/json + description: Update coupon for tenant + parameters: + - description: Tenant ID + format: int64 + in: path + name: tenantID + required: true + type: integer + - description: Coupon ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Update form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.CouponUpdateForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.CouponItem' + summary: Update coupon + tags: + - Coupon + /super/v1/tenants/{tenantID}/coupons/{id}/grant: + post: + consumes: + - application/json + description: Grant coupon to users + parameters: + - description: Tenant ID + format: int64 + in: path + name: tenantID + required: true + type: integer + - description: Coupon ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Grant form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.CouponGrantForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SuperCouponGrantResponse' + summary: Grant coupon + tags: + - Coupon + /super/v1/tenants/{tenantID}/invites: + post: + consumes: + - application/json + description: Create tenant invite code + parameters: + - description: Tenant ID + format: int64 + in: path + name: tenantID + required: true + type: integer + - description: Invite form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.TenantInviteCreateForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.TenantInviteItem' + summary: Create tenant invite + tags: + - Tenant /super/v1/tenants/{tenantID}/users: get: consumes: @@ -2767,6 +3145,102 @@ paths: summary: Get user tags: - User + /super/v1/users/{id}/coupons: + get: + consumes: + - application/json + description: List coupons of a user + parameters: + - description: User ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.SuperUserCouponItem' + type: array + type: object + summary: List user coupons + tags: + - User + /super/v1/users/{id}/notifications: + get: + consumes: + - application/json + description: List notifications of a user + parameters: + - description: User ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: limit + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.SuperUserNotificationItem' + type: array + type: object + summary: List user notifications + tags: + - User + /super/v1/users/{id}/realname: + get: + consumes: + - application/json + description: Get real-name verification detail of a user + parameters: + - description: User ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.SuperUserRealNameResponse' + summary: Get user real-name verification detail + tags: + - User /super/v1/users/{id}/roles: patch: consumes: diff --git a/docs/superadmin_progress.md b/docs/superadmin_progress.md index 191cd8f..0816e0a 100644 --- a/docs/superadmin_progress.md +++ b/docs/superadmin_progress.md @@ -4,7 +4,7 @@ ## 1) 总体结论 -- **已落地**:登录、租户/用户/订单/内容的基础管理、内容审核(含批量)、提现审核、报表概览与导出、用户钱包查看、创作者成员审核/邀请、优惠券创建/编辑/发放。 +- **已落地**:登录、租户/用户/订单/内容的基础管理、内容审核(含批量)、提现审核、报表概览与导出、用户钱包/通知/优惠券/实名/充值记录视图、创作者成员审核/邀请、优惠券创建/编辑/发放。 - **部分落地**:平台概览(缺少内容统计与运营趋势)、租户详情(缺财务/报表聚合)、用户管理/详情(缺通知/优惠券/实名/充值/收藏等明细)、创作者/优惠券(缺结算账户审核/发放记录/冻结等深度治理)。 - **未落地**:资产治理、通知中心、审计与系统配置类能力。 @@ -31,14 +31,14 @@ - 缺口:租户级财务与报表聚合入口(成员审核/邀请由超管入口完成)。 ### 2.5 用户管理 `/superadmin/users` -- 状态:**部分完成** -- 已有:用户列表、角色/状态变更、统计。 -- 缺口:实名认证详情、通知/优惠券明细、充值记录等超管视图接口。 +- 状态:**已完成** +- 已有:用户列表、角色/状态变更、统计、余额与实名认证概览。 +- 缺口:无显著功能缺口。 ### 2.6 用户详情 `/superadmin/users/:userID` - 状态:**部分完成** -- 已有:用户资料、租户关系、订单查询、钱包余额与流水。 -- 缺口:通知、优惠券、收藏/点赞、实名认证详情等明细。 +- 已有:用户资料、租户关系、订单查询、钱包余额与流水、充值记录、通知、优惠券、实名认证详情。 +- 缺口:收藏/点赞、关注、内容消费明细等用户互动视图。 ### 2.7 内容治理 `/superadmin/contents` - 状态:**部分完成** @@ -82,12 +82,12 @@ ## 3) `/super/v1` 接口覆盖度概览 -- **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包)、Contents、Orders、Withdrawals、Reports、Coupons(列表/创建/编辑/发放)、Creators(列表)。 -- **缺失/待补**:资产治理、通知中心、用户优惠券/通知明细、创作者申请/结算账户审核、优惠券冻结与发放记录。 +- **已具备**:Auth、Tenants(含成员审核/邀请)、Users(含钱包/通知/优惠券/实名)、Contents、Orders、Withdrawals、Reports、Coupons(列表/创建/编辑/发放)、Creators(列表)。 +- **缺失/待补**:资产治理、通知中心、用户互动明细(收藏/点赞/关注)、创作者申请/结算账户审核、优惠券冻结与发放记录。 ## 4) 建议的下一步(按优先级) -1. **完善用户与通知/优惠券明细**:补齐用户通知、优惠券、实名详情、充值记录等超管视图接口与页面。 -2. **创作者/优惠券深度治理**:补齐创作者申请/结算账户审核、优惠券冻结/发放记录。 -3. **平台概览增强**:补齐内容总量与趋势、退款率、订单漏斗等核心指标。 -4. **资产与通知中心**:补齐资产治理与通知中心接口/页面,形成治理闭环。 +1. **创作者/优惠券深度治理**:补齐创作者申请/结算账户审核、优惠券冻结/发放记录。 +2. **平台概览增强**:补齐内容总量与趋势、退款率、订单漏斗等核心指标。 +3. **资产与通知中心**:补齐资产治理与通知中心接口/页面,形成治理闭环。 +4. **用户互动明细**:补齐收藏/点赞/关注等互动明细视图与聚合能力。 diff --git a/frontend/superadmin/src/service/UserService.js b/frontend/superadmin/src/service/UserService.js index 3bd6b83..bf1f1bc 100644 --- a/frontend/superadmin/src/service/UserService.js +++ b/frontend/superadmin/src/service/UserService.js @@ -117,5 +117,68 @@ export const UserService = { total: data?.total ?? 0, items: normalizeItems(data?.items) }; + }, + async getUserRealName(userID) { + if (!userID) throw new Error('userID is required'); + return requestJson(`/super/v1/users/${userID}/realname`); + }, + async listUserNotifications(userID, { page, limit, tenant_id, type, read, created_at_from, created_at_to } = {}) { + if (!userID) throw new Error('userID is required'); + + const iso = (d) => { + if (!d) return undefined; + const date = d instanceof Date ? d : new Date(d); + if (Number.isNaN(date.getTime())) return undefined; + return date.toISOString(); + }; + + const query = { + page, + limit, + tenant_id, + type, + read, + created_at_from: iso(created_at_from), + created_at_to: iso(created_at_to) + }; + + const data = await requestJson(`/super/v1/users/${userID}/notifications`, { query }); + return { + page: data?.page ?? page ?? 1, + limit: data?.limit ?? limit ?? 10, + total: data?.total ?? 0, + items: normalizeItems(data?.items) + }; + }, + async listUserCoupons(userID, { page, limit, tenant_id, tenant_code, tenant_name, status, type, keyword, created_at_from, created_at_to } = {}) { + if (!userID) throw new Error('userID is required'); + + const iso = (d) => { + if (!d) return undefined; + const date = d instanceof Date ? d : new Date(d); + if (Number.isNaN(date.getTime())) return undefined; + return date.toISOString(); + }; + + const query = { + page, + limit, + tenant_id, + tenant_code, + tenant_name, + status, + type, + keyword, + created_at_from: iso(created_at_from), + created_at_to: iso(created_at_to) + }; + + const data = await requestJson(`/super/v1/users/${userID}/coupons`, { query }); + return { + page: data?.page ?? page ?? 1, + limit: data?.limit ?? limit ?? 10, + total: data?.total ?? 0, + items: normalizeItems(data?.items) + }; } }; diff --git a/frontend/superadmin/src/views/superadmin/UserDetail.vue b/frontend/superadmin/src/views/superadmin/UserDetail.vue index 1bd9735..d743bd1 100644 --- a/frontend/superadmin/src/views/superadmin/UserDetail.vue +++ b/frontend/superadmin/src/views/superadmin/UserDetail.vue @@ -1,6 +1,7 @@