HarmonyOS开发:应用上架流程与规范

HarmonyOS开发:应用上架流程与规范

📌 核心要点:应用上架不是"传个包就完事",从信息填写到截图规范,从描述文案到隐私声明,每个细节都可能成为被驳回的理由。一次过审的关键在于:理解规则、准备充分、不留死角。

背景与动机

应用开发完了,测试也过了,兴冲冲地去AppGallery提交上架------结果三天后收到一封驳回邮件。"应用描述与实际功能不符","截图包含其他应用市场标识","隐私政策链接无法访问"......

你改了又提,提了又被驳回。来回折腾两三周,项目排期直接崩了。

这不是段子,这是大量开发者的真实经历。华为应用市场的审核标准严格是出了名的,但严格不等于刁难。驳回原因90%都是开发者自己没仔细看规范。

这篇文章,把应用上架的完整流程、信息填写规范、截图描述要求、常见驳回原因全部讲清楚。照着做,一次过审不是梦。

核心原理

应用上架完整流程

#mermaid-svg-am9VP2RlZpjtANha{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-am9VP2RlZpjtANha .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-am9VP2RlZpjtANha .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-am9VP2RlZpjtANha .error-icon{fill:#552222;}#mermaid-svg-am9VP2RlZpjtANha .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-am9VP2RlZpjtANha .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-am9VP2RlZpjtANha .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-am9VP2RlZpjtANha .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-am9VP2RlZpjtANha .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-am9VP2RlZpjtANha .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-am9VP2RlZpjtANha .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-am9VP2RlZpjtANha .marker{fill:#333333;stroke:#333333;}#mermaid-svg-am9VP2RlZpjtANha .marker.cross{stroke:#333333;}#mermaid-svg-am9VP2RlZpjtANha svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-am9VP2RlZpjtANha p{margin:0;}#mermaid-svg-am9VP2RlZpjtANha .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-am9VP2RlZpjtANha .cluster-label text{fill:#333;}#mermaid-svg-am9VP2RlZpjtANha .cluster-label span{color:#333;}#mermaid-svg-am9VP2RlZpjtANha .cluster-label span p{background-color:transparent;}#mermaid-svg-am9VP2RlZpjtANha .label text,#mermaid-svg-am9VP2RlZpjtANha span{fill:#333;color:#333;}#mermaid-svg-am9VP2RlZpjtANha .node rect,#mermaid-svg-am9VP2RlZpjtANha .node circle,#mermaid-svg-am9VP2RlZpjtANha .node ellipse,#mermaid-svg-am9VP2RlZpjtANha .node polygon,#mermaid-svg-am9VP2RlZpjtANha .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-am9VP2RlZpjtANha .rough-node .label text,#mermaid-svg-am9VP2RlZpjtANha .node .label text,#mermaid-svg-am9VP2RlZpjtANha .image-shape .label,#mermaid-svg-am9VP2RlZpjtANha .icon-shape .label{text-anchor:middle;}#mermaid-svg-am9VP2RlZpjtANha .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-am9VP2RlZpjtANha .rough-node .label,#mermaid-svg-am9VP2RlZpjtANha .node .label,#mermaid-svg-am9VP2RlZpjtANha .image-shape .label,#mermaid-svg-am9VP2RlZpjtANha .icon-shape .label{text-align:center;}#mermaid-svg-am9VP2RlZpjtANha .node.clickable{cursor:pointer;}#mermaid-svg-am9VP2RlZpjtANha .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-am9VP2RlZpjtANha .arrowheadPath{fill:#333333;}#mermaid-svg-am9VP2RlZpjtANha .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-am9VP2RlZpjtANha .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-am9VP2RlZpjtANha .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-am9VP2RlZpjtANha .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-am9VP2RlZpjtANha .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-am9VP2RlZpjtANha .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-am9VP2RlZpjtANha .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-am9VP2RlZpjtANha .cluster text{fill:#333;}#mermaid-svg-am9VP2RlZpjtANha .cluster span{color:#333;}#mermaid-svg-am9VP2RlZpjtANha div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-am9VP2RlZpjtANha .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-am9VP2RlZpjtANha rect.text{fill:none;stroke-width:0;}#mermaid-svg-am9VP2RlZpjtANha .icon-shape,#mermaid-svg-am9VP2RlZpjtANha .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-am9VP2RlZpjtANha .icon-shape p,#mermaid-svg-am9VP2RlZpjtANha .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-am9VP2RlZpjtANha .icon-shape .label rect,#mermaid-svg-am9VP2RlZpjtANha .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-am9VP2RlZpjtANha .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-am9VP2RlZpjtANha .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-am9VP2RlZpjtANha :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;}#mermaid-svg-am9VP2RlZpjtANha .startStyle>*{fill:#FF6B35!important;stroke:#D4551F!important;color:#fff!important;font-weight:bold!important;}#mermaid-svg-am9VP2RlZpjtANha .startStyle span{fill:#FF6B35!important;stroke:#D4551F!important;color:#fff!important;font-weight:bold!important;}#mermaid-svg-am9VP2RlZpjtANha .startStyle tspan{fill:#fff!important;}#mermaid-svg-am9VP2RlZpjtANha .processStyle>*{fill:#4ECDC4!important;stroke:#3BA99C!important;color:#fff!important;}#mermaid-svg-am9VP2RlZpjtANha .processStyle span{fill:#4ECDC4!important;stroke:#3BA99C!important;color:#fff!important;}#mermaid-svg-am9VP2RlZpjtANha .processStyle tspan{fill:#fff!important;}#mermaid-svg-am9VP2RlZpjtANha .decisionStyle>*{fill:#FFE66D!important;stroke:#D4B93C!important;color:#333!important;font-weight:bold!important;}#mermaid-svg-am9VP2RlZpjtANha .decisionStyle span{fill:#FFE66D!important;stroke:#D4B93C!important;color:#333!important;font-weight:bold!important;}#mermaid-svg-am9VP2RlZpjtANha .decisionStyle tspan{fill:#333!important;}#mermaid-svg-am9VP2RlZpjtANha .endStyle>*{fill:#96CEB4!important;stroke:#6DAF8E!important;color:#fff!important;font-weight:bold!important;}#mermaid-svg-am9VP2RlZpjtANha .endStyle span{fill:#96CEB4!important;stroke:#6DAF8E!important;color:#fff!important;font-weight:bold!important;}#mermaid-svg-am9VP2RlZpjtANha .endStyle tspan{fill:#fff!important;}#mermaid-svg-am9VP2RlZpjtANha .errorStyle>*{fill:#FF6B6B!important;stroke:#CC5555!important;color:#fff!important;}#mermaid-svg-am9VP2RlZpjtANha .errorStyle span{fill:#FF6B6B!important;stroke:#CC5555!important;color:#fff!important;}#mermaid-svg-am9VP2RlZpjtANha .errorStyle tspan{fill:#fff!important;} 通过
驳回
开发完成
AGC创建应用
填写应用信息
上传应用包
填写版本信息
上传截图与预览
填写隐私政策
提交审核
审核结果
应用上架
查看驳回原因
修改并重新提交

整个流程看起来不复杂,但每个环节都有细节。下面逐个拆解。

应用信息填写规范

应用信息分为两类:必填项选填项。别觉得选填项不重要,填得越完整,审核通过率越高,用户转化率也越高。

信息项 是否必填 规范要求
应用名称 必填 2-64字符,不能含特殊符号,不能与其他应用重名
应用简介 必填 4-100字符,一句话说明应用核心功能
应用描述 必填 10-8000字符,详细描述功能特性
应用图标 必填 512×512px,PNG格式,无圆角
应用截图 必填 至少3张,最多10张
应用分类 必填 一级分类+二级分类
隐私政策 必填 有效HTTPS链接
版权信息 必填 软件著作权或ICP备案

代码实战

基础用法:应用信息配置脚本

手动在AGC控制台填写信息容易出错,尤其是多语言版本。写个脚本自动校验,省心。

typescript 复制代码
// scripts/validate_app_info.ets
// 应用信息校验工具------提交前跑一遍,提前发现问题

interface AppInfo {
  name: string;
  brief: string;
  description: string;
  iconPath: string;
  screenshots: string[];
  category: string;
  privacyPolicyUrl: string;
  copyrightInfo: string;
}

interface ValidationResult {
  isValid: boolean;
  errors: string[];
  warnings: string[];
}

class AppInfoValidator {
  // 校验应用名称
  private validateName(name: string): string | null {
    if (name.length < 2 || name.length > 64) {
      return '应用名称长度必须在2-64字符之间';
    }
    // 不能包含特殊符号
    const specialChars = /[!@#$%^&*()+=\[\]{}|\\:;<>,?\/~]/;
    if (specialChars.test(name)) {
      return '应用名称不能包含特殊符号';
    }
    return null;
  }

  // 校验应用简介
  private validateBrief(brief: string): string | null {
    if (brief.length < 4 || brief.length > 100) {
      return '应用简介长度必须在4-100字符之间';
    }
    return null;
  }

  // 校验应用描述
  private validateDescription(desc: string): string | null {
    if (desc.length < 10) {
      return '应用描述至少10个字符';
    }
    if (desc.length > 8000) {
      return '应用描述不能超过8000字符';
    }
    return null;
  }

  // 校验隐私政策URL
  private validatePrivacyUrl(url: string): string | null {
    if (!url.startsWith('https://')) {
      return '隐私政策链接必须使用HTTPS协议';
    }
    return null;
  }

  // 校验截图数量
  private validateScreenshots(screenshots: string[]): string | null {
    if (screenshots.length < 3) {
      return '至少需要3张截图';
    }
    if (screenshots.length > 10) {
      return '截图不能超过10张';
    }
    return null;
  }

  // 执行完整校验
  validate(appInfo: AppInfo): ValidationResult {
    const errors: string[] = [];
    const warnings: string[] = [];

    // 必填项校验
    const nameError = this.validateName(appInfo.name);
    if (nameError) errors.push(nameError);

    const briefError = this.validateBrief(appInfo.brief);
    if (briefError) errors.push(briefError);

    const descError = this.validateDescription(appInfo.description);
    if (descError) errors.push(descError);

    const urlError = this.validatePrivacyUrl(appInfo.privacyPolicyUrl);
    if (urlError) errors.push(urlError);

    const screenshotError = this.validateScreenshots(appInfo.screenshots);
    if (screenshotError) errors.push(screenshotError);

    // 警告项(不阻止提交,但建议修改)
    if (appInfo.description.length < 200) {
      warnings.push('应用描述较短,建议补充更多功能说明以提高转化率');
    }
    if (appInfo.screenshots.length < 5) {
      warnings.push('建议提供5张以上截图,展示核心功能页面');
    }

    return {
      isValid: errors.length === 0,
      errors,
      warnings
    };
  }
}

// 使用示例
const validator = new AppInfoValidator();
const result = validator.validate({
  name: '我的效率工具',
  brief: '一款专注时间管理的效率工具',
  description: '帮助用户管理日常任务',
  iconPath: './icon.png',
  screenshots: ['./s1.png', './s2.png'],
  category: '效率',
  privacyPolicyUrl: 'http://example.com/privacy', // 故意用http测试
  copyrightInfo: '2024 MyCompany'
});

console.log(`校验结果: ${result.isValid ? '通过' : '未通过'}`);
console.log(`错误: ${result.errors.join('; ')}`);
console.log(`警告: ${result.warnings.join('; ')}`);

进阶用法:截图自动生成与校验

应用截图是审核的重点检查项。华为对截图有严格要求:分辨率、格式、内容都要合规。手动截图容易遗漏,写个自动化工具。

typescript 复制代码
// entry/src/main/ets/utils/ScreenshotHelper.ets
// 截图辅助工具------自动生成符合AppGallery规范的截图

import { componentUtils } from '@kit.ArkUI';
import { image } from '@kit.ImageKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';

// 截图规格定义
interface ScreenshotSpec {
  width: number;      // 宽度(像素)
  height: number;     // 高度(像素)
  deviceType: string; // 设备类型
  minCount: number;   // 最少截图数
  maxCount: number;   // 最多截图数
}

// AppGallery要求的截图规格
const SCREENSHOT_SPECS: ScreenshotSpec[] = [
  { width: 1080, height: 1920, deviceType: 'phone', minCount: 3, maxCount: 10 },
  { width: 1600, height: 2560, deviceType: 'tablet', minCount: 3, maxCount: 10 }
];

class ScreenshotHelper {
  private context: common.Context;

  constructor(context: common.Context) {
    this.context = context;
  }

  // 校验截图是否符合规范
  validateScreenshot(imagePath: string, targetSpec: ScreenshotSpec): boolean {
    // 实际项目中需要读取图片尺寸进行校验
    // 这里简化为路径校验逻辑
    hilog.info(0x0000, 'Screenshot', 
      `校验截图: ${imagePath}, 目标规格: ${targetSpec.width}x${targetSpec.height}`);
    return true;
  }

  // 生成截图文件名(规范命名)
  generateFileName(deviceType: string, index: number): string {
    return `screenshot_${deviceType}_${index + 1}.png`;
  }

  // 批量校验截图
  batchValidate(screenshots: string[], deviceType: string): {
    valid: string[];
    invalid: string[];
  } {
    const spec = SCREENSHOT_SPECS.find(s => s.deviceType === deviceType);
    if (!spec) {
      return { valid: [], invalid: screenshots };
    }

    const valid: string[] = [];
    const invalid: string[] = [];

    screenshots.forEach(path => {
      if (this.validateScreenshot(path, spec)) {
        valid.push(path);
      } else {
        invalid.push(path);
      }
    });

    return { valid, invalid };
  }
}

完整示例:上架前自动化检查工具

把所有校验逻辑整合起来,提交审核前跑一遍,把问题扼杀在摇篮里。

typescript 复制代码
// entry/src/main/ets/utils/PublishChecker.ets
// 上架前自动化检查------一次跑完所有校验,不放过任何细节

import { hilog } from '@kit.PerformanceAnalysisKit';
import { common } from '@kit.AbilityKit';

// 检查项结果
interface CheckResult {
  name: string;       // 检查项名称
  passed: boolean;    // 是否通过
  message: string;    // 详细信息
  severity: 'error' | 'warning'; // 严重程度
}

// 应用发布信息
interface PublishInfo {
  appName: string;
  appBrief: string;
  appDescription: string;
  versionName: string;
  versionCode: number;
  packageName: string;
  privacyPolicyUrl: string;
  screenshotCount: number;
  hasIcon: boolean;
  hasCopyright: boolean;
  targetApiVersion: number;
  minApiVersion: number;
  permissions: string[];
}

export class PublishChecker {
  private context: common.Context;
  private results: CheckResult[] = [];

  constructor(context: common.Context) {
    this.context = context;
  }

  // 运行所有检查
  runAllChecks(info: PublishInfo): CheckResult[] {
    this.results = [];

    this.checkAppName(info.appName);
    this.checkAppBrief(info.appBrief);
    this.checkDescription(info.appDescription);
    this.checkVersion(info.versionName, info.versionCode);
    this.checkPackageName(info.packageName);
    this.checkPrivacyPolicy(info.privacyPolicyUrl);
    this.checkScreenshots(info.screenshotCount);
    this.checkIcon(info.hasIcon);
    this.checkCopyright(info.hasCopyright);
    this.checkApiVersion(info.targetApiVersion, info.minApiVersion);
    this.checkPermissions(info.permissions);

    return this.results;
  }

  // 检查应用名称
  private checkAppName(name: string): void {
    if (!name || name.length < 2) {
      this.results.push({
        name: '应用名称',
        passed: false,
        message: '应用名称至少2个字符',
        severity: 'error'
      });
      return;
    }
    if (name.length > 64) {
      this.results.push({
        name: '应用名称',
        passed: false,
        message: '应用名称不能超过64个字符',
        severity: 'error'
      });
      return;
    }
    this.results.push({
      name: '应用名称',
      passed: true,
      message: `应用名称"${name}"符合规范`,
      severity: 'error'
    });
  }

  // 检查应用简介
  private checkAppBrief(brief: string): void {
    if (!brief || brief.length < 4) {
      this.results.push({
        name: '应用简介',
        passed: false,
        message: '应用简介至少4个字符',
        severity: 'error'
      });
      return;
    }
    if (brief.length > 100) {
      this.results.push({
        name: '应用简介',
        passed: false,
        message: '应用简介不能超过100个字符',
        severity: 'error'
      });
      return;
    }
    this.results.push({
      name: '应用简介',
      passed: true,
      message: '应用简介符合规范',
      severity: 'error'
    });
  }

  // 检查应用描述
  private checkDescription(desc: string): void {
    if (!desc || desc.length < 10) {
      this.results.push({
        name: '应用描述',
        passed: false,
        message: '应用描述至少10个字符',
        severity: 'error'
      });
      return;
    }
    if (desc.length < 200) {
      this.results.push({
        name: '应用描述',
        passed: true,
        message: '应用描述较短,建议补充更多功能说明(当前' + desc.length + '字符)',
        severity: 'warning'
      });
      return;
    }
    this.results.push({
      name: '应用描述',
      passed: true,
      message: `应用描述符合规范(${desc.length}字符)`,
      severity: 'error'
    });
  }

  // 检查版本号
  private checkVersion(versionName: string, versionCode: number): void {
    // 版本名格式校验:x.y.z
    const versionPattern = /^\d+\.\d+\.\d+$/;
    if (!versionPattern.test(versionName)) {
      this.results.push({
        name: '版本号',
        passed: false,
        message: `版本名"${versionName}"格式不正确,应为x.y.z格式(如1.0.0)`,
        severity: 'error'
      });
      return;
    }
    if (versionCode <= 0) {
      this.results.push({
        name: '版本号',
        passed: false,
        message: '版本号必须大于0',
        severity: 'error'
      });
      return;
    }
    this.results.push({
      name: '版本号',
      passed: true,
      message: `版本${versionName}(${versionCode})格式正确`,
      severity: 'error'
    });
  }

  // 检查包名
  private checkPackageName(packageName: string): void {
    const packagePattern = /^[a-z][a-z0-9]*(\.[a-z][a-z0-9]*)+$/;
    if (!packagePattern.test(packageName)) {
      this.results.push({
        name: '包名',
        passed: false,
        message: `包名"${packageName}"格式不正确,应为全小写点分格式`,
        severity: 'error'
      });
      return;
    }
    this.results.push({
      name: '包名',
      passed: true,
      message: `包名"${packageName}"格式正确`,
      severity: 'error'
    });
  }

  // 检查隐私政策
  private checkPrivacyPolicy(url: string): void {
    if (!url) {
      this.results.push({
        name: '隐私政策',
        passed: false,
        message: '必须提供隐私政策链接',
        severity: 'error'
      });
      return;
    }
    if (!url.startsWith('https://')) {
      this.results.push({
        name: '隐私政策',
        passed: false,
        message: '隐私政策链接必须使用HTTPS协议',
        severity: 'error'
      });
      return;
    }
    this.results.push({
      name: '隐私政策',
      passed: true,
      message: '隐私政策链接格式正确',
      severity: 'error'
    });
  }

  // 检查截图
  private checkScreenshots(count: number): void {
    if (count < 3) {
      this.results.push({
        name: '应用截图',
        passed: false,
        message: `至少需要3张截图,当前只有${count}张`,
        severity: 'error'
      });
      return;
    }
    if (count < 5) {
      this.results.push({
        name: '应用截图',
        passed: true,
        message: `建议提供5张以上截图,当前${count}张`,
        severity: 'warning'
      });
      return;
    }
    this.results.push({
      name: '应用截图',
      passed: true,
      message: `截图数量${count}张,符合要求`,
      severity: 'error'
    });
  }

  // 检查图标
  private checkIcon(hasIcon: boolean): void {
    this.results.push({
      name: '应用图标',
      passed: hasIcon,
      message: hasIcon ? '应用图标已配置' : '必须上传应用图标(512×512px PNG)',
      severity: 'error'
    });
  }

  // 检查版权信息
  private checkCopyright(hasCopyright: boolean): void {
    this.results.push({
      name: '版权信息',
      passed: hasCopyright,
      message: hasCopyright ? '版权信息已配置' : '必须提供软件著作权或ICP备案信息',
      severity: 'error'
    });
  }

  // 检查API版本
  private checkApiVersion(targetApi: number, minApi: number): void {
    if (targetApi < 12) {
      this.results.push({
        name: 'API版本',
        passed: false,
        message: `目标API版本${targetApi}过低,HarmonyOS NEXT最低要求API 12`,
        severity: 'error'
      });
      return;
    }
    if (minApi > targetApi) {
      this.results.push({
        name: 'API版本',
        passed: false,
        message: '最低API版本不能高于目标API版本',
        severity: 'error'
      });
      return;
    }
    this.results.push({
      name: 'API版本',
      passed: true,
      message: `API版本范围: ${minApi} - ${targetApi}`,
      severity: 'error'
    });
  }

  // 检查权限声明
  private checkPermissions(permissions: string[]): void {
    // 检查是否有敏感权限但未在隐私政策中说明
    const sensitivePermissions = [
      'ohos.permission.LOCATION',
      'ohos.permission.CAMERA',
      'ohos.permission.MICROPHONE',
      'ohos.permission.READ_CONTACTS',
      'ohos.permission.READ_CALENDAR'
    ];

    const hasSensitive = permissions.some(p => sensitivePermissions.includes(p));
    if (hasSensitive) {
      this.results.push({
        name: '权限声明',
        passed: true,
        message: `应用使用了敏感权限,请确保隐私政策中说明了权限使用目的`,
        severity: 'warning'
      });
    } else {
      this.results.push({
        name: '权限声明',
        passed: true,
        message: '权限声明正常',
        severity: 'error'
      });
    }
  }

  // 生成检查报告
  generateReport(): string {
    const errors = this.results.filter(r => !r.passed && r.severity === 'error');
    const warnings = this.results.filter(r => r.severity === 'warning');
    const passed = this.results.filter(r => r.passed && r.severity === 'error');

    let report = '=== 上架前检查报告 ===\n\n';
    report += `✅ 通过: ${passed.length}项\n`;
    report += `❌ 错误: ${errors.length}项\n`;
    report += `⚠️ 警告: ${warnings.length}项\n\n`;

    if (errors.length > 0) {
      report += '--- 必须修复 ---\n';
      errors.forEach(e => {
        report += `[${e.name}] ${e.message}\n`;
      });
      report += '\n';
    }

    if (warnings.length > 0) {
      report += '--- 建议优化 ---\n';
      warnings.forEach(w => {
        report += `[${w.name}] ${w.message}\n`;
      });
    }

    return report;
  }
}

使用方式:

typescript 复制代码
// 在测试页面中使用
import { PublishChecker } from '../utils/PublishChecker';

@Entry
@Component
struct PublishCheckPage {
  @State report: string = '';

  aboutToAppear() {
    const checker = new PublishChecker(this.getContext());
    const results = checker.runAllChecks({
      appName: '我的效率工具',
      appBrief: '专注时间管理',
      appDescription: '帮助用户管理日常任务和日程安排,提供番茄钟、待办清单、日程提醒等功能',
      versionName: '1.0.0',
      versionCode: 1,
      packageName: 'com.example.myapp',
      privacyPolicyUrl: 'https://example.com/privacy',
      screenshotCount: 4,
      hasIcon: true,
      hasCopyright: true,
      targetApiVersion: 12,
      minApiVersion: 12,
      permissions: ['ohos.permission.LOCATION', 'ohos.permission.NOTIFICATION_CONTROLLER']
    });

    this.report = checker.generateReport();
  }

  build() {
    Scroll() {
      Text(this.report)
        .fontSize(14)
        .fontFamily('monospace')
        .padding(20)
    }
    .width('100%')
    .height('100%')
  }
}

踩坑与注意事项

坑1:截图包含其他应用市场标识

这是最常见的驳回原因之一。你的截图里如果出现了Google Play、小米应用商店等其他市场的logo、水印或者下载按钮,直接驳回。

正确做法:截图必须是纯净的应用界面截图,不能有任何第三方应用市场的标识。如果截图中有"下载自xxx"的水印,重新截图。

坑2:隐私政策链接无法访问

隐私政策链接必须是HTTPS,而且必须能正常打开。审核人员会点进去看的。

常见问题:

  • 链接指向内网地址(审核人员当然打不开)
  • 链接指向需要登录才能查看的页面
  • 链接指向的页面内容为空
  • 服务器偶尔宕机导致无法访问

正确做法:把隐私政策放在稳定的服务器上,确保7×24小时可访问。内容要完整,包括数据收集范围、使用目的、第三方SDK说明等。

坑3:应用描述与实际功能不符

描述里写了"支持视频编辑",但应用里根本没有这个功能------直接驳回。

正确做法:描述只写已实现的功能,计划中的功能别写。如果功能是付费解锁的,要在描述中说明。

坑4:应用图标不符合规范

图标常见问题:

  • 尺寸不是512×512px
  • 格式不是PNG
  • 图标有圆角(系统会自动加圆角,你不需要自己加)
  • 图标上有文字(小尺寸下看不清)
  • 图标和已有应用太相似(可能被判定为仿冒)

坑5:版本号和版本名写反了

  • 版本号(versionCode):整数,每次更新必须递增。如1、2、3
  • 版本名(versionName):字符串,给用户看的。如"1.0.0"、"2.1.3"

搞反了会导致更新机制异常。

坑6:缺少软件著作权

国内上架需要软件著作权。个人开发者可以申请"计算机软件著作权登记证书",流程大约30-60个工作日。提前规划!

如果实在来不及,可以先申请"APP电子版权证书",速度更快(约5-10个工作日)。

HarmonyOS 6适配说明

HarmonyOS 6对应用上架流程做了几项调整:

  1. 必须使用API 12+:HarmonyOS NEXT应用的目标API版本必须不低于12。低于12的包直接无法上传。

  2. 隐私声明增强:应用描述中必须明确说明收集了哪些用户数据、用于什么目的。模糊表述如"收集必要信息"不再被接受。

  3. 截图规格更新:新增折叠屏设备截图要求。如果你的应用适配折叠屏,需要额外提供折叠屏状态的截图。

  4. 权限使用说明 :每个敏感权限必须在module.json5中配置reason字段,说明申请该权限的理由。理由太笼统(如"为了应用正常运行")会被驳回。

typescript 复制代码
// module.json5 权限声明示例(HarmonyOS 6规范)
{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.LOCATION",
        "reason": "$string:location_reason",
        "usedScene": {
          "abilities": ["EntryAbility"],
          "when": "inuse"
        }
      }
    ]
  }
}

// 对应的字符串资源
// base/element/string.json
{
  "string": [
    {
      "name": "location_reason",
      "value": "用于记录您的运动轨迹并在地图上显示"
    }
  ]
}
  1. 应用签名校验更严格:上传的包必须使用AGC控制台关联的发布证书签名,自签名证书不再被接受。

总结

应用上架是个细致活,没有技术难度,但处处是坑。核心记住三点:

  1. 截图要干净:不能有其他市场标识,数量够,分辨率对
  2. 隐私政策要到位:HTTPS可访问,内容完整,权限说明清晰
  3. 信息要一致:描述和功能一致,包名和代码一致,版本号规范
维度 评价
学习难度 ⭐⭐ 没有技术门槛,但规范细节多
使用频率 ⭐⭐⭐⭐ 每次版本更新都要走一遍
重要程度 ⭐⭐⭐⭐⭐ 不过审,用户就下载不到你的应用