feat(desktop): drop parked review flow, add publish management
SaaS 侧人工审核后才创建 publish job,desktop 不再做二次审核:移除 manual/waiting_user/parked/from_parked 状态机与 LeaseFromParked 查询, desktop client 只执行发布并新增"发布管理"页(待发布队列 / 历史 / 再次 发送)。同时抽离 @geo/publisher-platforms 共享适配器包、新增 Redis-based desktop_presence 与 publish_record_support,刷新 admin-web 发布弹窗与 媒体库;plan A / spec 文档同步口径。
This commit is contained in:
@@ -3,10 +3,12 @@ package app
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5"
|
||||
goredis "github.com/redis/go-redis/v9"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/response"
|
||||
@@ -14,11 +16,23 @@ import (
|
||||
)
|
||||
|
||||
type DesktopAccountService struct {
|
||||
repo repository.DesktopAccountRepository
|
||||
repo repository.DesktopAccountRepository
|
||||
clientRepo repository.DesktopClientRepository
|
||||
redis *goredis.Client
|
||||
now func() time.Time
|
||||
}
|
||||
|
||||
func NewDesktopAccountService(repo repository.DesktopAccountRepository) *DesktopAccountService {
|
||||
return &DesktopAccountService{repo: repo}
|
||||
func NewDesktopAccountService(
|
||||
repo repository.DesktopAccountRepository,
|
||||
clientRepo repository.DesktopClientRepository,
|
||||
redis *goredis.Client,
|
||||
) *DesktopAccountService {
|
||||
return &DesktopAccountService{
|
||||
repo: repo,
|
||||
clientRepo: clientRepo,
|
||||
redis: redis,
|
||||
now: time.Now,
|
||||
}
|
||||
}
|
||||
|
||||
type DesktopAccountView struct {
|
||||
@@ -26,6 +40,7 @@ type DesktopAccountView struct {
|
||||
Platform string `json:"platform"`
|
||||
PlatformUID string `json:"platform_uid"`
|
||||
DisplayName string `json:"display_name"`
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
Health string `json:"health"`
|
||||
ClientID *string `json:"client_id"`
|
||||
AccountFingerprint *string `json:"account_fingerprint"`
|
||||
@@ -34,6 +49,9 @@ type DesktopAccountView struct {
|
||||
SyncVersion int64 `json:"sync_version"`
|
||||
DeletedAt *time.Time `json:"deleted_at"`
|
||||
DeleteRequestedAt *time.Time `json:"delete_requested_at"`
|
||||
ClientOnline *bool `json:"client_online"`
|
||||
ClientLastSeenAt *time.Time `json:"client_last_seen_at"`
|
||||
ClientDeviceName *string `json:"client_device_name"`
|
||||
}
|
||||
|
||||
type UpsertDesktopAccountRequest struct {
|
||||
@@ -41,6 +59,7 @@ type UpsertDesktopAccountRequest struct {
|
||||
PlatformUID string `json:"platform_uid" binding:"required"`
|
||||
AccountFingerprint *string `json:"account_fingerprint"`
|
||||
DisplayName string `json:"display_name" binding:"required"`
|
||||
AvatarURL *string `json:"avatar_url"`
|
||||
Health string `json:"health" binding:"required,oneof=live expired captcha risk"`
|
||||
VerifiedAt *time.Time `json:"verified_at"`
|
||||
Tags []string `json:"tags"`
|
||||
@@ -55,19 +74,38 @@ type PatchDesktopAccountRequest struct {
|
||||
IfSyncVersion int64 `json:"if_sync_version" binding:"required"`
|
||||
}
|
||||
|
||||
func (s *DesktopAccountService) ListByClient(ctx context.Context, client *repository.DesktopClient) ([]DesktopAccountView, error) {
|
||||
func (s *DesktopAccountService) ListByUser(ctx context.Context, client *repository.DesktopClient) ([]DesktopAccountView, error) {
|
||||
if client == nil {
|
||||
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
|
||||
}
|
||||
|
||||
rows, err := s.repo.ListByClient(ctx, client.WorkspaceID, client.ID)
|
||||
rows, err := s.repo.ListByUser(ctx, client.WorkspaceID, client.UserID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50081, "desktop_accounts_query_failed", "failed to list desktop accounts")
|
||||
}
|
||||
|
||||
clientMap, presenceMap := s.loadClientState(ctx, client.WorkspaceID, rows)
|
||||
items := make([]DesktopAccountView, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
items = append(items, buildDesktopAccountView(&item))
|
||||
items = append(items, s.buildDesktopAccountView(&item, clientMap, presenceMap))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *DesktopAccountService) ListForActor(ctx context.Context, actor auth.Actor) ([]DesktopAccountView, error) {
|
||||
if actor.PrimaryWorkspaceID == 0 || actor.UserID == 0 {
|
||||
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
||||
}
|
||||
|
||||
rows, err := s.repo.ListByUser(ctx, actor.PrimaryWorkspaceID, actor.UserID)
|
||||
if err != nil {
|
||||
return nil, response.ErrInternal(50081, "desktop_accounts_query_failed", "failed to list desktop accounts")
|
||||
}
|
||||
|
||||
clientMap, presenceMap := s.loadClientState(ctx, actor.PrimaryWorkspaceID, rows)
|
||||
items := make([]DesktopAccountView, 0, len(rows))
|
||||
for _, item := range rows {
|
||||
items = append(items, s.buildDesktopAccountView(&item, clientMap, presenceMap))
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -97,6 +135,7 @@ func (s *DesktopAccountService) Upsert(ctx context.Context, client *repository.D
|
||||
PlatformUID: req.PlatformUID,
|
||||
AccountFingerprint: req.AccountFingerprint,
|
||||
DisplayName: req.DisplayName,
|
||||
AvatarURL: req.AvatarURL,
|
||||
Health: req.Health,
|
||||
VerifiedAt: req.VerifiedAt,
|
||||
Tags: req.Tags,
|
||||
@@ -109,7 +148,7 @@ func (s *DesktopAccountService) Upsert(ctx context.Context, client *repository.D
|
||||
return nil, response.ErrInternal(50083, "desktop_account_upsert_failed", "failed to upsert desktop account")
|
||||
}
|
||||
|
||||
view := buildDesktopAccountView(account)
|
||||
view := s.buildDesktopAccountView(account, nil, nil)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
@@ -134,7 +173,7 @@ func (s *DesktopAccountService) Patch(ctx context.Context, client *repository.De
|
||||
return nil, response.ErrInternal(50084, "desktop_account_patch_failed", "failed to patch desktop account")
|
||||
}
|
||||
|
||||
view := buildDesktopAccountView(account)
|
||||
view := s.buildDesktopAccountView(account, nil, nil)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
@@ -151,7 +190,7 @@ func (s *DesktopAccountService) Tombstone(ctx context.Context, client *repositor
|
||||
return nil, response.ErrInternal(50085, "desktop_account_delete_failed", "failed to delete desktop account")
|
||||
}
|
||||
|
||||
view := buildDesktopAccountView(account)
|
||||
view := s.buildDesktopAccountView(account, nil, nil)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
@@ -177,22 +216,53 @@ func (s *DesktopAccountService) RequestDelete(ctx context.Context, actor auth.Ac
|
||||
return nil, response.ErrInternal(50086, "desktop_account_request_delete_failed", "failed to update desktop account delete request")
|
||||
}
|
||||
|
||||
view := buildDesktopAccountView(account)
|
||||
view := s.buildDesktopAccountView(account, nil, nil)
|
||||
return &view, nil
|
||||
}
|
||||
|
||||
func buildDesktopAccountView(account *repository.DesktopAccount) DesktopAccountView {
|
||||
func (s *DesktopAccountService) buildDesktopAccountView(
|
||||
account *repository.DesktopAccount,
|
||||
clientMap map[uuid.UUID]*repository.DesktopClient,
|
||||
presenceClientMap map[uuid.UUID]uuid.UUID,
|
||||
) DesktopAccountView {
|
||||
var clientID *string
|
||||
if account.ClientID != nil {
|
||||
value := account.ClientID.String()
|
||||
var clientOnline *bool
|
||||
var clientLastSeenAt *time.Time
|
||||
var clientDeviceName *string
|
||||
|
||||
resolvedClientID := account.ClientID
|
||||
if presenceClientMap != nil {
|
||||
if activeClientID, ok := presenceClientMap[account.DesktopID]; ok {
|
||||
resolvedClientID = &activeClientID
|
||||
}
|
||||
}
|
||||
|
||||
if resolvedClientID != nil {
|
||||
value := resolvedClientID.String()
|
||||
clientID = &value
|
||||
|
||||
online := false
|
||||
clientOnline = &online
|
||||
|
||||
if clientMap != nil {
|
||||
if client, ok := clientMap[*resolvedClientID]; ok && client != nil {
|
||||
clientLastSeenAt = client.LastSeenAt
|
||||
clientDeviceName = client.DeviceName
|
||||
if presenceClientMap != nil {
|
||||
if activeClientID, ok := presenceClientMap[account.DesktopID]; ok && activeClientID == *resolvedClientID {
|
||||
online = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return DesktopAccountView{
|
||||
ID: account.DesktopID.String(),
|
||||
Platform: account.Platform,
|
||||
PlatformUID: account.PlatformUID,
|
||||
PlatformUID: normalizePlatformUID(account.PlatformUID),
|
||||
DisplayName: account.DisplayName,
|
||||
AvatarURL: account.AvatarURL,
|
||||
Health: account.Health,
|
||||
ClientID: clientID,
|
||||
AccountFingerprint: account.AccountFingerprint,
|
||||
@@ -201,5 +271,82 @@ func buildDesktopAccountView(account *repository.DesktopAccount) DesktopAccountV
|
||||
SyncVersion: account.SyncVersion,
|
||||
DeletedAt: account.DeletedAt,
|
||||
DeleteRequestedAt: account.DeleteRequestedAt,
|
||||
ClientOnline: clientOnline,
|
||||
ClientLastSeenAt: clientLastSeenAt,
|
||||
ClientDeviceName: clientDeviceName,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *DesktopAccountService) loadClientState(
|
||||
ctx context.Context,
|
||||
workspaceID int64,
|
||||
accounts []repository.DesktopAccount,
|
||||
) (map[uuid.UUID]*repository.DesktopClient, map[uuid.UUID]uuid.UUID) {
|
||||
clientIDs := make([]uuid.UUID, 0, len(accounts)*2)
|
||||
accountIDs := make([]uuid.UUID, 0, len(accounts))
|
||||
seen := make(map[uuid.UUID]struct{}, len(accounts)*2)
|
||||
|
||||
for _, account := range accounts {
|
||||
accountIDs = append(accountIDs, account.DesktopID)
|
||||
|
||||
if account.ClientID == nil {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[*account.ClientID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[*account.ClientID] = struct{}{}
|
||||
clientIDs = append(clientIDs, *account.ClientID)
|
||||
}
|
||||
|
||||
presenceClientMap := loadDesktopAccountPresence(ctx, s.redis, accountIDs)
|
||||
for _, clientID := range presenceClientMap {
|
||||
if _, ok := seen[clientID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[clientID] = struct{}{}
|
||||
clientIDs = append(clientIDs, clientID)
|
||||
}
|
||||
|
||||
clientMap := make(map[uuid.UUID]*repository.DesktopClient, len(clientIDs))
|
||||
if s.clientRepo != nil {
|
||||
for _, clientID := range clientIDs {
|
||||
client, err := s.clientRepo.GetByID(ctx, clientID, workspaceID)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
continue
|
||||
}
|
||||
continue
|
||||
}
|
||||
clientMap[clientID] = client
|
||||
}
|
||||
}
|
||||
|
||||
return clientMap, presenceClientMap
|
||||
}
|
||||
|
||||
func resolveDesktopClientOnline(
|
||||
client *repository.DesktopClient,
|
||||
presence bool,
|
||||
presenceKnown bool,
|
||||
now time.Time,
|
||||
) bool {
|
||||
if client == nil || client.RevokedAt != nil {
|
||||
return false
|
||||
}
|
||||
if presenceKnown && presence {
|
||||
return true
|
||||
}
|
||||
if client.LastSeenAt == nil {
|
||||
return false
|
||||
}
|
||||
return now.UTC().Sub(client.LastSeenAt.UTC()) <= desktopClientPresenceTTL
|
||||
}
|
||||
|
||||
func normalizePlatformUID(value string) string {
|
||||
normalized := strings.TrimSpace(value)
|
||||
for strings.HasPrefix(strings.ToLower(normalized), "platform:") {
|
||||
normalized = strings.TrimSpace(normalized[len("platform:"):])
|
||||
}
|
||||
return normalized
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user