Files
digital-psychology/apps/api/internal/service/admin/cms_feed_slot_write.go
T
jackyu66gitandCursor 56f661748a feat(ECR-042): OpsCMS FeedSlot 薄写面
Admin POST/PUT 复用 admin.cms.write;C 端 GET /home/feed-slots;首页无 active 槽隐藏推荐区、失败回退展示;无新 migration。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-13 20:02:01 +08:00

82 lines
2.5 KiB
Go

package admin
import (
"context"
"encoding/json"
"errors"
"regexp"
"strings"
"unicode/utf8"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
"github.com/yuxingu/digital-psychology/apps/api/internal/repository"
)
var (
ErrInvalidFeedSlot = errors.New("invalid feed slot")
ErrFeedSlotConflict = errors.New("feed slot code conflict")
slotCodeRe = regexp.MustCompile(`^[a-z][a-z0-9_]{1,62}$`)
slotKeyRe = regexp.MustCompile(`^[a-z][a-z0-9_.]{1,62}$`)
)
// FeedSlotWriteBody is JSON for create/update.
type FeedSlotWriteBody struct {
Code string `json:"code"`
Title string `json:"title"`
SlotKey string `json:"slot_key"`
Placement string `json:"placement"`
Active bool `json:"active"`
}
// CreateFeedSlot validates, inserts, audits.
func (s *Service) CreateFeedSlot(ctx context.Context, adminID uuid.UUID, body FeedSlotWriteBody) (*repository.FeedSlotRow, error) {
in, err := normalizeFeedSlotWrite(body)
if err != nil {
return nil, err
}
meta, _ := json.Marshal(map[string]any{"code": in.Code, "active": in.Active})
row, err := s.Repo.CreateFeedSlotWithAudit(ctx, adminID, in, meta)
if repository.FeedSlotCodeConflict(err) {
return nil, ErrFeedSlotConflict
}
return row, err
}
// UpdateFeedSlot validates, updates, audits.
func (s *Service) UpdateFeedSlot(ctx context.Context, adminID, id uuid.UUID, body FeedSlotWriteBody) (*repository.FeedSlotRow, error) {
in, err := normalizeFeedSlotWrite(body)
if err != nil {
return nil, err
}
meta, _ := json.Marshal(map[string]any{"code": in.Code, "active": in.Active})
row, err := s.Repo.UpdateFeedSlotWithAudit(ctx, adminID, id, in, meta)
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrFeedSlotNotFound
}
if repository.FeedSlotCodeConflict(err) {
return nil, ErrFeedSlotConflict
}
return row, err
}
func normalizeFeedSlotWrite(body FeedSlotWriteBody) (repository.FeedSlotWriteInput, error) {
code := strings.TrimSpace(body.Code)
title := strings.TrimSpace(body.Title)
slotKey := strings.TrimSpace(body.SlotKey)
placement := strings.TrimSpace(body.Placement)
if !slotCodeRe.MatchString(code) || !slotKeyRe.MatchString(slotKey) {
return repository.FeedSlotWriteInput{}, ErrInvalidFeedSlot
}
if title == "" || utf8.RuneCountInString(title) > 128 {
return repository.FeedSlotWriteInput{}, ErrInvalidFeedSlot
}
if _, ok := bannerPlacements[placement]; !ok {
return repository.FeedSlotWriteInput{}, ErrInvalidFeedSlot
}
return repository.FeedSlotWriteInput{
Code: code, Title: title, SlotKey: slotKey, Placement: placement, Active: body.Active,
}, nil
}