feat(tenant): add brand publish-records list and harden enterprise site management

- Add GET /api/tenant/publish-records: brand-scoped paginated list that merges
  in-flight (queued/publishing) records ahead of history (success/failed/cancelled),
  enriched with article title and latest desktop task id; wire up
  service/handler/router/swagger/router_test
- Rework PublishManagementView to consume publish-records instead of desktop
  publish tasks; add publishRecordsApi + shared-types
  (ListPublishRecordsParams, PublishRecordListResponse)
- Enterprise site Update: distinguish explicit null from missing PATCH fields via
  EnterpriseSitePatchString, dedup site_url before write, verify RowsAffected,
  and map unique-constraint violations to a clear conflict
- MediaView: enterprise site edit/delete flows with favicon fallback handling
- Localize enterprise site / PBootCMS error messages to Chinese across the
  backend and the admin-web error map, plus legacy raw-message translation
- deploy: gate migration-coupled service rollouts behind RUN_MIGRATIONS unless
  ALLOW_SKIP_MIGRATIONS_FOR_APP_ROLLOUT=true; handle empty SERVICES safely

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 11:40:46 +08:00
parent 67be43319e
commit a44ed21967
15 changed files with 1418 additions and 764 deletions
@@ -2,14 +2,71 @@ package app
import (
"context"
"encoding/json"
"os"
"strings"
"testing"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/geo-platform/tenant-api/internal/shared/auth"
"github.com/geo-platform/tenant-api/internal/shared/config"
"github.com/geo-platform/tenant-api/internal/shared/publicasset"
"github.com/sergi/go-diff/diffmatchpatch"
)
func TestUpdateEnterpriseSiteConnectionRequestTracksNullAndMissingFields(t *testing.T) {
var req UpdateEnterpriseSiteConnectionRequest
if err := json.Unmarshal([]byte(`{"name":"官网","default_scode":null,"secret":""}`), &req); err != nil {
t.Fatalf("unmarshal request: %v", err)
}
if !req.Name.Set || req.Name.Value == nil || *req.Name.Value != "官网" {
t.Fatalf("name patch = %#v, want set string", req.Name)
}
if !req.DefaultScode.Set || req.DefaultScode.Value != nil {
t.Fatalf("default_scode patch = %#v, want explicit null", req.DefaultScode)
}
if !req.Secret.Set || req.Secret.Value == nil || *req.Secret.Value != "" {
t.Fatalf("secret patch = %#v, want set empty string", req.Secret)
}
if req.Status.Set {
t.Fatalf("status patch = %#v, want missing", req.Status)
}
}
func TestEnterpriseSiteUpdateRealDatabaseRepro(t *testing.T) {
dsn := os.Getenv("TEST_ENTERPRISE_SITE_UPDATE_DSN")
if dsn == "" {
t.Skip("set TEST_ENTERPRISE_SITE_UPDATE_DSN to run the real database repro")
}
ctx := auth.WithCurrentWorkspaceID(
auth.WithActor(context.Background(), auth.Actor{UserID: 1, TenantID: 1, PrimaryWorkspaceID: 1}),
1,
)
pool, err := pgxpool.New(ctx, dsn)
if err != nil {
t.Fatalf("connect database: %v", err)
}
defer pool.Close()
name := "海翔"
siteURL := "https://youjayou.com"
appID := "admin"
acode := "cn"
mcode := "2"
req := UpdateEnterpriseSiteConnectionRequest{
Name: EnterpriseSitePatchString{Set: true, Value: &name},
SiteURL: EnterpriseSitePatchString{Set: true, Value: &siteURL},
AppID: EnterpriseSitePatchString{Set: true, Value: &appID},
DefaultAcode: EnterpriseSitePatchString{Set: true, Value: &acode},
DefaultMcode: EnterpriseSitePatchString{Set: true, Value: &mcode},
DefaultScode: EnterpriseSitePatchString{Set: true, Value: nil},
}
if _, err := NewEnterpriseSiteService(pool, config.NewStaticProvider(&config.Config{})).Update(ctx, 1, req); err != nil {
t.Fatalf("update enterprise site: %v", err)
}
}
func TestEnterpriseSiteDescriptionUsesRenderedHTMLText(t *testing.T) {
got := enterpriseSiteDescription(
"<h2>2026年合肥全屋定制行业发展现状</h2><p>消费者需求升级。</p>",