5ff2e2e74c
Hard cutover from the browser-extension plugin flow to desktop clients: remove plugin_installations/plugin_sessions tables and related service, handler, router, and generated model code; migrate monitoring quotas and collector types to desktop_clients (UUID primary_client_id); recreate platform_access_snapshots keyed by client_id; update dev-seed and callback types accordingly; mark legacy design docs as historical. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
170 lines
4.9 KiB
Go
170 lines
4.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
|
"github.com/geo-platform/tenant-api/internal/shared/response"
|
|
)
|
|
|
|
type MediaService struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func NewMediaService(pool *pgxpool.Pool) *MediaService {
|
|
return &MediaService{pool: pool}
|
|
}
|
|
|
|
type MediaPlatformResponse struct {
|
|
ID int64 `json:"id"`
|
|
PlatformID string `json:"platform_id"`
|
|
Name string `json:"name"`
|
|
Category string `json:"category"`
|
|
ShortName string `json:"short_name"`
|
|
AccentColor string `json:"accent_color"`
|
|
LoginURL *string `json:"login_url"`
|
|
LogoURL *string `json:"logo_url"`
|
|
Status string `json:"status"`
|
|
SortOrder int `json:"sort_order"`
|
|
}
|
|
|
|
type PublishRecordResponse struct {
|
|
ID int64 `json:"id"`
|
|
PublishBatchID int64 `json:"publish_batch_id"`
|
|
ArticleID int64 `json:"article_id"`
|
|
PlatformAccountID int64 `json:"platform_account_id"`
|
|
PlatformID string `json:"platform_id"`
|
|
PlatformName string `json:"platform_name"`
|
|
PlatformNickname string `json:"platform_nickname"`
|
|
Status string `json:"status"`
|
|
ExternalArticleID *string `json:"external_article_id"`
|
|
ExternalArticleURL *string `json:"external_article_url"`
|
|
ExternalManageURL *string `json:"external_manage_url"`
|
|
PublishedAt *time.Time `json:"published_at"`
|
|
ErrorMessage *string `json:"error_message"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
func (s *MediaService) ListPlatforms(ctx context.Context) ([]MediaPlatformResponse, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT id, platform_id, name, category, short_name, accent_color, login_url, logo_url, status, sort_order
|
|
FROM media_platforms
|
|
WHERE status = 'active'
|
|
ORDER BY sort_order ASC, id ASC
|
|
`)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50030, "media_platform_query_failed", "failed to list media platforms")
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]MediaPlatformResponse, 0)
|
|
for rows.Next() {
|
|
var item MediaPlatformResponse
|
|
if err := rows.Scan(
|
|
&item.ID,
|
|
&item.PlatformID,
|
|
&item.Name,
|
|
&item.Category,
|
|
&item.ShortName,
|
|
&item.AccentColor,
|
|
&item.LoginURL,
|
|
&item.LogoURL,
|
|
&item.Status,
|
|
&item.SortOrder,
|
|
); err != nil {
|
|
return nil, response.ErrInternal(50030, "media_platform_scan_failed", err.Error())
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func (s *MediaService) ListArticlePublishRecords(ctx context.Context, articleID int64) ([]PublishRecordResponse, error) {
|
|
actor := auth.MustActor(ctx)
|
|
return s.listArticlePublishRecords(ctx, actor.TenantID, articleID)
|
|
}
|
|
|
|
func (s *MediaService) listArticlePublishRecords(ctx context.Context, tenantID, articleID int64) ([]PublishRecordResponse, error) {
|
|
rows, err := s.pool.Query(ctx, `
|
|
SELECT pr.id, pr.publish_batch_id, pr.article_id, pr.platform_account_id, pr.platform_id,
|
|
mp.name, pa.nickname, pr.status, pr.external_article_id, pr.external_article_url,
|
|
pr.external_manage_url, pr.published_at, pr.error_message, pr.created_at, pr.updated_at
|
|
FROM publish_records pr
|
|
JOIN media_platforms mp ON mp.platform_id = pr.platform_id
|
|
JOIN platform_accounts pa ON pa.id = pr.platform_account_id
|
|
WHERE pr.tenant_id = $1 AND pr.article_id = $2
|
|
ORDER BY pr.created_at DESC, pr.id DESC
|
|
`, tenantID, articleID)
|
|
if err != nil {
|
|
return nil, response.ErrInternal(50043, "publish_record_query_failed", "failed to list publish records")
|
|
}
|
|
defer rows.Close()
|
|
|
|
items := make([]PublishRecordResponse, 0)
|
|
for rows.Next() {
|
|
var item PublishRecordResponse
|
|
if err := rows.Scan(
|
|
&item.ID,
|
|
&item.PublishBatchID,
|
|
&item.ArticleID,
|
|
&item.PlatformAccountID,
|
|
&item.PlatformID,
|
|
&item.PlatformName,
|
|
&item.PlatformNickname,
|
|
&item.Status,
|
|
&item.ExternalArticleID,
|
|
&item.ExternalArticleURL,
|
|
&item.ExternalManageURL,
|
|
&item.PublishedAt,
|
|
&item.ErrorMessage,
|
|
&item.CreatedAt,
|
|
&item.UpdatedAt,
|
|
); err != nil {
|
|
return nil, response.ErrInternal(50043, "publish_record_scan_failed", err.Error())
|
|
}
|
|
items = append(items, item)
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
func publishBatchRequiresCover(accounts []platformAccountSeed) bool {
|
|
for _, account := range accounts {
|
|
if account.PlatformID == "baijiahao" {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func pointerStringValue(value *string) string {
|
|
if value == nil {
|
|
return ""
|
|
}
|
|
return *value
|
|
}
|
|
|
|
func normalizePublishStatus(status string) string {
|
|
switch status {
|
|
case "success", "published", "publish_success":
|
|
return "success"
|
|
case "publishing", "pending", "queued", "running":
|
|
return "publishing"
|
|
default:
|
|
return "failed"
|
|
}
|
|
}
|
|
|
|
func newSessionToken() (string, error) {
|
|
buf := make([]byte, 16)
|
|
if _, err := rand.Read(buf); err != nil {
|
|
return "", err
|
|
}
|
|
return hex.EncodeToString(buf), nil
|
|
}
|