feat: implement browser extension for media publishing and add backend support for media management
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,139 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/bootstrap"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/response"
|
||||
"github.com/geo-platform/tenant-api/internal/tenant/app"
|
||||
)
|
||||
|
||||
type MediaHandler struct {
|
||||
svc *app.MediaService
|
||||
}
|
||||
|
||||
func NewMediaHandler(a *bootstrap.App) *MediaHandler {
|
||||
return &MediaHandler{svc: app.NewMediaService(a.DB)}
|
||||
}
|
||||
|
||||
func (h *MediaHandler) ListPlatforms(c *gin.Context) {
|
||||
data, err := h.svc.ListPlatforms(c.Request.Context())
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) ListPlatformAccounts(c *gin.Context) {
|
||||
data, err := h.svc.ListPlatformAccounts(c.Request.Context())
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) DeletePlatformAccount(c *gin.Context) {
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "platform account id must be a number"))
|
||||
return
|
||||
}
|
||||
if err := h.svc.DeletePlatformAccount(c.Request.Context(), id); err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) CreatePluginSession(c *gin.Context) {
|
||||
var req app.CreatePluginSessionRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
||||
return
|
||||
}
|
||||
data, err := h.svc.CreatePluginSession(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) RegisterPluginInstallation(c *gin.Context) {
|
||||
var req app.RegisterPluginInstallationRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
||||
return
|
||||
}
|
||||
data, err := h.svc.RegisterPluginInstallation(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) CreatePublishBatch(c *gin.Context) {
|
||||
articleID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "article id must be a number"))
|
||||
return
|
||||
}
|
||||
var req app.CreatePublishBatchRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
||||
return
|
||||
}
|
||||
data, err := h.svc.CreatePublishBatch(c.Request.Context(), articleID, req)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.SuccessWithStatus(c, 201, data)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) ListPublishRecords(c *gin.Context) {
|
||||
articleID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_id", "article id must be a number"))
|
||||
return
|
||||
}
|
||||
data, err := h.svc.ListArticlePublishRecords(c.Request.Context(), articleID)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) PluginBindCallback(c *gin.Context) {
|
||||
var req app.PluginBindCallbackRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
||||
return
|
||||
}
|
||||
data, err := h.svc.HandleBindCallback(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *MediaHandler) PluginPublishCallback(c *gin.Context) {
|
||||
var req app.PluginPublishCallbackRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, response.ErrBadRequest(40001, "invalid_params", err.Error()))
|
||||
return
|
||||
}
|
||||
data, err := h.svc.HandlePublishCallback(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
@@ -12,6 +12,11 @@ func RegisterRoutes(app *bootstrap.App) {
|
||||
authAPI.POST("/login", authHandler.Login)
|
||||
authAPI.POST("/refresh", authHandler.Refresh)
|
||||
|
||||
pluginHandler := NewMediaHandler(app)
|
||||
callbacks := app.Engine.Group("/api/callback/plugin")
|
||||
callbacks.POST("/bind", pluginHandler.PluginBindCallback)
|
||||
callbacks.POST("/publish", pluginHandler.PluginPublishCallback)
|
||||
|
||||
protected := app.Engine.Group("/api")
|
||||
protected.Use(auth.Middleware(app.JWT, app.Sessions))
|
||||
protected.Use(middleware.TenantScope())
|
||||
@@ -47,8 +52,17 @@ func RegisterRoutes(app *bootstrap.App) {
|
||||
articles.PUT("/:id", artHandler.Update)
|
||||
articles.GET("/:id/stream", artHandler.Stream)
|
||||
articles.GET("/:id/versions", artHandler.Versions)
|
||||
articles.GET("/:id/publish-records", pluginHandler.ListPublishRecords)
|
||||
articles.POST("/:id/publish-batches", pluginHandler.CreatePublishBatch)
|
||||
articles.DELETE("/:id", artHandler.Delete)
|
||||
|
||||
media := protected.Group("/tenant/media")
|
||||
media.GET("/platforms", pluginHandler.ListPlatforms)
|
||||
media.GET("/platform-accounts", pluginHandler.ListPlatformAccounts)
|
||||
media.POST("/plugin-installations/register", pluginHandler.RegisterPluginInstallation)
|
||||
media.POST("/plugin-sessions", pluginHandler.CreatePluginSession)
|
||||
media.DELETE("/platform-accounts/:id", pluginHandler.DeletePlatformAccount)
|
||||
|
||||
brands := protected.Group("/tenant/brands")
|
||||
brandHandler := NewBrandHandler(app)
|
||||
brands.GET("", brandHandler.List)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
DROP TABLE IF EXISTS publish_records;
|
||||
DROP TABLE IF EXISTS publish_batches;
|
||||
DROP TABLE IF EXISTS plugin_sessions;
|
||||
DROP TABLE IF EXISTS plugin_installations;
|
||||
DROP TABLE IF EXISTS platform_accounts;
|
||||
DROP TABLE IF EXISTS media_platforms;
|
||||
@@ -0,0 +1,137 @@
|
||||
CREATE TABLE media_platforms (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
platform_id VARCHAR(64) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
category VARCHAR(32) NOT NULL DEFAULT 'ugc',
|
||||
short_name VARCHAR(16) NOT NULL,
|
||||
accent_color VARCHAR(16) NOT NULL DEFAULT '#1677ff',
|
||||
login_url TEXT,
|
||||
logo_url TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
sort_order INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_media_platforms_platform_id ON media_platforms(platform_id);
|
||||
CREATE INDEX idx_media_platforms_status_sort ON media_platforms(status, sort_order, id);
|
||||
|
||||
INSERT INTO media_platforms (platform_id, name, category, short_name, accent_color, login_url, sort_order)
|
||||
VALUES
|
||||
('toutiaohao', '头条号', 'news', '头', '#f5222d', 'https://mp.toutiao.com/auth/page/login', 10),
|
||||
('baijiahao', '百家号', 'news', '百', '#31445a', 'https://baijiahao.baidu.com/builder/theme/bjh/login', 20),
|
||||
('sohuhao', '搜狐号', 'news', '搜', '#fa8c16', 'https://mp.sohu.com/mpfe/v4/login', 30),
|
||||
('qiehao', '企鹅号', 'social', 'Q', '#111827', 'https://om.qq.com', 40),
|
||||
('zhihu', '知乎', 'social', '知', '#1677ff', 'https://www.zhihu.com/signin', 50),
|
||||
('wangyihao', '网易号', 'news', '网', '#ff4d4f', 'https://mp.163.com/login.html', 60),
|
||||
('jianshu', '简书', 'social', '简', '#f56a5e', 'https://www.jianshu.com/sign_in', 70),
|
||||
('bilibili', 'bilibili', 'video', 'B', '#eb2f96', 'https://www.bilibili.com', 80),
|
||||
('juejin', '稀土掘金', 'social', '掘', '#1677ff', 'https://juejin.cn/login', 90),
|
||||
('smzdm', '什么值得买', 'social', '值', '#f5222d', 'https://zhiyou.smzdm.com/user/login', 100),
|
||||
('weixin_gzh', '微信公众号', 'social', '微', '#13c26b', 'https://mp.weixin.qq.com/cgi-bin/loginpage', 110),
|
||||
('zol', '中关村在线', 'news', 'Z', '#ff4d4f', 'https://post.zol.com.cn/v2/manage/works/all', 120),
|
||||
('dongchedi', '懂车帝', 'news', '懂', '#fadb14', 'https://mp.dcdapp.com/login', 130);
|
||||
|
||||
CREATE TABLE platform_accounts (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
platform_id VARCHAR(64) NOT NULL REFERENCES media_platforms(platform_id),
|
||||
platform_uid VARCHAR(128) NOT NULL,
|
||||
nickname VARCHAR(255) NOT NULL,
|
||||
avatar_url TEXT,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
metadata_json JSONB,
|
||||
last_check_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_platform_accounts_identity_active
|
||||
ON platform_accounts(tenant_id, platform_id, platform_uid)
|
||||
WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_platform_accounts_tenant_platform ON platform_accounts(tenant_id, platform_id, status, created_at DESC);
|
||||
|
||||
CREATE TABLE plugin_installations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
installation_key VARCHAR(128) NOT NULL,
|
||||
installation_name VARCHAR(255) NOT NULL,
|
||||
browser_name VARCHAR(64),
|
||||
client_version VARCHAR(32),
|
||||
installation_token_hash VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
last_seen_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_plugin_installations_key_active
|
||||
ON plugin_installations(tenant_id, installation_key)
|
||||
WHERE deleted_at IS NULL;
|
||||
CREATE INDEX idx_plugin_installations_tenant_status
|
||||
ON plugin_installations(tenant_id, status, created_at DESC);
|
||||
|
||||
CREATE TABLE plugin_sessions (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
plugin_installation_id BIGINT REFERENCES plugin_installations(id),
|
||||
action_type VARCHAR(20) NOT NULL,
|
||||
resource_type VARCHAR(30),
|
||||
resource_id BIGINT,
|
||||
platform_account_id BIGINT REFERENCES platform_accounts(id),
|
||||
platform_id VARCHAR(64) NOT NULL REFERENCES media_platforms(platform_id),
|
||||
session_token VARCHAR(128) NOT NULL,
|
||||
client_version VARCHAR(32),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
expire_at TIMESTAMPTZ NOT NULL,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_plugin_sessions_token ON plugin_sessions(session_token);
|
||||
CREATE INDEX idx_plugin_sessions_status_expire ON plugin_sessions(status, expire_at);
|
||||
CREATE INDEX idx_plugin_sessions_tenant_action ON plugin_sessions(tenant_id, action_type, created_at DESC);
|
||||
|
||||
CREATE TABLE publish_batches (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
article_id BIGINT NOT NULL REFERENCES articles(id),
|
||||
initiator_user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
publish_type VARCHAR(20) NOT NULL DEFAULT 'publish',
|
||||
cover_asset_url TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_publish_batches_article_created ON publish_batches(article_id, created_at DESC);
|
||||
CREATE INDEX idx_publish_batches_tenant_status ON publish_batches(tenant_id, status, created_at DESC);
|
||||
|
||||
CREATE TABLE publish_records (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
publish_batch_id BIGINT NOT NULL REFERENCES publish_batches(id) ON DELETE CASCADE,
|
||||
article_id BIGINT NOT NULL REFERENCES articles(id),
|
||||
platform_account_id BIGINT NOT NULL REFERENCES platform_accounts(id),
|
||||
platform_id VARCHAR(64) NOT NULL REFERENCES media_platforms(platform_id),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'queued',
|
||||
external_article_id VARCHAR(128),
|
||||
external_article_url TEXT,
|
||||
external_manage_url TEXT,
|
||||
published_at TIMESTAMPTZ,
|
||||
request_payload_json JSONB,
|
||||
response_payload_json JSONB,
|
||||
error_message TEXT,
|
||||
retry_count INT NOT NULL DEFAULT 0,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX uk_publish_records_batch_account ON publish_records(publish_batch_id, platform_account_id);
|
||||
CREATE INDEX idx_publish_records_article_created ON publish_records(article_id, created_at DESC);
|
||||
CREATE INDEX idx_publish_records_status ON publish_records(status, created_at DESC);
|
||||
@@ -0,0 +1,4 @@
|
||||
-- No-op rollback.
|
||||
-- This migration repairs databases that already applied an older variant of
|
||||
-- 20260402140000 without plugin_installations. Dropping these objects on
|
||||
-- rollback would break databases created from the current baseline migration.
|
||||
@@ -0,0 +1,39 @@
|
||||
CREATE TABLE IF NOT EXISTS plugin_installations (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
tenant_id BIGINT NOT NULL REFERENCES tenants(id),
|
||||
user_id BIGINT NOT NULL REFERENCES users(id),
|
||||
installation_key VARCHAR(128) NOT NULL,
|
||||
installation_name VARCHAR(255) NOT NULL,
|
||||
browser_name VARCHAR(64),
|
||||
client_version VARCHAR(32),
|
||||
installation_token_hash VARCHAR(128) NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'active',
|
||||
last_seen_at TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS uk_plugin_installations_key_active
|
||||
ON plugin_installations(tenant_id, installation_key)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_plugin_installations_tenant_status
|
||||
ON plugin_installations(tenant_id, status, created_at DESC);
|
||||
|
||||
ALTER TABLE plugin_sessions
|
||||
ADD COLUMN IF NOT EXISTS plugin_installation_id BIGINT;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1
|
||||
FROM pg_constraint
|
||||
WHERE conname = 'plugin_sessions_plugin_installation_id_fkey'
|
||||
) THEN
|
||||
ALTER TABLE plugin_sessions
|
||||
ADD CONSTRAINT plugin_sessions_plugin_installation_id_fkey
|
||||
FOREIGN KEY (plugin_installation_id)
|
||||
REFERENCES plugin_installations(id);
|
||||
END IF;
|
||||
END $$;
|
||||
Reference in New Issue
Block a user