feat(ECR-041): OpsCMS Banner 薄写面(Write-Wave 首刀)
Admin POST/PUT + admin.cms.write/审计;C 端 GET /home/banners;首页投影回退静态 homeFeeds;migration 000051;不碰 FeedSlot/支付/UGC。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
"github.com/jackc/pgx/v5/pgconn"
|
||||
)
|
||||
|
||||
// BannerWriteInput is create/update payload for ops_banners.
|
||||
type BannerWriteInput struct {
|
||||
Code string
|
||||
Title string
|
||||
Placement string
|
||||
ImageURL *string
|
||||
LinkPath *string
|
||||
SortOrder int
|
||||
Active bool
|
||||
}
|
||||
|
||||
// ListActiveBanners returns active banners for a placement (C-end).
|
||||
func (r *AdminRepo) ListActiveBanners(ctx context.Context, placement string) ([]BannerRow, error) {
|
||||
rows, err := r.Pool.Query(ctx, `
|
||||
SELECT id, code, title, placement, image_url, link_path, sort_order, active, system, updated_at
|
||||
FROM ops_banners
|
||||
WHERE active = true AND placement = $1
|
||||
ORDER BY sort_order ASC, code ASC
|
||||
LIMIT 100`, placement)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []BannerRow
|
||||
for rows.Next() {
|
||||
var b BannerRow
|
||||
if err := rows.Scan(
|
||||
&b.ID, &b.Code, &b.Title, &b.Placement, &b.ImageURL, &b.LinkPath,
|
||||
&b.SortOrder, &b.Active, &b.System, &b.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, b)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// CreateBannerWithAudit inserts a banner and audits.
|
||||
func (r *AdminRepo) CreateBannerWithAudit(
|
||||
ctx context.Context,
|
||||
adminID uuid.UUID,
|
||||
in BannerWriteInput,
|
||||
meta json.RawMessage,
|
||||
) (*BannerRow, error) {
|
||||
tx, err := r.Pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var b BannerRow
|
||||
err = tx.QueryRow(ctx, `
|
||||
INSERT INTO ops_banners(code, title, placement, image_url, link_path, sort_order, active, system)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,false)
|
||||
RETURNING id, code, title, placement, image_url, link_path, sort_order, active, system, updated_at`,
|
||||
in.Code, in.Title, in.Placement, in.ImageURL, in.LinkPath, in.SortOrder, in.Active,
|
||||
).Scan(
|
||||
&b.ID, &b.Code, &b.Title, &b.Placement, &b.ImageURL, &b.LinkPath,
|
||||
&b.SortOrder, &b.Active, &b.System, &b.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, mapBannerWriteErr(err)
|
||||
}
|
||||
if meta == nil {
|
||||
meta = json.RawMessage(`{}`)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO admin_audit_logs(admin_id, action, target_type, target_id, meta)
|
||||
VALUES ($1,'cms.banner.create','banner',$2,$3)`,
|
||||
adminID, b.ID.String(), meta,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
// UpdateBannerWithAudit updates mutable fields and audits.
|
||||
func (r *AdminRepo) UpdateBannerWithAudit(
|
||||
ctx context.Context,
|
||||
adminID, id uuid.UUID,
|
||||
in BannerWriteInput,
|
||||
meta json.RawMessage,
|
||||
) (*BannerRow, error) {
|
||||
tx, err := r.Pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
|
||||
var system bool
|
||||
var oldCode string
|
||||
err = tx.QueryRow(ctx, `SELECT system, code FROM ops_banners WHERE id=$1`, id).Scan(&system, &oldCode)
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return nil, pgx.ErrNoRows
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
code := in.Code
|
||||
if system {
|
||||
code = oldCode
|
||||
}
|
||||
|
||||
var b BannerRow
|
||||
err = tx.QueryRow(ctx, `
|
||||
UPDATE ops_banners
|
||||
SET code=$2, title=$3, placement=$4, image_url=$5, link_path=$6,
|
||||
sort_order=$7, active=$8, updated_at=now()
|
||||
WHERE id=$1
|
||||
RETURNING id, code, title, placement, image_url, link_path, sort_order, active, system, updated_at`,
|
||||
id, code, in.Title, in.Placement, in.ImageURL, in.LinkPath, in.SortOrder, in.Active,
|
||||
).Scan(
|
||||
&b.ID, &b.Code, &b.Title, &b.Placement, &b.ImageURL, &b.LinkPath,
|
||||
&b.SortOrder, &b.Active, &b.System, &b.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, mapBannerWriteErr(err)
|
||||
}
|
||||
if meta == nil {
|
||||
meta = json.RawMessage(`{}`)
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO admin_audit_logs(admin_id, action, target_type, target_id, meta)
|
||||
VALUES ($1,'cms.banner.update','banner',$2,$3)`,
|
||||
adminID, id.String(), meta,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Commit(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &b, nil
|
||||
}
|
||||
|
||||
func mapBannerWriteErr(err error) error {
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return errString("banner code conflict")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// BannerCodeConflict reports unique code violation.
|
||||
func BannerCodeConflict(err error) bool {
|
||||
return err != nil && strings.Contains(err.Error(), "banner code conflict")
|
||||
}
|
||||
Reference in New Issue
Block a user