package services import ( "context" "strings" "time" "quyun/v2/app/errorx" superdto "quyun/v2/app/http/super/dto" "quyun/v2/app/requests" "quyun/v2/database" "quyun/v2/database/models" "quyun/v2/pkg/consts" "github.com/pkg/errors" "github.com/samber/lo" log "github.com/sirupsen/logrus" "go.ipao.vip/gen" "go.ipao.vip/gen/field" ) // SuperTenantContentsPage returns tenant contents list for superadmin. func (s *content) SuperTenantContentsPage(ctx context.Context, tenantID int64, filter *superdto.TenantContentFilter) (*requests.Pager, error) { if tenantID <= 0 { return nil, errors.New("tenant_id must be > 0") } if filter == nil { filter = &superdto.TenantContentFilter{} } log.WithFields(log.Fields{ "tenant_id": tenantID, "page": filter.Page, "limit": filter.Limit, }).Info("services.content.super_tenant_contents_page") tbl, query := models.ContentQuery.QueryContext(ctx) conds := []gen.Condition{ tbl.TenantID.Eq(tenantID), tbl.DeletedAt.IsNull(), } if kw := strings.TrimSpace(filter.KeywordTrimmed()); kw != "" { conds = append(conds, tbl.Title.Like(database.WrapLike(kw))) } if filter.Status != nil { conds = append(conds, tbl.Status.Eq(*filter.Status)) } if filter.Visibility != nil { conds = append(conds, tbl.Visibility.Eq(*filter.Visibility)) } if filter.UserID != nil && *filter.UserID > 0 { conds = append(conds, tbl.UserID.Eq(*filter.UserID)) } if filter.PublishedAtFrom != nil { conds = append(conds, tbl.PublishedAt.Gte(*filter.PublishedAtFrom)) } if filter.PublishedAtTo != nil { conds = append(conds, tbl.PublishedAt.Lte(*filter.PublishedAtTo)) } if filter.CreatedAtFrom != nil { conds = append(conds, tbl.CreatedAt.Gte(*filter.CreatedAtFrom)) } if filter.CreatedAtTo != nil { conds = append(conds, tbl.CreatedAt.Lte(*filter.CreatedAtTo)) } filter.Pagination.Format() orderBys := make([]field.Expr, 0, 6) allowedAsc := map[string]field.Expr{ "id": tbl.ID.Asc(), "title": tbl.Title.Asc(), "user_id": tbl.UserID.Asc(), "status": tbl.Status.Asc(), "visibility": tbl.Visibility.Asc(), "published_at": tbl.PublishedAt.Asc(), "created_at": tbl.CreatedAt.Asc(), "updated_at": tbl.UpdatedAt.Asc(), } allowedDesc := map[string]field.Expr{ "id": tbl.ID.Desc(), "title": tbl.Title.Desc(), "user_id": tbl.UserID.Desc(), "status": tbl.Status.Desc(), "visibility": tbl.Visibility.Desc(), "published_at": tbl.PublishedAt.Desc(), "created_at": tbl.CreatedAt.Desc(), "updated_at": tbl.UpdatedAt.Desc(), } for _, f := range filter.AscFields() { f = strings.TrimSpace(f) if f == "" { continue } if ob, ok := allowedAsc[f]; ok { orderBys = append(orderBys, ob) } } for _, f := range filter.DescFields() { f = strings.TrimSpace(f) if f == "" { continue } if ob, ok := allowedDesc[f]; ok { orderBys = append(orderBys, ob) } } if len(orderBys) == 0 { orderBys = append(orderBys, tbl.ID.Desc()) } else { orderBys = append(orderBys, tbl.ID.Desc()) } items, total, err := query.Where(conds...).Order(orderBys...).FindByPage(int(filter.Offset()), int(filter.Limit)) if err != nil { return nil, err } contentIDs := lo.Map(items, func(item *models.Content, _ int) int64 { if item == nil { return 0 } return item.ID }) contentIDs = lo.Filter(contentIDs, func(id int64, _ int) bool { return id > 0 }) priceByContent, err := s.contentPriceMapping(ctx, tenantID, contentIDs) if err != nil { return nil, err } ownerIDs := lo.Uniq(lo.FilterMap(items, func(item *models.Content, _ int) (int64, bool) { if item == nil || item.UserID <= 0 { return 0, false } return item.UserID, true })) ownerMap := map[int64]*superdto.SuperUserLite{} if len(ownerIDs) > 0 { uTbl, uQuery := models.UserQuery.QueryContext(ctx) users, err := uQuery.Where(uTbl.ID.In(ownerIDs...)).Find() if err != nil { return nil, err } for _, u := range users { if u == nil { continue } ownerMap[u.ID] = &superdto.SuperUserLite{ ID: u.ID, Username: u.Username, Status: u.Status, Roles: u.Roles, VerifiedAt: u.VerifiedAt, CreatedAt: u.CreatedAt, UpdatedAt: u.UpdatedAt, StatusDescription: u.Status.Description(), } } } respItems := lo.Map(items, func(model *models.Content, _ int) *superdto.SuperTenantContentItem { if model == nil { return nil } return &superdto.SuperTenantContentItem{ Content: model, Price: priceByContent[model.ID], Owner: ownerMap[model.UserID], StatusDescription: model.Status.Description(), VisibilityDescription: model.Visibility.Description(), } }) return &requests.Pager{ Pagination: filter.Pagination, Total: total, Items: respItems, }, nil } func (s *content) SuperContentPage(ctx context.Context, filter *superdto.SuperContentPageFilter) (*requests.Pager, error) { if filter == nil { filter = &superdto.SuperContentPageFilter{} } log.WithFields(log.Fields{ "tenant_id": lo.FromPtr(filter.TenantID), "tenant_code": filter.TenantCodeTrimmed(), "tenant_name": filter.TenantNameTrimmed(), "user_id": lo.FromPtr(filter.UserID), "username": filter.UsernameTrimmed(), "keyword": filter.KeywordTrimmed(), "status": lo.FromPtr(filter.Status), "visibility": lo.FromPtr(filter.Visibility), "page": filter.Page, "limit": filter.Limit, }).Info("services.content.super_page") filter.Pagination.Format() cTbl, query := models.ContentQuery.QueryContext(ctx) // 注意:该查询会按需 join users/tenants/content_prices;必须显式 select contents.*, // 否则重复列名(id/created_at/updated_at 等)会被扫描到 Content 模型上导致字段错乱。 query = query.Select(cTbl.ALL) conds := []gen.Condition{ cTbl.DeletedAt.IsNull(), } if filter.ID != nil && *filter.ID > 0 { conds = append(conds, cTbl.ID.Eq(*filter.ID)) } if filter.TenantID != nil && *filter.TenantID > 0 { conds = append(conds, cTbl.TenantID.Eq(*filter.TenantID)) } if filter.UserID != nil && *filter.UserID > 0 { conds = append(conds, cTbl.UserID.Eq(*filter.UserID)) } if kw := strings.TrimSpace(filter.KeywordTrimmed()); kw != "" { conds = append(conds, cTbl.Title.Like(database.WrapLike(kw))) } if filter.Status != nil { conds = append(conds, cTbl.Status.Eq(*filter.Status)) } if filter.Visibility != nil { conds = append(conds, cTbl.Visibility.Eq(*filter.Visibility)) } if filter.PublishedAtFrom != nil { conds = append(conds, cTbl.PublishedAt.Gte(*filter.PublishedAtFrom)) } if filter.PublishedAtTo != nil { conds = append(conds, cTbl.PublishedAt.Lte(*filter.PublishedAtTo)) } if filter.CreatedAtFrom != nil { conds = append(conds, cTbl.CreatedAt.Gte(*filter.CreatedAtFrom)) } if filter.CreatedAtTo != nil { conds = append(conds, cTbl.CreatedAt.Lte(*filter.CreatedAtTo)) } // Owner username keyword. if username := filter.UsernameTrimmed(); username != "" { uTbl, _ := models.UserQuery.QueryContext(ctx) query = query.LeftJoin(uTbl, uTbl.ID.EqCol(cTbl.UserID)) conds = append(conds, uTbl.Username.Like(database.WrapLike(username))) } // Tenant code/name keyword. tenantCode := filter.TenantCodeTrimmed() tenantName := filter.TenantNameTrimmed() if tenantCode != "" || tenantName != "" { tTbl, _ := models.TenantQuery.QueryContext(ctx) query = query.LeftJoin(tTbl, tTbl.ID.EqCol(cTbl.TenantID)) if tenantCode != "" { conds = append(conds, tTbl.Code.Like(database.WrapLike(tenantCode))) } if tenantName != "" { conds = append(conds, tTbl.Name.Like(database.WrapLike(tenantName))) } } // Price amount range filter (content_prices is 1:1 by content_id within tenant). needPriceJoin := (filter.PriceAmountMin != nil && *filter.PriceAmountMin >= 0) || (filter.PriceAmountMax != nil && *filter.PriceAmountMax >= 0) if needPriceJoin { cpTbl, _ := models.ContentPriceQuery.QueryContext(ctx) query = query.LeftJoin(cpTbl, cpTbl.ContentID.EqCol(cTbl.ID)) if filter.PriceAmountMin != nil && *filter.PriceAmountMin >= 0 { conds = append(conds, cpTbl.PriceAmount.Gte(*filter.PriceAmountMin)) } if filter.PriceAmountMax != nil && *filter.PriceAmountMax >= 0 { conds = append(conds, cpTbl.PriceAmount.Lte(*filter.PriceAmountMax)) } } // Sort whitelist. orderBys := make([]field.Expr, 0, 8) allowedAsc := map[string]field.Expr{ "id": cTbl.ID.Asc(), "tenant_id": cTbl.TenantID.Asc(), "user_id": cTbl.UserID.Asc(), "title": cTbl.Title.Asc(), "status": cTbl.Status.Asc(), "visibility": cTbl.Visibility.Asc(), "published_at": cTbl.PublishedAt.Asc(), "created_at": cTbl.CreatedAt.Asc(), "updated_at": cTbl.UpdatedAt.Asc(), } allowedDesc := map[string]field.Expr{ "id": cTbl.ID.Desc(), "tenant_id": cTbl.TenantID.Desc(), "user_id": cTbl.UserID.Desc(), "title": cTbl.Title.Desc(), "status": cTbl.Status.Desc(), "visibility": cTbl.Visibility.Desc(), "published_at": cTbl.PublishedAt.Desc(), "created_at": cTbl.CreatedAt.Desc(), "updated_at": cTbl.UpdatedAt.Desc(), } for _, f := range filter.AscFields() { f = strings.TrimSpace(f) if f == "" { continue } if ob, ok := allowedAsc[f]; ok { orderBys = append(orderBys, ob) } } for _, f := range filter.DescFields() { f = strings.TrimSpace(f) if f == "" { continue } if ob, ok := allowedDesc[f]; ok { orderBys = append(orderBys, ob) } } if len(orderBys) == 0 { orderBys = append(orderBys, cTbl.ID.Desc()) } else { orderBys = append(orderBys, cTbl.ID.Desc()) } items, total, err := query.Where(conds...).Order(orderBys...).FindByPage(int(filter.Offset()), int(filter.Limit)) if err != nil { return nil, err } tenantIDs := lo.Uniq(lo.FilterMap(items, func(item *models.Content, _ int) (int64, bool) { if item == nil || item.TenantID <= 0 { return 0, false } return item.TenantID, true })) ownerIDs := lo.Uniq(lo.FilterMap(items, func(item *models.Content, _ int) (int64, bool) { if item == nil || item.UserID <= 0 { return 0, false } return item.UserID, true })) contentIDs := lo.Uniq(lo.FilterMap(items, func(item *models.Content, _ int) (int64, bool) { if item == nil || item.ID <= 0 { return 0, false } return item.ID, true })) tenantMap := map[int64]*models.Tenant{} if len(tenantIDs) > 0 { tTbl, tQuery := models.TenantQuery.QueryContext(ctx) tenants, err := tQuery.Where(tTbl.ID.In(tenantIDs...)).Find() if err != nil { return nil, err } for _, te := range tenants { if te == nil { continue } tenantMap[te.ID] = te } } ownerMap := map[int64]*superdto.SuperUserLite{} if len(ownerIDs) > 0 { uTbl, uQuery := models.UserQuery.QueryContext(ctx) users, err := uQuery.Where(uTbl.ID.In(ownerIDs...)).Find() if err != nil { return nil, err } for _, u := range users { if u == nil { continue } ownerMap[u.ID] = &superdto.SuperUserLite{ ID: u.ID, Username: u.Username, Status: u.Status, Roles: u.Roles, VerifiedAt: u.VerifiedAt, CreatedAt: u.CreatedAt, UpdatedAt: u.UpdatedAt, StatusDescription: u.Status.Description(), } } } priceByContent := map[int64]*models.ContentPrice{} if len(contentIDs) > 0 { cpTbl, cpQuery := models.ContentPriceQuery.QueryContext(ctx) conds := []gen.Condition{ cpTbl.ContentID.In(contentIDs...), } if len(tenantIDs) > 0 { conds = append(conds, cpTbl.TenantID.In(tenantIDs...)) } prices, err := cpQuery.Where(conds...).Find() if err != nil { return nil, err } for _, p := range prices { if p == nil { continue } priceByContent[p.ContentID] = p } } respItems := lo.Map(items, func(model *models.Content, _ int) *superdto.SuperContentItem { if model == nil { return nil } te := tenantMap[model.TenantID] var lite *superdto.SuperContentTenantLite if te != nil { lite = &superdto.SuperContentTenantLite{ ID: te.ID, Code: te.Code, Name: te.Name, } } return &superdto.SuperContentItem{ Content: model, Price: priceByContent[model.ID], Tenant: lite, Owner: ownerMap[model.UserID], StatusDescription: model.Status.Description(), VisibilityDescription: model.Visibility.Description(), } }) return &requests.Pager{ Pagination: filter.Pagination, Total: total, Items: respItems, }, nil } func (s *content) SuperUpdateTenantContentStatus( ctx context.Context, operatorUserID, tenantID, contentID int64, status consts.ContentStatus, now time.Time, ) (*models.Content, error) { if operatorUserID <= 0 { return nil, errorx.ErrTokenInvalid } if tenantID <= 0 { return nil, errors.New("tenant_id must be > 0") } if contentID <= 0 { return nil, errors.New("content_id must be > 0") } if status != consts.ContentStatusUnpublished && status != consts.ContentStatusBlocked { return nil, errorx.ErrInvalidParameter.WithMsg("invalid status") } log.WithFields(log.Fields{ "operator_user_id": operatorUserID, "tenant_id": tenantID, "content_id": contentID, "status": status, }).Info("services.content.super_update_tenant_content_status") tbl, query := models.ContentQuery.QueryContext(ctx) model, err := query.Where( tbl.TenantID.Eq(tenantID), tbl.ID.Eq(contentID), tbl.DeletedAt.IsNull(), ).First() if err != nil { return nil, err } if status == consts.ContentStatusUnpublished && model.Status != consts.ContentStatusPublished { return nil, errorx.ErrPreconditionFailed.WithMsg("content is not published") } if _, err := query.Where( tbl.TenantID.Eq(tenantID), tbl.ID.Eq(contentID), tbl.DeletedAt.IsNull(), ).UpdateSimple( tbl.Status.Value(status), ); err != nil { return nil, err } model.Status = status model.UpdatedAt = now return model, nil }