feat(ops): ECR-007 行为分析与 ECR-008 内容运营后台
落地埋点 ingest/数据看板、首页宫格 CMS 与测评上下架;含账号引导、问答流式与免责声明去重,以及 review P1 审计同事务修复。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// HomeTool is one homepage grid entry.
|
||||
type HomeTool struct {
|
||||
ID uuid.UUID `json:"id"`
|
||||
RowIndex int `json:"row_index"`
|
||||
SortOrder int `json:"sort_order"`
|
||||
Path string `json:"path"`
|
||||
Icon string `json:"icon"`
|
||||
Label string `json:"label"`
|
||||
Badge *string `json:"badge,omitempty"`
|
||||
BadgeTone *string `json:"badge_tone,omitempty"`
|
||||
Enabled bool `json:"enabled"`
|
||||
UpdatedAt time.Time `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
// HomeToolsRepo persists homepage grid tools.
|
||||
type HomeToolsRepo struct {
|
||||
Pool *pgxpool.Pool
|
||||
}
|
||||
|
||||
// ListAll returns all tools ordered by row then sort.
|
||||
func (r *HomeToolsRepo) ListAll(ctx context.Context) ([]HomeTool, error) {
|
||||
return r.query(ctx, false)
|
||||
}
|
||||
|
||||
// ListEnabled returns enabled tools for C-end.
|
||||
func (r *HomeToolsRepo) ListEnabled(ctx context.Context) ([]HomeTool, error) {
|
||||
return r.query(ctx, true)
|
||||
}
|
||||
|
||||
func (r *HomeToolsRepo) query(ctx context.Context, onlyEnabled bool) ([]HomeTool, error) {
|
||||
q := `
|
||||
SELECT id, row_index, sort_order, path, icon, label, badge, badge_tone, enabled, updated_at
|
||||
FROM home_tools`
|
||||
if onlyEnabled {
|
||||
q += ` WHERE enabled = true`
|
||||
}
|
||||
q += ` ORDER BY row_index, sort_order, label`
|
||||
rows, err := r.Pool.Query(ctx, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var out []HomeTool
|
||||
for rows.Next() {
|
||||
var t HomeTool
|
||||
if err := rows.Scan(
|
||||
&t.ID, &t.RowIndex, &t.SortOrder, &t.Path, &t.Icon, &t.Label,
|
||||
&t.Badge, &t.BadgeTone, &t.Enabled, &t.UpdatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, t)
|
||||
}
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// ReplaceAll deletes all rows and inserts items in one transaction.
|
||||
func (r *HomeToolsRepo) ReplaceAll(ctx context.Context, items []HomeTool) error {
|
||||
return r.ReplaceAllWithAudit(ctx, items, uuid.Nil, nil)
|
||||
}
|
||||
|
||||
// ReplaceAllWithAudit replaces tools and optionally writes admin_audit_logs in one tx.
|
||||
// If adminID is uuid.Nil, skips audit insert.
|
||||
func (r *HomeToolsRepo) ReplaceAllWithAudit(
|
||||
ctx context.Context,
|
||||
items []HomeTool,
|
||||
adminID uuid.UUID,
|
||||
meta json.RawMessage,
|
||||
) error {
|
||||
tx, err := r.Pool.Begin(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer tx.Rollback(ctx)
|
||||
if _, err := tx.Exec(ctx, `DELETE FROM home_tools`); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, it := range items {
|
||||
id := it.ID
|
||||
if id == uuid.Nil {
|
||||
id = uuid.New()
|
||||
}
|
||||
if _, err := tx.Exec(ctx, `
|
||||
INSERT INTO home_tools(id, row_index, sort_order, path, icon, label, badge, badge_tone, enabled, updated_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,now())`,
|
||||
id, it.RowIndex, it.SortOrder, it.Path, it.Icon, it.Label, it.Badge, it.BadgeTone, it.Enabled,
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if adminID != uuid.Nil {
|
||||
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,'home_tools.replace','home_tools','all',$2)`, adminID, meta); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tx.Commit(ctx)
|
||||
}
|
||||
Reference in New Issue
Block a user