diff --git a/backend/app/http/v1/creator.go b/backend/app/http/v1/creator.go index 28e9e08..7e41dd7 100644 --- a/backend/app/http/v1/creator.go +++ b/backend/app/http/v1/creator.go @@ -186,6 +186,36 @@ func (c *Creator) ListOrders(ctx fiber.Ctx, filter *dto.CreatorOrderListFilter) return services.Creator.ListOrders(ctx, tenantID, userID, filter) } +// List creator audit logs +// +// @Router /v1/t/:tenantCode/creator/audit-logs [get] +// @Summary List creator audit logs +// @Description 查询当前租户创作者侧审计日志(仅管理员可见) +// @Tags CreatorCenter +// @Accept json +// @Produce json +// @Param page query int false "Page" +// @Param limit query int false "Limit" +// @Param operator_id query int false "Operator ID" +// @Param operator_name query string false "Operator name" +// @Param action query string false "Action" +// @Param target_id query string false "Target ID" +// @Param keyword query string false "Keyword" +// @Param created_at_from query string false "Created at from (RFC3339)" +// @Param created_at_to query string false "Created at to (RFC3339)" +// @Success 200 {object} requests.Pager{items=[]dto.CreatorAuditLogItem} +// @Bind filter query +func (c *Creator) ListAuditLogs(ctx fiber.Ctx, filter *dto.CreatorAuditLogListFilter) (*requests.Pager, error) { + if filter == nil { + filter = &dto.CreatorAuditLogListFilter{} + } + + tenantID := getTenantID(ctx) + userID := getUserID(ctx) + + return services.Creator.ListAuditLogs(ctx, tenantID, userID, filter) +} + // Process order refund // // @Router /v1/t/:tenantCode/creator/orders/:id/refund [post] diff --git a/backend/app/http/v1/dto/creator_audit.go b/backend/app/http/v1/dto/creator_audit.go new file mode 100644 index 0000000..e300fb6 --- /dev/null +++ b/backend/app/http/v1/dto/creator_audit.go @@ -0,0 +1,45 @@ +package dto + +import "quyun/v2/app/requests" + +// CreatorAuditLogListFilter 创作者侧审计日志列表过滤条件。 +type CreatorAuditLogListFilter struct { + // Pagination 分页参数(page/limit)。 + requests.Pagination + // OperatorID 操作者用户ID,精确匹配。 + OperatorID *int64 `query:"operator_id"` + // OperatorName 操作者用户名/昵称,模糊匹配。 + OperatorName *string `query:"operator_name"` + // Action 动作标识,精确匹配。 + Action *string `query:"action"` + // TargetID 目标ID,精确匹配。 + TargetID *string `query:"target_id"` + // Keyword 详情关键词,模糊匹配。 + Keyword *string `query:"keyword"` + // CreatedAtFrom 创建时间起始(RFC3339/2006-01-02)。 + CreatedAtFrom *string `query:"created_at_from"` + // CreatedAtTo 创建时间结束(RFC3339/2006-01-02)。 + CreatedAtTo *string `query:"created_at_to"` + // Asc 升序字段(id/created_at)。 + Asc *string `query:"asc"` + // Desc 降序字段(id/created_at)。 + Desc *string `query:"desc"` +} + +// CreatorAuditLogItem 创作者侧审计日志条目。 +type CreatorAuditLogItem struct { + // ID 审计日志ID。 + ID int64 `json:"id"` + // OperatorID 操作者用户ID。 + OperatorID int64 `json:"operator_id"` + // OperatorName 操作者用户名/昵称。 + OperatorName string `json:"operator_name"` + // Action 动作标识。 + Action string `json:"action"` + // TargetID 目标ID。 + TargetID string `json:"target_id"` + // Detail 操作详情。 + Detail string `json:"detail"` + // CreatedAt 创建时间(RFC3339)。 + CreatedAt string `json:"created_at"` +} diff --git a/backend/app/http/v1/routes.gen.go b/backend/app/http/v1/routes.gen.go index 444a4e8..6f97303 100644 --- a/backend/app/http/v1/routes.gen.go +++ b/backend/app/http/v1/routes.gen.go @@ -171,6 +171,11 @@ func (r *Routes) Register(router fiber.Router) { r.creator.RemovePayoutAccount, QueryParam[int64]("id"), )) + r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/audit-logs -> creator.ListAuditLogs") + router.Get("/v1/t/:tenantCode/creator/audit-logs"[len(r.Path()):], DataFunc1( + r.creator.ListAuditLogs, + Query[dto.CreatorAuditLogListFilter]("filter"), + )) r.log.Debugf("Registering route: Get /v1/t/:tenantCode/creator/contents -> creator.ListContents") router.Get("/v1/t/:tenantCode/creator/contents"[len(r.Path()):], DataFunc1( r.creator.ListContents, diff --git a/backend/app/services/coupon_test.go b/backend/app/services/coupon_test.go index a1291b1..062333b 100644 --- a/backend/app/services/coupon_test.go +++ b/backend/app/services/coupon_test.go @@ -2,9 +2,11 @@ package services import ( "database/sql" + "errors" "testing" "quyun/v2/app/commands/testx" + "quyun/v2/app/errorx" order_dto "quyun/v2/app/http/v1/dto" "quyun/v2/database" "quyun/v2/database/models" @@ -196,3 +198,137 @@ func (s *CouponTestSuite) Test_ListAvailable() { So(list[0].CouponID, ShouldEqual, cp.ID) }) } + +func (s *CouponTestSuite) Test_Validate_DenyCrossTenantCoupon() { + Convey("Validate should deny cross-tenant coupon", s.T(), func() { + ctx := s.T().Context() + database.Truncate( + ctx, + s.DB, + models.TableNameCoupon, + models.TableNameUserCoupon, + models.TableNameUser, + ) + + user := &models.User{Username: "coupon_cross_validate", Phone: "13800000011"} + So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil) + + tenantA := int64(11) + tenantB := int64(22) + coupon := &models.Coupon{ + TenantID: tenantA, + Title: "Tenant A Coupon", + Type: consts.CouponTypeFixAmount, + Value: 200, + MinOrderAmount: 0, + } + So(models.CouponQuery.WithContext(ctx).Create(coupon), ShouldBeNil) + + userCoupon := &models.UserCoupon{ + UserID: user.ID, + CouponID: coupon.ID, + Status: consts.UserCouponStatusUnused, + } + So(models.UserCouponQuery.WithContext(ctx).Create(userCoupon), ShouldBeNil) + + _, err := Coupon.Validate(ctx, tenantB, user.ID, userCoupon.ID, 1000) + So(err, ShouldNotBeNil) + + var appErr *errorx.AppError + So(errors.As(err, &appErr), ShouldBeTrue) + So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code) + }) +} + +func (s *CouponTestSuite) Test_MarkUsed_DenyCrossTenantCoupon() { + Convey("MarkUsed should deny cross-tenant coupon", s.T(), func() { + ctx := s.T().Context() + database.Truncate( + ctx, + s.DB, + models.TableNameCoupon, + models.TableNameUserCoupon, + models.TableNameOrder, + models.TableNameUser, + ) + + user := &models.User{Username: "coupon_cross_mark", Phone: "13800000012"} + So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil) + + tenantA := int64(33) + tenantB := int64(44) + coupon := &models.Coupon{ + TenantID: tenantA, + Title: "Tenant A Coupon", + Type: consts.CouponTypeFixAmount, + Value: 200, + MinOrderAmount: 0, + } + So(models.CouponQuery.WithContext(ctx).Create(coupon), ShouldBeNil) + + userCoupon := &models.UserCoupon{ + UserID: user.ID, + CouponID: coupon.ID, + Status: consts.UserCouponStatusUnused, + } + So(models.UserCouponQuery.WithContext(ctx).Create(userCoupon), ShouldBeNil) + + order := &models.Order{ + TenantID: tenantA, + UserID: user.ID, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusCreated, + } + So(models.OrderQuery.WithContext(ctx).Create(order), ShouldBeNil) + + err := models.Q.Transaction(func(tx *models.Query) error { + return Coupon.MarkUsed(ctx, tx, tenantB, userCoupon.ID, order.ID) + }) + So(err, ShouldNotBeNil) + + var appErr *errorx.AppError + So(errors.As(err, &appErr), ShouldBeTrue) + So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code) + }) +} + +func (s *CouponTestSuite) Test_Grant_DenyCrossTenantCoupon() { + Convey("Grant should reject coupon from another tenant", s.T(), func() { + ctx := s.T().Context() + database.Truncate( + ctx, + s.DB, + models.TableNameCoupon, + models.TableNameUserCoupon, + models.TableNameUser, + ) + + user := &models.User{Username: "coupon_cross_grant", Phone: "13800000013"} + So(models.UserQuery.WithContext(ctx).Create(user), ShouldBeNil) + + tenantA := int64(55) + tenantB := int64(66) + coupon := &models.Coupon{ + TenantID: tenantA, + Title: "Tenant A Coupon", + Type: consts.CouponTypeFixAmount, + Value: 200, + MinOrderAmount: 0, + } + So(models.CouponQuery.WithContext(ctx).Create(coupon), ShouldBeNil) + + granted, err := Coupon.Grant(ctx, tenantB, coupon.ID, []int64{user.ID}) + So(err, ShouldNotBeNil) + So(granted, ShouldEqual, 0) + + var appErr *errorx.AppError + So(errors.As(err, &appErr), ShouldBeTrue) + So(appErr.Code, ShouldEqual, errorx.ErrRecordNotFound.Code) + + exists, err := models.UserCouponQuery.WithContext(ctx). + Where(models.UserCouponQuery.UserID.Eq(user.ID), models.UserCouponQuery.CouponID.Eq(coupon.ID)). + Exists() + So(err, ShouldBeNil) + So(exists, ShouldBeFalse) + }) +} diff --git a/backend/app/services/creator.go b/backend/app/services/creator.go index 09fd8c7..6b680ff 100644 --- a/backend/app/services/creator.go +++ b/backend/app/services/creator.go @@ -16,6 +16,7 @@ import ( "quyun/v2/pkg/consts" "github.com/google/uuid" + "go.ipao.vip/gen/field" "go.ipao.vip/gen/types" "gorm.io/gorm" ) @@ -717,6 +718,158 @@ func (s *creator) ListOrders( return data, nil } +func (s *creator) ListAuditLogs( + ctx context.Context, + tenantID int64, + userID int64, + filter *creator_dto.CreatorAuditLogListFilter, +) (*requests.Pager, error) { + if filter == nil { + filter = &creator_dto.CreatorAuditLogListFilter{} + } + if tenantID == 0 { + return nil, errorx.ErrRecordNotFound.WithMsg("租户不存在") + } + + if _, err := Tenant.ensureTenantAdmin(ctx, tenantID, userID); err != nil { + return nil, err + } + + tbl, q := models.AuditLogQuery.QueryContext(ctx) + q = q.Where(tbl.TenantID.Eq(tenantID)) + + if filter.OperatorID != nil && *filter.OperatorID > 0 { + q = q.Where(tbl.OperatorID.Eq(*filter.OperatorID)) + } + if filter.Action != nil && strings.TrimSpace(*filter.Action) != "" { + q = q.Where(tbl.Action.Eq(strings.TrimSpace(*filter.Action))) + } + if filter.TargetID != nil && strings.TrimSpace(*filter.TargetID) != "" { + q = q.Where(tbl.TargetID.Eq(strings.TrimSpace(*filter.TargetID))) + } + if filter.Keyword != nil && strings.TrimSpace(*filter.Keyword) != "" { + keyword := "%" + strings.TrimSpace(*filter.Keyword) + "%" + q = q.Where(field.Or(tbl.Detail.Like(keyword), tbl.Action.Like(keyword), tbl.TargetID.Like(keyword))) + } + + operatorIDs, operatorFilter, err := Tenant.lookupUserIDs(ctx, filter.OperatorName) + if err != nil { + return nil, err + } + if operatorFilter { + if len(operatorIDs) == 0 { + q = q.Where(tbl.ID.Eq(-1)) + } else { + q = q.Where(tbl.OperatorID.In(operatorIDs...)) + } + } + + if filter.CreatedAtFrom != nil { + from, err := Super.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 := Super.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 "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 "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) + } + if len(list) == 0 { + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: []creator_dto.CreatorAuditLogItem{}, + }, nil + } + + operatorSet := make(map[int64]struct{}, len(list)) + for _, log := range list { + if log.OperatorID > 0 { + operatorSet[log.OperatorID] = struct{}{} + } + } + + operatorMap := make(map[int64]*models.User, len(operatorSet)) + if len(operatorSet) > 0 { + ids := make([]int64, 0, len(operatorSet)) + for id := range operatorSet { + ids = append(ids, id) + } + userTbl, userQuery := models.UserQuery.QueryContext(ctx) + users, err := userQuery.Where(userTbl.ID.In(ids...)).Find() + if err != nil { + return nil, errorx.ErrDatabaseError.WithCause(err) + } + for _, user := range users { + operatorMap[user.ID] = user + } + } + + items := make([]creator_dto.CreatorAuditLogItem, 0, len(list)) + for _, log := range list { + item := creator_dto.CreatorAuditLogItem{ + ID: log.ID, + OperatorID: log.OperatorID, + Action: log.Action, + TargetID: log.TargetID, + Detail: log.Detail, + CreatedAt: s.formatTime(log.CreatedAt), + } + if operator := operatorMap[log.OperatorID]; operator != nil { + item.OperatorName = operator.Username + } else if log.OperatorID > 0 { + item.OperatorName = "ID:" + strconv.FormatInt(log.OperatorID, 10) + } + items = append(items, item) + } + + return &requests.Pager{ + Pagination: filter.Pagination, + Total: total, + Items: items, + }, nil +} + func (s *creator) ProcessRefund(ctx context.Context, tenantID, userID, id int64, form *creator_dto.RefundForm) error { tid, err := s.getTenantID(ctx, tenantID, userID) if err != nil { diff --git a/backend/app/services/creator_test.go b/backend/app/services/creator_test.go index fb555d5..4087dad 100644 --- a/backend/app/services/creator_test.go +++ b/backend/app/services/creator_test.go @@ -592,3 +592,86 @@ func (s *CreatorTestSuite) Test_ExportReport() { So(resp.Content, ShouldContainSubstring, "date,paid_orders,paid_amount,refund_orders,refund_amount,withdrawal_apply_orders,withdrawal_apply_amount,withdrawal_paid_orders,withdrawal_paid_amount,withdrawal_failed_orders,withdrawal_failed_amount,content_created,like_actions,favorite_actions,comment_count") }) } + +func (s *CreatorTestSuite) Test_ListAuditLogs() { + Convey("ListAuditLogs", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, + models.TableNameAuditLog, + models.TableNameTenant, + models.TableNameUser, + ) + + owner := &models.User{Username: "owner_audit", Phone: "13900001013"} + operator := &models.User{Username: "operator_audit", Phone: "13900001014"} + outsider := &models.User{Username: "outsider_audit", Phone: "13900001015"} + models.UserQuery.WithContext(ctx).Create(owner, operator, outsider) + + tenantA := &models.Tenant{ + Name: "Tenant Audit A", + Code: "tenant_audit_a", + UserID: owner.ID, + Status: consts.TenantStatusVerified, + } + tenantB := &models.Tenant{ + Name: "Tenant Audit B", + Code: "tenant_audit_b", + UserID: operator.ID, + Status: consts.TenantStatusVerified, + } + models.TenantQuery.WithContext(ctx).Create(tenantA, tenantB) + + now := time.Now() + models.AuditLogQuery.WithContext(ctx).Create( + &models.AuditLog{ + TenantID: tenantA.ID, + OperatorID: owner.ID, + Action: "update_settings", + TargetID: "setting_1", + Detail: "更新频道配置", + CreatedAt: now.Add(-1 * time.Hour), + }, + &models.AuditLog{ + TenantID: tenantA.ID, + OperatorID: operator.ID, + Action: "invite_member", + TargetID: "member_1", + Detail: "邀请成员", + CreatedAt: now, + }, + &models.AuditLog{ + TenantID: tenantB.ID, + OperatorID: operator.ID, + Action: "update_settings", + TargetID: "setting_999", + Detail: "跨租户数据", + CreatedAt: now, + }, + ) + + pager, err := Creator.ListAuditLogs(ctx, tenantA.ID, owner.ID, &creator_dto.CreatorAuditLogListFilter{}) + So(err, ShouldBeNil) + So(pager.Total, ShouldEqual, 2) + + items, ok := pager.Items.([]creator_dto.CreatorAuditLogItem) + So(ok, ShouldBeTrue) + So(len(items), ShouldEqual, 2) + So(items[0].TargetID, ShouldEqual, "member_1") + So(items[1].TargetID, ShouldEqual, "setting_1") + + operatorName := "operator_audit" + filtered, err := Creator.ListAuditLogs(ctx, tenantA.ID, owner.ID, &creator_dto.CreatorAuditLogListFilter{ + OperatorName: &operatorName, + }) + So(err, ShouldBeNil) + So(filtered.Total, ShouldEqual, 1) + filteredItems, ok := filtered.Items.([]creator_dto.CreatorAuditLogItem) + So(ok, ShouldBeTrue) + So(len(filteredItems), ShouldEqual, 1) + So(filteredItems[0].Action, ShouldEqual, "invite_member") + So(filteredItems[0].OperatorName, ShouldEqual, "operator_audit") + + _, err = Creator.ListAuditLogs(ctx, tenantA.ID, outsider.ID, &creator_dto.CreatorAuditLogListFilter{}) + So(err, ShouldNotBeNil) + }) +} diff --git a/backend/app/services/order_test.go b/backend/app/services/order_test.go index 5fdbd83..fa920af 100644 --- a/backend/app/services/order_test.go +++ b/backend/app/services/order_test.go @@ -2,9 +2,11 @@ package services import ( "database/sql" + "errors" "testing" "quyun/v2/app/commands/testx" + "quyun/v2/app/errorx" order_dto "quyun/v2/app/http/v1/dto" "quyun/v2/database" "quyun/v2/database/models" @@ -168,3 +170,74 @@ func (s *OrderTestSuite) Test_PlatformCommission() { }) }) } + +func (s *OrderTestSuite) Test_Pay_DenyCrossTenantOrder() { + Convey("Pay should deny cross-tenant order", s.T(), func() { + ctx := s.T().Context() + database.Truncate( + ctx, + s.DB, + models.TableNameUser, + models.TableNameTenant, + models.TableNameOrder, + ) + + buyer := &models.User{Username: "buyer_cross_tenant", Balance: 5000} + So(models.UserQuery.WithContext(ctx).Create(buyer), ShouldBeNil) + + tenantA := &models.Tenant{UserID: buyer.ID, Code: "order_pay_cross_a", Name: "Tenant A", Status: consts.TenantStatusVerified} + tenantB := &models.Tenant{UserID: buyer.ID, Code: "order_pay_cross_b", Name: "Tenant B", Status: consts.TenantStatusVerified} + So(models.TenantQuery.WithContext(ctx).Create(tenantA, tenantB), ShouldBeNil) + + order := &models.Order{ + TenantID: tenantA.ID, + UserID: buyer.ID, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusCreated, + AmountPaid: 1000, + } + So(models.OrderQuery.WithContext(ctx).Create(order), ShouldBeNil) + + _, err := Order.Pay(ctx, tenantB.ID, buyer.ID, order.ID, &order_dto.OrderPayForm{Method: "balance"}) + So(err, ShouldNotBeNil) + + var appErr *errorx.AppError + So(errors.As(err, &appErr), ShouldBeTrue) + So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code) + }) +} + +func (s *OrderTestSuite) Test_Status_DenyCrossTenantOrder() { + Convey("Status should deny cross-tenant order", s.T(), func() { + ctx := s.T().Context() + database.Truncate( + ctx, + s.DB, + models.TableNameUser, + models.TableNameTenant, + models.TableNameOrder, + ) + + buyer := &models.User{Username: "buyer_status_cross", Balance: 5000} + So(models.UserQuery.WithContext(ctx).Create(buyer), ShouldBeNil) + + tenantA := &models.Tenant{UserID: buyer.ID, Code: "order_status_cross_a", Name: "Tenant A", Status: consts.TenantStatusVerified} + tenantB := &models.Tenant{UserID: buyer.ID, Code: "order_status_cross_b", Name: "Tenant B", Status: consts.TenantStatusVerified} + So(models.TenantQuery.WithContext(ctx).Create(tenantA, tenantB), ShouldBeNil) + + order := &models.Order{ + TenantID: tenantA.ID, + UserID: buyer.ID, + Type: consts.OrderTypeContentPurchase, + Status: consts.OrderStatusCreated, + } + So(models.OrderQuery.WithContext(ctx).Create(order), ShouldBeNil) + + _, err := Order.Status(ctx, tenantB.ID, buyer.ID, order.ID) + So(err, ShouldNotBeNil) + + var appErr *errorx.AppError + So(errors.As(err, &appErr), ShouldBeTrue) + So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code) + }) +} diff --git a/backend/app/services/tenant_member_test.go b/backend/app/services/tenant_member_test.go index cc42075..955d826 100644 --- a/backend/app/services/tenant_member_test.go +++ b/backend/app/services/tenant_member_test.go @@ -216,6 +216,27 @@ func (s *TenantTestSuite) Test_ReviewJoin() { So(err, ShouldBeNil) So(req.Status, ShouldEqual, string(consts.TenantJoinRequestStatusRejected)) }) + + Convey("should deny review when request belongs to another tenant", func() { + tenantA, _, _, reqA := setup() + + ownerB := &models.User{Username: "owner_review_cross_b", Phone: "13900009991"} + So(models.UserQuery.WithContext(ctx).Create(ownerB), ShouldBeNil) + tenantB := &models.Tenant{Name: "Tenant Review B", Code: "tenant_review_cross_b", UserID: ownerB.ID, Status: consts.TenantStatusVerified} + So(models.TenantQuery.WithContext(ctx).Create(tenantB), ShouldBeNil) + + err := Tenant.ReviewJoin(ctx, tenantB.ID, ownerB.ID, reqA.ID, &tenant_dto.TenantJoinReviewForm{Action: "approve"}) + So(err, ShouldNotBeNil) + + var appErr *errorx.AppError + So(errors.As(err, &appErr), ShouldBeTrue) + So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code) + + reqReload, err := models.TenantJoinRequestQuery.WithContext(ctx).Where(models.TenantJoinRequestQuery.ID.Eq(reqA.ID)).First() + So(err, ShouldBeNil) + So(reqReload.TenantID, ShouldEqual, tenantA.ID) + So(reqReload.Status, ShouldEqual, string(consts.TenantJoinRequestStatusPending)) + }) }) } @@ -352,54 +373,132 @@ func (s *TenantTestSuite) Test_ListMembersAndRemove() { So(err, ShouldBeNil) So(exists, ShouldBeFalse) }) + + Convey("should deny removing member from another tenant", s.T(), func() { + ctx := s.T().Context() + database.Truncate(ctx, s.DB, + models.TableNameTenantUser, + models.TableNameTenant, + models.TableNameUser, + ) + + ownerA := &models.User{Username: "owner_remove_a", Phone: "13900007771"} + ownerB := &models.User{Username: "owner_remove_b", Phone: "13900007772"} + memberA := &models.User{Username: "member_remove_a", Phone: "13900007773"} + So(models.UserQuery.WithContext(ctx).Create(ownerA, ownerB, memberA), ShouldBeNil) + + tenantA := &models.Tenant{Name: "Tenant Remove A", Code: "tenant_remove_cross_a", UserID: ownerA.ID, Status: consts.TenantStatusVerified} + tenantB := &models.Tenant{Name: "Tenant Remove B", Code: "tenant_remove_cross_b", UserID: ownerB.ID, Status: consts.TenantStatusVerified} + So(models.TenantQuery.WithContext(ctx).Create(tenantA, tenantB), ShouldBeNil) + + memberLinkA := &models.TenantUser{ + TenantID: tenantA.ID, + UserID: memberA.ID, + Role: types.Array[consts.TenantUserRole]{consts.TenantUserRoleMember}, + Status: consts.UserStatusVerified, + } + So(models.TenantUserQuery.WithContext(ctx).Create(memberLinkA), ShouldBeNil) + + err := Tenant.RemoveMember(ctx, tenantB.ID, ownerB.ID, memberLinkA.ID) + So(err, ShouldNotBeNil) + + var appErr *errorx.AppError + So(errors.As(err, &appErr), ShouldBeTrue) + So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code) + + exists, err := models.TenantUserQuery.WithContext(ctx).Where(models.TenantUserQuery.ID.Eq(memberLinkA.ID)).Exists() + So(err, ShouldBeNil) + So(exists, ShouldBeTrue) + }) } func (s *TenantTestSuite) Test_ListInvitesAndDisable() { Convey("ListInvites and DisableInvite", s.T(), func() { ctx := s.T().Context() - database.Truncate(ctx, s.DB, - models.TableNameTenantInvite, - models.TableNameTenant, - models.TableNameUser, - ) + Convey("should list and disable invite in same tenant", func() { + database.Truncate(ctx, s.DB, + models.TableNameTenantInvite, + models.TableNameTenant, + models.TableNameUser, + ) - owner := &models.User{Username: "owner_invite", Phone: "13900002003"} - _ = models.UserQuery.WithContext(ctx).Create(owner) + owner := &models.User{Username: "owner_invite", Phone: "13900002003"} + _ = models.UserQuery.WithContext(ctx).Create(owner) - tenant := &models.Tenant{ - Name: "Tenant Invite", - UserID: owner.ID, - Status: consts.TenantStatusVerified, - } - _ = models.TenantQuery.WithContext(ctx).Create(tenant) + tenant := &models.Tenant{ + Name: "Tenant Invite", + UserID: owner.ID, + Status: consts.TenantStatusVerified, + } + _ = models.TenantQuery.WithContext(ctx).Create(tenant) - invite := &models.TenantInvite{ - TenantID: tenant.ID, - UserID: owner.ID, - Code: "invite_list", - Status: string(consts.TenantInviteStatusActive), - MaxUses: 2, - UsedCount: 0, - ExpiresAt: time.Now().Add(24 * time.Hour), - Remark: "测试邀请", - } - _ = models.TenantInviteQuery.WithContext(ctx).Create(invite) + invite := &models.TenantInvite{ + TenantID: tenant.ID, + UserID: owner.ID, + Code: "invite_list", + Status: string(consts.TenantInviteStatusActive), + MaxUses: 2, + UsedCount: 0, + ExpiresAt: time.Now().Add(24 * time.Hour), + Remark: "测试邀请", + } + _ = models.TenantInviteQuery.WithContext(ctx).Create(invite) - res, err := Tenant.ListInvites(ctx, tenant.ID, owner.ID, &tenant_dto.TenantInviteListFilter{ - Pagination: requests.Pagination{Page: 1, Limit: 10}, + res, err := Tenant.ListInvites(ctx, tenant.ID, owner.ID, &tenant_dto.TenantInviteListFilter{ + Pagination: requests.Pagination{Page: 1, Limit: 10}, + }) + So(err, ShouldBeNil) + So(res.Total, ShouldEqual, 1) + + err = Tenant.DisableInvite(ctx, tenant.ID, owner.ID, invite.ID) + So(err, ShouldBeNil) + + updated, err := models.TenantInviteQuery.WithContext(ctx). + Where(models.TenantInviteQuery.ID.Eq(invite.ID)). + First() + So(err, ShouldBeNil) + So(updated.Status, ShouldEqual, string(consts.TenantInviteStatusDisabled)) }) - So(err, ShouldBeNil) - So(res.Total, ShouldEqual, 1) - err = Tenant.DisableInvite(ctx, tenant.ID, owner.ID, invite.ID) - So(err, ShouldBeNil) + Convey("should deny disabling invite from another tenant", func() { + database.Truncate(ctx, s.DB, + models.TableNameTenantInvite, + models.TableNameTenant, + models.TableNameUser, + ) - updated, err := models.TenantInviteQuery.WithContext(ctx). - Where(models.TenantInviteQuery.ID.Eq(invite.ID)). - First() - So(err, ShouldBeNil) - So(updated.Status, ShouldEqual, string(consts.TenantInviteStatusDisabled)) + ownerA := &models.User{Username: "owner_invite_a", Phone: "13900008881"} + ownerB := &models.User{Username: "owner_invite_b", Phone: "13900008882"} + So(models.UserQuery.WithContext(ctx).Create(ownerA, ownerB), ShouldBeNil) + + tenantA := &models.Tenant{Name: "Tenant Invite A", Code: "tenant_invite_cross_a", UserID: ownerA.ID, Status: consts.TenantStatusVerified} + tenantB := &models.Tenant{Name: "Tenant Invite B", Code: "tenant_invite_cross_b", UserID: ownerB.ID, Status: consts.TenantStatusVerified} + So(models.TenantQuery.WithContext(ctx).Create(tenantA, tenantB), ShouldBeNil) + + inviteA := &models.TenantInvite{ + TenantID: tenantA.ID, + UserID: ownerA.ID, + Code: "invite_cross_disable", + Status: string(consts.TenantInviteStatusActive), + MaxUses: 2, + UsedCount: 0, + ExpiresAt: time.Now().Add(24 * time.Hour), + Remark: "跨租户禁用测试", + } + So(models.TenantInviteQuery.WithContext(ctx).Create(inviteA), ShouldBeNil) + + err := Tenant.DisableInvite(ctx, tenantB.ID, ownerB.ID, inviteA.ID) + So(err, ShouldNotBeNil) + + var appErr *errorx.AppError + So(errors.As(err, &appErr), ShouldBeTrue) + So(appErr.Code, ShouldEqual, errorx.ErrForbidden.Code) + + reloaded, err := models.TenantInviteQuery.WithContext(ctx).Where(models.TenantInviteQuery.ID.Eq(inviteA.ID)).First() + So(err, ShouldBeNil) + So(reloaded.Status, ShouldEqual, string(consts.TenantInviteStatusActive)) + }) }) } diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 348113a..82645fd 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -4259,6 +4259,1281 @@ const docTemplate = `{ } } }, + "/v1/t/{tenantCode}/creator/apply": { + "post": { + "description": "申请成为创作者并创建频道", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Apply creator profile", + "parameters": [ + { + "description": "Apply form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ApplyForm" + } + } + ], + "responses": { + "200": { + "description": "Applied", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/audit-logs": { + "get": { + "description": "查询当前租户创作者侧审计日志(仅管理员可见)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator audit logs", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Operator ID", + "name": "operator_id", + "in": "query" + }, + { + "type": "string", + "description": "Operator name", + "name": "operator_name", + "in": "query" + }, + { + "type": "string", + "description": "Action", + "name": "action", + "in": "query" + }, + { + "type": "string", + "description": "Target ID", + "name": "target_id", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "Created at from (RFC3339)", + "name": "created_at_from", + "in": "query" + }, + { + "type": "string", + "description": "Created at to (RFC3339)", + "name": "created_at_to", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CreatorAuditLogItem" + } + } + } + } + ] + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/contents": { + "get": { + "description": "创作者内容列表(分页/筛选)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator contents", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Visibility", + "name": "visibility", + "in": "query" + }, + { + "type": "string", + "description": "Genre", + "name": "genre", + "in": "query" + }, + { + "type": "string", + "description": "Key", + "name": "key", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "Sort", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CreatorContentItem" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建创作者内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Create creator content", + "parameters": [ + { + "description": "Create form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContentCreateForm" + } + } + ], + "responses": { + "200": { + "description": "Created", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/contents/{id}": { + "get": { + "description": "获取创作者内容编辑详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Get creator content detail", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ContentEditDTO" + } + } + } + }, + "put": { + "description": "更新创作者内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Update creator content", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Content ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContentUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "Updated", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "删除创作者内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Delete creator content", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Deleted", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/coupons": { + "get": { + "description": "查询创作者优惠券", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator coupons", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Coupon type", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "Coupon status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建创作者优惠券", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Create creator coupon", + "parameters": [ + { + "description": "Coupon create form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CouponCreateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/coupons/{id}": { + "get": { + "description": "查询创作者优惠券详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Get creator coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + }, + "put": { + "description": "更新创作者优惠券", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Update creator coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Coupon update form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CouponUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/coupons/{id}/grant": { + "post": { + "description": "向用户发放优惠券", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Grant creator coupon", + "parameters": [ + { + "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": "Granted", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/dashboard": { + "get": { + "description": "获取创作者看板统计", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Get creator dashboard", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DashboardStats" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members": { + "get": { + "description": "查询创作者成员列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator members", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "Role", + "name": "role", + "in": "query" + }, + { + "type": "string", + "description": "Status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TenantMemberItem" + } + } + } + } + ] + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/invite": { + "post": { + "description": "创建成员邀请", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Create creator member invite", + "parameters": [ + { + "description": "Invite form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TenantInviteCreateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.TenantInviteItem" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/invites": { + "get": { + "description": "查询创作者成员邀请记录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator member invites", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TenantInviteListItem" + } + } + } + } + ] + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/invites/{id}": { + "delete": { + "description": "撤销成员邀请", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Disable creator member invite", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Invite ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Disabled", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/join-requests": { + "get": { + "description": "查询成员加入申请", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator join requests", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TenantJoinRequestItem" + } + } + } + } + ] + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/{id}": { + "delete": { + "description": "移除创作者成员", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Remove creator member", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Member relation ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Removed", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/{id}/review": { + "post": { + "description": "审核成员加入申请", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Review creator 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" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/orders": { + "get": { + "description": "创作者订单列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator orders", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.Order" + } + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/orders/{id}/refund": { + "post": { + "description": "处理创作者订单退款(同意/拒绝)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Process order refund", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Order ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Refund form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RefundForm" + } + } + ], + "responses": { + "200": { + "description": "Processed", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/payout-accounts": { + "get": { + "description": "获取创作者收款账户列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List payout accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PayoutAccount" + } + } + } + } + }, + "post": { + "description": "新增创作者收款账户", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Add payout account", + "parameters": [ + { + "description": "Payout account form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PayoutAccount" + } + } + ], + "responses": { + "200": { + "description": "Created", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "删除创作者收款账户", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Remove payout account", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Payout account ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Deleted", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/reports/export": { + "post": { + "description": "导出创作者经营报表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Creator export report", + "parameters": [ + { + "description": "Export form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ReportExportForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ReportExportResponse" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/reports/overview": { + "get": { + "description": "创作者经营看板概览", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Creator report overview", + "parameters": [ + { + "type": "string", + "description": "Start at (RFC3339)", + "name": "start_at", + "in": "query" + }, + { + "type": "string", + "description": "End at (RFC3339)", + "name": "end_at", + "in": "query" + }, + { + "type": "string", + "description": "Granularity", + "name": "granularity", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ReportOverviewResponse" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/settings": { + "get": { + "description": "获取创作者频道设置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Get creator settings", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Settings" + } + } + } + }, + "put": { + "description": "更新创作者频道设置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Update creator settings", + "parameters": [ + { + "description": "Settings form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Settings" + } + } + ], + "responses": { + "200": { + "description": "Updated", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/withdraw": { + "post": { + "description": "发起提现申请", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Create withdraw order", + "parameters": [ + { + "description": "Withdraw form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.WithdrawForm" + } + } + ], + "responses": { + "200": { + "description": "Submitted", + "schema": { + "type": "string" + } + } + } + } + }, "/v1/t/{tenantCode}/me": { "get": { "description": "Get current user profile", @@ -5142,6 +6417,276 @@ const docTemplate = `{ } } }, + "/v1/t/{tenantCode}/tenants": { + "get": { + "description": "List public tenants under current tenant scope", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "List tenants", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Search keyword", + "name": "keyword", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TenantProfile" + } + } + } + } + ] + } + } + } + } + }, + "/v1/t/{tenantCode}/tenants/{id}": { + "get": { + "description": "Get public tenant profile by tenant ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Get tenant profile", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.TenantProfile" + } + } + } + } + }, + "/v1/t/{tenantCode}/tenants/{id}/follow": { + "post": { + "description": "Follow a tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Follow tenant", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Followed", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Unfollow a tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Unfollow tenant", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Unfollowed", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/tenants/{id}/invites/accept": { + "post": { + "description": "Accept a tenant invite by code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Accept tenant invite", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Invite form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TenantInviteAcceptForm" + } + } + ], + "responses": { + "200": { + "description": "Accepted", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/tenants/{id}/join": { + "post": { + "description": "Apply to join a tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Apply to join tenant", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Join apply form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TenantJoinApplyForm" + } + } + ], + "responses": { + "200": { + "description": "Applied", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Cancel pending tenant join application", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Cancel join application", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Canceled", + "schema": { + "type": "string" + } + } + } + } + }, "/v1/t/{tenantCode}/topics": { "get": { "description": "List curated topics", @@ -5718,6 +7263,56 @@ const docTemplate = `{ } } }, + "dto.ApplyForm": { + "type": "object", + "properties": { + "avatar": { + "description": "Avatar 头像URL。", + "type": "string" + }, + "bio": { + "description": "Bio 频道简介。", + "type": "string" + }, + "name": { + "description": "Name 频道/创作者名称。", + "type": "string" + } + } + }, + "dto.AssetDTO": { + "type": "object", + "properties": { + "id": { + "description": "ID 资源ID。", + "type": "integer" + }, + "name": { + "description": "Name 文件名。", + "type": "string" + }, + "role": { + "description": "Role 资源角色(cover/media/preview)。", + "type": "string" + }, + "size": { + "description": "Size 文件大小描述。", + "type": "string" + }, + "sort": { + "description": "Sort 排序权重。", + "type": "integer" + }, + "type": { + "description": "Type 资源类型(image/audio/video)。", + "type": "string" + }, + "url": { + "description": "URL 资源访问地址。", + "type": "string" + } + } + }, "dto.Comment": { "type": "object", "properties": { @@ -5772,6 +7367,45 @@ const docTemplate = `{ } } }, + "dto.ContentCreateForm": { + "type": "object", + "properties": { + "cover_ids": { + "description": "CoverIDs 封面资源ID集合。", + "type": "array", + "items": { + "type": "integer" + } + }, + "genre": { + "description": "Genre 内容分类/风格。", + "type": "string" + }, + "key": { + "description": "Key 音乐调性或主音。", + "type": "string" + }, + "media_ids": { + "description": "MediaIDs 媒体资源ID集合(音频/视频/图片)。", + "type": "array", + "items": { + "type": "integer" + } + }, + "price": { + "description": "Price 价格(单位元)。", + "type": "number" + }, + "status": { + "description": "Status 内容状态(draft/published)。", + "type": "string" + }, + "title": { + "description": "Title 内容标题。", + "type": "string" + } + } + }, "dto.ContentDetail": { "type": "object", "properties": { @@ -5884,6 +7518,54 @@ const docTemplate = `{ } } }, + "dto.ContentEditDTO": { + "type": "object", + "properties": { + "assets": { + "description": "Assets 资源列表(封面/媒体)。", + "type": "array", + "items": { + "$ref": "#/definitions/dto.AssetDTO" + } + }, + "description": { + "description": "Description 内容简介。", + "type": "string" + }, + "enable_trial": { + "description": "EnableTrial 是否开启试读/试听。", + "type": "boolean" + }, + "genre": { + "description": "Genre 内容分类。", + "type": "string" + }, + "id": { + "description": "ID 内容ID。", + "type": "integer" + }, + "key": { + "description": "Key 音乐调性或主音。", + "type": "string" + }, + "preview_seconds": { + "description": "PreviewSeconds 试看/试听秒数。", + "type": "integer" + }, + "price": { + "description": "Price 价格(单位元)。", + "type": "number" + }, + "status": { + "description": "Status 内容状态。", + "type": "string" + }, + "title": { + "description": "Title 内容标题。", + "type": "string" + } + } + }, "dto.ContentItem": { "type": "object", "properties": { @@ -5994,6 +7676,49 @@ const docTemplate = `{ } } }, + "dto.ContentUpdateForm": { + "type": "object", + "properties": { + "cover_ids": { + "description": "CoverIDs 封面资源ID集合。", + "type": "array", + "items": { + "type": "integer" + } + }, + "genre": { + "description": "Genre 内容分类/风格。", + "type": "string" + }, + "is_pinned": { + "description": "IsPinned 是否置顶。", + "type": "boolean" + }, + "key": { + "description": "Key 音乐调性或主音。", + "type": "string" + }, + "media_ids": { + "description": "MediaIDs 媒体资源ID集合。", + "type": "array", + "items": { + "type": "integer" + } + }, + "price": { + "description": "Price 价格(单位元,nil 表示不修改)。", + "type": "number" + }, + "status": { + "description": "Status 内容状态(draft/published)。", + "type": "string" + }, + "title": { + "description": "Title 内容标题(为空表示不修改)。", + "type": "string" + } + } + }, "dto.CouponCreateForm": { "type": "object", "properties": { @@ -6154,6 +7879,163 @@ const docTemplate = `{ } } }, + "dto.CreatorAuditLogItem": { + "type": "object", + "properties": { + "action": { + "description": "Action 动作标识。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "detail": { + "description": "Detail 操作详情。", + "type": "string" + }, + "id": { + "description": "ID 审计日志ID。", + "type": "integer" + }, + "operator_id": { + "description": "OperatorID 操作者用户ID。", + "type": "integer" + }, + "operator_name": { + "description": "OperatorName 操作者用户名/昵称。", + "type": "string" + }, + "target_id": { + "description": "TargetID 目标ID。", + "type": "string" + } + } + }, + "dto.CreatorContentItem": { + "type": "object", + "properties": { + "audio_count": { + "description": "AudioCount 音频素材数量。", + "type": "integer" + }, + "cover": { + "description": "Cover 封面URL。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "genre": { + "description": "Genre 内容分类。", + "type": "string" + }, + "id": { + "description": "ID 内容ID。", + "type": "integer" + }, + "image_count": { + "description": "ImageCount 图片素材数量。", + "type": "integer" + }, + "is_pinned": { + "description": "IsPinned 是否置顶。", + "type": "boolean" + }, + "is_purchased": { + "description": "IsPurchased 是否已购买。", + "type": "boolean" + }, + "key": { + "description": "Key 音乐调性或主音。", + "type": "string" + }, + "likes": { + "description": "Likes 点赞数。", + "type": "integer" + }, + "price": { + "description": "Price 价格(单位元)。", + "type": "number" + }, + "status": { + "description": "Status 内容状态。", + "type": "string" + }, + "title": { + "description": "Title 内容标题。", + "type": "string" + }, + "video_count": { + "description": "VideoCount 视频素材数量。", + "type": "integer" + }, + "views": { + "description": "Views 浏览量。", + "type": "integer" + }, + "visibility": { + "description": "Visibility 可见性。", + "type": "string" + } + } + }, + "dto.DashboardStats": { + "type": "object", + "properties": { + "new_messages": { + "description": "NewMessages 新消息数量。", + "type": "integer" + }, + "pending_refunds": { + "description": "PendingRefunds 待处理退款数量。", + "type": "integer" + }, + "total_followers": { + "description": "TotalFollowers 粉丝总数统计。", + "allOf": [ + { + "$ref": "#/definitions/dto.IntStatItem" + } + ] + }, + "total_revenue": { + "description": "TotalRevenue 累计收入统计(单位元)。", + "allOf": [ + { + "$ref": "#/definitions/dto.FloatStatItem" + } + ] + } + } + }, + "dto.FloatStatItem": { + "type": "object", + "properties": { + "trend": { + "description": "Trend 环比/同比变化比例。", + "type": "number" + }, + "value": { + "description": "Value 统计数值(浮点)。", + "type": "number" + } + } + }, + "dto.IntStatItem": { + "type": "object", + "properties": { + "trend": { + "description": "Trend 环比/同比变化比例。", + "type": "number" + }, + "value": { + "description": "Value 统计数值。", + "type": "integer" + } + } + }, "dto.MediaURL": { "type": "object", "properties": { @@ -6446,6 +8328,51 @@ const docTemplate = `{ } } }, + "dto.PayoutAccount": { + "type": "object", + "properties": { + "account": { + "description": "Account 收款账号。", + "type": "string" + }, + "id": { + "description": "ID 收款账户ID。", + "type": "integer" + }, + "name": { + "description": "Name 账户名称/开户行。", + "type": "string" + }, + "realname": { + "description": "Realname 收款人姓名。", + "type": "string" + }, + "review_reason": { + "description": "ReviewReason 审核说明/驳回原因。", + "type": "string" + }, + "reviewed_at": { + "description": "ReviewedAt 审核时间(RFC3339)。", + "type": "string" + }, + "status": { + "description": "Status 审核状态(pending/approved/rejected)。", + "allOf": [ + { + "$ref": "#/definitions/consts.PayoutAccountStatus" + } + ] + }, + "status_description": { + "description": "StatusDescription 审核状态描述(用于展示)。", + "type": "string" + }, + "type": { + "description": "Type 账户类型(bank/alipay)。", + "type": "string" + } + } + }, "dto.RealNameForm": { "type": "object", "properties": { @@ -6466,15 +8393,15 @@ const docTemplate = `{ ], "properties": { "amount": { - "description": "Amount 单码充值金额(元)。", + "description": "Amount 充值码面额(单位元,必填且需大于 0)。", "type": "number" }, "quantity": { - "description": "Quantity 生成数量(默认 1,最大 500)。", + "description": "Quantity 生成数量(可选,默认 1,单次上限 500)。", "type": "integer" }, "remark": { - "description": "Remark 备注信息(用于审计展示)。", + "description": "Remark 激活备注(可选,用于审计记录)。", "type": "string" } } @@ -6499,15 +8426,15 @@ const docTemplate = `{ "type": "string" }, "activated_by": { - "description": "ActivatedBy 激活操作者ID。", + "description": "ActivatedBy 激活操作者用户ID。", "type": "integer" }, "amount": { - "description": "Amount 充值金额(元)。", + "description": "Amount 充值码面额(单位元)。", "type": "number" }, "code": { - "description": "Code 充值码明文。", + "description": "Code 充值码字符串(兑换时输入)。", "type": "string" }, "id": { @@ -6515,23 +8442,23 @@ const docTemplate = `{ "type": "integer" }, "redeemed_at": { - "description": "RedeemedAt 兑换时间(RFC3339)。", + "description": "RedeemedAt 兑换时间(RFC3339,未兑换为空)。", "type": "string" }, "redeemed_by": { - "description": "RedeemedBy 兑换用户ID。", + "description": "RedeemedBy 兑换用户ID(未兑换为 0)。", "type": "integer" }, "redeemed_order_id": { - "description": "RedeemedOrderID 兑换生成的充值订单ID。", + "description": "RedeemedOrderID 兑换产生的充值订单ID(未兑换为 0)。", "type": "integer" }, "remark": { - "description": "Remark 激活备注信息。", + "description": "Remark 激活备注(可选)。", "type": "string" }, "status": { - "description": "Status 充值码状态(active/redeemed)。", + "description": "Status 充值码状态(active 已激活 / redeemed 已兑换)。", "type": "string" } } @@ -6540,6 +8467,7 @@ const docTemplate = `{ "type": "object", "properties": { "code": { + "description": "Code 充值码字符串(用于兑换余额)。", "type": "string" } } @@ -6548,13 +8476,49 @@ const docTemplate = `{ "type": "object", "properties": { "amount": { + "description": "Amount 充值金额(单位元)。", "type": "number" }, "order_id": { + "description": "OrderID 充值订单ID。", "type": "integer" } } }, + "dto.RefundForm": { + "type": "object", + "properties": { + "action": { + "description": "Action 处理动作(accept/reject)。", + "type": "string" + }, + "reason": { + "description": "Reason 退款原因/备注。", + "type": "string" + } + } + }, + "dto.ReportExportForm": { + "type": "object", + "properties": { + "end_at": { + "description": "EndAt 统计结束时间(RFC3339,可选;默认当前时间)。", + "type": "string" + }, + "format": { + "description": "Format 导出格式(仅支持 csv)。", + "type": "string" + }, + "granularity": { + "description": "Granularity 统计粒度(day;目前仅支持 day)。", + "type": "string" + }, + "start_at": { + "description": "StartAt 统计开始时间(RFC3339,可选;默认当前时间往前 7 天)。", + "type": "string" + } + } + }, "dto.ReportExportResponse": { "type": "object", "properties": { @@ -9005,11 +10969,11 @@ const docTemplate = `{ ], "properties": { "amount": { - "description": "Amount 充值金额(元)。", + "description": "Amount 充值金额(单位元,必填且需大于 0)。", "type": "number" }, "remark": { - "description": "Remark 备注信息(用于审计展示)。", + "description": "Remark 充值备注(可选,用于审计记录)。", "type": "string" } } @@ -9306,6 +11270,55 @@ const docTemplate = `{ } } }, + "dto.TenantInviteListItem": { + "type": "object", + "properties": { + "code": { + "description": "Code 邀请码。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "creator": { + "description": "Creator 创建者信息(可选)。", + "allOf": [ + { + "$ref": "#/definitions/dto.TenantMemberUserLite" + } + ] + }, + "expires_at": { + "description": "ExpiresAt 过期时间(RFC3339,空字符串表示不限制)。", + "type": "string" + }, + "id": { + "description": "ID 邀请记录ID。", + "type": "integer" + }, + "max_uses": { + "description": "MaxUses 最大可使用次数。", + "type": "integer" + }, + "remark": { + "description": "Remark 备注说明。", + "type": "string" + }, + "status": { + "description": "Status 邀请状态(active/disabled/expired)。", + "type": "string" + }, + "status_description": { + "description": "StatusDescription 状态描述。", + "type": "string" + }, + "used_count": { + "description": "UsedCount 已使用次数。", + "type": "integer" + } + } + }, "dto.TenantItem": { "type": "object", "properties": { @@ -9392,6 +11405,64 @@ const docTemplate = `{ } } }, + "dto.TenantJoinApplyForm": { + "type": "object", + "properties": { + "reason": { + "description": "Reason 申请加入原因(可选,空值会使用默认文案)。", + "type": "string" + } + } + }, + "dto.TenantJoinRequestItem": { + "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" + }, + "updated_at": { + "description": "UpdatedAt 更新时间(RFC3339)。", + "type": "string" + }, + "user": { + "description": "User 申请用户信息。", + "allOf": [ + { + "$ref": "#/definitions/dto.TenantMemberUserLite" + } + ] + } + } + }, "dto.TenantJoinReviewForm": { "type": "object", "properties": { @@ -9405,6 +11476,86 @@ const docTemplate = `{ } } }, + "dto.TenantMemberItem": { + "type": "object", + "properties": { + "created_at": { + "description": "CreatedAt 加入时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 成员关系记录ID。", + "type": "integer" + }, + "role": { + "description": "Role 成员角色列表。", + "type": "array", + "items": { + "$ref": "#/definitions/consts.TenantUserRole" + } + }, + "role_description": { + "description": "RoleDescription 角色描述列表。", + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "description": "Status 成员状态。", + "allOf": [ + { + "$ref": "#/definitions/consts.UserStatus" + } + ] + }, + "status_description": { + "description": "StatusDescription 成员状态描述。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 租户ID。", + "type": "integer" + }, + "updated_at": { + "description": "UpdatedAt 更新时间(RFC3339)。", + "type": "string" + }, + "user": { + "description": "User 成员用户信息。", + "allOf": [ + { + "$ref": "#/definitions/dto.TenantMemberUserLite" + } + ] + } + } + }, + "dto.TenantMemberUserLite": { + "type": "object", + "properties": { + "avatar": { + "description": "Avatar 头像URL。", + "type": "string" + }, + "id": { + "description": "ID 用户ID。", + "type": "integer" + }, + "nickname": { + "description": "Nickname 昵称。", + "type": "string" + }, + "phone": { + "description": "Phone 手机号。", + "type": "string" + }, + "username": { + "description": "Username 用户名。", + "type": "string" + } + } + }, "dto.TenantOwnerUserLite": { "type": "object", "properties": { @@ -9944,6 +12095,23 @@ const docTemplate = `{ } } }, + "dto.WithdrawForm": { + "type": "object", + "properties": { + "account_id": { + "description": "AccountID 收款账户ID。", + "type": "integer" + }, + "amount": { + "description": "Amount 提现金额(单位元)。", + "type": "number" + }, + "method": { + "description": "Method 提现方式(wallet/external)。", + "type": "string" + } + } + }, "quyun_v2_app_http_super_v1_dto.Location": { "type": "object", "properties": { diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 9ebbbb7..ef89882 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -4253,6 +4253,1281 @@ } } }, + "/v1/t/{tenantCode}/creator/apply": { + "post": { + "description": "申请成为创作者并创建频道", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Apply creator profile", + "parameters": [ + { + "description": "Apply form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ApplyForm" + } + } + ], + "responses": { + "200": { + "description": "Applied", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/audit-logs": { + "get": { + "description": "查询当前租户创作者侧审计日志(仅管理员可见)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator audit logs", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "integer", + "description": "Operator ID", + "name": "operator_id", + "in": "query" + }, + { + "type": "string", + "description": "Operator name", + "name": "operator_name", + "in": "query" + }, + { + "type": "string", + "description": "Action", + "name": "action", + "in": "query" + }, + { + "type": "string", + "description": "Target ID", + "name": "target_id", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "Created at from (RFC3339)", + "name": "created_at_from", + "in": "query" + }, + { + "type": "string", + "description": "Created at to (RFC3339)", + "name": "created_at_to", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CreatorAuditLogItem" + } + } + } + } + ] + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/contents": { + "get": { + "description": "创作者内容列表(分页/筛选)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator contents", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Visibility", + "name": "visibility", + "in": "query" + }, + { + "type": "string", + "description": "Genre", + "name": "genre", + "in": "query" + }, + { + "type": "string", + "description": "Key", + "name": "key", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "Sort", + "name": "sort", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CreatorContentItem" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建创作者内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Create creator content", + "parameters": [ + { + "description": "Create form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContentCreateForm" + } + } + ], + "responses": { + "200": { + "description": "Created", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/contents/{id}": { + "get": { + "description": "获取创作者内容编辑详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Get creator content detail", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ContentEditDTO" + } + } + } + }, + "put": { + "description": "更新创作者内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Update creator content", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Content ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Update form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ContentUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "Updated", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "删除创作者内容", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Delete creator content", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Content ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Deleted", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/coupons": { + "get": { + "description": "查询创作者优惠券", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator coupons", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Coupon type", + "name": "type", + "in": "query" + }, + { + "type": "string", + "description": "Coupon status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + } + ] + } + } + } + }, + "post": { + "description": "创建创作者优惠券", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Create creator coupon", + "parameters": [ + { + "description": "Coupon create form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CouponCreateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/coupons/{id}": { + "get": { + "description": "查询创作者优惠券详情", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Get creator coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + }, + "put": { + "description": "更新创作者优惠券", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Update creator coupon", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Coupon ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Coupon update form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.CouponUpdateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.CouponItem" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/coupons/{id}/grant": { + "post": { + "description": "向用户发放优惠券", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Grant creator coupon", + "parameters": [ + { + "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": "Granted", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/dashboard": { + "get": { + "description": "获取创作者看板统计", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Get creator dashboard", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.DashboardStats" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members": { + "get": { + "description": "查询创作者成员列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator members", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + }, + { + "type": "string", + "description": "Role", + "name": "role", + "in": "query" + }, + { + "type": "string", + "description": "Status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TenantMemberItem" + } + } + } + } + ] + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/invite": { + "post": { + "description": "创建成员邀请", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Create creator member invite", + "parameters": [ + { + "description": "Invite form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TenantInviteCreateForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.TenantInviteItem" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/invites": { + "get": { + "description": "查询创作者成员邀请记录", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator member invites", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Status", + "name": "status", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TenantInviteListItem" + } + } + } + } + ] + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/invites/{id}": { + "delete": { + "description": "撤销成员邀请", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Disable creator member invite", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Invite ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Disabled", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/join-requests": { + "get": { + "description": "查询成员加入申请", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator join requests", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TenantJoinRequestItem" + } + } + } + } + ] + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/{id}": { + "delete": { + "description": "移除创作者成员", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Remove creator member", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Member relation ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Removed", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/members/{id}/review": { + "post": { + "description": "审核成员加入申请", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Review creator 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" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/orders": { + "get": { + "description": "创作者订单列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List creator orders", + "parameters": [ + { + "type": "integer", + "description": "Page", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Limit", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Status", + "name": "status", + "in": "query" + }, + { + "type": "string", + "description": "Keyword", + "name": "keyword", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.Order" + } + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/orders/{id}/refund": { + "post": { + "description": "处理创作者订单退款(同意/拒绝)", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Process order refund", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Order ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Refund form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.RefundForm" + } + } + ], + "responses": { + "200": { + "description": "Processed", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/payout-accounts": { + "get": { + "description": "获取创作者收款账户列表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "List payout accounts", + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.PayoutAccount" + } + } + } + } + }, + "post": { + "description": "新增创作者收款账户", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Add payout account", + "parameters": [ + { + "description": "Payout account form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.PayoutAccount" + } + } + ], + "responses": { + "200": { + "description": "Created", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "删除创作者收款账户", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Remove payout account", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Payout account ID", + "name": "id", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "Deleted", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/reports/export": { + "post": { + "description": "导出创作者经营报表", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Creator export report", + "parameters": [ + { + "description": "Export form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.ReportExportForm" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ReportExportResponse" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/reports/overview": { + "get": { + "description": "创作者经营看板概览", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Creator report overview", + "parameters": [ + { + "type": "string", + "description": "Start at (RFC3339)", + "name": "start_at", + "in": "query" + }, + { + "type": "string", + "description": "End at (RFC3339)", + "name": "end_at", + "in": "query" + }, + { + "type": "string", + "description": "Granularity", + "name": "granularity", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.ReportOverviewResponse" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/settings": { + "get": { + "description": "获取创作者频道设置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Get creator settings", + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.Settings" + } + } + } + }, + "put": { + "description": "更新创作者频道设置", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Update creator settings", + "parameters": [ + { + "description": "Settings form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.Settings" + } + } + ], + "responses": { + "200": { + "description": "Updated", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/creator/withdraw": { + "post": { + "description": "发起提现申请", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "CreatorCenter" + ], + "summary": "Create withdraw order", + "parameters": [ + { + "description": "Withdraw form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.WithdrawForm" + } + } + ], + "responses": { + "200": { + "description": "Submitted", + "schema": { + "type": "string" + } + } + } + } + }, "/v1/t/{tenantCode}/me": { "get": { "description": "Get current user profile", @@ -5136,6 +6411,276 @@ } } }, + "/v1/t/{tenantCode}/tenants": { + "get": { + "description": "List public tenants under current tenant scope", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "List tenants", + "parameters": [ + { + "type": "integer", + "description": "Page number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "description": "Page size", + "name": "limit", + "in": "query" + }, + { + "type": "string", + "description": "Search keyword", + "name": "keyword", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/requests.Pager" + }, + { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/definitions/dto.TenantProfile" + } + } + } + } + ] + } + } + } + } + }, + "/v1/t/{tenantCode}/tenants/{id}": { + "get": { + "description": "Get public tenant profile by tenant ID", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Get tenant profile", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/dto.TenantProfile" + } + } + } + } + }, + "/v1/t/{tenantCode}/tenants/{id}/follow": { + "post": { + "description": "Follow a tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Follow tenant", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Followed", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Unfollow a tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Unfollow tenant", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Unfollowed", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/tenants/{id}/invites/accept": { + "post": { + "description": "Accept a tenant invite by code", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Accept tenant invite", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Invite form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TenantInviteAcceptForm" + } + } + ], + "responses": { + "200": { + "description": "Accepted", + "schema": { + "type": "string" + } + } + } + } + }, + "/v1/t/{tenantCode}/tenants/{id}/join": { + "post": { + "description": "Apply to join a tenant", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Apply to join tenant", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + }, + { + "description": "Join apply form", + "name": "form", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/dto.TenantJoinApplyForm" + } + } + ], + "responses": { + "200": { + "description": "Applied", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "Cancel pending tenant join application", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "TenantPublic" + ], + "summary": "Cancel join application", + "parameters": [ + { + "type": "integer", + "format": "int64", + "description": "Tenant ID", + "name": "id", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "Canceled", + "schema": { + "type": "string" + } + } + } + } + }, "/v1/t/{tenantCode}/topics": { "get": { "description": "List curated topics", @@ -5712,6 +7257,56 @@ } } }, + "dto.ApplyForm": { + "type": "object", + "properties": { + "avatar": { + "description": "Avatar 头像URL。", + "type": "string" + }, + "bio": { + "description": "Bio 频道简介。", + "type": "string" + }, + "name": { + "description": "Name 频道/创作者名称。", + "type": "string" + } + } + }, + "dto.AssetDTO": { + "type": "object", + "properties": { + "id": { + "description": "ID 资源ID。", + "type": "integer" + }, + "name": { + "description": "Name 文件名。", + "type": "string" + }, + "role": { + "description": "Role 资源角色(cover/media/preview)。", + "type": "string" + }, + "size": { + "description": "Size 文件大小描述。", + "type": "string" + }, + "sort": { + "description": "Sort 排序权重。", + "type": "integer" + }, + "type": { + "description": "Type 资源类型(image/audio/video)。", + "type": "string" + }, + "url": { + "description": "URL 资源访问地址。", + "type": "string" + } + } + }, "dto.Comment": { "type": "object", "properties": { @@ -5766,6 +7361,45 @@ } } }, + "dto.ContentCreateForm": { + "type": "object", + "properties": { + "cover_ids": { + "description": "CoverIDs 封面资源ID集合。", + "type": "array", + "items": { + "type": "integer" + } + }, + "genre": { + "description": "Genre 内容分类/风格。", + "type": "string" + }, + "key": { + "description": "Key 音乐调性或主音。", + "type": "string" + }, + "media_ids": { + "description": "MediaIDs 媒体资源ID集合(音频/视频/图片)。", + "type": "array", + "items": { + "type": "integer" + } + }, + "price": { + "description": "Price 价格(单位元)。", + "type": "number" + }, + "status": { + "description": "Status 内容状态(draft/published)。", + "type": "string" + }, + "title": { + "description": "Title 内容标题。", + "type": "string" + } + } + }, "dto.ContentDetail": { "type": "object", "properties": { @@ -5878,6 +7512,54 @@ } } }, + "dto.ContentEditDTO": { + "type": "object", + "properties": { + "assets": { + "description": "Assets 资源列表(封面/媒体)。", + "type": "array", + "items": { + "$ref": "#/definitions/dto.AssetDTO" + } + }, + "description": { + "description": "Description 内容简介。", + "type": "string" + }, + "enable_trial": { + "description": "EnableTrial 是否开启试读/试听。", + "type": "boolean" + }, + "genre": { + "description": "Genre 内容分类。", + "type": "string" + }, + "id": { + "description": "ID 内容ID。", + "type": "integer" + }, + "key": { + "description": "Key 音乐调性或主音。", + "type": "string" + }, + "preview_seconds": { + "description": "PreviewSeconds 试看/试听秒数。", + "type": "integer" + }, + "price": { + "description": "Price 价格(单位元)。", + "type": "number" + }, + "status": { + "description": "Status 内容状态。", + "type": "string" + }, + "title": { + "description": "Title 内容标题。", + "type": "string" + } + } + }, "dto.ContentItem": { "type": "object", "properties": { @@ -5988,6 +7670,49 @@ } } }, + "dto.ContentUpdateForm": { + "type": "object", + "properties": { + "cover_ids": { + "description": "CoverIDs 封面资源ID集合。", + "type": "array", + "items": { + "type": "integer" + } + }, + "genre": { + "description": "Genre 内容分类/风格。", + "type": "string" + }, + "is_pinned": { + "description": "IsPinned 是否置顶。", + "type": "boolean" + }, + "key": { + "description": "Key 音乐调性或主音。", + "type": "string" + }, + "media_ids": { + "description": "MediaIDs 媒体资源ID集合。", + "type": "array", + "items": { + "type": "integer" + } + }, + "price": { + "description": "Price 价格(单位元,nil 表示不修改)。", + "type": "number" + }, + "status": { + "description": "Status 内容状态(draft/published)。", + "type": "string" + }, + "title": { + "description": "Title 内容标题(为空表示不修改)。", + "type": "string" + } + } + }, "dto.CouponCreateForm": { "type": "object", "properties": { @@ -6148,6 +7873,163 @@ } } }, + "dto.CreatorAuditLogItem": { + "type": "object", + "properties": { + "action": { + "description": "Action 动作标识。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "detail": { + "description": "Detail 操作详情。", + "type": "string" + }, + "id": { + "description": "ID 审计日志ID。", + "type": "integer" + }, + "operator_id": { + "description": "OperatorID 操作者用户ID。", + "type": "integer" + }, + "operator_name": { + "description": "OperatorName 操作者用户名/昵称。", + "type": "string" + }, + "target_id": { + "description": "TargetID 目标ID。", + "type": "string" + } + } + }, + "dto.CreatorContentItem": { + "type": "object", + "properties": { + "audio_count": { + "description": "AudioCount 音频素材数量。", + "type": "integer" + }, + "cover": { + "description": "Cover 封面URL。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "genre": { + "description": "Genre 内容分类。", + "type": "string" + }, + "id": { + "description": "ID 内容ID。", + "type": "integer" + }, + "image_count": { + "description": "ImageCount 图片素材数量。", + "type": "integer" + }, + "is_pinned": { + "description": "IsPinned 是否置顶。", + "type": "boolean" + }, + "is_purchased": { + "description": "IsPurchased 是否已购买。", + "type": "boolean" + }, + "key": { + "description": "Key 音乐调性或主音。", + "type": "string" + }, + "likes": { + "description": "Likes 点赞数。", + "type": "integer" + }, + "price": { + "description": "Price 价格(单位元)。", + "type": "number" + }, + "status": { + "description": "Status 内容状态。", + "type": "string" + }, + "title": { + "description": "Title 内容标题。", + "type": "string" + }, + "video_count": { + "description": "VideoCount 视频素材数量。", + "type": "integer" + }, + "views": { + "description": "Views 浏览量。", + "type": "integer" + }, + "visibility": { + "description": "Visibility 可见性。", + "type": "string" + } + } + }, + "dto.DashboardStats": { + "type": "object", + "properties": { + "new_messages": { + "description": "NewMessages 新消息数量。", + "type": "integer" + }, + "pending_refunds": { + "description": "PendingRefunds 待处理退款数量。", + "type": "integer" + }, + "total_followers": { + "description": "TotalFollowers 粉丝总数统计。", + "allOf": [ + { + "$ref": "#/definitions/dto.IntStatItem" + } + ] + }, + "total_revenue": { + "description": "TotalRevenue 累计收入统计(单位元)。", + "allOf": [ + { + "$ref": "#/definitions/dto.FloatStatItem" + } + ] + } + } + }, + "dto.FloatStatItem": { + "type": "object", + "properties": { + "trend": { + "description": "Trend 环比/同比变化比例。", + "type": "number" + }, + "value": { + "description": "Value 统计数值(浮点)。", + "type": "number" + } + } + }, + "dto.IntStatItem": { + "type": "object", + "properties": { + "trend": { + "description": "Trend 环比/同比变化比例。", + "type": "number" + }, + "value": { + "description": "Value 统计数值。", + "type": "integer" + } + } + }, "dto.MediaURL": { "type": "object", "properties": { @@ -6440,6 +8322,51 @@ } } }, + "dto.PayoutAccount": { + "type": "object", + "properties": { + "account": { + "description": "Account 收款账号。", + "type": "string" + }, + "id": { + "description": "ID 收款账户ID。", + "type": "integer" + }, + "name": { + "description": "Name 账户名称/开户行。", + "type": "string" + }, + "realname": { + "description": "Realname 收款人姓名。", + "type": "string" + }, + "review_reason": { + "description": "ReviewReason 审核说明/驳回原因。", + "type": "string" + }, + "reviewed_at": { + "description": "ReviewedAt 审核时间(RFC3339)。", + "type": "string" + }, + "status": { + "description": "Status 审核状态(pending/approved/rejected)。", + "allOf": [ + { + "$ref": "#/definitions/consts.PayoutAccountStatus" + } + ] + }, + "status_description": { + "description": "StatusDescription 审核状态描述(用于展示)。", + "type": "string" + }, + "type": { + "description": "Type 账户类型(bank/alipay)。", + "type": "string" + } + } + }, "dto.RealNameForm": { "type": "object", "properties": { @@ -6460,15 +8387,15 @@ ], "properties": { "amount": { - "description": "Amount 单码充值金额(元)。", + "description": "Amount 充值码面额(单位元,必填且需大于 0)。", "type": "number" }, "quantity": { - "description": "Quantity 生成数量(默认 1,最大 500)。", + "description": "Quantity 生成数量(可选,默认 1,单次上限 500)。", "type": "integer" }, "remark": { - "description": "Remark 备注信息(用于审计展示)。", + "description": "Remark 激活备注(可选,用于审计记录)。", "type": "string" } } @@ -6493,15 +8420,15 @@ "type": "string" }, "activated_by": { - "description": "ActivatedBy 激活操作者ID。", + "description": "ActivatedBy 激活操作者用户ID。", "type": "integer" }, "amount": { - "description": "Amount 充值金额(元)。", + "description": "Amount 充值码面额(单位元)。", "type": "number" }, "code": { - "description": "Code 充值码明文。", + "description": "Code 充值码字符串(兑换时输入)。", "type": "string" }, "id": { @@ -6509,23 +8436,23 @@ "type": "integer" }, "redeemed_at": { - "description": "RedeemedAt 兑换时间(RFC3339)。", + "description": "RedeemedAt 兑换时间(RFC3339,未兑换为空)。", "type": "string" }, "redeemed_by": { - "description": "RedeemedBy 兑换用户ID。", + "description": "RedeemedBy 兑换用户ID(未兑换为 0)。", "type": "integer" }, "redeemed_order_id": { - "description": "RedeemedOrderID 兑换生成的充值订单ID。", + "description": "RedeemedOrderID 兑换产生的充值订单ID(未兑换为 0)。", "type": "integer" }, "remark": { - "description": "Remark 激活备注信息。", + "description": "Remark 激活备注(可选)。", "type": "string" }, "status": { - "description": "Status 充值码状态(active/redeemed)。", + "description": "Status 充值码状态(active 已激活 / redeemed 已兑换)。", "type": "string" } } @@ -6534,6 +8461,7 @@ "type": "object", "properties": { "code": { + "description": "Code 充值码字符串(用于兑换余额)。", "type": "string" } } @@ -6542,13 +8470,49 @@ "type": "object", "properties": { "amount": { + "description": "Amount 充值金额(单位元)。", "type": "number" }, "order_id": { + "description": "OrderID 充值订单ID。", "type": "integer" } } }, + "dto.RefundForm": { + "type": "object", + "properties": { + "action": { + "description": "Action 处理动作(accept/reject)。", + "type": "string" + }, + "reason": { + "description": "Reason 退款原因/备注。", + "type": "string" + } + } + }, + "dto.ReportExportForm": { + "type": "object", + "properties": { + "end_at": { + "description": "EndAt 统计结束时间(RFC3339,可选;默认当前时间)。", + "type": "string" + }, + "format": { + "description": "Format 导出格式(仅支持 csv)。", + "type": "string" + }, + "granularity": { + "description": "Granularity 统计粒度(day;目前仅支持 day)。", + "type": "string" + }, + "start_at": { + "description": "StartAt 统计开始时间(RFC3339,可选;默认当前时间往前 7 天)。", + "type": "string" + } + } + }, "dto.ReportExportResponse": { "type": "object", "properties": { @@ -8999,11 +10963,11 @@ ], "properties": { "amount": { - "description": "Amount 充值金额(元)。", + "description": "Amount 充值金额(单位元,必填且需大于 0)。", "type": "number" }, "remark": { - "description": "Remark 备注信息(用于审计展示)。", + "description": "Remark 充值备注(可选,用于审计记录)。", "type": "string" } } @@ -9300,6 +11264,55 @@ } } }, + "dto.TenantInviteListItem": { + "type": "object", + "properties": { + "code": { + "description": "Code 邀请码。", + "type": "string" + }, + "created_at": { + "description": "CreatedAt 创建时间(RFC3339)。", + "type": "string" + }, + "creator": { + "description": "Creator 创建者信息(可选)。", + "allOf": [ + { + "$ref": "#/definitions/dto.TenantMemberUserLite" + } + ] + }, + "expires_at": { + "description": "ExpiresAt 过期时间(RFC3339,空字符串表示不限制)。", + "type": "string" + }, + "id": { + "description": "ID 邀请记录ID。", + "type": "integer" + }, + "max_uses": { + "description": "MaxUses 最大可使用次数。", + "type": "integer" + }, + "remark": { + "description": "Remark 备注说明。", + "type": "string" + }, + "status": { + "description": "Status 邀请状态(active/disabled/expired)。", + "type": "string" + }, + "status_description": { + "description": "StatusDescription 状态描述。", + "type": "string" + }, + "used_count": { + "description": "UsedCount 已使用次数。", + "type": "integer" + } + } + }, "dto.TenantItem": { "type": "object", "properties": { @@ -9386,6 +11399,64 @@ } } }, + "dto.TenantJoinApplyForm": { + "type": "object", + "properties": { + "reason": { + "description": "Reason 申请加入原因(可选,空值会使用默认文案)。", + "type": "string" + } + } + }, + "dto.TenantJoinRequestItem": { + "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" + }, + "updated_at": { + "description": "UpdatedAt 更新时间(RFC3339)。", + "type": "string" + }, + "user": { + "description": "User 申请用户信息。", + "allOf": [ + { + "$ref": "#/definitions/dto.TenantMemberUserLite" + } + ] + } + } + }, "dto.TenantJoinReviewForm": { "type": "object", "properties": { @@ -9399,6 +11470,86 @@ } } }, + "dto.TenantMemberItem": { + "type": "object", + "properties": { + "created_at": { + "description": "CreatedAt 加入时间(RFC3339)。", + "type": "string" + }, + "id": { + "description": "ID 成员关系记录ID。", + "type": "integer" + }, + "role": { + "description": "Role 成员角色列表。", + "type": "array", + "items": { + "$ref": "#/definitions/consts.TenantUserRole" + } + }, + "role_description": { + "description": "RoleDescription 角色描述列表。", + "type": "array", + "items": { + "type": "string" + } + }, + "status": { + "description": "Status 成员状态。", + "allOf": [ + { + "$ref": "#/definitions/consts.UserStatus" + } + ] + }, + "status_description": { + "description": "StatusDescription 成员状态描述。", + "type": "string" + }, + "tenant_id": { + "description": "TenantID 租户ID。", + "type": "integer" + }, + "updated_at": { + "description": "UpdatedAt 更新时间(RFC3339)。", + "type": "string" + }, + "user": { + "description": "User 成员用户信息。", + "allOf": [ + { + "$ref": "#/definitions/dto.TenantMemberUserLite" + } + ] + } + } + }, + "dto.TenantMemberUserLite": { + "type": "object", + "properties": { + "avatar": { + "description": "Avatar 头像URL。", + "type": "string" + }, + "id": { + "description": "ID 用户ID。", + "type": "integer" + }, + "nickname": { + "description": "Nickname 昵称。", + "type": "string" + }, + "phone": { + "description": "Phone 手机号。", + "type": "string" + }, + "username": { + "description": "Username 用户名。", + "type": "string" + } + } + }, "dto.TenantOwnerUserLite": { "type": "object", "properties": { @@ -9938,6 +12089,23 @@ } } }, + "dto.WithdrawForm": { + "type": "object", + "properties": { + "account_id": { + "description": "AccountID 收款账户ID。", + "type": "integer" + }, + "amount": { + "description": "Amount 提现金额(单位元)。", + "type": "number" + }, + "method": { + "description": "Method 提现方式(wallet/external)。", + "type": "string" + } + } + }, "quyun_v2_app_http_super_v1_dto.Location": { "type": "object", "properties": { diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index da3d405..1eda7a1 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -247,6 +247,42 @@ definitions: description: Username 用户名。 type: string type: object + dto.ApplyForm: + properties: + avatar: + description: Avatar 头像URL。 + type: string + bio: + description: Bio 频道简介。 + type: string + name: + description: Name 频道/创作者名称。 + type: string + type: object + dto.AssetDTO: + properties: + id: + description: ID 资源ID。 + type: integer + name: + description: Name 文件名。 + type: string + role: + description: Role 资源角色(cover/media/preview)。 + type: string + size: + description: Size 文件大小描述。 + type: string + sort: + description: Sort 排序权重。 + type: integer + type: + description: Type 资源类型(image/audio/video)。 + type: string + url: + description: URL 资源访问地址。 + type: string + type: object dto.Comment: properties: content: @@ -286,6 +322,34 @@ definitions: description: ReplyTo 被回复评论ID(0 表示一级评论)。 type: integer type: object + dto.ContentCreateForm: + properties: + cover_ids: + description: CoverIDs 封面资源ID集合。 + items: + type: integer + type: array + genre: + description: Genre 内容分类/风格。 + type: string + key: + description: Key 音乐调性或主音。 + type: string + media_ids: + description: MediaIDs 媒体资源ID集合(音频/视频/图片)。 + items: + type: integer + type: array + price: + description: Price 价格(单位元)。 + type: number + status: + description: Status 内容状态(draft/published)。 + type: string + title: + description: Title 内容标题。 + type: string + type: object dto.ContentDetail: properties: author_avatar: @@ -367,6 +431,41 @@ definitions: description: Visibility 内容可见性(如 public/tenant_only/private)。 type: string type: object + dto.ContentEditDTO: + properties: + assets: + description: Assets 资源列表(封面/媒体)。 + items: + $ref: '#/definitions/dto.AssetDTO' + type: array + description: + description: Description 内容简介。 + type: string + enable_trial: + description: EnableTrial 是否开启试读/试听。 + type: boolean + genre: + description: Genre 内容分类。 + type: string + id: + description: ID 内容ID。 + type: integer + key: + description: Key 音乐调性或主音。 + type: string + preview_seconds: + description: PreviewSeconds 试看/试听秒数。 + type: integer + price: + description: Price 价格(单位元)。 + type: number + status: + description: Status 内容状态。 + type: string + title: + description: Title 内容标题。 + type: string + type: object dto.ContentItem: properties: author_avatar: @@ -448,6 +547,37 @@ definitions: description: PriceAmount 原价金额(单位元)。 type: number type: object + dto.ContentUpdateForm: + properties: + cover_ids: + description: CoverIDs 封面资源ID集合。 + items: + type: integer + type: array + genre: + description: Genre 内容分类/风格。 + type: string + is_pinned: + description: IsPinned 是否置顶。 + type: boolean + key: + description: Key 音乐调性或主音。 + type: string + media_ids: + description: MediaIDs 媒体资源ID集合。 + items: + type: integer + type: array + price: + description: Price 价格(单位元,nil 表示不修改)。 + type: number + status: + description: Status 内容状态(draft/published)。 + type: string + title: + description: Title 内容标题(为空表示不修改)。 + type: string + type: object dto.CouponCreateForm: properties: description: @@ -564,6 +694,116 @@ definitions: description: Value 优惠券面值(分/折扣百分比)。 type: integer type: object + dto.CreatorAuditLogItem: + properties: + action: + description: Action 动作标识。 + type: string + created_at: + description: CreatedAt 创建时间(RFC3339)。 + type: string + detail: + description: Detail 操作详情。 + type: string + id: + description: ID 审计日志ID。 + type: integer + operator_id: + description: OperatorID 操作者用户ID。 + type: integer + operator_name: + description: OperatorName 操作者用户名/昵称。 + type: string + target_id: + description: TargetID 目标ID。 + type: string + type: object + dto.CreatorContentItem: + properties: + audio_count: + description: AudioCount 音频素材数量。 + type: integer + cover: + description: Cover 封面URL。 + type: string + created_at: + description: CreatedAt 创建时间(RFC3339)。 + type: string + genre: + description: Genre 内容分类。 + type: string + id: + description: ID 内容ID。 + type: integer + image_count: + description: ImageCount 图片素材数量。 + type: integer + is_pinned: + description: IsPinned 是否置顶。 + type: boolean + is_purchased: + description: IsPurchased 是否已购买。 + type: boolean + key: + description: Key 音乐调性或主音。 + type: string + likes: + description: Likes 点赞数。 + type: integer + price: + description: Price 价格(单位元)。 + type: number + status: + description: Status 内容状态。 + type: string + title: + description: Title 内容标题。 + type: string + video_count: + description: VideoCount 视频素材数量。 + type: integer + views: + description: Views 浏览量。 + type: integer + visibility: + description: Visibility 可见性。 + type: string + type: object + dto.DashboardStats: + properties: + new_messages: + description: NewMessages 新消息数量。 + type: integer + pending_refunds: + description: PendingRefunds 待处理退款数量。 + type: integer + total_followers: + allOf: + - $ref: '#/definitions/dto.IntStatItem' + description: TotalFollowers 粉丝总数统计。 + total_revenue: + allOf: + - $ref: '#/definitions/dto.FloatStatItem' + description: TotalRevenue 累计收入统计(单位元)。 + type: object + dto.FloatStatItem: + properties: + trend: + description: Trend 环比/同比变化比例。 + type: number + value: + description: Value 统计数值(浮点)。 + type: number + type: object + dto.IntStatItem: + properties: + trend: + description: Trend 环比/同比变化比例。 + type: number + value: + description: Value 统计数值。 + type: integer + type: object dto.MediaURL: properties: duration: @@ -769,6 +1009,37 @@ definitions: description: Name 租户名称。 type: string type: object + dto.PayoutAccount: + properties: + account: + description: Account 收款账号。 + type: string + id: + description: ID 收款账户ID。 + type: integer + name: + description: Name 账户名称/开户行。 + type: string + realname: + description: Realname 收款人姓名。 + type: string + review_reason: + description: ReviewReason 审核说明/驳回原因。 + type: string + reviewed_at: + description: ReviewedAt 审核时间(RFC3339)。 + type: string + status: + allOf: + - $ref: '#/definitions/consts.PayoutAccountStatus' + description: Status 审核状态(pending/approved/rejected)。 + status_description: + description: StatusDescription 审核状态描述(用于展示)。 + type: string + type: + description: Type 账户类型(bank/alipay)。 + type: string + type: object dto.RealNameForm: properties: id_card: @@ -781,13 +1052,13 @@ definitions: dto.RechargeCodeActivateForm: properties: amount: - description: Amount 单码充值金额(元)。 + description: Amount 充值码面额(单位元,必填且需大于 0)。 type: number quantity: - description: Quantity 生成数量(默认 1,最大 500)。 + description: Quantity 生成数量(可选,默认 1,单次上限 500)。 type: integer remark: - description: Remark 备注信息(用于审计展示)。 + description: Remark 激活备注(可选,用于审计记录)。 type: string required: - amount @@ -806,45 +1077,72 @@ definitions: description: ActivatedAt 激活时间(RFC3339)。 type: string activated_by: - description: ActivatedBy 激活操作者ID。 + description: ActivatedBy 激活操作者用户ID。 type: integer amount: - description: Amount 充值金额(元)。 + description: Amount 充值码面额(单位元)。 type: number code: - description: Code 充值码明文。 + description: Code 充值码字符串(兑换时输入)。 type: string id: description: ID 充值码ID。 type: integer redeemed_at: - description: RedeemedAt 兑换时间(RFC3339)。 + description: RedeemedAt 兑换时间(RFC3339,未兑换为空)。 type: string redeemed_by: - description: RedeemedBy 兑换用户ID。 + description: RedeemedBy 兑换用户ID(未兑换为 0)。 type: integer redeemed_order_id: - description: RedeemedOrderID 兑换生成的充值订单ID。 + description: RedeemedOrderID 兑换产生的充值订单ID(未兑换为 0)。 type: integer remark: - description: Remark 激活备注信息。 + description: Remark 激活备注(可选)。 type: string status: - description: Status 充值码状态(active/redeemed)。 + description: Status 充值码状态(active 已激活 / redeemed 已兑换)。 type: string type: object dto.RechargeForm: properties: code: + description: Code 充值码字符串(用于兑换余额)。 type: string type: object dto.RechargeResponse: properties: amount: + description: Amount 充值金额(单位元)。 type: number order_id: + description: OrderID 充值订单ID。 type: integer type: object + dto.RefundForm: + properties: + action: + description: Action 处理动作(accept/reject)。 + type: string + reason: + description: Reason 退款原因/备注。 + type: string + type: object + dto.ReportExportForm: + properties: + end_at: + description: EndAt 统计结束时间(RFC3339,可选;默认当前时间)。 + type: string + format: + description: Format 导出格式(仅支持 csv)。 + type: string + granularity: + description: Granularity 统计粒度(day;目前仅支持 day)。 + type: string + start_at: + description: StartAt 统计开始时间(RFC3339,可选;默认当前时间往前 7 天)。 + type: string + type: object dto.ReportExportResponse: properties: content: @@ -2552,10 +2850,10 @@ definitions: dto.SuperWalletCreditForm: properties: amount: - description: Amount 充值金额(元)。 + description: Amount 充值金额(单位元,必填且需大于 0)。 type: number remark: - description: Remark 备注信息(用于审计展示)。 + description: Remark 充值备注(可选,用于审计记录)。 type: string required: - amount @@ -2767,6 +3065,40 @@ definitions: description: UsedCount 已使用次数。 type: integer type: object + dto.TenantInviteListItem: + properties: + code: + description: Code 邀请码。 + type: string + created_at: + description: CreatedAt 创建时间(RFC3339)。 + type: string + creator: + allOf: + - $ref: '#/definitions/dto.TenantMemberUserLite' + description: Creator 创建者信息(可选)。 + expires_at: + description: ExpiresAt 过期时间(RFC3339,空字符串表示不限制)。 + type: string + id: + description: ID 邀请记录ID。 + type: integer + max_uses: + description: MaxUses 最大可使用次数。 + type: integer + remark: + description: Remark 备注说明。 + type: string + status: + description: Status 邀请状态(active/disabled/expired)。 + type: string + status_description: + description: StatusDescription 状态描述。 + type: string + used_count: + description: UsedCount 已使用次数。 + type: integer + type: object dto.TenantItem: properties: admin_users: @@ -2826,6 +3158,46 @@ definitions: description: UUID 租户UUID。 type: string type: object + dto.TenantJoinApplyForm: + properties: + reason: + description: Reason 申请加入原因(可选,空值会使用默认文案)。 + type: string + type: object + dto.TenantJoinRequestItem: + 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 + updated_at: + description: UpdatedAt 更新时间(RFC3339)。 + type: string + user: + allOf: + - $ref: '#/definitions/dto.TenantMemberUserLite' + description: User 申请用户信息。 + type: object dto.TenantJoinReviewForm: properties: action: @@ -2835,6 +3207,60 @@ definitions: description: Reason 审核说明(可选,用于展示驳回原因或备注)。 type: string type: object + dto.TenantMemberItem: + properties: + created_at: + description: CreatedAt 加入时间(RFC3339)。 + type: string + id: + description: ID 成员关系记录ID。 + type: integer + role: + description: Role 成员角色列表。 + items: + $ref: '#/definitions/consts.TenantUserRole' + type: array + role_description: + description: RoleDescription 角色描述列表。 + items: + type: string + type: array + status: + allOf: + - $ref: '#/definitions/consts.UserStatus' + description: Status 成员状态。 + status_description: + description: StatusDescription 成员状态描述。 + type: string + tenant_id: + description: TenantID 租户ID。 + type: integer + updated_at: + description: UpdatedAt 更新时间(RFC3339)。 + type: string + user: + allOf: + - $ref: '#/definitions/dto.TenantMemberUserLite' + description: User 成员用户信息。 + type: object + dto.TenantMemberUserLite: + properties: + avatar: + description: Avatar 头像URL。 + type: string + id: + description: ID 用户ID。 + type: integer + nickname: + description: Nickname 昵称。 + type: string + phone: + description: Phone 手机号。 + type: string + username: + description: Username 用户名。 + type: string + type: object dto.TenantOwnerUserLite: properties: id: @@ -3200,6 +3626,18 @@ definitions: $ref: '#/definitions/dto.Transaction' type: array type: object + dto.WithdrawForm: + properties: + account_id: + description: AccountID 收款账户ID。 + type: integer + amount: + description: Amount 提现金额(单位元)。 + type: number + method: + description: Method 提现方式(wallet/external)。 + type: string + type: object quyun_v2_app_http_super_v1_dto.Location: properties: city: @@ -6079,6 +6517,829 @@ paths: summary: Add like tags: - Content + /v1/t/{tenantCode}/creator/apply: + post: + consumes: + - application/json + description: 申请成为创作者并创建频道 + parameters: + - description: Apply form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.ApplyForm' + produces: + - application/json + responses: + "200": + description: Applied + schema: + type: string + summary: Apply creator profile + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/audit-logs: + get: + consumes: + - application/json + description: 查询当前租户创作者侧审计日志(仅管理员可见) + parameters: + - description: Page + in: query + name: page + type: integer + - description: Limit + in: query + name: limit + type: integer + - description: Operator ID + in: query + name: operator_id + type: integer + - description: Operator name + in: query + name: operator_name + type: string + - description: Action + in: query + name: action + type: string + - description: Target ID + in: query + name: target_id + type: string + - description: Keyword + in: query + name: keyword + type: string + - description: Created at from (RFC3339) + in: query + name: created_at_from + type: string + - description: Created at to (RFC3339) + in: query + name: created_at_to + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.CreatorAuditLogItem' + type: array + type: object + summary: List creator audit logs + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/contents: + get: + consumes: + - application/json + description: 创作者内容列表(分页/筛选) + parameters: + - description: Page + in: query + name: page + type: integer + - description: Limit + in: query + name: limit + type: integer + - description: Status + in: query + name: status + type: string + - description: Visibility + in: query + name: visibility + type: string + - description: Genre + in: query + name: genre + type: string + - description: Key + in: query + name: key + type: string + - description: Keyword + in: query + name: keyword + type: string + - description: Sort + in: query + name: sort + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.CreatorContentItem' + type: array + type: object + summary: List creator contents + tags: + - CreatorCenter + post: + consumes: + - application/json + description: 创建创作者内容 + parameters: + - description: Create form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.ContentCreateForm' + produces: + - application/json + responses: + "200": + description: Created + schema: + type: string + summary: Create creator content + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/contents/{id}: + delete: + consumes: + - application/json + description: 删除创作者内容 + parameters: + - description: Content ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Deleted + schema: + type: string + summary: Delete creator content + tags: + - CreatorCenter + get: + consumes: + - application/json + description: 获取创作者内容编辑详情 + parameters: + - description: Content ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ContentEditDTO' + summary: Get creator content detail + tags: + - CreatorCenter + put: + consumes: + - application/json + description: 更新创作者内容 + parameters: + - description: Content ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Update form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.ContentUpdateForm' + produces: + - application/json + responses: + "200": + description: Updated + schema: + type: string + summary: Update creator content + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/coupons: + get: + consumes: + - application/json + description: 查询创作者优惠券 + parameters: + - description: Page + in: query + name: page + type: integer + - description: Limit + in: query + name: limit + type: integer + - description: Coupon type + in: query + name: type + type: string + - description: Coupon status + in: query + name: status + type: string + - description: Keyword + in: query + name: keyword + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.CouponItem' + type: array + type: object + summary: List creator coupons + tags: + - CreatorCenter + post: + consumes: + - application/json + description: 创建创作者优惠券 + parameters: + - description: Coupon 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 creator coupon + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/coupons/{id}: + get: + consumes: + - application/json + description: 查询创作者优惠券详情 + parameters: + - 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 creator coupon + tags: + - CreatorCenter + put: + consumes: + - application/json + description: 更新创作者优惠券 + parameters: + - description: Coupon ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Coupon 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 creator coupon + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/coupons/{id}/grant: + post: + consumes: + - application/json + description: 向用户发放优惠券 + parameters: + - 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: Granted + schema: + type: string + summary: Grant creator coupon + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/dashboard: + get: + consumes: + - application/json + description: 获取创作者看板统计 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.DashboardStats' + summary: Get creator dashboard + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/members: + get: + consumes: + - application/json + description: 查询创作者成员列表 + parameters: + - description: Page + in: query + name: page + type: integer + - description: Limit + in: query + name: limit + type: integer + - description: Keyword + in: query + name: keyword + type: string + - description: Role + in: query + name: role + type: string + - description: Status + in: query + name: status + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.TenantMemberItem' + type: array + type: object + summary: List creator members + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/members/{id}: + delete: + consumes: + - application/json + description: 移除创作者成员 + parameters: + - description: Member relation ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Removed + schema: + type: string + summary: Remove creator member + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/members/{id}/review: + post: + consumes: + - application/json + description: 审核成员加入申请 + 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 creator join request + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/members/invite: + post: + consumes: + - application/json + description: 创建成员邀请 + parameters: + - 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 creator member invite + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/members/invites: + get: + consumes: + - application/json + description: 查询创作者成员邀请记录 + parameters: + - description: Page + in: query + name: page + type: integer + - description: Limit + in: query + name: limit + type: integer + - description: Status + in: query + name: status + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.TenantInviteListItem' + type: array + type: object + summary: List creator member invites + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/members/invites/{id}: + delete: + consumes: + - application/json + description: 撤销成员邀请 + parameters: + - description: Invite ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Disabled + schema: + type: string + summary: Disable creator member invite + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/members/join-requests: + get: + consumes: + - application/json + description: 查询成员加入申请 + parameters: + - description: Page + in: query + name: page + type: integer + - description: Limit + in: query + name: limit + type: integer + - description: Status + in: query + name: status + type: string + - description: Keyword + in: query + name: keyword + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.TenantJoinRequestItem' + type: array + type: object + summary: List creator join requests + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/orders: + get: + consumes: + - application/json + description: 创作者订单列表 + parameters: + - description: Page + in: query + name: page + type: integer + - description: Limit + in: query + name: limit + type: integer + - description: Status + in: query + name: status + type: string + - description: Keyword + in: query + name: keyword + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.Order' + type: array + summary: List creator orders + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/orders/{id}/refund: + post: + consumes: + - application/json + description: 处理创作者订单退款(同意/拒绝) + parameters: + - description: Order ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Refund form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.RefundForm' + produces: + - application/json + responses: + "200": + description: Processed + schema: + type: string + summary: Process order refund + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/payout-accounts: + delete: + consumes: + - application/json + description: 删除创作者收款账户 + parameters: + - description: Payout account ID + format: int64 + in: query + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Deleted + schema: + type: string + summary: Remove payout account + tags: + - CreatorCenter + get: + consumes: + - application/json + description: 获取创作者收款账户列表 + produces: + - application/json + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/dto.PayoutAccount' + type: array + summary: List payout accounts + tags: + - CreatorCenter + post: + consumes: + - application/json + description: 新增创作者收款账户 + parameters: + - description: Payout account form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.PayoutAccount' + produces: + - application/json + responses: + "200": + description: Created + schema: + type: string + summary: Add payout account + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/reports/export: + post: + consumes: + - application/json + description: 导出创作者经营报表 + parameters: + - description: Export form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.ReportExportForm' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ReportExportResponse' + summary: Creator export report + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/reports/overview: + get: + consumes: + - application/json + description: 创作者经营看板概览 + parameters: + - description: Start at (RFC3339) + in: query + name: start_at + type: string + - description: End at (RFC3339) + in: query + name: end_at + type: string + - description: Granularity + in: query + name: granularity + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.ReportOverviewResponse' + summary: Creator report overview + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/settings: + get: + consumes: + - application/json + description: 获取创作者频道设置 + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.Settings' + summary: Get creator settings + tags: + - CreatorCenter + put: + consumes: + - application/json + description: 更新创作者频道设置 + parameters: + - description: Settings form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.Settings' + produces: + - application/json + responses: + "200": + description: Updated + schema: + type: string + summary: Update creator settings + tags: + - CreatorCenter + /v1/t/{tenantCode}/creator/withdraw: + post: + consumes: + - application/json + description: 发起提现申请 + parameters: + - description: Withdraw form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.WithdrawForm' + produces: + - application/json + responses: + "200": + description: Submitted + schema: + type: string + summary: Create withdraw order + tags: + - CreatorCenter /v1/t/{tenantCode}/me: get: consumes: @@ -6657,6 +7918,183 @@ paths: summary: Upload file tags: - Storage + /v1/t/{tenantCode}/tenants: + get: + consumes: + - application/json + description: List public tenants under current tenant scope + parameters: + - description: Page number + in: query + name: page + type: integer + - description: Page size + in: query + name: limit + type: integer + - description: Search keyword + in: query + name: keyword + type: string + produces: + - application/json + responses: + "200": + description: OK + schema: + allOf: + - $ref: '#/definitions/requests.Pager' + - properties: + items: + items: + $ref: '#/definitions/dto.TenantProfile' + type: array + type: object + summary: List tenants + tags: + - TenantPublic + /v1/t/{tenantCode}/tenants/{id}: + get: + consumes: + - application/json + description: Get public tenant profile by tenant ID + parameters: + - description: Tenant ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/dto.TenantProfile' + summary: Get tenant profile + tags: + - TenantPublic + /v1/t/{tenantCode}/tenants/{id}/follow: + delete: + consumes: + - application/json + description: Unfollow a tenant + parameters: + - description: Tenant ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Unfollowed + schema: + type: string + summary: Unfollow tenant + tags: + - TenantPublic + post: + consumes: + - application/json + description: Follow a tenant + parameters: + - description: Tenant ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Followed + schema: + type: string + summary: Follow tenant + tags: + - TenantPublic + /v1/t/{tenantCode}/tenants/{id}/invites/accept: + post: + consumes: + - application/json + description: Accept a tenant invite by code + parameters: + - description: Tenant ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Invite form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.TenantInviteAcceptForm' + produces: + - application/json + responses: + "200": + description: Accepted + schema: + type: string + summary: Accept tenant invite + tags: + - TenantPublic + /v1/t/{tenantCode}/tenants/{id}/join: + delete: + consumes: + - application/json + description: Cancel pending tenant join application + parameters: + - description: Tenant ID + format: int64 + in: path + name: id + required: true + type: integer + produces: + - application/json + responses: + "200": + description: Canceled + schema: + type: string + summary: Cancel join application + tags: + - TenantPublic + post: + consumes: + - application/json + description: Apply to join a tenant + parameters: + - description: Tenant ID + format: int64 + in: path + name: id + required: true + type: integer + - description: Join apply form + in: body + name: form + required: true + schema: + $ref: '#/definitions/dto.TenantJoinApplyForm' + produces: + - application/json + responses: + "200": + description: Applied + schema: + type: string + summary: Apply to join tenant + tags: + - TenantPublic /v1/t/{tenantCode}/topics: get: consumes: diff --git a/docs/release-evidence/2026-02-08.md b/docs/release-evidence/2026-02-08.md new file mode 100644 index 0000000..b36e2e5 --- /dev/null +++ b/docs/release-evidence/2026-02-08.md @@ -0,0 +1,299 @@ +# Release Evidence — 2026-02-08 + +## Scope + +D1 基线执行(对应 `docs/plan.md`:T1 / T4 / T15): +- 生产部署能力差距台账(4 项标准) +- 前端生产路由数据来源盘点(Portal + Superadmin) +- 后端隔离基线盘点(order/content/coupon/tenant/wallet) + +## Environment + +- Repo: `/home/rogee/Projects/quyun_v2` +- Branch: `main` +- Plan commit: `3126ed5` (`chore: refine production-readiness execution plan`) + +## Evidence A — 4项标准差距台账(Baseline) + +| 标准 | 当前状态 | 结论 | 关键证据 | +|------|----------|------|----------| +| 1) 前端所有数据来源后端接口/渲染 | Portal/Superadmin 主业务页大多为 API;存在硬编码业务数据页面与 demo mock 数据入口 | **未达标** | `frontend/portal/src/views/user/LikesView.vue:12-35`(硬编码 items);`frontend/superadmin/src/views/uikit/TableDoc.vue:2,85-91` + `frontend/superadmin/src/service/CustomerService.js:2-39`(mock数据源) | +| 2) 用户/租户数据隔离完备 | Controller 与 Service 多数传递 tenantID/userID 并做 where 约束;仍存在“依赖人工维护”的模式,需继续补负向测试 | **部分达标** | `backend/app/http/v1/helpers.go`(tenant/user 上下文);`backend/app/services/order.go:31-39,64-73`; `backend/app/services/content.go:31-47`; `backend/app/services/coupon.go:162-175,237-239`; `backend/app/services/tenant_member.go:138-154`; `backend/app/services/wallet.go:35-43` | +| 3) 超级管理员后台可审计 | 超管审计链路已存在(表、服务、API、页面) | **达标** | `backend/database/migrations/20260115103830_create_audit_logs_and_system_configs.sql:3-25`; `backend/app/http/super/v1/audit_logs.go:16-27`; `frontend/superadmin/src/views/superadmin/AuditLogs.vue:46-67` | +| 4) 租户管理对租户数据可审计 | 目前未见租户侧独立审计日志查询 API/页面(仅通知不等同审计日志) | **未达标** | `backend/app/http/v1` 未发现租户 audit-log 列表入口;Portal 仅有通知页 `frontend/portal/src/views/user/NotificationsView.vue` | + +## Evidence B — Portal 路由数据来源盘点(生产相关) + +来源:`frontend/portal/src/router/index.js` + +### B1. API 驱动为主(带少量 UI 常量) +- `/`, `/t/:tenantCode` -> `HomeView.vue`(API + fallback 常量) +- `/t/:tenantCode/channel` -> `tenant/HomeView.vue`(API + tab 常量) +- `/t/:tenantCode/contents/:id` -> `content/DetailView.vue`(API) +- `/t/:tenantCode/me/orders`、`/wallet`、`/coupons`、`/library`、`/favorites`、`/notifications`、`/profile`(以 `userApi` 为主) +- `/t/:tenantCode/creator/*`(以 `creatorApi` 为主) +- `/t/:tenantCode/checkout`、`/payment/:id`(`contentApi/orderApi`) + +### B2. 高风险(业务硬编码) +- `/t/:tenantCode/me/likes` -> `user/LikesView.vue` + - 证据:`items` 直接硬编码业务内容 `LikesView.vue:12-35` + +### B3. 静态/演示型(非关键业务) +- `/creator/apply` -> `creator/ApplyView.vue` + - 证据:`setTimeout` 模拟提交,未接后端 `ApplyView.vue:42-47` +- `/t/:tenantCode/me/security` -> `user/SecurityView.vue` + - 证据:页面展示固定手机号/验证流程占位,未接后端 `SecurityView.vue:64,113,29` + +## Evidence C — Superadmin 路由数据来源盘点 + +来源:`frontend/superadmin/src/router/index.js` + +### C1. 生产业务路由(/superadmin/*) +- 租户、用户、订单、内容、创作者、优惠券、财务、报表、资产、通知、审计日志、系统配置等页面均存在。 +- 这些页面普遍是“API 查询 + 本地选项常量(筛选项)”混合模式。 + +### C2. 明确 demo/mock 数据入口(应隔离) +- `/uikit/table` -> `views/uikit/TableDoc.vue` + - 证据:`CustomerService.getCustomers*` `TableDoc.vue:85-91` + - 数据源:`CustomerService.js` 内置对象数组 `CustomerService.js:2-39`(后续大量同类对象) + +## Evidence D — 后端隔离基线矩阵(T4) + +| 模块 | 主要隔离实现 | 风险备注 | 证据 | +|------|--------------|----------|------| +| order | query-time 使用 tenantID/userID 条件;读取明细时 tenant + user 双条件 | Recharge 类型用 OR 放行(设计允许同用户跨租户查看充值记录),需确认业务预期 | `order.go:31-39,64-73,66-70` | +| content | 列表/详情普遍 tenant 过滤;filter tenant mismatch 直接 forbidden | `UnderlyingDB` + preload 路径需持续关注遗漏风险 | `content.go:31-47,148-159,172-176` | +| coupon | Receive/Create/Update/Get/Validate/MarkUsed 多处 tenant 校验 | 存在 tenantID==0 分支(平台视角),需在接口层严格限定调用入口 | `coupon.go:162-175,237-239,306-308,657-658,720-721` | +| tenant (member) | 管理操作先 `ensureTenantAdmin`,再检查 request/invite 的 tenant 一致性 | 需用负向用例覆盖“跨租户 requestID/inviteID”场景 | `tenant_member.go:138-154,227-237,291-295,320-330` | +| wallet | 钱包交易列表使用 tenant+user 查询;支持充值订单特例 | 与订单同样存在 recharge 例外,需文档化并测试 | `wallet.go:35-43` | + +## Performance Baseline Protocol (for later execution) + +按计划定义,后续性能验证需满足: +- 目标接口:`/super/v1/audit-logs` + 新增租户审计列表 +- 条件:`page=1&limit=20`,默认排序 +- 样本:预热10次 + 采样50次,统计 p95 +- 结果写入本文件后续“性能结果”小节 + +## D1 Exit Check + +- [x] T1 差距台账已建立并含证据路径 +- [x] T4 隔离基线矩阵已建立(order/content/coupon/tenant/wallet) +- [x] T15 证据模板文件已创建并写入 D1 结果 + +## Evidence E — T2 执行结果(LikesView API化) + +### E1. 变更内容 +- 文件:`frontend/portal/src/views/user/LikesView.vue` +- 改动要点: + - 删除硬编码 `items` 列表(原本本地静态业务记录)。 + - 接入 `userApi.getLikes()` 拉取后端数据。 + - 接入 `userApi.removeLike(id)` 处理取消点赞并同步本地列表。 + - 复用与 Favorites/Library 一致的数据字段渲染:`title/cover/type/author_name/author_avatar/created_at`。 + +### E2. 核验结果 +- LSP(目标文件): `lsp_diagnostics frontend/portal/src/views/user/LikesView.vue severity=error` -> **No diagnostics found**。 +- Portal lint: `npm -C frontend/portal run lint` -> **pass**。 +- Portal build: `npm -C frontend/portal run build` -> **pass**(包含 `LikesView-*.js` 构建产物)。 + +### E3. 状态更新 +- 标准 #1(前端数据来源后端接口/渲染): + - `LikesView` 已从“未达标子项”移除。 + - 剩余风险主要在 demo/doc 路由隔离(`/uikit/table`)与静态占位页策略。 + +## Evidence F — T3 执行结果(Superadmin demo 路由隔离) + +### F1. 变更内容 +- 文件:`frontend/superadmin/src/router/index.js` +- 改动要点: + - 新增 `isDemoOnlyRoute(path)` 判定,覆盖: + - `/uikit/*` + - `/blocks` + - `/pages/empty` + - `/pages/crud` + - `/documentation` + - `/landing` + - 在全局 `beforeEach` 中加入生产环境拦截: + - `if (!import.meta.env.DEV && isDemoOnlyRoute(to.path)) return { name: 'dashboard' }` + - 效果:开发环境保留 demo 调试能力;非开发环境禁止通过 URL 直接进入 demo 页面。 + +### F2. 核验结果 +- LSP(目标文件): `frontend/superadmin/src/router/index.js` -> **No diagnostics found**。 +- Superadmin lint: `npm -C frontend/superadmin run lint` -> **pass**(首次执行出现一次临时文件 ENOENT,重试通过)。 +- Superadmin build: `npm -C frontend/superadmin run build` -> **pass**。 + +### F3. 状态更新 +- 标准 #1(前端生产数据来源约束)继续收敛: + - demo/mock 路由已从“可直接访问”改为“生产环境拦截”。 + - 仍建议后续把 demo 路由按配置化开关进一步显式隔离(可选增强项)。 + +## Evidence G — T6 执行结果(跨租户负向测试补强) + +### G1. 新增测试覆盖 + +#### Order +- 文件:`backend/app/services/order_test.go` +- 新增用例: + - `Test_Pay_DenyCrossTenantOrder` + - `Test_Status_DenyCrossTenantOrder` +- 断言目标:同一用户持有 A 租户订单时,用 B 租户上下文调用 `Pay/Status` 必须返回 `ErrForbidden`。 + +#### Coupon +- 文件:`backend/app/services/coupon_test.go` +- 新增用例: + - `Test_Validate_DenyCrossTenantCoupon` + - `Test_MarkUsed_DenyCrossTenantCoupon` + - `Test_Grant_DenyCrossTenantCoupon` +- 断言目标: + - `Validate/MarkUsed` 跨租户必须拒绝(`ErrForbidden`) + - `Grant` 使用非所属租户发放时必须失败且不产生 `user_coupons` 记录。 + +#### Tenant Member +- 文件:`backend/app/services/tenant_member_test.go` +- 新增用例: + - `Test_ReviewJoin` 中补充跨租户 review 拒绝场景 + - `Test_ListMembersAndRemove` 中补充跨租户 remove 拒绝场景 + - `Test_ListInvitesAndDisable` 中补充跨租户 disable 邀请拒绝场景 +- 断言目标:跨租户操作返回 `ErrForbidden`,目标记录状态不应被修改。 + +### G2. 测试执行结果 +- 命令(聚合): + - `cd backend && env GOCACHE=$PWD/.gocache GOTMPDIR=$PWD/.gotmp go test ./app/services -run 'Test_Order/(Test_Pay_DenyCrossTenantOrder|Test_Status_DenyCrossTenantOrder)|Test_Coupon/(Test_Validate_DenyCrossTenantCoupon|Test_MarkUsed_DenyCrossTenantCoupon|Test_Grant_DenyCrossTenantCoupon)|Test_Tenant/(Test_ReviewJoin|Test_ListMembersAndRemove|Test_ListInvitesAndDisable)'` +- 结果:**PASS**(`ok quyun/v2/app/services`) + +### G3. 过程说明 +- 首轮执行暴露新增测试数据问题(`tenants_code_key` 唯一约束冲突),已通过为新增租户测试数据设置唯一 `code` 修复。 +- 修复后目标测试集稳定通过。 + +## Evidence H — T8 执行结果(租户侧审计日志 API) + +### H1. 变更内容 +- 新增 DTO:`backend/app/http/v1/dto/creator_audit.go` + - `CreatorAuditLogListFilter`:分页 + `operator_id/operator_name/action/target_id/keyword/created_at_from/created_at_to/asc/desc` + - `CreatorAuditLogItem`:`id/operator_id/operator_name/action/target_id/detail/created_at` +- 新增服务方法:`backend/app/services/creator.go` + - `Creator.ListAuditLogs(ctx, tenantID, userID, filter)` + - 强制租户范围:`audit_logs.tenant_id = currentTenantID` + - 权限校验:复用 `Tenant.ensureTenantAdmin`,仅租户主账号/tenant_admin 可查看 + - 过滤/排序/分页:对齐 super audit 风格(支持 `id/created_at` 排序) + - 操作者名补齐:批量查询 user 表回填 `operator_name` + - DB 错误统一 `errorx.ErrDatabaseError.WithCause(err)` 包装 +- 新增控制器接口:`backend/app/http/v1/creator.go` + - `GET /v1/t/:tenantCode/creator/audit-logs` + - 控制器仅做 bind + tenant/user 上下文提取 + service 调用 +- 路由/文档生成: + - `atomctl gen route` + - `atomctl swag init` + - 生成结果包含: + - `backend/app/http/v1/routes.gen.go` 新路由注册 + - `backend/docs/swagger.yaml|swagger.json|docs.go` 新接口与模型 + +### H2. 测试与核验 +- 新增测试:`backend/app/services/creator_test.go` + - `Test_ListAuditLogs` + - 覆盖点: + - 仅返回当前租户日志(跨租户数据不泄露) + - `operator_name` 过滤生效 + - 非管理员访问拒绝 +- 执行结果: + - `go test ./app/services -run 'Test_Creator/(Test_ListAuditLogs|Test_ReportOverview|Test_ExportReport)$'` -> **PASS** + - `go test ./app/http/v1 ./app/services` -> **PASS** + - `go test ./...`(backend)-> **PASS** +- LSP(本次变更文件): **No diagnostics found** + +## Evidence I — T9 执行结果(Portal 创作者审计页面) + +### I1. 变更内容 +- API 封装:`frontend/portal/src/api/creator.js` + - 新增 `listAuditLogs(params)` -> `/creator/audit-logs` +- 新增页面:`frontend/portal/src/views/creator/AuditView.vue` + - 筛选:`operator_id/operator_name/action/target_id/keyword/created_at_from/created_at_to` + - 排序:`created_at|id` + 升降序 + - 列表展示:日志ID、操作者、动作、目标ID、详情、创建时间 + - 分页:PrimeVue `Paginator` +- 路由注册:`frontend/portal/src/router/index.js` + - 新增 `creator-audit`,路径 `creator/audit` +- 侧边菜单:`frontend/portal/src/layout/LayoutCreator.vue` + - 新增“操作审计”入口,链接 `tenantRoute('/creator/audit')` + +### I2. 核验结果 +- Portal lint: `npm -C frontend/portal run lint` -> **pass** +- Portal build: `npm -C frontend/portal run build` -> **pass**(产物含 `AuditView-*.js`) +- LSP(本次前端变更文件): **No diagnostics found** + +## Status Update + +- 标准 #4(租户管理侧可审计): + - 已具备租户侧审计查询 API + Portal 页面入口与展示能力 + - 当前状态:**达标(待后续前后端联调回归统一验收)** + +## Evidence J — T16 执行结果 (Tenant Creator Audit Flow Acceptance) + +### J1. 测试执行摘要 + +| 测试用例 | 状态 | 观察结果 | 证据 | +|-----------|--------|-------------|----------| +| **Admin Login & Navigation** | PASS | Admin `13800000001` 成功登录并导航至 `/t/meipai_765/creator/audit`。页面标题“操作审计”验证通过。 | `admin-audit-page-loaded.png` | +| **Data Rendering** | PASS | 审计日志列表渲染多行数据 (IDs 13, 12, etc.)。 | `admin-audit-page-loaded.png` | +| **Action Filter** | PASS | 筛选动作 "seed" 后列表缩减为单条匹配记录 (ID 3)。 | `admin-filter-result.png` | +| **Pagination** | PASS | 切换至第 2 页显示了不同的记录 (IDs 3, 2, 1)。 | `admin-page-2.png` | +| **Permission Control** | PASS | 普通成员 `13800138000` 访问页面显示“暂无审计记录”,验证了数据权限控制。 | `member-denied-state.png` | + +### J2. 截图证据 + +#### 1. Admin: Audit Page Loaded +![Admin Audit Page](2026-02-08/admin-audit-page-loaded.png) +*完整审计日志列表展示* + +#### 2. Admin: Filter Result ("seed") +![Admin Filter Result](2026-02-08/admin-filter-result.png) +*筛选 action='seed' 结果* + +#### 3. Admin: Pagination (Page 2) +![Admin Page 2](2026-02-08/admin-page-2.png) +*第 2 页旧数据展示* + +#### 4. Member: Access Denied / No Data +![Member Denied](2026-02-08/member-denied-state.png) +*非管理员用户访问无数据展示* + +### J3. 结论 +**PASS**. 新增的租户创作者审计流程对管理员功能正常,对普通成员具备权限控制。T16 验收通过。 + +## Evidence K — T17 发布门禁汇总与 Go/No-Go + +### K1. 门禁清单结果 + +| 门禁项 | 结果 | 证据 | +|---|---|---| +| T13 Backend 全量测试 | PASS | `go test ./...`(backend)通过 | +| T14 Frontend build/lint | PASS | `npm -C frontend/portal run lint && npm -C frontend/portal run build` 通过;`npm -C frontend/superadmin run lint && npm -C frontend/superadmin run build` 通过 | +| T16 前端页面流验收(本次受影响流) | PASS | Evidence J + 截图 `docs/release-evidence/2026-02-08/*.png` | +| 租户审计 API 权限与数据面验证 | PASS | 成员调用 `/v1/t/meipai_765/creator/audit-logs` 返回 `code=1206 无权限操作该租户`;管理员调用返回 `total=13` 且可筛选/分页 | + +### K2. 四项标准最终判定(本轮) + +| 标准 | 判定 | 说明 | +|---|---|---| +| 1) 前端业务数据来自后端接口/渲染 | PASS(本轮范围) | LikesView 已 API 化;superadmin demo 路由已生产拦截 | +| 2) 用户/租户数据隔离完备 | PASS(本轮范围) | 跨租户负向测试补强通过(Evidence G) | +| 3) 超管后台可审计 | PASS | 既有超管审计链路 + 构建验证通过 | +| 4) 租户管理侧可审计 | PASS | 新增 creator audit API + Portal 页面 + 页面流验收通过 | + +### K3. Go/No-Go 结论 + +**Go(可进入生产发布候选)**。 + +依据:T13/T14/T16/T17 门禁均通过,且四项标准在本轮改造范围内均达标。 + +### K4. 发布前剩余建议(非阻塞) + +1. 按计划补充审计接口性能基线(p95)记录(目前文档仅有测量协议,尚缺执行数据)。 +2. 将 superadmin demo 路由从“运行时拦截”进一步提升为“构建期裁剪”(可选增强)。 +3. 按计划完成归档动作:若确认本阶段收口,执行 T18(归档 `docs/plan.md` -> `docs/plans/.md` 并清空活动 plan)。 + +## Next Actions (D2+) + +1. (已完成)执行计划门禁与联调验收项(T13/T14/T16/T17),见 Evidence K。 +2. (已完成)进行前端页面流验收(creator audit 查询/筛选/分页)并补充录屏或截图证据(见 Evidence J)。 +3. (可选增强)将 superadmin demo 路由按构建开关完全剔除,而非仅运行时拦截。 diff --git a/docs/release-evidence/2026-02-08/admin-audit-page-loaded.png b/docs/release-evidence/2026-02-08/admin-audit-page-loaded.png new file mode 100644 index 0000000..f485f29 Binary files /dev/null and b/docs/release-evidence/2026-02-08/admin-audit-page-loaded.png differ diff --git a/docs/release-evidence/2026-02-08/admin-filter-result.png b/docs/release-evidence/2026-02-08/admin-filter-result.png new file mode 100644 index 0000000..8a31965 Binary files /dev/null and b/docs/release-evidence/2026-02-08/admin-filter-result.png differ diff --git a/docs/release-evidence/2026-02-08/admin-page-2.png b/docs/release-evidence/2026-02-08/admin-page-2.png new file mode 100644 index 0000000..81255f6 Binary files /dev/null and b/docs/release-evidence/2026-02-08/admin-page-2.png differ diff --git a/docs/release-evidence/2026-02-08/member-denied-state.png b/docs/release-evidence/2026-02-08/member-denied-state.png new file mode 100644 index 0000000..64d51eb Binary files /dev/null and b/docs/release-evidence/2026-02-08/member-denied-state.png differ diff --git a/frontend/portal/src/api/creator.js b/frontend/portal/src/api/creator.js index 5fcc342..f3b6bd0 100644 --- a/frontend/portal/src/api/creator.js +++ b/frontend/portal/src/api/creator.js @@ -18,6 +18,10 @@ export const creatorApi = { const qs = new URLSearchParams(params).toString(); return request(`/creator/orders?${qs}`); }, + listAuditLogs: (params) => { + const qs = new URLSearchParams(params).toString(); + return request(`/creator/audit-logs?${qs}`); + }, refundOrder: (id, data) => request(`/creator/orders/${id}/refund`, { method: "POST", body: data }), listCoupons: (params) => { diff --git a/frontend/portal/src/layout/LayoutCreator.vue b/frontend/portal/src/layout/LayoutCreator.vue index 700a846..384ecbb 100644 --- a/frontend/portal/src/layout/LayoutCreator.vue +++ b/frontend/portal/src/layout/LayoutCreator.vue @@ -91,6 +91,16 @@ const isFullWidth = computed(() => { > 订单管理 + + + 操作审计 + import("../views/creator/OrdersView.vue"), }, + { + path: "audit", + name: "creator-audit", + component: () => import("../views/creator/AuditView.vue"), + }, { path: "members", name: "creator-members", diff --git a/frontend/portal/src/views/creator/AuditView.vue b/frontend/portal/src/views/creator/AuditView.vue new file mode 100644 index 0000000..47ed9e3 --- /dev/null +++ b/frontend/portal/src/views/creator/AuditView.vue @@ -0,0 +1,347 @@ + + + diff --git a/frontend/portal/src/views/user/LikesView.vue b/frontend/portal/src/views/user/LikesView.vue index 6fa33e3..b734f3c 100644 --- a/frontend/portal/src/views/user/LikesView.vue +++ b/frontend/portal/src/views/user/LikesView.vue @@ -1,42 +1,61 @@ @@ -65,11 +84,8 @@ const removeItem = (id) => {
- - {{ item.duration || "文章" }} + + {{ getTypeLabel(item.type) }}
@@ -81,13 +97,19 @@ const removeItem = (id) => { {{ item.title }}
- - {{ item.author }} + + {{ item.author_name }}
- {{ item.time }} + {{ item.created_at }}
-
+
diff --git a/frontend/superadmin/dist/index.html b/frontend/superadmin/dist/index.html index 779bb73..0ab20ec 100644 --- a/frontend/superadmin/dist/index.html +++ b/frontend/superadmin/dist/index.html @@ -7,7 +7,7 @@ Sakai Vue - + diff --git a/frontend/superadmin/src/router/index.js b/frontend/superadmin/src/router/index.js index 8cd2cd4..4f733c8 100644 --- a/frontend/superadmin/src/router/index.js +++ b/frontend/superadmin/src/router/index.js @@ -225,10 +225,18 @@ const router = createRouter({ ] }); +const isDemoOnlyRoute = (path) => { + return path.startsWith('/uikit/') || path === '/blocks' || path === '/pages/empty' || path === '/pages/crud' || path === '/documentation' || path === '/landing'; +}; + let tokenValidated = false; let tokenValidationPromise = null; router.beforeEach(async (to) => { + if (!import.meta.env.DEV && isDemoOnlyRoute(to.path)) { + return { name: 'dashboard' }; + } + if (to.meta?.requiresAuth !== true) return true; const isAuthed = hasSuperAuthToken();