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对应用上架流程做了几项调整:
-
必须使用API 12+:HarmonyOS NEXT应用的目标API版本必须不低于12。低于12的包直接无法上传。
-
隐私声明增强:应用描述中必须明确说明收集了哪些用户数据、用于什么目的。模糊表述如"收集必要信息"不再被接受。
-
截图规格更新:新增折叠屏设备截图要求。如果你的应用适配折叠屏,需要额外提供折叠屏状态的截图。
-
权限使用说明 :每个敏感权限必须在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": "用于记录您的运动轨迹并在地图上显示"
}
]
}
- 应用签名校验更严格:上传的包必须使用AGC控制台关联的发布证书签名,自签名证书不再被接受。
总结
应用上架是个细致活,没有技术难度,但处处是坑。核心记住三点:
- 截图要干净:不能有其他市场标识,数量够,分辨率对
- 隐私政策要到位:HTTPS可访问,内容完整,权限说明清晰
- 信息要一致:描述和功能一致,包名和代码一致,版本号规范
| 维度 | 评价 |
|---|---|
| 学习难度 | ⭐⭐ 没有技术门槛,但规范细节多 |
| 使用频率 | ⭐⭐⭐⭐ 每次版本更新都要走一遍 |
| 重要程度 | ⭐⭐⭐⭐⭐ 不过审,用户就下载不到你的应用 |