476 lines
17 KiB
Go
476 lines
17 KiB
Go
// Package mysql 实现 room-service 的 MySQL 持久化边界。
|
|
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"errors"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"hyapp/pkg/appcode"
|
|
roomservice "hyapp/services/room-service/internal/room/service"
|
|
)
|
|
|
|
// GetRoomSeatConfig 读取当前 App 的房间座位数配置。
|
|
func (r *Repository) GetRoomSeatConfig(ctx context.Context) (roomservice.RoomSeatConfig, bool, error) {
|
|
row := r.db.QueryRowContext(ctx,
|
|
`SELECT app_code, allowed_seat_counts, default_seat_count, updated_at_ms
|
|
FROM room_seat_configs
|
|
WHERE app_code = ?`,
|
|
appcode.FromContext(ctx),
|
|
)
|
|
|
|
var config roomservice.RoomSeatConfig
|
|
var rawAllowed string
|
|
if err := row.Scan(&config.AppCode, &rawAllowed, &config.DefaultSeatCount, &config.UpdatedAtMS); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return roomservice.RoomSeatConfig{}, false, nil
|
|
}
|
|
return roomservice.RoomSeatConfig{}, false, err
|
|
}
|
|
if err := json.Unmarshal([]byte(rawAllowed), &config.AllowedSeatCounts); err != nil {
|
|
return roomservice.RoomSeatConfig{}, false, err
|
|
}
|
|
|
|
return config, true, nil
|
|
}
|
|
|
|
// UpsertRoomSeatConfig 写入当前 App 的房间座位数配置。
|
|
func (r *Repository) UpsertRoomSeatConfig(ctx context.Context, config roomservice.RoomSeatConfig) error {
|
|
appCode := normalizedRecordAppCode(ctx, config.AppCode)
|
|
rawAllowed, err := json.Marshal(config.AllowedSeatCounts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nowMS := config.UpdatedAtMS
|
|
if nowMS <= 0 {
|
|
nowMS = time.Now().UTC().UnixMilli()
|
|
}
|
|
_, err = r.db.ExecContext(ctx,
|
|
`INSERT INTO room_seat_configs (app_code, allowed_seat_counts, default_seat_count, created_at_ms, updated_at_ms)
|
|
VALUES (?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
allowed_seat_counts = VALUES(allowed_seat_counts),
|
|
default_seat_count = VALUES(default_seat_count),
|
|
updated_at_ms = VALUES(updated_at_ms)`,
|
|
appCode,
|
|
string(rawAllowed),
|
|
config.DefaultSeatCount,
|
|
nowMS,
|
|
nowMS,
|
|
)
|
|
return err
|
|
}
|
|
|
|
// GetRoomRocketConfig 读取当前 App 的语音房火箭配置。
|
|
func (r *Repository) GetRoomRocketConfig(ctx context.Context) (roomservice.RoomRocketConfig, bool, error) {
|
|
row := r.db.QueryRowContext(ctx,
|
|
`SELECT app_code, enabled, config_version, fuel_source, launch_delay_ms,
|
|
broadcast_enabled, broadcast_scope, broadcast_delay_ms, reward_stack_policy,
|
|
levels_json, gift_fuel_rules_json, updated_by_admin_id, created_at_ms, updated_at_ms
|
|
FROM room_rocket_configs
|
|
WHERE app_code = ?`,
|
|
appcode.FromContext(ctx),
|
|
)
|
|
|
|
var config roomservice.RoomRocketConfig
|
|
var enabled int
|
|
var broadcastEnabled int
|
|
var rawLevels string
|
|
var rawRules string
|
|
if err := row.Scan(
|
|
&config.AppCode,
|
|
&enabled,
|
|
&config.ConfigVersion,
|
|
&config.FuelSource,
|
|
&config.LaunchDelayMS,
|
|
&broadcastEnabled,
|
|
&config.BroadcastScope,
|
|
&config.BroadcastDelayMS,
|
|
&config.RewardStackPolicy,
|
|
&rawLevels,
|
|
&rawRules,
|
|
&config.UpdatedByAdminID,
|
|
&config.CreatedAtMS,
|
|
&config.UpdatedAtMS,
|
|
); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return roomservice.RoomRocketConfig{}, false, nil
|
|
}
|
|
return roomservice.RoomRocketConfig{}, false, err
|
|
}
|
|
config.Enabled = enabled == 1
|
|
config.BroadcastEnabled = broadcastEnabled == 1
|
|
if err := json.Unmarshal([]byte(rawLevels), &config.Levels); err != nil {
|
|
return roomservice.RoomRocketConfig{}, false, err
|
|
}
|
|
if err := json.Unmarshal([]byte(rawRules), &config.GiftFuelRules); err != nil {
|
|
return roomservice.RoomRocketConfig{}, false, err
|
|
}
|
|
|
|
return config, true, nil
|
|
}
|
|
|
|
// UpsertRoomRocketConfig 写入当前 App 的语音房火箭配置。
|
|
func (r *Repository) UpsertRoomRocketConfig(ctx context.Context, config roomservice.RoomRocketConfig) error {
|
|
appCode := normalizedRecordAppCode(ctx, config.AppCode)
|
|
rawLevels, err := json.Marshal(config.Levels)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rawRules, err := json.Marshal(config.GiftFuelRules)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nowMS := config.UpdatedAtMS
|
|
if nowMS <= 0 {
|
|
nowMS = time.Now().UTC().UnixMilli()
|
|
}
|
|
createdAtMS := config.CreatedAtMS
|
|
if createdAtMS <= 0 {
|
|
createdAtMS = nowMS
|
|
}
|
|
_, err = r.db.ExecContext(ctx,
|
|
`INSERT INTO room_rocket_configs (
|
|
app_code, enabled, config_version, fuel_source, launch_delay_ms,
|
|
broadcast_enabled, broadcast_scope, broadcast_delay_ms, reward_stack_policy,
|
|
levels_json, gift_fuel_rules_json, updated_by_admin_id, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
enabled = VALUES(enabled),
|
|
config_version = VALUES(config_version),
|
|
fuel_source = VALUES(fuel_source),
|
|
launch_delay_ms = VALUES(launch_delay_ms),
|
|
broadcast_enabled = VALUES(broadcast_enabled),
|
|
broadcast_scope = VALUES(broadcast_scope),
|
|
broadcast_delay_ms = VALUES(broadcast_delay_ms),
|
|
reward_stack_policy = VALUES(reward_stack_policy),
|
|
levels_json = VALUES(levels_json),
|
|
gift_fuel_rules_json = VALUES(gift_fuel_rules_json),
|
|
updated_by_admin_id = VALUES(updated_by_admin_id),
|
|
updated_at_ms = VALUES(updated_at_ms)`,
|
|
appCode,
|
|
boolToInt(config.Enabled),
|
|
config.ConfigVersion,
|
|
config.FuelSource,
|
|
config.LaunchDelayMS,
|
|
boolToInt(config.BroadcastEnabled),
|
|
config.BroadcastScope,
|
|
config.BroadcastDelayMS,
|
|
config.RewardStackPolicy,
|
|
string(rawLevels),
|
|
string(rawRules),
|
|
config.UpdatedByAdminID,
|
|
createdAtMS,
|
|
nowMS,
|
|
)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) AdminListRooms(ctx context.Context, query roomservice.AdminRoomListQuery) ([]roomservice.AdminRoomListEntry, int64, error) {
|
|
if query.Page <= 0 {
|
|
query.Page = 1
|
|
}
|
|
if query.PageSize <= 0 {
|
|
query.PageSize = 20
|
|
}
|
|
if query.PageSize > 100 {
|
|
query.PageSize = 100
|
|
}
|
|
if query.NowMS <= 0 {
|
|
query.NowMS = time.Now().UTC().UnixMilli()
|
|
}
|
|
where, args := adminRoomWhere(ctx, query)
|
|
var total int64
|
|
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM room_list_entries rle `+where, args...).Scan(&total); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
contributionExpr := adminRoomContributionExpr(query.NowMS)
|
|
rows, err := r.db.QueryContext(ctx, adminRoomSelectSQL(contributionExpr)+where+` ORDER BY `+adminRoomOrderBy(query, contributionExpr)+` LIMIT ? OFFSET ?`, append(args, query.PageSize, (query.Page-1)*query.PageSize)...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
items := make([]roomservice.AdminRoomListEntry, 0, query.PageSize)
|
|
for rows.Next() {
|
|
item, err := scanAdminRoom(rows)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func (r *Repository) AdminGetRoom(ctx context.Context, roomID string, nowMS int64) (roomservice.AdminRoomListEntry, bool, error) {
|
|
contributionExpr := adminRoomContributionExpr(nowMS)
|
|
row := r.db.QueryRowContext(ctx, adminRoomSelectSQL(contributionExpr)+` WHERE rle.app_code = ? AND rle.room_id = ?`, appcode.FromContext(ctx), strings.TrimSpace(roomID))
|
|
item, err := scanAdminRoom(row)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return roomservice.AdminRoomListEntry{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return roomservice.AdminRoomListEntry{}, false, err
|
|
}
|
|
return item, true, nil
|
|
}
|
|
|
|
func adminRoomSelectSQL(contributionExpr string) string {
|
|
return `
|
|
SELECT rle.room_id, rle.room_short_id, rle.visible_region_id, rle.owner_user_id,
|
|
rle.title, rle.cover_url, rle.mode, rle.status, rle.close_reason,
|
|
rle.closed_by_admin_id, rle.closed_by_admin_name, rle.closed_at_ms,
|
|
` + contributionExpr + ` AS room_contribution, rle.online_count, rle.seat_count, rle.occupied_seat_count,
|
|
rle.sort_score, rle.created_at_ms, rle.updated_at_ms
|
|
FROM room_list_entries rle `
|
|
}
|
|
|
|
func adminRoomWhere(ctx context.Context, query roomservice.AdminRoomListQuery) (string, []any) {
|
|
where := []string{"rle.app_code = ?"}
|
|
app := appcode.Normalize(query.AppCode)
|
|
if app == "" {
|
|
app = appcode.FromContext(ctx)
|
|
}
|
|
args := []any{app}
|
|
switch strings.TrimSpace(query.Status) {
|
|
case "closed":
|
|
where = append(where, "rle.status <> ?")
|
|
args = append(args, "active")
|
|
case "":
|
|
default:
|
|
where = append(where, "rle.status = ?")
|
|
args = append(args, strings.TrimSpace(query.Status))
|
|
}
|
|
if query.RegionID > 0 {
|
|
where = append(where, "rle.visible_region_id = ?")
|
|
args = append(args, query.RegionID)
|
|
}
|
|
if query.OwnerUserID > 0 {
|
|
where = append(where, "rle.owner_user_id = ?")
|
|
args = append(args, query.OwnerUserID)
|
|
}
|
|
if strings.TrimSpace(query.Keyword) != "" {
|
|
like := "%" + strings.ReplaceAll(strings.TrimSpace(query.Keyword), "%", "\\%") + "%"
|
|
where = append(where, "(rle.room_id LIKE ? ESCAPE '\\\\' OR rle.room_short_id LIKE ? ESCAPE '\\\\' OR rle.title LIKE ? ESCAPE '\\\\' OR CAST(rle.owner_user_id AS CHAR) LIKE ? ESCAPE '\\\\')")
|
|
args = append(args, like, like, like, like)
|
|
}
|
|
return " WHERE " + strings.Join(where, " AND "), args
|
|
}
|
|
|
|
func adminRoomOrderBy(query roomservice.AdminRoomListQuery, contributionExpr string) string {
|
|
direction := "DESC"
|
|
if strings.EqualFold(strings.TrimSpace(query.SortDirection), "asc") {
|
|
direction = "ASC"
|
|
}
|
|
switch strings.TrimSpace(query.SortBy) {
|
|
case "room_contribution", "heat":
|
|
return contributionExpr + " " + direction + ", rle.created_at_ms DESC, rle.room_id DESC"
|
|
default:
|
|
return "rle.created_at_ms DESC, rle.room_id DESC"
|
|
}
|
|
}
|
|
|
|
func adminRoomContributionExpr(nowMS int64) string {
|
|
weekStartMS := roomContributionWeekStartMS(nowMS)
|
|
return "CASE WHEN rle.contribution_week_start_ms = " + strconv.FormatInt(weekStartMS, 10) + " THEN rle.weekly_contribution ELSE 0 END"
|
|
}
|
|
|
|
func roomContributionWeekStartMS(nowMS int64) int64 {
|
|
if nowMS <= 0 {
|
|
nowMS = time.Now().UTC().UnixMilli()
|
|
}
|
|
dayStart := time.UnixMilli(nowMS).UTC()
|
|
dayStart = time.Date(dayStart.Year(), dayStart.Month(), dayStart.Day(), 0, 0, 0, 0, time.UTC)
|
|
// 房间贡献榜按 UTC 周一 01:00 切周,给 00:00 后的结算和榜单消费留出缓冲窗口。
|
|
cutoff := dayStart.Add(time.Hour)
|
|
if time.UnixMilli(nowMS).UTC().Before(cutoff) {
|
|
dayStart = dayStart.AddDate(0, 0, -1)
|
|
}
|
|
weekday := int(dayStart.Weekday())
|
|
if weekday == 0 {
|
|
weekday = 7
|
|
}
|
|
return dayStart.AddDate(0, 0, 1-weekday).Add(time.Hour).UnixMilli()
|
|
}
|
|
|
|
func scanAdminRoom(scanner interface{ Scan(dest ...any) error }) (roomservice.AdminRoomListEntry, error) {
|
|
var item roomservice.AdminRoomListEntry
|
|
err := scanner.Scan(
|
|
&item.RoomID,
|
|
&item.RoomShortID,
|
|
&item.VisibleRegionID,
|
|
&item.OwnerUserID,
|
|
&item.Title,
|
|
&item.CoverURL,
|
|
&item.Mode,
|
|
&item.Status,
|
|
&item.CloseReason,
|
|
&item.ClosedByAdminID,
|
|
&item.ClosedByAdminName,
|
|
&item.ClosedAtMS,
|
|
&item.Heat,
|
|
&item.OnlineCount,
|
|
&item.SeatCount,
|
|
&item.OccupiedSeatCount,
|
|
&item.SortScore,
|
|
&item.CreatedAtMS,
|
|
&item.UpdatedAtMS,
|
|
)
|
|
return item, err
|
|
}
|
|
|
|
func (r *Repository) AdminListRoomPins(ctx context.Context, query roomservice.AdminRoomPinQuery) ([]roomservice.AdminRoomPinEntry, int64, error) {
|
|
if query.Page <= 0 {
|
|
query.Page = 1
|
|
}
|
|
if query.PageSize <= 0 {
|
|
query.PageSize = 20
|
|
}
|
|
if query.PageSize > 100 {
|
|
query.PageSize = 100
|
|
}
|
|
where, args := adminRoomPinWhere(ctx, query)
|
|
var total int64
|
|
if err := r.db.QueryRowContext(ctx, `SELECT COUNT(*) `+adminRoomPinFromSQL()+where, args...).Scan(&total); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, adminRoomPinSelectSQL()+where+` ORDER BY p.weight DESC, p.expires_at_ms DESC, p.id DESC LIMIT ? OFFSET ?`, append(args, query.PageSize, (query.Page-1)*query.PageSize)...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
items := make([]roomservice.AdminRoomPinEntry, 0, query.PageSize)
|
|
for rows.Next() {
|
|
item, err := scanAdminRoomPin(rows)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, total, rows.Err()
|
|
}
|
|
|
|
func (r *Repository) AdminCreateRoomPin(ctx context.Context, input roomservice.AdminCreateRoomPinInput) (roomservice.AdminRoomPinEntry, bool, error) {
|
|
app := appcode.FromContext(ctx)
|
|
room, exists, err := r.AdminGetRoom(ctx, input.RoomID, input.NowMS)
|
|
if err != nil || !exists {
|
|
return roomservice.AdminRoomPinEntry{}, false, err
|
|
}
|
|
if room.Status != "active" {
|
|
return roomservice.AdminRoomPinEntry{}, false, nil
|
|
}
|
|
pinType := strings.TrimSpace(input.PinType)
|
|
if pinType == "" {
|
|
pinType = "region"
|
|
}
|
|
visibleRegionID := room.VisibleRegionID
|
|
if pinType == "global" {
|
|
visibleRegionID = 0
|
|
}
|
|
pinnedAtMS := input.PinnedAtMS
|
|
if pinnedAtMS <= 0 {
|
|
pinnedAtMS = input.NowMS
|
|
}
|
|
expiresAtMS := input.ExpiresAtMS
|
|
if expiresAtMS <= 0 {
|
|
expiresAtMS = pinnedAtMS + input.DurationDays*24*60*60*1000
|
|
}
|
|
if _, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO room_region_pins (
|
|
app_code, visible_region_id, pin_type, room_id, weight, status, pinned_at_ms, expires_at_ms,
|
|
cancelled_at_ms, created_by_admin_id, cancelled_by_admin_id, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, ?, ?, 'active', ?, ?, 0, ?, 0, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
weight = VALUES(weight), status = VALUES(status), pinned_at_ms = VALUES(pinned_at_ms),
|
|
expires_at_ms = VALUES(expires_at_ms), cancelled_at_ms = 0,
|
|
created_by_admin_id = VALUES(created_by_admin_id), cancelled_by_admin_id = 0,
|
|
created_at_ms = VALUES(created_at_ms), updated_at_ms = VALUES(updated_at_ms)
|
|
`, app, visibleRegionID, pinType, room.RoomID, input.Weight, pinnedAtMS, expiresAtMS, input.AdminID, input.NowMS, input.NowMS); err != nil {
|
|
return roomservice.AdminRoomPinEntry{}, false, err
|
|
}
|
|
return r.adminGetRoomPin(ctx, `WHERE p.app_code = ? AND p.pin_type = ? AND p.visible_region_id = ? AND p.room_id = ?`, app, pinType, visibleRegionID, room.RoomID)
|
|
}
|
|
|
|
func (r *Repository) AdminCancelRoomPin(ctx context.Context, pinID int64, adminID uint64, nowMS int64) (roomservice.AdminRoomPinEntry, bool, error) {
|
|
app := appcode.FromContext(ctx)
|
|
if _, err := r.db.ExecContext(ctx, `
|
|
UPDATE room_region_pins
|
|
SET status = 'cancelled', cancelled_at_ms = ?, cancelled_by_admin_id = ?, updated_at_ms = ?
|
|
WHERE app_code = ? AND id = ? AND status <> 'cancelled'
|
|
`, nowMS, adminID, nowMS, app, pinID); err != nil {
|
|
return roomservice.AdminRoomPinEntry{}, false, err
|
|
}
|
|
return r.adminGetRoomPin(ctx, `WHERE p.app_code = ? AND p.id = ?`, app, pinID)
|
|
}
|
|
|
|
func (r *Repository) adminGetRoomPin(ctx context.Context, where string, args ...any) (roomservice.AdminRoomPinEntry, bool, error) {
|
|
item, err := scanAdminRoomPin(r.db.QueryRowContext(ctx, adminRoomPinSelectSQL()+where+` LIMIT 1`, args...))
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return roomservice.AdminRoomPinEntry{}, false, nil
|
|
}
|
|
if err != nil {
|
|
return roomservice.AdminRoomPinEntry{}, false, err
|
|
}
|
|
return item, true, nil
|
|
}
|
|
|
|
func adminRoomPinSelectSQL() string {
|
|
return `
|
|
SELECT p.app_code, p.id, p.visible_region_id, p.pin_type, p.room_id, p.weight, p.status,
|
|
p.pinned_at_ms, p.expires_at_ms, p.cancelled_at_ms,
|
|
p.created_by_admin_id, p.cancelled_by_admin_id, p.created_at_ms, p.updated_at_ms,
|
|
r.room_short_id, r.visible_region_id, r.title, r.cover_url, r.owner_user_id, r.status
|
|
` + adminRoomPinFromSQL()
|
|
}
|
|
|
|
func adminRoomPinFromSQL() string {
|
|
return ` FROM room_region_pins p
|
|
JOIN room_list_entries r ON r.app_code = p.app_code AND r.room_id = p.room_id `
|
|
}
|
|
|
|
func adminRoomPinWhere(ctx context.Context, query roomservice.AdminRoomPinQuery) (string, []any) {
|
|
nowMS := query.NowMS
|
|
if nowMS <= 0 {
|
|
nowMS = time.Now().UTC().UnixMilli()
|
|
}
|
|
where := []string{"p.app_code = ?"}
|
|
args := []any{appcode.FromContext(ctx)}
|
|
switch strings.TrimSpace(query.Status) {
|
|
case "active":
|
|
where = append(where, "p.status = ?", "p.expires_at_ms > ?")
|
|
args = append(args, "active", nowMS)
|
|
case "expired":
|
|
where = append(where, "p.status = ?", "p.expires_at_ms <= ?")
|
|
args = append(args, "active", nowMS)
|
|
case "cancelled":
|
|
where = append(where, "p.status = ?")
|
|
args = append(args, "cancelled")
|
|
}
|
|
if query.RegionID > 0 {
|
|
where = append(where, "p.visible_region_id = ?")
|
|
args = append(args, query.RegionID)
|
|
}
|
|
if strings.TrimSpace(query.PinType) != "" {
|
|
where = append(where, "p.pin_type = ?")
|
|
args = append(args, strings.TrimSpace(query.PinType))
|
|
}
|
|
if strings.TrimSpace(query.Keyword) != "" {
|
|
like := "%" + strings.ReplaceAll(strings.TrimSpace(query.Keyword), "%", "\\%") + "%"
|
|
where = append(where, "(p.room_id LIKE ? ESCAPE '\\\\' OR r.room_short_id LIKE ? ESCAPE '\\\\' OR r.title LIKE ? ESCAPE '\\\\' OR CAST(r.owner_user_id AS CHAR) LIKE ? ESCAPE '\\\\')")
|
|
args = append(args, like, like, like, like)
|
|
}
|
|
return ` WHERE ` + strings.Join(where, " AND "), args
|
|
}
|
|
|
|
func scanAdminRoomPin(scanner interface{ Scan(dest ...any) error }) (roomservice.AdminRoomPinEntry, error) {
|
|
var item roomservice.AdminRoomPinEntry
|
|
err := scanner.Scan(&item.AppCode, &item.ID, &item.VisibleRegionID, &item.PinType, &item.RoomID, &item.Weight, &item.Status, &item.PinnedAtMS, &item.ExpiresAtMS, &item.CancelledAtMS, &item.CreatedByAdminID, &item.CancelledByAdminID, &item.CreatedAtMS, &item.UpdatedAtMS, &item.RoomShortID, &item.RoomVisibleRegionID, &item.Title, &item.CoverURL, &item.OwnerUserID, &item.RoomStatus)
|
|
return item, err
|
|
}
|