|
|
|
@@ -137,15 +137,35 @@ type CreateEnterpriseSiteConnectionRequest struct {
|
|
|
|
|
DefaultScode *string `json:"default_scode"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type EnterpriseSitePatchString struct {
|
|
|
|
|
Set bool
|
|
|
|
|
Value *string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (v *EnterpriseSitePatchString) UnmarshalJSON(data []byte) error {
|
|
|
|
|
v.Set = true
|
|
|
|
|
v.Value = nil
|
|
|
|
|
trimmed := bytes.TrimSpace(data)
|
|
|
|
|
if bytes.Equal(trimmed, []byte("null")) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
var value string
|
|
|
|
|
if err := json.Unmarshal(trimmed, &value); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
v.Value = &value
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type UpdateEnterpriseSiteConnectionRequest struct {
|
|
|
|
|
Name *string `json:"name"`
|
|
|
|
|
SiteURL *string `json:"site_url"`
|
|
|
|
|
AppID *string `json:"appid"`
|
|
|
|
|
Secret *string `json:"secret"`
|
|
|
|
|
DefaultAcode *string `json:"default_acode"`
|
|
|
|
|
DefaultMcode *string `json:"default_mcode"`
|
|
|
|
|
DefaultScode *string `json:"default_scode"`
|
|
|
|
|
Status *string `json:"status"`
|
|
|
|
|
Name EnterpriseSitePatchString `json:"name"`
|
|
|
|
|
SiteURL EnterpriseSitePatchString `json:"site_url"`
|
|
|
|
|
AppID EnterpriseSitePatchString `json:"appid"`
|
|
|
|
|
Secret EnterpriseSitePatchString `json:"secret"`
|
|
|
|
|
DefaultAcode EnterpriseSitePatchString `json:"default_acode"`
|
|
|
|
|
DefaultMcode EnterpriseSitePatchString `json:"default_mcode"`
|
|
|
|
|
DefaultScode EnterpriseSitePatchString `json:"default_scode"`
|
|
|
|
|
Status EnterpriseSitePatchString `json:"status"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type PublishEnterpriseSiteArticleRequest struct {
|
|
|
|
@@ -219,7 +239,7 @@ func (s *EnterpriseSiteService) List(ctx context.Context) ([]EnterpriseSiteConne
|
|
|
|
|
actor := auth.MustActor(ctx)
|
|
|
|
|
workspaceID := auth.CurrentWorkspaceID(ctx)
|
|
|
|
|
if actor.TenantID == 0 || workspaceID == 0 {
|
|
|
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
|
|
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "缺少工作区上下文")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rows, err := s.pool.Query(ctx, `
|
|
|
|
@@ -292,7 +312,7 @@ func (s *EnterpriseSiteService) List(ctx context.Context) ([]EnterpriseSiteConne
|
|
|
|
|
ORDER BY c.created_at DESC, c.id DESC
|
|
|
|
|
`, actor.TenantID, workspaceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50220, "enterprise_site_query_failed", "failed to list enterprise sites")
|
|
|
|
|
return nil, response.ErrInternal(50220, "enterprise_site_query_failed", "企业站点列表读取失败")
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
@@ -300,7 +320,7 @@ func (s *EnterpriseSiteService) List(ctx context.Context) ([]EnterpriseSiteConne
|
|
|
|
|
for rows.Next() {
|
|
|
|
|
record, latest, scanErr := scanEnterpriseSiteConnectionRow(rows)
|
|
|
|
|
if scanErr != nil {
|
|
|
|
|
return nil, response.ErrInternal(50220, "enterprise_site_scan_failed", "failed to parse enterprise site")
|
|
|
|
|
return nil, response.ErrInternal(50220, "enterprise_site_scan_failed", "企业站点数据解析失败")
|
|
|
|
|
}
|
|
|
|
|
item := enterpriseSiteConnectionResponse(record)
|
|
|
|
|
item.LatestRecord = latest
|
|
|
|
@@ -308,7 +328,7 @@ func (s *EnterpriseSiteService) List(ctx context.Context) ([]EnterpriseSiteConne
|
|
|
|
|
items = append(items, item)
|
|
|
|
|
}
|
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50220, "enterprise_site_scan_failed", "failed to iterate enterprise sites")
|
|
|
|
|
return nil, response.ErrInternal(50220, "enterprise_site_scan_failed", "企业站点数据解析失败")
|
|
|
|
|
}
|
|
|
|
|
return items, nil
|
|
|
|
|
}
|
|
|
|
@@ -317,7 +337,7 @@ func (s *EnterpriseSiteService) Create(ctx context.Context, req CreateEnterprise
|
|
|
|
|
actor := auth.MustActor(ctx)
|
|
|
|
|
workspaceID := auth.CurrentWorkspaceID(ctx)
|
|
|
|
|
if actor.TenantID == 0 || workspaceID == 0 || actor.UserID == 0 {
|
|
|
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
|
|
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "缺少工作区上下文")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
normalized, err := normalizeCreateEnterpriseSiteConnectionRequest(req)
|
|
|
|
@@ -326,7 +346,7 @@ func (s *EnterpriseSiteService) Create(ctx context.Context, req CreateEnterprise
|
|
|
|
|
}
|
|
|
|
|
ciphertext, err := encryptEnterpriseSiteCredential(normalized.Secret, s.credentialKeyMaterial())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50221, "enterprise_site_secret_encrypt_failed", "failed to store enterprise site credential")
|
|
|
|
|
return nil, response.ErrInternal(50221, "enterprise_site_secret_encrypt_failed", "企业站点凭证保存失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var id int64
|
|
|
|
@@ -350,7 +370,10 @@ func (s *EnterpriseSiteService) Create(ctx context.Context, req CreateEnterprise
|
|
|
|
|
normalized.DefaultMcode,
|
|
|
|
|
normalized.DefaultScode,
|
|
|
|
|
).Scan(&id); err != nil {
|
|
|
|
|
return nil, response.ErrConflict(40941, "enterprise_site_already_exists", "enterprise site connection already exists")
|
|
|
|
|
if isUniqueConstraintError(err, "uk_enterprise_site_connections_active_site") {
|
|
|
|
|
return nil, response.ErrConflict(40941, "enterprise_site_already_exists", "该站点域名已被其他企业站点占用")
|
|
|
|
|
}
|
|
|
|
|
return nil, response.ErrInternal(50232, "enterprise_site_create_failed", "企业站点创建失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return s.Get(ctx, id)
|
|
|
|
@@ -360,65 +383,96 @@ func (s *EnterpriseSiteService) Update(ctx context.Context, id int64, req Update
|
|
|
|
|
actor := auth.MustActor(ctx)
|
|
|
|
|
workspaceID := auth.CurrentWorkspaceID(ctx)
|
|
|
|
|
if actor.TenantID == 0 || workspaceID == 0 {
|
|
|
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
|
|
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "缺少工作区上下文")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
current, err := s.loadConnection(ctx, actor.TenantID, workspaceID, id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if current.Status == "disabled" && req.Status == nil {
|
|
|
|
|
return nil, response.ErrConflict(40942, "enterprise_site_disabled", "enterprise site connection is disabled")
|
|
|
|
|
if current.Status == "disabled" && !req.Status.Set {
|
|
|
|
|
return nil, response.ErrConflict(40942, "enterprise_site_disabled", "企业站点已禁用")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next := *current
|
|
|
|
|
if req.Name != nil {
|
|
|
|
|
name := strings.TrimSpace(*req.Name)
|
|
|
|
|
if req.Name.Set {
|
|
|
|
|
if req.Name.Value == nil {
|
|
|
|
|
return nil, response.ErrBadRequest(40041, "enterprise_site_name_required", "请输入站点名称")
|
|
|
|
|
}
|
|
|
|
|
name := strings.TrimSpace(*req.Name.Value)
|
|
|
|
|
if name == "" {
|
|
|
|
|
return nil, response.ErrBadRequest(40041, "enterprise_site_name_required", "site name is required")
|
|
|
|
|
return nil, response.ErrBadRequest(40041, "enterprise_site_name_required", "请输入站点名称")
|
|
|
|
|
}
|
|
|
|
|
next.Name = name
|
|
|
|
|
}
|
|
|
|
|
if req.SiteURL != nil {
|
|
|
|
|
siteURL, normalizeErr := normalizeEnterpriseSiteURL(*req.SiteURL)
|
|
|
|
|
if req.SiteURL.Set {
|
|
|
|
|
if req.SiteURL.Value == nil {
|
|
|
|
|
return nil, response.ErrBadRequest(40041, "enterprise_site_url_required", "请填写站点域名")
|
|
|
|
|
}
|
|
|
|
|
siteURL, normalizeErr := normalizeEnterpriseSiteURL(*req.SiteURL.Value)
|
|
|
|
|
if normalizeErr != nil {
|
|
|
|
|
return nil, normalizeErr
|
|
|
|
|
}
|
|
|
|
|
next.SiteURL = siteURL
|
|
|
|
|
}
|
|
|
|
|
if req.AppID != nil {
|
|
|
|
|
appid := strings.TrimSpace(*req.AppID)
|
|
|
|
|
if req.AppID.Set {
|
|
|
|
|
if req.AppID.Value == nil {
|
|
|
|
|
return nil, response.ErrBadRequest(40042, "enterprise_site_appid_required", "请输入 AppID")
|
|
|
|
|
}
|
|
|
|
|
appid := strings.TrimSpace(*req.AppID.Value)
|
|
|
|
|
if appid == "" {
|
|
|
|
|
return nil, response.ErrBadRequest(40042, "enterprise_site_appid_required", "appid is required")
|
|
|
|
|
return nil, response.ErrBadRequest(40042, "enterprise_site_appid_required", "请输入 AppID")
|
|
|
|
|
}
|
|
|
|
|
next.AppID = appid
|
|
|
|
|
}
|
|
|
|
|
if req.DefaultAcode != nil {
|
|
|
|
|
next.DefaultAcode = normalizeOptionalEnterpriseCode(*req.DefaultAcode)
|
|
|
|
|
if req.DefaultAcode.Set {
|
|
|
|
|
next.DefaultAcode = nil
|
|
|
|
|
if req.DefaultAcode.Value != nil {
|
|
|
|
|
next.DefaultAcode = normalizeOptionalEnterpriseCode(*req.DefaultAcode.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if req.DefaultMcode != nil {
|
|
|
|
|
next.DefaultMcode = normalizeOptionalEnterpriseCode(*req.DefaultMcode)
|
|
|
|
|
if req.DefaultMcode.Set {
|
|
|
|
|
next.DefaultMcode = nil
|
|
|
|
|
if req.DefaultMcode.Value != nil {
|
|
|
|
|
next.DefaultMcode = normalizeOptionalEnterpriseCode(*req.DefaultMcode.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if req.DefaultScode != nil {
|
|
|
|
|
next.DefaultScode = normalizeOptionalEnterpriseCode(*req.DefaultScode)
|
|
|
|
|
if req.DefaultScode.Set {
|
|
|
|
|
next.DefaultScode = nil
|
|
|
|
|
if req.DefaultScode.Value != nil {
|
|
|
|
|
next.DefaultScode = normalizeOptionalEnterpriseCode(*req.DefaultScode.Value)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if req.Status != nil {
|
|
|
|
|
status := strings.TrimSpace(*req.Status)
|
|
|
|
|
if req.Status.Set {
|
|
|
|
|
if req.Status.Value == nil {
|
|
|
|
|
return nil, response.ErrBadRequest(40043, "invalid_enterprise_site_status", "状态参数不正确")
|
|
|
|
|
}
|
|
|
|
|
status := strings.TrimSpace(*req.Status.Value)
|
|
|
|
|
if !validEnterpriseSiteStatus(status) {
|
|
|
|
|
return nil, response.ErrBadRequest(40043, "invalid_enterprise_site_status", "status is invalid")
|
|
|
|
|
return nil, response.ErrBadRequest(40043, "invalid_enterprise_site_status", "状态参数不正确")
|
|
|
|
|
}
|
|
|
|
|
next.Status = status
|
|
|
|
|
}
|
|
|
|
|
ciphertext := next.CredentialCiphertext
|
|
|
|
|
if req.Secret != nil && strings.TrimSpace(*req.Secret) != "" {
|
|
|
|
|
encrypted, encryptErr := encryptEnterpriseSiteCredential(*req.Secret, s.credentialKeyMaterial())
|
|
|
|
|
if req.Secret.Set && req.Secret.Value != nil && strings.TrimSpace(*req.Secret.Value) != "" {
|
|
|
|
|
encrypted, encryptErr := encryptEnterpriseSiteCredential(*req.Secret.Value, s.credentialKeyMaterial())
|
|
|
|
|
if encryptErr != nil {
|
|
|
|
|
return nil, response.ErrInternal(50221, "enterprise_site_secret_encrypt_failed", "failed to store enterprise site credential")
|
|
|
|
|
return nil, response.ErrInternal(50221, "enterprise_site_secret_encrypt_failed", "企业站点凭证保存失败")
|
|
|
|
|
}
|
|
|
|
|
ciphertext = encrypted
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := s.pool.Exec(ctx, `
|
|
|
|
|
if !validEnterpriseSiteStatus(next.Status) {
|
|
|
|
|
next.Status = "draft"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !strings.EqualFold(next.SiteURL, current.SiteURL) {
|
|
|
|
|
if err := s.ensureEnterpriseSiteURLAvailable(ctx, actor.TenantID, workspaceID, id, next.SiteURL, next.CMSType); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
command, err := s.pool.Exec(ctx, `
|
|
|
|
|
UPDATE enterprise_site_connections
|
|
|
|
|
SET name = $1,
|
|
|
|
|
site_url = $2,
|
|
|
|
@@ -427,8 +481,8 @@ func (s *EnterpriseSiteService) Update(ctx context.Context, id int64, req Update
|
|
|
|
|
default_acode = $5,
|
|
|
|
|
default_mcode = $6,
|
|
|
|
|
default_scode = $7,
|
|
|
|
|
status = $8,
|
|
|
|
|
last_error = CASE WHEN $8 IN ('draft', 'connected') THEN NULL ELSE last_error END,
|
|
|
|
|
status = $8::varchar,
|
|
|
|
|
last_error = CASE WHEN $8::text IN ('draft', 'connected') THEN NULL ELSE last_error END,
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
WHERE id = $9
|
|
|
|
|
AND tenant_id = $10
|
|
|
|
@@ -439,20 +493,51 @@ func (s *EnterpriseSiteService) Update(ctx context.Context, id int64, req Update
|
|
|
|
|
next.SiteURL,
|
|
|
|
|
next.AppID,
|
|
|
|
|
ciphertext,
|
|
|
|
|
next.DefaultAcode,
|
|
|
|
|
next.DefaultMcode,
|
|
|
|
|
next.DefaultScode,
|
|
|
|
|
enterpriseSiteNullableText(next.DefaultAcode),
|
|
|
|
|
enterpriseSiteNullableText(next.DefaultMcode),
|
|
|
|
|
enterpriseSiteNullableText(next.DefaultScode),
|
|
|
|
|
next.Status,
|
|
|
|
|
id,
|
|
|
|
|
actor.TenantID,
|
|
|
|
|
workspaceID,
|
|
|
|
|
); err != nil {
|
|
|
|
|
return nil, response.ErrConflict(40941, "enterprise_site_already_exists", "enterprise site connection already exists")
|
|
|
|
|
)
|
|
|
|
|
if err != nil {
|
|
|
|
|
if isUniqueConstraintError(err, "uk_enterprise_site_connections_active_site") {
|
|
|
|
|
return nil, response.ErrConflict(40941, "enterprise_site_already_exists", "该站点域名已被其他企业站点占用")
|
|
|
|
|
}
|
|
|
|
|
appErr := response.ErrInternal(50233, "enterprise_site_update_failed", "企业站点更新失败,请检查站点信息后重试")
|
|
|
|
|
appErr.Cause = err
|
|
|
|
|
return nil, appErr
|
|
|
|
|
}
|
|
|
|
|
if command.RowsAffected() == 0 {
|
|
|
|
|
return nil, response.ErrNotFound(40441, "enterprise_site_not_found", "企业站点连接不存在或已删除")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return s.Get(ctx, id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *EnterpriseSiteService) ensureEnterpriseSiteURLAvailable(ctx context.Context, tenantID, workspaceID, currentID int64, siteURL, cmsType string) error {
|
|
|
|
|
var existingID int64
|
|
|
|
|
err := s.pool.QueryRow(ctx, `
|
|
|
|
|
SELECT id
|
|
|
|
|
FROM enterprise_site_connections
|
|
|
|
|
WHERE tenant_id = $1
|
|
|
|
|
AND workspace_id = $2
|
|
|
|
|
AND lower(site_url) = lower($3)
|
|
|
|
|
AND cms_type = $4
|
|
|
|
|
AND deleted_at IS NULL
|
|
|
|
|
AND id <> $5
|
|
|
|
|
LIMIT 1
|
|
|
|
|
`, tenantID, workspaceID, siteURL, cmsType, currentID).Scan(&existingID)
|
|
|
|
|
if err == nil {
|
|
|
|
|
return response.ErrConflict(40941, "enterprise_site_already_exists", "该站点域名已被其他企业站点占用")
|
|
|
|
|
}
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return response.ErrInternal(50233, "enterprise_site_update_failed", "企业站点连接重复校验失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (s *EnterpriseSiteService) Get(ctx context.Context, id int64) (*EnterpriseSiteConnectionResponse, error) {
|
|
|
|
|
actor := auth.MustActor(ctx)
|
|
|
|
|
workspaceID := auth.CurrentWorkspaceID(ctx)
|
|
|
|
@@ -477,10 +562,10 @@ func (s *EnterpriseSiteService) Delete(ctx context.Context, id int64) error {
|
|
|
|
|
AND deleted_at IS NULL
|
|
|
|
|
`, id, actor.TenantID, workspaceID)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return response.ErrInternal(50222, "enterprise_site_delete_failed", "failed to delete enterprise site")
|
|
|
|
|
return response.ErrInternal(50222, "enterprise_site_delete_failed", "企业站点删除失败")
|
|
|
|
|
}
|
|
|
|
|
if command.RowsAffected() == 0 {
|
|
|
|
|
return response.ErrNotFound(40441, "enterprise_site_not_found", "enterprise site connection not found")
|
|
|
|
|
return response.ErrNotFound(40441, "enterprise_site_not_found", "企业站点连接不存在或已删除")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
@@ -514,7 +599,7 @@ func (s *EnterpriseSiteService) Ping(ctx context.Context, id int64) (*Enterprise
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
WHERE id = $3 AND tenant_id = $4 AND workspace_id = $5 AND deleted_at IS NULL
|
|
|
|
|
`, pluginVersion, siteName, connection.ID, actor.TenantID, workspaceID); err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50223, "enterprise_site_ping_update_failed", "failed to update enterprise site ping")
|
|
|
|
|
return nil, response.ErrInternal(50223, "enterprise_site_ping_update_failed", "企业站点连接状态更新失败")
|
|
|
|
|
}
|
|
|
|
|
_ = raw
|
|
|
|
|
return capability, nil
|
|
|
|
@@ -537,12 +622,12 @@ func (s *EnterpriseSiteService) SyncCategories(ctx context.Context, id int64) ([
|
|
|
|
|
return nil, response.ErrBadRequest(40045, "enterprise_site_category_sync_failed", sanitizeEnterpriseSiteError(err))
|
|
|
|
|
}
|
|
|
|
|
if len(items) == 0 {
|
|
|
|
|
return nil, response.ErrBadRequest(40046, "enterprise_site_categories_empty", "CMS did not return any publishable categories")
|
|
|
|
|
return nil, response.ErrBadRequest(40046, "enterprise_site_categories_empty", "CMS 未返回可发布栏目")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
tx, err := s.pool.Begin(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_category_sync_begin_failed", "failed to sync enterprise site categories")
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_category_sync_begin_failed", "企业站点栏目同步失败")
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback(ctx)
|
|
|
|
|
|
|
|
|
@@ -550,13 +635,13 @@ func (s *EnterpriseSiteService) SyncCategories(ctx context.Context, id int64) ([
|
|
|
|
|
DELETE FROM enterprise_site_categories
|
|
|
|
|
WHERE tenant_id = $1 AND workspace_id = $2 AND connection_id = $3
|
|
|
|
|
`, actor.TenantID, workspaceID, connection.ID); err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_category_clear_failed", "failed to sync enterprise site categories")
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_category_clear_failed", "企业站点栏目同步失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, item := range items {
|
|
|
|
|
rawJSON, marshalErr := json.Marshal(item.Raw)
|
|
|
|
|
if marshalErr != nil {
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_category_payload_failed", "failed to sync enterprise site categories")
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_category_payload_failed", "企业站点栏目同步失败")
|
|
|
|
|
}
|
|
|
|
|
if _, err := tx.Exec(ctx, `
|
|
|
|
|
INSERT INTO enterprise_site_categories (
|
|
|
|
@@ -573,7 +658,7 @@ func (s *EnterpriseSiteService) SyncCategories(ctx context.Context, id int64) ([
|
|
|
|
|
item.Slug,
|
|
|
|
|
rawJSON,
|
|
|
|
|
); err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_category_insert_failed", "failed to sync enterprise site categories")
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_category_insert_failed", "企业站点栏目同步失败")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -585,11 +670,11 @@ func (s *EnterpriseSiteService) SyncCategories(ctx context.Context, id int64) ([
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
WHERE id = $1 AND tenant_id = $2 AND workspace_id = $3 AND deleted_at IS NULL
|
|
|
|
|
`, connection.ID, actor.TenantID, workspaceID); err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_sync_update_failed", "failed to update enterprise site sync state")
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_sync_update_failed", "企业站点同步状态更新失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_category_sync_commit_failed", "failed to sync enterprise site categories")
|
|
|
|
|
return nil, response.ErrInternal(50224, "enterprise_site_category_sync_commit_failed", "企业站点栏目同步失败")
|
|
|
|
|
}
|
|
|
|
|
return s.Categories(ctx, id)
|
|
|
|
|
}
|
|
|
|
@@ -609,7 +694,7 @@ func (s *EnterpriseSiteService) Categories(ctx context.Context, id int64) ([]Ent
|
|
|
|
|
ORDER BY COALESCE(parent_remote_id, ''), name ASC, id ASC
|
|
|
|
|
`, actor.TenantID, workspaceID, id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50225, "enterprise_site_category_query_failed", "failed to list enterprise site categories")
|
|
|
|
|
return nil, response.ErrInternal(50225, "enterprise_site_category_query_failed", "企业站点栏目读取失败")
|
|
|
|
|
}
|
|
|
|
|
defer rows.Close()
|
|
|
|
|
|
|
|
|
@@ -618,7 +703,7 @@ func (s *EnterpriseSiteService) Categories(ctx context.Context, id int64) ([]Ent
|
|
|
|
|
var item EnterpriseSiteCategoryResponse
|
|
|
|
|
var raw []byte
|
|
|
|
|
if err := rows.Scan(&item.ID, &item.RemoteID, &item.ParentRemoteID, &item.Name, &item.Slug, &raw, &item.SyncedAt); err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50225, "enterprise_site_category_scan_failed", "failed to parse enterprise site category")
|
|
|
|
|
return nil, response.ErrInternal(50225, "enterprise_site_category_scan_failed", "企业站点栏目解析失败")
|
|
|
|
|
}
|
|
|
|
|
if len(raw) > 0 {
|
|
|
|
|
item.Raw = map[string]any{}
|
|
|
|
@@ -627,7 +712,7 @@ func (s *EnterpriseSiteService) Categories(ctx context.Context, id int64) ([]Ent
|
|
|
|
|
items = append(items, item)
|
|
|
|
|
}
|
|
|
|
|
if err := rows.Err(); err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50225, "enterprise_site_category_scan_failed", "failed to iterate enterprise site categories")
|
|
|
|
|
return nil, response.ErrInternal(50225, "enterprise_site_category_scan_failed", "企业站点栏目解析失败")
|
|
|
|
|
}
|
|
|
|
|
return items, nil
|
|
|
|
|
}
|
|
|
|
@@ -640,7 +725,7 @@ func (s *EnterpriseSiteService) PublishArticle(ctx context.Context, connectionID
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if actor.TenantID == 0 || workspaceID == 0 || actor.UserID == 0 {
|
|
|
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "workspace context is required")
|
|
|
|
|
return nil, response.ErrUnauthorized(40132, "workspace_scope_missing", "缺少工作区上下文")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
connection, err := s.loadConnection(ctx, actor.TenantID, workspaceID, connectionID)
|
|
|
|
@@ -648,7 +733,7 @@ func (s *EnterpriseSiteService) PublishArticle(ctx context.Context, connectionID
|
|
|
|
|
return nil, err
|
|
|
|
|
}
|
|
|
|
|
if connection.Status == "disabled" {
|
|
|
|
|
return nil, response.ErrConflict(40942, "enterprise_site_disabled", "enterprise site connection is disabled")
|
|
|
|
|
return nil, response.ErrConflict(40942, "enterprise_site_disabled", "企业站点已禁用")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
article, err := s.loadPublishArticle(ctx, actor.TenantID, brandID, req.ArticleID, req.ArticleVersionID)
|
|
|
|
@@ -660,7 +745,7 @@ func (s *EnterpriseSiteService) PublishArticle(ctx context.Context, connectionID
|
|
|
|
|
categoryID = strings.TrimSpace(enterpriseSiteStringPointerValue(connection.DefaultScode))
|
|
|
|
|
}
|
|
|
|
|
if categoryID == "" {
|
|
|
|
|
return nil, response.ErrBadRequest(40047, "enterprise_site_category_required", "category_id is required")
|
|
|
|
|
return nil, response.ErrBadRequest(40047, "enterprise_site_category_required", "请选择企业站点栏目")
|
|
|
|
|
}
|
|
|
|
|
if err := s.ensureCategoryExists(ctx, actor.TenantID, workspaceID, connection.ID, categoryID); err != nil {
|
|
|
|
|
return nil, err
|
|
|
|
@@ -678,12 +763,12 @@ func (s *EnterpriseSiteService) PublishArticle(ctx context.Context, connectionID
|
|
|
|
|
if contentHTML == "" {
|
|
|
|
|
contentHTML, err = markdownToEnterpriseSiteHTML(markdownContent)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50226, "enterprise_site_markdown_render_failed", "failed to render article content")
|
|
|
|
|
return nil, response.ErrInternal(50226, "enterprise_site_markdown_render_failed", "文章内容渲染失败")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
contentHTML = enterpriseSiteStripLeadingTitleHeadingHTML(contentHTML, article.Title)
|
|
|
|
|
if strings.TrimSpace(contentHTML) == "" {
|
|
|
|
|
return nil, response.ErrBadRequest(40048, "enterprise_site_article_empty", "article content is empty")
|
|
|
|
|
return nil, response.ErrBadRequest(40048, "enterprise_site_article_empty", "文章内容为空")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
effectiveVersionID, err := s.compliance.ResolvePublishableVersion(ctx, actor.TenantID, article.ID, article.CurrentVersionID)
|
|
|
|
@@ -705,9 +790,9 @@ func (s *EnterpriseSiteService) PublishArticle(ctx context.Context, connectionID
|
|
|
|
|
if gate != nil {
|
|
|
|
|
switch gate.Decision {
|
|
|
|
|
case sharedcompliance.GateDecisionBlock:
|
|
|
|
|
return nil, compliancePublishError(response.ErrConflict(41001, "compliance_blocked", "content compliance blocked this publish request"), gate)
|
|
|
|
|
return nil, compliancePublishError(response.ErrConflict(41001, "compliance_blocked", "内容合规检查未通过,无法发布"), gate)
|
|
|
|
|
case sharedcompliance.GateDecisionNeedsAck:
|
|
|
|
|
return nil, compliancePublishError(response.ErrConflict(41002, "compliance_needs_ack", "content compliance requires acknowledgement before publishing"), gate)
|
|
|
|
|
return nil, compliancePublishError(response.ErrConflict(41002, "compliance_needs_ack", "发布前需要先确认合规提示"), gate)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -753,7 +838,7 @@ func (s *EnterpriseSiteService) PublishArticle(ctx context.Context, connectionID
|
|
|
|
|
"cover_image_asset_id": coverImageAssetID,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50227, "enterprise_site_publish_payload_failed", "failed to create publish record")
|
|
|
|
|
return nil, response.ErrInternal(50227, "enterprise_site_publish_payload_failed", "企业站点发布记录创建失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
publishBatchID, publishRecordID, err := s.createEnterprisePublishRecord(ctx, actor, article, connection, publishType, requestJSON, effectiveVersionID, req.AckRecordID, gate)
|
|
|
|
@@ -779,7 +864,7 @@ func (s *EnterpriseSiteService) PublishArticle(ctx context.Context, connectionID
|
|
|
|
|
|
|
|
|
|
rawJSON, err := json.Marshal(raw)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return nil, response.ErrInternal(50228, "enterprise_site_publish_response_failed", "failed to persist CMS response")
|
|
|
|
|
return nil, response.ErrInternal(50228, "enterprise_site_publish_response_failed", "CMS 响应保存失败")
|
|
|
|
|
}
|
|
|
|
|
externalID := normalizeStringPointer(&result.RemoteID)
|
|
|
|
|
externalURL := normalizeStringPointer(&result.URL)
|
|
|
|
@@ -818,7 +903,7 @@ func (s *EnterpriseSiteService) createEnterprisePublishRecord(
|
|
|
|
|
) (int64, int64, error) {
|
|
|
|
|
tx, err := s.pool.Begin(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return 0, 0, response.ErrInternal(50227, "enterprise_site_publish_begin_failed", "failed to create publish record")
|
|
|
|
|
return 0, 0, response.ErrInternal(50227, "enterprise_site_publish_begin_failed", "企业站点发布记录创建失败")
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback(ctx)
|
|
|
|
|
|
|
|
|
@@ -828,7 +913,7 @@ func (s *EnterpriseSiteService) createEnterprisePublishRecord(
|
|
|
|
|
VALUES ($1, $2, $3, 'publishing', $4, $5)
|
|
|
|
|
RETURNING id
|
|
|
|
|
`, actor.TenantID, article.ID, actor.UserID, publishType, normalizeStringPointer(article.CoverAssetURL)).Scan(&publishBatchID); err != nil {
|
|
|
|
|
return 0, 0, response.ErrInternal(50227, "enterprise_site_publish_batch_failed", "failed to create publish record")
|
|
|
|
|
return 0, 0, response.ErrInternal(50227, "enterprise_site_publish_batch_failed", "企业站点发布记录创建失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var publishRecordID int64
|
|
|
|
@@ -840,7 +925,7 @@ func (s *EnterpriseSiteService) createEnterprisePublishRecord(
|
|
|
|
|
VALUES ($1, $2, $3, NULL, $4, 'publishing', $5, 'enterprise_site', $6)
|
|
|
|
|
RETURNING id
|
|
|
|
|
`, actor.TenantID, publishBatchID, article.ID, connection.CMSType, requestJSON, connection.ID).Scan(&publishRecordID); err != nil {
|
|
|
|
|
return 0, 0, response.ErrInternal(50227, "enterprise_site_publish_record_failed", "failed to create publish record")
|
|
|
|
|
return 0, 0, response.ErrInternal(50227, "enterprise_site_publish_record_failed", "企业站点发布记录创建失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if _, err := tx.Exec(ctx, `
|
|
|
|
@@ -848,7 +933,7 @@ func (s *EnterpriseSiteService) createEnterprisePublishRecord(
|
|
|
|
|
SET publish_status = 'publishing', updated_at = NOW()
|
|
|
|
|
WHERE id = $1 AND tenant_id = $2 AND deleted_at IS NULL
|
|
|
|
|
`, article.ID, actor.TenantID); err != nil {
|
|
|
|
|
return 0, 0, response.ErrInternal(50227, "enterprise_site_article_publish_status_failed", "failed to update article publish status")
|
|
|
|
|
return 0, 0, response.ErrInternal(50227, "enterprise_site_article_publish_status_failed", "文章发布状态更新失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := s.consumeEnterprisePublishAckTx(ctx, tx, ackRecordID, gate, actor.TenantID, article.ID, articleVersionID); err != nil {
|
|
|
|
@@ -856,7 +941,7 @@ func (s *EnterpriseSiteService) createEnterprisePublishRecord(
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
|
|
|
return 0, 0, response.ErrInternal(50227, "enterprise_site_publish_commit_failed", "failed to create publish record")
|
|
|
|
|
return 0, 0, response.ErrInternal(50227, "enterprise_site_publish_commit_failed", "企业站点发布记录创建失败")
|
|
|
|
|
}
|
|
|
|
|
invalidateArticleCaches(ctx, s.cache, actor.TenantID, &article.ID)
|
|
|
|
|
return publishBatchID, publishRecordID, nil
|
|
|
|
@@ -875,7 +960,7 @@ func (s *EnterpriseSiteService) consumeEnterprisePublishAckTx(
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
if gate == nil || gate.Result == nil {
|
|
|
|
|
return response.ErrBadRequest(41003, "compliance_ack_mismatch", "ack record cannot be consumed without a matching compliance result")
|
|
|
|
|
return response.ErrBadRequest(41003, "compliance_ack_mismatch", "合规确认记录与当前文章不匹配")
|
|
|
|
|
}
|
|
|
|
|
consumed, err := s.compliance.ConsumeAckTx(ctx, tx, *ackRecordID, tenantcompliance.ConsumeAckQuery{
|
|
|
|
|
TenantID: tenantID,
|
|
|
|
@@ -887,10 +972,10 @@ func (s *EnterpriseSiteService) consumeEnterprisePublishAckTx(
|
|
|
|
|
ConsumedByJobID: nil,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return response.ErrInternal(50227, "enterprise_site_publish_ack_failed", "failed to consume compliance acknowledgement")
|
|
|
|
|
return response.ErrInternal(50227, "enterprise_site_publish_ack_failed", "合规确认处理失败")
|
|
|
|
|
}
|
|
|
|
|
if !consumed {
|
|
|
|
|
return response.ErrBadRequest(41003, "compliance_ack_mismatch", "ack record is invalid or expired")
|
|
|
|
|
return response.ErrBadRequest(41003, "compliance_ack_mismatch", "合规确认记录无效或已过期")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
@@ -912,7 +997,7 @@ func (s *EnterpriseSiteService) finishEnterprisePublishRecord(
|
|
|
|
|
) error {
|
|
|
|
|
tx, err := s.pool.Begin(ctx)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_publish_finish_begin_failed", "failed to update publish record")
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_publish_finish_begin_failed", "企业站点发布结果更新失败")
|
|
|
|
|
}
|
|
|
|
|
defer tx.Rollback(ctx)
|
|
|
|
|
|
|
|
|
@@ -932,7 +1017,7 @@ func (s *EnterpriseSiteService) finishEnterprisePublishRecord(
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
WHERE id = $7 AND tenant_id = $8
|
|
|
|
|
`, status, externalID, externalURL, nullableJSON(responsePayload), publishedAt, errorMessage, publishRecordID, tenantID); err != nil {
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_publish_record_update_failed", "failed to update publish record")
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_publish_record_update_failed", "企业站点发布结果更新失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
batchStatus, err := recalculatePublishBatchStatus(ctx, tx, publishBatchID)
|
|
|
|
@@ -944,7 +1029,7 @@ func (s *EnterpriseSiteService) finishEnterprisePublishRecord(
|
|
|
|
|
SET status = $1, updated_at = NOW()
|
|
|
|
|
WHERE id = $2 AND tenant_id = $3
|
|
|
|
|
`, batchStatus, publishBatchID, tenantID); err != nil {
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_publish_batch_update_failed", "failed to update publish batch")
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_publish_batch_update_failed", "企业站点发布批次更新失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
articleStatus := publishBatchStatusToArticleStatus(batchStatus)
|
|
|
|
@@ -953,7 +1038,7 @@ func (s *EnterpriseSiteService) finishEnterprisePublishRecord(
|
|
|
|
|
SET publish_status = $1, updated_at = NOW()
|
|
|
|
|
WHERE id = $2 AND tenant_id = $3 AND deleted_at IS NULL
|
|
|
|
|
`, articleStatus, articleID, tenantID); err != nil {
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_article_publish_update_failed", "failed to update article publish status")
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_article_publish_update_failed", "文章发布状态更新失败")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if status == "success" {
|
|
|
|
@@ -965,12 +1050,12 @@ func (s *EnterpriseSiteService) finishEnterprisePublishRecord(
|
|
|
|
|
updated_at = NOW()
|
|
|
|
|
WHERE id = $1 AND tenant_id = $2 AND workspace_id = $3 AND deleted_at IS NULL
|
|
|
|
|
`, connectionID, tenantID, workspaceID); err != nil {
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_publish_connection_update_failed", "failed to update enterprise site")
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_publish_connection_update_failed", "企业站点连接状态更新失败")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err := tx.Commit(ctx); err != nil {
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_publish_finish_commit_failed", "failed to update publish record")
|
|
|
|
|
return response.ErrInternal(50228, "enterprise_site_publish_finish_commit_failed", "企业站点发布结果更新失败")
|
|
|
|
|
}
|
|
|
|
|
invalidateArticleCaches(ctx, s.cache, tenantID, &articleID)
|
|
|
|
|
return nil
|
|
|
|
@@ -1007,7 +1092,7 @@ func (s *EnterpriseSiteService) loadPublishArticle(ctx context.Context, tenantID
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
return article, response.ErrNotFound(40411, "article_not_found", "article not found")
|
|
|
|
|
}
|
|
|
|
|
return article, response.ErrInternal(50229, "enterprise_site_article_lookup_failed", "failed to load article")
|
|
|
|
|
return article, response.ErrInternal(50229, "enterprise_site_article_lookup_failed", "文章读取失败")
|
|
|
|
|
}
|
|
|
|
|
if err := s.populatePublishArticleContent(ctx, &article, wizardStateJSON, requestedVersionID); err != nil {
|
|
|
|
|
return article, err
|
|
|
|
@@ -1071,7 +1156,7 @@ func (s *EnterpriseSiteService) loadPublishArticleVersionContent(ctx context.Con
|
|
|
|
|
}
|
|
|
|
|
return response.ErrBadRequest(41005, "invalid_article_version", "article_version_id does not belong to this article")
|
|
|
|
|
}
|
|
|
|
|
return response.ErrInternal(50231, "enterprise_site_article_version_reconstruct_failed", "failed to reconstruct article version")
|
|
|
|
|
return response.ErrInternal(50231, "enterprise_site_article_version_reconstruct_failed", "文章版本内容恢复失败")
|
|
|
|
|
}
|
|
|
|
|
if content != nil {
|
|
|
|
|
article.HTMLContent = strings.TrimSpace(enterpriseSiteStringPointerValue(content.HTMLContent))
|
|
|
|
@@ -1094,7 +1179,7 @@ func (s *EnterpriseSiteService) loadPublishArticleVersionMetadata(ctx context.Co
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
return response.ErrBadRequest(41005, "invalid_article_version", "article_version_id does not belong to this article")
|
|
|
|
|
}
|
|
|
|
|
return response.ErrInternal(50231, "enterprise_site_article_version_lookup_failed", "failed to load article version")
|
|
|
|
|
return response.ErrInternal(50231, "enterprise_site_article_version_lookup_failed", "文章版本读取失败")
|
|
|
|
|
}
|
|
|
|
|
if strings.TrimSpace(title) != "" {
|
|
|
|
|
article.Title = strings.TrimSpace(title)
|
|
|
|
@@ -1121,7 +1206,7 @@ func (s *EnterpriseSiteService) loadPublishArticleVersionSnapshotContent(ctx con
|
|
|
|
|
}
|
|
|
|
|
return response.ErrBadRequest(41005, "invalid_article_version", "article_version_id does not belong to this article")
|
|
|
|
|
}
|
|
|
|
|
return response.ErrInternal(50231, "enterprise_site_article_version_snapshot_lookup_failed", "failed to load article version")
|
|
|
|
|
return response.ErrInternal(50231, "enterprise_site_article_version_snapshot_lookup_failed", "文章版本读取失败")
|
|
|
|
|
}
|
|
|
|
|
snapshot = enterpriseSiteNormalizeStoredContent(snapshot)
|
|
|
|
|
if strings.TrimSpace(snapshot) != "" {
|
|
|
|
@@ -1180,10 +1265,10 @@ func (s *EnterpriseSiteService) ensureCategoryExists(ctx context.Context, tenant
|
|
|
|
|
AND remote_id = $4
|
|
|
|
|
)
|
|
|
|
|
`, tenantID, workspaceID, connectionID, categoryID).Scan(&exists); err != nil {
|
|
|
|
|
return response.ErrInternal(50230, "enterprise_site_category_lookup_failed", "failed to validate enterprise site category")
|
|
|
|
|
return response.ErrInternal(50230, "enterprise_site_category_lookup_failed", "企业站点栏目校验失败")
|
|
|
|
|
}
|
|
|
|
|
if !exists {
|
|
|
|
|
return response.ErrBadRequest(40049, "enterprise_site_category_not_found", "category is not synced for this site")
|
|
|
|
|
return response.ErrBadRequest(40049, "enterprise_site_category_not_found", "企业站点栏目不存在,请先同步栏目")
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
@@ -1250,9 +1335,9 @@ func (s *EnterpriseSiteService) loadConnection(ctx context.Context, tenantID, wo
|
|
|
|
|
&item.UpdatedAt,
|
|
|
|
|
); err != nil {
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
return nil, response.ErrNotFound(40441, "enterprise_site_not_found", "enterprise site connection not found")
|
|
|
|
|
return nil, response.ErrNotFound(40441, "enterprise_site_not_found", "企业站点连接不存在或已删除")
|
|
|
|
|
}
|
|
|
|
|
return nil, response.ErrInternal(50220, "enterprise_site_lookup_failed", "failed to load enterprise site")
|
|
|
|
|
return nil, response.ErrInternal(50220, "enterprise_site_lookup_failed", "企业站点连接读取失败")
|
|
|
|
|
}
|
|
|
|
|
return &item, nil
|
|
|
|
|
}
|
|
|
|
@@ -1260,7 +1345,7 @@ func (s *EnterpriseSiteService) loadConnection(ctx context.Context, tenantID, wo
|
|
|
|
|
func (s *EnterpriseSiteService) connectionCredential(connection *enterpriseSiteConnectionRecord) (enterpriseSiteCredential, error) {
|
|
|
|
|
secret, err := decryptEnterpriseSiteCredential(connection.CredentialCiphertext, s.credentialKeyMaterial())
|
|
|
|
|
if err != nil {
|
|
|
|
|
return enterpriseSiteCredential{}, response.ErrInternal(50231, "enterprise_site_secret_decrypt_failed", "failed to read enterprise site credential")
|
|
|
|
|
return enterpriseSiteCredential{}, response.ErrInternal(50231, "enterprise_site_secret_decrypt_failed", "企业站点凭证读取失败")
|
|
|
|
|
}
|
|
|
|
|
return enterpriseSiteCredential{
|
|
|
|
|
SiteURL: connection.SiteURL,
|
|
|
|
@@ -1430,15 +1515,15 @@ func normalizeCreateEnterpriseSiteConnectionRequest(req CreateEnterpriseSiteConn
|
|
|
|
|
}
|
|
|
|
|
cmsType := normalizeEnterpriseCMSType(req.CMSType)
|
|
|
|
|
if cmsType != pbootCMSPlatformID {
|
|
|
|
|
return normalizedEnterpriseSiteConnectionRequest{}, response.ErrBadRequest(40040, "enterprise_site_cms_not_supported", "only PBootCMS is supported in this version")
|
|
|
|
|
return normalizedEnterpriseSiteConnectionRequest{}, response.ErrBadRequest(40040, "enterprise_site_cms_not_supported", "当前版本暂不支持该 CMS 类型")
|
|
|
|
|
}
|
|
|
|
|
appid := strings.TrimSpace(req.AppID)
|
|
|
|
|
if appid == "" {
|
|
|
|
|
return normalizedEnterpriseSiteConnectionRequest{}, response.ErrBadRequest(40042, "enterprise_site_appid_required", "appid is required")
|
|
|
|
|
return normalizedEnterpriseSiteConnectionRequest{}, response.ErrBadRequest(40042, "enterprise_site_appid_required", "请输入 AppID")
|
|
|
|
|
}
|
|
|
|
|
secret := strings.TrimSpace(req.Secret)
|
|
|
|
|
if secret == "" {
|
|
|
|
|
return normalizedEnterpriseSiteConnectionRequest{}, response.ErrBadRequest(40042, "enterprise_site_secret_required", "secret is required")
|
|
|
|
|
return normalizedEnterpriseSiteConnectionRequest{}, response.ErrBadRequest(40042, "enterprise_site_secret_required", "请输入 Secret")
|
|
|
|
|
}
|
|
|
|
|
name := strings.TrimSpace(req.Name)
|
|
|
|
|
if name == "" {
|
|
|
|
@@ -1464,18 +1549,18 @@ func normalizeCreateEnterpriseSiteConnectionRequest(req CreateEnterpriseSiteConn
|
|
|
|
|
func normalizeEnterpriseSiteURL(value string) (string, error) {
|
|
|
|
|
trimmed := strings.TrimSpace(value)
|
|
|
|
|
if trimmed == "" {
|
|
|
|
|
return "", response.ErrBadRequest(40041, "enterprise_site_url_required", "site_url is required")
|
|
|
|
|
return "", response.ErrBadRequest(40041, "enterprise_site_url_required", "请填写站点域名")
|
|
|
|
|
}
|
|
|
|
|
if !strings.Contains(trimmed, "://") {
|
|
|
|
|
trimmed = "https://" + trimmed
|
|
|
|
|
}
|
|
|
|
|
parsed, err := url.Parse(trimmed)
|
|
|
|
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
|
|
|
|
return "", response.ErrBadRequest(40041, "invalid_enterprise_site_url", "site_url is invalid")
|
|
|
|
|
return "", response.ErrBadRequest(40041, "invalid_enterprise_site_url", "站点域名格式不正确")
|
|
|
|
|
}
|
|
|
|
|
scheme := strings.ToLower(parsed.Scheme)
|
|
|
|
|
if scheme != "https" && scheme != "http" {
|
|
|
|
|
return "", response.ErrBadRequest(40041, "invalid_enterprise_site_url", "site_url must be http or https")
|
|
|
|
|
return "", response.ErrBadRequest(40041, "invalid_enterprise_site_url", "站点域名必须使用 http 或 https")
|
|
|
|
|
}
|
|
|
|
|
parsed.Scheme = scheme
|
|
|
|
|
parsed.Path = strings.TrimRight(parsed.Path, "/")
|
|
|
|
@@ -2372,13 +2457,35 @@ func sanitizeEnterpriseSiteError(err error) string {
|
|
|
|
|
}
|
|
|
|
|
message := strings.TrimSpace(err.Error())
|
|
|
|
|
if message == "" {
|
|
|
|
|
return "CMS request failed"
|
|
|
|
|
return "CMS 请求失败"
|
|
|
|
|
}
|
|
|
|
|
lower := strings.ToLower(message)
|
|
|
|
|
for _, marker := range []string{"api_secret", "secret", "signature", "appid="} {
|
|
|
|
|
if strings.Contains(strings.ToLower(message), strings.ToLower(marker)) {
|
|
|
|
|
return "CMS request failed; please verify site credentials and plugin installation"
|
|
|
|
|
if strings.Contains(lower, strings.ToLower(marker)) {
|
|
|
|
|
return "CMS 请求失败,请检查站点凭证和接口安装状态"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(lower, "request pbootcms ") {
|
|
|
|
|
return "CMS 请求失败,请检查站点地址和网络连通性"
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(lower, "parse pbootcms response") {
|
|
|
|
|
return "CMS 响应解析失败,请确认接口返回 JSON 格式"
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(lower, "parse pbootcms data") {
|
|
|
|
|
return "CMS 数据解析失败,请确认接口返回数据格式"
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(lower, "invalid pbootcms site url") {
|
|
|
|
|
return "站点域名格式不正确"
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(lower, "pbootcms ") && strings.Contains(lower, " returned http ") {
|
|
|
|
|
return "CMS 接口请求失败,请检查站点接口是否可访问"
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(lower, "pbootcms ") && strings.Contains(lower, " failed with code ") {
|
|
|
|
|
return "CMS 接口返回失败,请检查站点接口配置"
|
|
|
|
|
}
|
|
|
|
|
if strings.HasPrefix(lower, "pbootcms ") && strings.Contains(lower, " endpoint is unavailable") {
|
|
|
|
|
return "CMS 接口不可用,请确认接口安装路径"
|
|
|
|
|
}
|
|
|
|
|
if len([]rune(message)) > 300 {
|
|
|
|
|
runes := []rune(message)
|
|
|
|
|
return string(runes[:300])
|
|
|
|
@@ -2393,6 +2500,13 @@ func enterpriseSiteStringPointerValue(value *string) string {
|
|
|
|
|
return *value
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func enterpriseSiteNullableText(value *string) pgtype.Text {
|
|
|
|
|
if value == nil {
|
|
|
|
|
return pgtype.Text{}
|
|
|
|
|
}
|
|
|
|
|
return pgtype.Text{String: *value, Valid: true}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func enterpriseSiteTextPointer(value pgtype.Text) *string {
|
|
|
|
|
if !value.Valid {
|
|
|
|
|
return nil
|
|
|
|
|