410 lines
12 KiB
Go
410 lines
12 KiB
Go
package roomadmin
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"hyapp-admin-server/internal/appctx"
|
|
)
|
|
|
|
const (
|
|
roomPinStatusActive = "active"
|
|
roomPinStatusCancelled = "cancelled"
|
|
roomPinStatusExpired = "expired"
|
|
roomPinStatusAll = "all"
|
|
defaultRoomPinDays = int64(30)
|
|
maxRoomPinDays = int64(3650)
|
|
roomPinDayMillis = int64(24 * 60 * 60 * 1000)
|
|
)
|
|
|
|
type RoomPinRoom struct {
|
|
CoverURL string `json:"coverUrl"`
|
|
RegionName string `json:"regionName"`
|
|
RoomID string `json:"roomId"`
|
|
RoomShortID string `json:"roomShortId"`
|
|
Status string `json:"status"`
|
|
Title string `json:"title"`
|
|
VisibleRegionID int64 `json:"visibleRegionId"`
|
|
}
|
|
|
|
type RoomPin struct {
|
|
CancelledAtMS int64 `json:"cancelledAtMs"`
|
|
CancelledByAdminID uint `json:"cancelledByAdminId"`
|
|
CreatedAtMS int64 `json:"createdAtMs"`
|
|
CreatedByAdminID uint `json:"createdByAdminId"`
|
|
ExpiresAtMS int64 `json:"expiresAtMs"`
|
|
ID int64 `json:"id"`
|
|
PinnedAtMS int64 `json:"pinnedAtMs"`
|
|
RemainingMS int64 `json:"remainingMs"`
|
|
Room RoomPinRoom `json:"room"`
|
|
Status string `json:"status"`
|
|
UpdatedAtMS int64 `json:"updatedAtMs"`
|
|
User RoomOwner `json:"user"`
|
|
Weight int64 `json:"weight"`
|
|
}
|
|
|
|
func (s *Service) ListRoomPins(ctx context.Context, query roomPinListQuery) ([]RoomPin, int64, error) {
|
|
if s.db == nil {
|
|
return nil, 0, fmt.Errorf("room mysql is not configured")
|
|
}
|
|
query = normalizeRoomPinListQuery(query)
|
|
whereSQL, args := roomPinListWhere(ctx, query, time.Now().UTC().UnixMilli())
|
|
total, err := countRows(ctx, s.db, whereSQL, args...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
rows, err := s.db.QueryContext(ctx, `
|
|
SELECT p.id,
|
|
p.visible_region_id,
|
|
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.title,
|
|
r.cover_url,
|
|
r.owner_user_id,
|
|
r.status
|
|
`+whereSQL+`
|
|
ORDER BY p.weight DESC, p.expires_at_ms DESC, p.id DESC
|
|
LIMIT ? OFFSET ?
|
|
`, append(args, query.PageSize, offset(query.Page, query.PageSize))...)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]RoomPin, 0, query.PageSize)
|
|
for rows.Next() {
|
|
item, err := scanRoomPin(rows)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
if err := s.fillRoomPinDetails(ctx, items); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
return items, total, nil
|
|
}
|
|
|
|
func (s *Service) CreateRoomPin(ctx context.Context, req createRoomPinRequest, actorID uint) (RoomPin, error) {
|
|
if s.db == nil {
|
|
return RoomPin{}, fmt.Errorf("room mysql is not configured")
|
|
}
|
|
input, err := normalizeCreateRoomPinRequest(req)
|
|
if err != nil {
|
|
return RoomPin{}, err
|
|
}
|
|
|
|
room, _, err := s.getRoomForPin(ctx, input.RoomID)
|
|
if err != nil {
|
|
return RoomPin{}, err
|
|
}
|
|
if room.Status != "active" {
|
|
return RoomPin{}, fmt.Errorf("%w: 只能置顶正常状态房间", ErrInvalidArgument)
|
|
}
|
|
|
|
nowMS := time.Now().UTC().UnixMilli()
|
|
expiresAtMS := nowMS + input.DurationDays*roomPinDayMillis
|
|
appCode := appctx.FromContext(ctx)
|
|
if _, err := s.db.ExecContext(ctx, `
|
|
INSERT INTO room_region_pins (
|
|
app_code, visible_region_id, 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 (?, ?, ?, ?, ?, ?, ?, 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)
|
|
`, appCode, room.VisibleRegionID, room.RoomID, input.Weight, roomPinStatusActive, nowMS, expiresAtMS, actorID, nowMS, nowMS); err != nil {
|
|
return RoomPin{}, err
|
|
}
|
|
|
|
pin, err := s.getRoomPinByRoom(ctx, room.VisibleRegionID, room.RoomID)
|
|
if err != nil {
|
|
return RoomPin{}, err
|
|
}
|
|
return pin, nil
|
|
}
|
|
|
|
func (s *Service) CancelRoomPin(ctx context.Context, pinID int64, actorID uint) (RoomPin, error) {
|
|
if s.db == nil {
|
|
return RoomPin{}, fmt.Errorf("room mysql is not configured")
|
|
}
|
|
if pinID <= 0 {
|
|
return RoomPin{}, fmt.Errorf("%w: 置顶 ID 不正确", ErrInvalidArgument)
|
|
}
|
|
|
|
nowMS := time.Now().UTC().UnixMilli()
|
|
appCode := appctx.FromContext(ctx)
|
|
result, err := s.db.ExecContext(ctx, `
|
|
UPDATE room_region_pins
|
|
SET status = ?, cancelled_at_ms = ?, cancelled_by_admin_id = ?, updated_at_ms = ?
|
|
WHERE app_code = ? AND id = ? AND status <> ?
|
|
`, roomPinStatusCancelled, nowMS, actorID, nowMS, appCode, pinID, roomPinStatusCancelled)
|
|
if err != nil {
|
|
return RoomPin{}, err
|
|
}
|
|
if affected, err := result.RowsAffected(); err == nil && affected == 0 {
|
|
if _, err := s.getRoomPinByID(ctx, pinID); errors.Is(err, ErrNotFound) {
|
|
return RoomPin{}, ErrNotFound
|
|
}
|
|
}
|
|
return s.getRoomPinByID(ctx, pinID)
|
|
}
|
|
|
|
func (s *Service) getRoomForPin(ctx context.Context, roomID string) (RoomPinRoom, int64, error) {
|
|
appCode := appctx.FromContext(ctx)
|
|
row := s.db.QueryRowContext(ctx, `
|
|
SELECT room_id,
|
|
room_short_id,
|
|
visible_region_id,
|
|
owner_user_id,
|
|
title,
|
|
cover_url,
|
|
status
|
|
FROM room_list_entries
|
|
WHERE app_code = ? AND (room_id = ? OR room_short_id = ?)
|
|
LIMIT 1
|
|
`, appCode, roomID, roomID)
|
|
|
|
var room RoomPinRoom
|
|
var ownerUserID int64
|
|
if err := row.Scan(&room.RoomID, &room.RoomShortID, &room.VisibleRegionID, &ownerUserID, &room.Title, &room.CoverURL, &room.Status); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return RoomPinRoom{}, 0, ErrNotFound
|
|
}
|
|
return RoomPinRoom{}, 0, err
|
|
}
|
|
if room.Status != "active" {
|
|
room.Status = "closed"
|
|
}
|
|
return room, ownerUserID, nil
|
|
}
|
|
|
|
func (s *Service) getRoomPinByRoom(ctx context.Context, regionID int64, roomID string) (RoomPin, error) {
|
|
return s.getRoomPin(ctx, `
|
|
WHERE p.app_code = ? AND p.visible_region_id = ? AND p.room_id = ?
|
|
`, appctx.FromContext(ctx), regionID, roomID)
|
|
}
|
|
|
|
func (s *Service) getRoomPinByID(ctx context.Context, pinID int64) (RoomPin, error) {
|
|
return s.getRoomPin(ctx, `
|
|
WHERE p.app_code = ? AND p.id = ?
|
|
`, appctx.FromContext(ctx), pinID)
|
|
}
|
|
|
|
func (s *Service) getRoomPin(ctx context.Context, whereSQL string, args ...any) (RoomPin, error) {
|
|
row := s.db.QueryRowContext(ctx, `
|
|
SELECT p.id,
|
|
p.visible_region_id,
|
|
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.title,
|
|
r.cover_url,
|
|
r.owner_user_id,
|
|
r.status
|
|
FROM room_region_pins p
|
|
JOIN room_list_entries r ON r.app_code = p.app_code AND r.room_id = p.room_id
|
|
`+whereSQL+`
|
|
LIMIT 1
|
|
`, args...)
|
|
item, err := scanRoomPin(row)
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return RoomPin{}, ErrNotFound
|
|
}
|
|
if err != nil {
|
|
return RoomPin{}, err
|
|
}
|
|
items := []RoomPin{item}
|
|
if err := s.fillRoomPinDetails(ctx, items); err != nil {
|
|
return RoomPin{}, err
|
|
}
|
|
return items[0], nil
|
|
}
|
|
|
|
func roomPinListWhere(ctx context.Context, query roomPinListQuery, nowMS int64) (string, []any) {
|
|
where := []string{"p.app_code = ?"}
|
|
args := []any{appctx.FromContext(ctx)}
|
|
switch query.Status {
|
|
case roomPinStatusActive:
|
|
where = append(where, "p.status = ?", "p.expires_at_ms > ?")
|
|
args = append(args, roomPinStatusActive, nowMS)
|
|
case roomPinStatusExpired:
|
|
where = append(where, "p.status = ?", "p.expires_at_ms <= ?")
|
|
args = append(args, roomPinStatusActive, nowMS)
|
|
case roomPinStatusCancelled:
|
|
where = append(where, "p.status = ?")
|
|
args = append(args, roomPinStatusCancelled)
|
|
}
|
|
if query.RegionID > 0 {
|
|
where = append(where, "p.visible_region_id = ?")
|
|
args = append(args, query.RegionID)
|
|
}
|
|
if query.Keyword != "" {
|
|
like := "%" + query.Keyword + "%"
|
|
where = append(where, "(p.room_id LIKE ? OR r.room_short_id LIKE ? OR r.title LIKE ? OR CAST(r.owner_user_id AS CHAR) LIKE ?)")
|
|
args = append(args, like, like, like, like)
|
|
}
|
|
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
|
|
WHERE ` + strings.Join(where, " AND "), args
|
|
}
|
|
|
|
func normalizeRoomPinListQuery(query roomPinListQuery) roomPinListQuery {
|
|
if query.Page < 1 {
|
|
query.Page = 1
|
|
}
|
|
if query.PageSize < 1 {
|
|
query.PageSize = 20
|
|
}
|
|
if query.PageSize > 100 {
|
|
query.PageSize = 100
|
|
}
|
|
query.Keyword = strings.TrimSpace(query.Keyword)
|
|
query.Status = normalizeRoomPinStatus(query.Status)
|
|
if query.RegionID < 0 {
|
|
query.RegionID = 0
|
|
}
|
|
return query
|
|
}
|
|
|
|
func normalizeRoomPinStatus(status string) string {
|
|
switch strings.ToLower(strings.TrimSpace(status)) {
|
|
case "", roomPinStatusActive:
|
|
return roomPinStatusActive
|
|
case roomPinStatusExpired:
|
|
return roomPinStatusExpired
|
|
case roomPinStatusCancelled:
|
|
return roomPinStatusCancelled
|
|
case roomPinStatusAll:
|
|
return roomPinStatusAll
|
|
default:
|
|
return roomPinStatusActive
|
|
}
|
|
}
|
|
|
|
func normalizeCreateRoomPinRequest(req createRoomPinRequest) (createRoomPinRequest, error) {
|
|
req.RoomID = strings.TrimSpace(req.RoomID)
|
|
if req.RoomID == "" || len(req.RoomID) > 64 {
|
|
return req, fmt.Errorf("%w: 请选择房间", ErrInvalidArgument)
|
|
}
|
|
if req.Weight < 0 {
|
|
return req, fmt.Errorf("%w: 权重不能小于 0", ErrInvalidArgument)
|
|
}
|
|
if req.DurationDays == 0 {
|
|
req.DurationDays = defaultRoomPinDays
|
|
}
|
|
if req.DurationDays < 0 || req.DurationDays > maxRoomPinDays {
|
|
return req, fmt.Errorf("%w: 置顶时间必须在 1 到 3650 天之间", ErrInvalidArgument)
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
type roomPinScanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func scanRoomPin(row roomPinScanner) (RoomPin, error) {
|
|
var item RoomPin
|
|
var ownerUserID int64
|
|
err := row.Scan(
|
|
&item.ID,
|
|
&item.Room.VisibleRegionID,
|
|
&item.Room.RoomID,
|
|
&item.Weight,
|
|
&item.Status,
|
|
&item.PinnedAtMS,
|
|
&item.ExpiresAtMS,
|
|
&item.CancelledAtMS,
|
|
&item.CreatedByAdminID,
|
|
&item.CancelledByAdminID,
|
|
&item.CreatedAtMS,
|
|
&item.UpdatedAtMS,
|
|
&item.Room.RoomShortID,
|
|
&item.Room.Title,
|
|
&item.Room.CoverURL,
|
|
&ownerUserID,
|
|
&item.Room.Status,
|
|
)
|
|
item.User = RoomOwner{UserID: strconv.FormatInt(ownerUserID, 10)}
|
|
item.RemainingMS = item.ExpiresAtMS - time.Now().UTC().UnixMilli()
|
|
if item.RemainingMS < 0 {
|
|
item.RemainingMS = 0
|
|
}
|
|
if item.Room.Status != "active" {
|
|
item.Room.Status = "closed"
|
|
}
|
|
return item, err
|
|
}
|
|
|
|
func (s *Service) fillRoomPinDetails(ctx context.Context, items []RoomPin) error {
|
|
if s.userDB == nil || len(items) == 0 {
|
|
return nil
|
|
}
|
|
|
|
ownerIDs := make([]int64, 0, len(items))
|
|
regionIDs := make([]int64, 0, len(items))
|
|
for _, item := range items {
|
|
if ownerID, err := strconv.ParseInt(item.User.UserID, 10, 64); err == nil && ownerID > 0 {
|
|
ownerIDs = append(ownerIDs, ownerID)
|
|
}
|
|
if item.Room.VisibleRegionID > 0 {
|
|
regionIDs = append(regionIDs, item.Room.VisibleRegionID)
|
|
}
|
|
}
|
|
|
|
owners, err := s.queryRoomOwners(ctx, uniqueInt64s(ownerIDs))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
regions, err := s.queryRegionNames(ctx, uniqueInt64s(regionIDs))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for index := range items {
|
|
if ownerID, err := strconv.ParseInt(items[index].User.UserID, 10, 64); err == nil {
|
|
if owner, ok := owners[ownerID]; ok {
|
|
items[index].User = owner
|
|
}
|
|
}
|
|
if name, ok := regions[items[index].Room.VisibleRegionID]; ok {
|
|
items[index].Room.RegionName = name
|
|
}
|
|
}
|
|
return nil
|
|
}
|