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:
2026-04-20 09:52:48 +08:00
parent b16e9f0bd1
commit a617d39a4a
93 changed files with 5519 additions and 3594 deletions
@@ -6,10 +6,12 @@ import (
"crypto/sha256"
"encoding/hex"
"fmt"
"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"
@@ -19,14 +21,16 @@ import (
const DesktopClientTokenTTL = 30 * 24 * time.Hour
type DesktopClientService struct {
repo repository.DesktopClientRepository
repo repository.DesktopClientRepository
redis *goredis.Client
}
func NewDesktopClientService(repo repository.DesktopClientRepository) *DesktopClientService {
return &DesktopClientService{repo: repo}
func NewDesktopClientService(repo repository.DesktopClientRepository, redis *goredis.Client) *DesktopClientService {
return &DesktopClientService{repo: repo, redis: redis}
}
type RegisterDesktopClientRequest struct {
ClientID string `json:"client_id"`
DeviceName string `json:"device_name" binding:"required"`
OS string `json:"os" binding:"required"`
CPUArch string `json:"cpu_arch"`
@@ -35,11 +39,12 @@ type RegisterDesktopClientRequest struct {
}
type HeartbeatDesktopClientRequest struct {
DeviceName *string `json:"device_name"`
OS *string `json:"os"`
CPUArch *string `json:"cpu_arch"`
ClientVersion *string `json:"client_version"`
Channel *string `json:"channel"`
DeviceName *string `json:"device_name"`
OS *string `json:"os"`
CPUArch *string `json:"cpu_arch"`
ClientVersion *string `json:"client_version"`
Channel *string `json:"channel"`
AccountIDs []string `json:"account_ids"`
}
type DesktopClientView struct {
@@ -76,6 +81,10 @@ type HeartbeatDesktopClientResponse struct {
ServerTime time.Time `json:"server_time"`
}
type OfflineDesktopClientResponse struct {
ServerTime time.Time `json:"server_time"`
}
func HashDesktopClientToken(raw string) []byte {
sum := sha256.Sum256([]byte(raw))
return sum[:]
@@ -91,8 +100,9 @@ func (s *DesktopClientService) Register(ctx context.Context, actor auth.Actor, r
return nil, response.ErrInternal(50071, "desktop_client_token_failed", "failed to generate desktop client token")
}
clientID := resolveRegisterClientID(req.ClientID)
client, err := s.repo.Register(ctx, repository.RegisterDesktopClientParams{
ID: uuid.New(),
ID: clientID,
TenantID: actor.TenantID,
WorkspaceID: actor.PrimaryWorkspaceID,
UserID: actor.UserID,
@@ -107,6 +117,8 @@ func (s *DesktopClientService) Register(ctx context.Context, actor auth.Actor, r
return nil, response.ErrInternal(50072, "desktop_client_register_failed", "failed to persist desktop client")
}
markDesktopClientPresent(ctx, s.redis, client.ID)
return &RegisterDesktopClientResponse{
ClientID: client.ID.String(),
ClientToken: rawToken,
@@ -137,6 +149,8 @@ func (s *DesktopClientService) Rotate(ctx context.Context, client *repository.De
return nil, response.ErrInternal(50074, "desktop_client_rotate_failed", "failed to rotate desktop client token")
}
markDesktopClientPresent(ctx, s.redis, updated.ID)
return &RotateDesktopClientResponse{
ClientToken: rawToken,
ExpiresAt: time.Now().Add(DesktopClientTokenTTL).Unix(),
@@ -165,6 +179,9 @@ func (s *DesktopClientService) Heartbeat(ctx context.Context, client *repository
return nil, response.ErrInternal(50075, "desktop_client_heartbeat_failed", "failed to persist desktop client heartbeat")
}
markDesktopClientPresent(ctx, s.redis, updated.ID)
markDesktopAccountsPresent(ctx, s.redis, updated.ID, parseDesktopAccountIDs(req.AccountIDs))
return &HeartbeatDesktopClientResponse{
Client: buildDesktopClientView(updated),
ServerTime: time.Now().UTC(),
@@ -184,10 +201,24 @@ func (s *DesktopClientService) Revoke(ctx context.Context, client *repository.De
return nil, response.ErrInternal(50076, "desktop_client_revoke_failed", "failed to revoke desktop client")
}
clearDesktopClientPresenceState(ctx, s.redis, updated.ID)
view := buildDesktopClientView(updated)
return &view, nil
}
func (s *DesktopClientService) Offline(ctx context.Context, client *repository.DesktopClient) (*OfflineDesktopClientResponse, error) {
if client == nil {
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
}
clearDesktopClientPresenceState(ctx, s.redis, client.ID)
return &OfflineDesktopClientResponse{
ServerTime: time.Now().UTC(),
}, nil
}
func newDesktopClientToken() (string, []byte, error) {
raw := make([]byte, 32)
if _, err := rand.Read(raw); err != nil {
@@ -227,3 +258,27 @@ func trimmedStringPtr(value string) *string {
trimmed := value
return &trimmed
}
func parseDesktopAccountIDs(values []string) []uuid.UUID {
if len(values) == 0 {
return nil
}
accountIDs := make([]uuid.UUID, 0, len(values))
for _, value := range values {
parsed, err := uuid.Parse(strings.TrimSpace(value))
if err != nil {
continue
}
accountIDs = append(accountIDs, parsed)
}
return accountIDs
}
func resolveRegisterClientID(value string) uuid.UUID {
parsed, err := uuid.Parse(strings.TrimSpace(value))
if err == nil && parsed != uuid.Nil {
return parsed
}
return uuid.New()
}