{"record":{"id":"a58e8477d1e61078","repo":"yikart/AiToEarn","slug":"channelpublishvalidationfailed","errorCode":"ChannelPublishValidationFailed","errorMessage":"ChannelPublishValidationFailed","messagePattern":"ChannelPublishValidationFailed","errorType":"error_code","errorClass":"AppException","httpStatus":null,"severity":"warning","filePath":"project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/douyin/offline-qr/douyin-offline-qr.service.ts","lineNumber":96,"sourceCode":"    if (!materialGroup) {\n      throw new AppException(ResponseCode.MaterialGroupNotFound)\n    }\n\n    const material = await this.materialRepo.getInfo(materialId)\n    if (!material || material.groupId !== materialGroupId) {\n      throw new AppException(ResponseCode.MaterialNotFound)\n    }\n  }\n\n  private async prepareContent(content: PublishContentInput): Promise<PublishContentInput> {\n    const prepared = await this.mediaService.preparePublishContentMedia({\n      userId: '',\n      content,\n      mediaRules: DOUYIN_METADATA.mediaRules,\n    })\n    if (prepared.issues.length) {\n      const locale = getLocale()\n      throw new AppException(ResponseCode.ChannelPublishValidationFailed, {\n        platform: AccountType.Douyin,\n        accountId: '',\n        issues: prepared.issues.map(issue => formatPublishValidationIssue(issue, locale)),\n      })\n    }\n    return prepared.content\n  }\n\n  private resolvePublishType(content: PublishContentInput): PublishType {\n    return content.media.some(media => this.getMediaType(media) === DouyinMediaType.Video)\n      ? PublishType.VIDEO\n      : PublishType.ARTICLE\n  }\n\n  private resolveRecordMedia(\n    content: PublishContentInput,\n    type: PublishType,\n  ): { videoUrl?: string, imgUrlList?: string[] } {","sourceCodeStart":78,"sourceCodeEnd":114,"githubUrl":"https://github.com/yikart/AiToEarn/blob/d3aa8bea5b146a8675607cf0144d891aad3e9683/project/aitoearn-backend/apps/aitoearn-server/src/core/channels/platforms/douyin/offline-qr/douyin-offline-qr.service.ts#L78-L114","documentation":"ChannelPublishValidationFailed is thrown by DouyinOfflineQrService.prepareContent when mediaService.preparePublishContentMedia reports non-empty issues against DOUYIN_METADATA.mediaRules. Each issue is formatted with the request locale and returned as structured detail, meaning the content (text/media mix) violates Douyin's publish constraints (e.g. too many images, missing required media, unsupported attachment types).","triggerScenarios":"createPublish (via content()) with publish content that breaks Douyin mediaRules: exceeding the max image/video count, empty media list for a required-media type, mismatched media types (e.g. mixing video and images where disallowed), or oversized counts/attachments per the platform metadata rules.","commonSituations":"Client builds content for another platform's rules (e.g. 18 images) and submits to Douyin; content prepared before materials were trimmed; platform metadata updated so previously valid content is now invalid; automation scripts posting empty-content records.","solutions":["Read the returned issues array — each entry names the violated Douyin rule and is localized via the request locale.","Adjust the publish content to satisfy DOUYIN_METADATA.mediaRules (media count, types, required fields) before retrying.","Validate content client-side against the platform's mediaRules metadata fetched from the capabilities API before submitting.","If rules seem wrong, confirm the DOUYIN_METADATA mediaRules version matches the current platform requirements."],"exampleFix":"// before\ncontent = { text: 'hi', images: [1..18 imgs] } // exceeds Douyin limit -> issues thrown\n// after\ncontent = { text: 'hi', images: images.slice(0, DOUYIN_METADATA.mediaRules.maxImages) }\nawait offlineQr.createPublish({ materialGroupId, materialId, content })","handlingStrategy":"validation","validationCode":"import { DOUYIN_METADATA } from './douyin.metadata'\nfunction validateAgainstDouyinRules(content: PublishContentInput): string[] {\n  const issues: string[] = []\n  const rules = DOUYIN_METADATA.mediaRules\n  if (content.images && rules.maxImages && content.images.length > rules.maxImages)\n    issues.push(`max ${rules.maxImages} images allowed`)\n  if (rules.requireMedia && !content.images?.length && !content.video)\n    issues.push('media is required')\n  return issues\n}\n// run before createPublish and fix or surface issues","typeGuard":null,"tryCatchPattern":"try {\n  await offlineQrService.createPublish(dto)\n} catch (err) {\n  if (err instanceof AppException && err.code === ResponseCode.ChannelPublishValidationFailed) {\n    // err.data.issues holds localized rule violations; show them to the user\n  } else throw err\n}","preventionTips":["Fetch and honor platform mediaRules metadata client-side before composing content","Build Douyin content with Douyin rules, not another platform's limits","Display the localized issues array from the error payload directly to users","Re-validate content when platform metadata versions change"],"tags":["douyin","validation","publish-content","media-rules"],"backgroundTag":"publish-validation-failed","analyzedSha":"d3aa8bea5b146a8675607cf0144d891aad3e9683","analyzedAt":"2026-08-31T14:19:24.185Z","schemaVersion":2},"datasetVersion":"2026-08-31T19:17:28.585Z"}