Posted in

Go截图工具通过苹果App Store审核的全部17项要求:沙盒配置、隐私清单、NSPermissions、公证流程全记录

第一章:Go语言屏幕截图技术原理与架构设计

屏幕截图在自动化测试、远程桌面、录屏工具等场景中扮演关键角色。Go语言虽无官方图形库支持,但通过底层系统调用与跨平台封装,可实现高效、低延迟的截屏能力。其核心原理依赖操作系统提供的图形子系统接口:Windows 使用 GDI 或 DirectX 截取前台/后台窗口;macOS 依赖 Core Graphics 的 CGDisplayCreateImage;Linux 则通过 X11 的 XGetImage 或 Wayland 的 xdg-desktop-portal(需 D-Bus 交互)获取帧缓冲。

截图技术选型对比

方案 跨平台性 性能 权限要求 适用场景
github.com/kbinani/screenshot 高(自动适配三端) 中高(C绑定) 无特殊权限(macOS需辅助功能授权) 快速集成、通用截屏
golang.org/x/exp/shiny/screen(已归档) 需直接访问显示服务 已不推荐
自研 CG/X11/DirectX 封装 低(需分平台维护) 最高 Windows需前台窗口句柄,macOS需Accessibility权限 对延迟敏感的嵌入式监控

核心架构分层设计

整体采用“采集层—处理层—输出层”三级解耦结构。采集层负责调用平台原生API并返回原始像素数据(RGBA格式);处理层可选进行缩放、裁剪、色彩空间转换(如sRGB→linear RGB);输出层支持编码为PNG/JPEG或直接写入内存缓冲区供后续处理。

实现一个基础截屏函数

package main

import (
    "image/png"
    "os"
    "github.com/kbinani/screenshot"
)

func captureAndSave(filename string) error {
    // 获取主显示器截图(索引0)
    img, err := screenshot.Capture(0, 0, 1920, 1080) // 指定区域:x,y,width,height
    if err != nil {
        return err // 如:macOS未开启辅助功能权限时返回error
    }
    // 写入PNG文件(Go标准库原生支持,无需额外编码库)
    f, _ := os.Create(filename)
    defer f.Close()
    return png.Encode(f, img) // 自动处理RGBA通道顺序与压缩
}

该函数执行逻辑为:先触发平台特定采集(如 macOS 调用 CGDisplayCreateImage),将返回的 *C.CGImageRef 安全转换为 Go 的 *image.RGBA,再经 PNG 编码器序列化。整个过程避免内存拷贝冗余,像素数据在 C 与 Go 运行时间通过 unsafe.Pointer 零拷贝桥接。

第二章:macOS沙盒环境下的截图能力适配

2.1 macOS Screen Capture API 与 Go CGO 封装原理

macOS 提供了 AVCaptureScreenInput(较新)与底层 CGDisplayStream(推荐用于高性能捕获)两套屏幕采集机制。Go 无法直接调用 Objective-C/C 接口,需通过 CGO 桥接。

核心封装策略

  • 使用 .h 头文件声明 C 函数原型
  • .c 文件中实现 Objective-C++ 逻辑(.mm 编译)
  • Go 侧通过 //export 暴露纯 C 接口

CGDisplayStream 创建关键参数

参数 类型 说明
displayIDs CFArrayRef 目标显示器 ID 列表,空则捕获全部
outputQueue dispatch_queue_t 帧回调执行队列(必须串行)
handler CGDisplayStreamFrameHandler 每帧回调,接收 IOSurfaceRef
// screen_capture.c
#include <CoreGraphics/CoreGraphics.h>
#include <IOSurface/IOSurface.h>

//export start_stream
void start_stream(uint32_t display_id) {
    CFArrayRef displays = CFArrayCreate(NULL, (const void**)&display_id, 1, &kCFTypeArrayCallBacks);
    dispatch_queue_t queue = dispatch_queue_create("capture", DISPATCH_QUEUE_SERIAL);
    CGDisplayStreamRef stream = CGDisplayStreamCreate(
        displays,
        1920, 1080, // width, height
        kCVPixelFormatType_32BGRA,
        NULL, // pixel buffer pool
        queue,
        ^(CGDisplayStreamFrameStatus status, uint64_t displayTime,
          IOSurfaceRef surface, CGDisplayStreamUpdateRef update) {
            // 帧处理:surface 可直接传入 OpenGL/Vulkan 或转为 Go []byte
        }
    );
    CGDisplayStreamStart(stream);
}

该函数封装了流创建、启动及线程安全回调绑定。displayTime 提供 VSync 对齐时间戳;surface 是零拷贝共享内存句柄,避免像素数据复制开销。

graph TD
    A[Go 调用 start_stream] --> B[C 层创建 CGDisplayStream]
    B --> C[系统注入帧到 outputQueue]
    C --> D[触发 handler 回调]
    D --> E[IOSurfaceRef 传递至 Go runtime]

2.2 沙盒权限声明机制解析:com.apple.security.screensharing 与 entitlements.plist 实战配置

macOS App Sandbox 要求所有敏感能力必须通过 entitlements.plist 显式声明。com.apple.security.screensharing 是启用屏幕录制权限的关键开关,仅当用户首次调用 AVCaptureScreenInputCGDisplayStreamCreate 时触发系统授权弹窗。

权限声明结构

<!-- entitlements.plist -->
<key>com.apple.security.screensharing</key>
<true/>

该键值无参数,布尔 true 即表示应用声明具备屏幕捕获意图;不声明则运行时直接拒绝访问,且不会弹出授权界面。

授权流程示意

graph TD
    A[App 启动] --> B{entitlements.plist 包含 screensharing?}
    B -- 是 --> C[调用 AVCaptureScreenInput]
    B -- 否 --> D[API 返回 nil / crash]
    C --> E[系统弹出首次授权框]
    E --> F[用户授予权限 → 进入屏幕流]

注意事项

  • 必须配合 NSCameraUsageDescription 等 Info.plist 描述键(即使不涉及摄像头);
  • macOS 12+ 要求在 Xcode Signing & Capabilities 中勾选 Screen Recording 才能自动注入 entitlement;
  • 权限状态可通过 AXIsProcessTrustedWithOptions 运行时校验。

2.3 截图进程生命周期管理:从 NSWorkspace 到 CGDisplayStream 的跨进程资源协调

macOS 截图服务需在用户交互(如 NSWorkspace.shared.screenshot() 触发)与底层帧捕获(CGDisplayStream)间建立稳定、低延迟的生命周期契约。

资源绑定与释放时序

  • 用户发起截图 → NSWorkspace 启动辅助进程并传递 display ID
  • 辅助进程创建 CGDisplayStream 实例,注册回调
  • 进程退出前必须显式调用 CGDisplayStreamStop,否则内核流句柄泄漏

核心协调机制

let stream = CGDisplayStreamCreateWithDispatchQueue(
    displayID,        // UInt32,目标显示器唯一标识
    0, 0,             // 输出尺寸:0 表示原始分辨率
    pixelFormat,      // kCVPixelFormatType_32BGRA
    nil,              // 不使用自定义缓冲区池
    queue,            // 专用串行队列,避免回调重入
    { _, _, _, _, _ in /* frame handler */ }
)

该调用将 displayID 绑定至当前进程的 Mach task port,内核据此限制帧数据仅向该 task 分发;若进程异常终止,系统自动回收流资源,但延迟可达数秒——故需主动 CGDisplayStreamStop(stream) + CFRelease(stream) 双保险。

生命周期状态对照表

状态 NSWorkspace 触发点 CGDisplayStream 响应行为
初始化 screenshot(_:) 调用 创建流对象,但未启动
活跃捕获 辅助进程调用 start() 内核开始推送帧,CPU 占用上升
安全终止 主进程 exit(0) 必须 stop() + CFRelease()
graph TD
    A[NSWorkspace screenshot] --> B[Launch Helper Process]
    B --> C[CGDisplayStreamCreate...]
    C --> D{Stream Started?}
    D -->|Yes| E[Frame Callbacks Flow]
    D -->|No| F[Idle Resource]
    E --> G[Process Exit Signal]
    G --> H[stop() → CFRelease()]

2.4 沙盒受限场景下的降级策略:区域截图 vs 全屏截图的权限回退实现

在 iOS 17+ 或 macOS Sandbox 容器中,CGDisplayCreateImageForRect(全屏截图)需 screen-capture entitlement,而区域截图可降级使用 CGWindowListCreateImage(仅需 window-management 权限)。

权限回退决策流程

graph TD
    A[尝试全屏截图] --> B{权限可用?}
    B -->|是| C[执行 CGDisplayCreateImageForRect]
    B -->|否| D[切换区域截图:指定窗口/屏幕坐标]
    D --> E[调用 CGWindowListCreateImage]

降级调用示例

// 优先尝试全屏截图(需 entitlement)
if let fullImage = CGDisplayCreateImage(mainDisplayID) {
    return fullImage
} else {
    // 降级:捕获主屏幕可见窗口区域(无需 screen-capture)
    let options: CGWindowListOption = [.optionOnScreenOnly, .optionIncludingWindowShadow]
    return CGWindowListCreateImage(
        CGRect.null,     // 整个屏幕区域
        options,
        kCGNullWindowID, // 所有窗口
        .optionBestResolution
    )!
}

CGWindowListCreateImageoptions 参数控制可见性范围;CGRect.null 表示捕获整个主屏幕可视内容,不依赖屏幕录制权限。

权限兼容性对比

截图方式 所需 entitlement 沙盒兼容性 输出完整性
全屏截图 screen-capture ❌ 严格受限 ✅ 含 Dock/菜单栏
区域截图(窗口列表) window-management ✅ 默认允许 ⚠️ 不含系统UI层

2.5 沙盒调试技巧:使用 codesign –display 与 sandbox-exec 验证权限执行边界

沙盒调试的核心在于可观测性可验证性——先确认签名权限,再实测策略边界。

查看二进制签名权限

codesign --display --entitlements - /Applications/TextEdit.app

--entitlements - 输出 XML 格式授权列表(如 com.apple.security.app-sandboxcom.apple.security.network.client),是沙盒策略的声明源头;--display 显示签名标识与团队 ID,验证是否为开发者签名或 Mac App Store 分发包。

实时沙盒策略执行验证

sandbox-exec -f /var/db/sandbox/com.apple.TextEdit.sb /Applications/TextEdit.app/Contents/MacOS/TextEdit

-f 指定沙盒配置文件路径(.sb 文件需预编译);若进程因越权访问(如读取 ~/Downloads)被拒,系统日志将记录 SandboxViolation,精准定位策略缺口。

工具 作用阶段 输出关键信息
codesign 编译/分发后 Entitlements 声明、签名有效性
sandbox-exec 运行时验证 策略拒绝日志、实际能力边界
graph TD
    A[App Bundle] --> B{codesign --display}
    B --> C[Entitlements 声明]
    C --> D[sandbox-exec 启动]
    D --> E[系统内核强制策略]
    E --> F[syslog 中 SandboxViolation]

第三章:App Store 隐私合规核心要求落地

3.1 Privacy Manifest 文件结构解析与截图场景字段精准映射(NSPrivacyAccessedAPITypes)

Privacy Manifest 是 iOS 18+ 强制要求的隐私声明文件,以 PrivacyInfo.xcprivacy 命名,采用 XML 格式定义应用实际调用的受监管隐私 API。

核心字段语义对齐

NSPrivacyAccessedAPITypes 数组需逐项对应真实运行时行为,尤其截图场景(如 UIGraphicsImageRenderer, UIScreen.capture)必须显式声明:

<key>NSPrivacyAccessedAPITypes</key>
<array>
  <dict>
    <key>NSPrivacyAccessedAPIType</key>
    <string>NSPrivacyAccessedAPICategoryScreenCapture</string>
    <key>NSPrivacyAccessedAPITypeReasons</key>
    <array>
      <string>564F.1</string> <!-- Screen capture for UI testing & screenshot sharing -->
    </array>
  </dict>
</array>

564F.1 表示“屏幕捕获用于用户主动分享或调试”,不可误用 564F.2(后台录屏)。Xcode 15.3+ 将校验该 reason code 与代码调用上下文的一致性。

常见截图 API 映射表

API 调用方式 对应 NSPrivacyAccessedAPICategory
UIScreen.main.capture() ScreenCapture
UIGraphicsImageRenderer ScreenCapture
AVCaptureScreenInput ScreenCapture + AudioCapture

静态分析验证流程

graph TD
  A[源码扫描截图API调用] --> B{是否含 NSPrivacyAccessedAPITypes?}
  B -->|否| C[编译失败]
  B -->|是| D[匹配 reason code 语义]
  D --> E[通过 App Store Connect 审核]

3.2 NSPermissions 声明实践:NSCameraUsageDescription 与 NSMicrophoneUsageDescription 的语义规避与最小化声明

权限声明的语义边界

iOS 审核要求描述必须精确匹配实际用途,禁止模糊表述(如“用于提升体验”)。NSCameraUsageDescriptionNSMicrophoneUsageDescription 的文案需绑定具体功能动词(如“扫描二维码”“录制语音笔记”),而非泛化能力。

最小化声明实践

  • ✅ 正确:仅在 Info.plist 中声明当前版本真正调用的权限
  • ❌ 错误:预埋未使用的权限键(即使代码未调用,App Store 仍会触发隐私清单审查)

典型错误配置示例

<!-- Info.plist 片段 -->
<key>NSCameraUsageDescription</key>
<string>访问相机以支持多种功能</string> <!-- 语义模糊,审核拒收 -->
<key>NSMicrophoneUsageDescription</key>
<string>需要麦克风权限</string> <!-- 缺失用途动词,不通过 -->

逻辑分析NSCameraUsageDescription 的字符串值是 App 审核时人工审阅项,系统不校验内容真实性;但 Apple 隐私团队会比对文案与实际功能流。参数 string 必须为用户可理解的自然语言,且与 AVCaptureDevice.requestAccess(for:) 调用上下文严格一致。

审核维度 合规文案示例 违规特征
相机用途 “扫描商品条形码以快速下单” 使用“可能”“未来”等不确定性词汇
麦克风用途 “录制会议语音并转文字存档” 混用“录音”“通话”等非当前功能场景

3.3 隐私清单动态校验:基于 go-runewidth 与 plist 库的自动化 manifest 合规性扫描工具开发

为精准识别 iOS/macOS Info.plist 中隐私权限键(如 NSCameraUsageDescription)的描述文本宽度合规性,需兼顾 Unicode 双宽字符(如中文、Emoji)的真实显示长度。

核心校验逻辑

import "github.com/mattn/go-runewidth"

func isDescriptionWidthValid(desc string) bool {
    // runewidth.StringWidth 计算视觉列宽(非 rune 数)
    return runewidth.StringWidth(desc) <= 255 // Apple 官方限制
}

runewidth.StringWidth() 正确处理 CJK 字符、全角标点及组合 Emoji(如 👨‍💻),避免 len([]rune(desc)) 的语义误判。

支持的隐私键类型

权限类型 plist 键名 是否强制描述
相机 NSCameraUsageDescription
通讯录 NSContactsUsageDescription
本地网络 NSLocalNetworkUsageDescription

扫描流程

graph TD
    A[加载 Info.plist] --> B[解析 XML/二进制 plist]
    B --> C[提取所有 *UsageDescription 键值]
    C --> D[逐项 runewidth 校验]
    D --> E[生成 JSON 报告含违规位置与截断建议]

第四章:Apple 公证(Notarization)全流程工程化实践

4.1 Go 构建产物签名链构建:codesign + notarytool 的 CI/CD 自动化流水线设计

为保障 macOS 上 Go 应用分发可信性,需构建「二进制签名 → Apple 公证 → 全平台分发」的完整签名链。

核心流程概览

graph TD
    A[Go build -o app] --> B[codesign --sign 'Developer ID Application' app]
    B --> C[notarytool submit app --keychain-profile NotaryAPI --wait]
    C --> D[staple app]

关键步骤实现

# 签名前校验 entitlements 并注入 hardened runtime
codesign --sign "Developer ID Application: Acme Inc (ABC123)" \
         --entitlements entitlements.plist \
         --options=runtime \
         --timestamp \
         ./dist/myapp

--options=runtime 启用运行时防护(如 library validation);--timestamp 确保签名长期有效;--entitlements 声明所需系统权限(如辅助功能、网络)。

公证与钉扎自动化

步骤 工具 关键参数
提交公证 notarytool --keychain-profile 指向已配置的 API 凭据
轮询状态 内置 --wait 避免手动轮询,超时自动失败
钉扎公证票证 xattr -wx com.apple.security.assessment.timestamp ... stapler staple 封装为单命令

凭据安全实践

  • 使用 GitHub Secrets 或 HashiCorp Vault 注入 NOTARY_API_KEY, NOTARY_ISSUER_ID, NOTARY_KEYCHAIN_PROFILE
  • CI 中动态创建临时 keychain,避免凭据残留

4.2 公证失败典型日志诊断:从 “ITMS-90296” 到 “ITMS-90683” 的 Go 二进制特有报错归因分析

Go 构建的 macOS 应用在公证(Notarization)阶段频繁触发 Apple 的静态检查拦截,根源在于其默认链接行为与 Apple 安全策略的隐式冲突。

ITMS-90296:缺失 com.apple.security.app-sandbox 权限

此错误常被误判为权限缺失,实则因 Go 运行时动态加载符号时未声明 entitlements.plist 中的 com.apple.security.cs.allow-jit(macOS 12+ 要求):

<!-- entitlements.plist -->
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>com.apple.security.app-sandbox</key>
  <true/>
  <key>com.apple.security.cs.allow-jit</key>
  <true/> <!-- Go runtime 依赖 JIT 编译器模拟栈保护 -->
</dict>
</plist>

allow-jit 是 Go 1.21+ 在 macOS 上启用 runtime/cgo 和信号处理的关键开关;缺失将导致公证链在 codesign --verify --deep --strict 阶段拒绝签名。

常见错误码归因对照表

错误码 根本原因 Go 特定诱因
ITMS-90296 沙盒权限不完整 allow-jitallow-unsigned-executable-memory 缺失
ITMS-90683 Info.plist 中 CFBundleExecutable 指向非 Mach-O go build -buildmode=c-archive 生成非可执行产物

公证失败决策流

graph TD
  A[提交公证] --> B{是否含有效 entitlements?}
  B -->|否| C[ITMS-90296]
  B -->|是| D{CFBundleExecutable 是否 Mach-O?}
  D -->|否| E[ITMS-90683]
  D -->|是| F[通过]

4.3 签名完整性验证工具链:基于 go-syscall 和 security framework 的本地公证状态校验器实现

该工具链在 macOS 平台实现轻量级本地签名验证,绕过远程公证服务依赖,直接调用系统底层接口完成 SecStaticCodeCreateWithPathSecStaticCodeCheckValidity 调用。

核心验证流程

// 创建静态代码引用(指向待验二进制)
code, err := syscall.SecStaticCodeCreateWithPath("/usr/local/bin/app", 0)
// 执行完整性与公证状态联合校验
status, err := syscall.SecStaticCodeCheckValidity(code,
    syscall.kSecCSBasicValidateOnly|syscall.kSecCSCheckAllArchitectures,
    &syscall.CSFlags{ // 指定校验策略
        RequireHardenedRuntime: true,
        RequireLibraryValidation: true,
        RequireNotarization: true, // 强制检查公证戳
    })

逻辑分析:kSecCSCheckAllArchitectures 确保多架构 Fat Binary 全路径校验;RequireNotarization 触发 com.apple.security.notarization 属性解析,失败时返回 errSecCSUnsignederrSecCSNotarized

验证结果映射表

状态码 含义 公证就绪
errSecSuccess 签名有效且已公证
errSecCSNotarized 签名有效但未公证
errSecCSUnsigned 无有效签名

数据同步机制

  • 通过 SecTrustSettingsCopyCertificates() 动态加载系统信任锚点
  • 利用 kSecCSRestrictToSystem 标志限制仅使用 Apple Root CA 证书链
graph TD
    A[输入二进制路径] --> B[SecStaticCodeCreateWithPath]
    B --> C[SecStaticCodeCheckValidity]
    C --> D{RequireNotarization?}
    D -->|true| E[解析com.apple.security.notarization]
    D -->|false| F[仅验证签名链]

4.4 多架构支持与公证兼容性:darwin/arm64 与 darwin/amd64 双目标构建与 stapler 集成策略

为满足 Apple Silicon 与 Intel Mac 的全平台分发需求,需在单次构建中生成 darwin/arm64darwin/amd64 双架构二进制,并确保通过 Apple Notarization(公证)。

构建策略

使用 go buildGOOS=darwin + GOARCH 组合交叉编译:

# 并行构建双架构 fat binary(通过 lipo 合并)
GOOS=darwin GOARCH=arm64 go build -o bin/app-arm64 .
GOOS=darwin GOARCH=amd64 go build -o bin/app-amd64 .
lipo -create bin/app-arm64 bin/app-amd64 -output bin/app

此流程生成通用二进制(Universal 2),lipo -info bin/app 可验证双架构存在;-o 指定输出路径,避免覆盖;GOARM 等参数在此场景无需设置(仅影响 linux/arm)。

公证关键约束

要求 说明
签名必须含 --deep 确保嵌入式框架、资源均签名
stapler 需传 -r 强制重签名以适配公证后回滚机制

stapler 集成流程

graph TD
    A[构建双架构二进制] --> B[ad-hoc 签名]
    B --> C[上传至 Apple Notary Service]
    C --> D[下载公证票证]
    D --> E[stapler staple -r bin/app]

第五章:从审核通过到持续交付的演进思考

在某大型金融级SaaS平台V3.2版本上线过程中,团队经历了典型的“审核通过即终点”到“交付即起点”的认知跃迁。该产品需通过银保监会备案审核、等保三级复测及内部安全红线评审三重关卡,2023年Q3初版包于7月12日获得全部合规签章——但此时距业务方要求的8月1日灰度启动仅剩20天。

审核材料反哺自动化流水线

团队将备案文档中明确的27项运行时安全约束(如JWT令牌最大有效期≤3600s、敏感字段AES-256-GCM加密、审计日志保留≥180天)转化为流水线中的可执行检查项。例如,在CI阶段嵌入如下策略校验脚本:

# 验证Spring Boot配置中token过期时间
grep -r "spring.security.jwt.expiration" ./src/main/resources/ | \
  awk -F'=' '{print $2}' | sed 's/[^0-9]//g' | \
  while read sec; do [[ $sec -gt 3600 ]] && echo "FAIL: JWT expiry > 3600s" && exit 1; done

灰度发布与合规状态动态对齐

为满足监管要求的“最小可控变更”,团队设计了四层发布门禁: 环境层级 流量比例 合规验证方式 自动化触发条件
Canary集群 0.5% 实时扫描HTTP响应头X-Content-Security-Policy Prometheus告警阈值>0.1% CSP违规率
区域A生产 15% 每小时调用等保API校验日志留存完整性 日志服务返回HTTP 200且size≥12MB/h
全量生产 100% 每日02:00执行GDPR数据映射表比对 对比结果diff行数=0

监控告警驱动的闭环反馈

当2023年7月28日区域A生产环境出现/api/v3/report/export接口平均延迟突增至842ms(SLA阈值为≤300ms)时,系统自动触发三级响应:①立即熔断该接口并切换至降级CSV模板;②调用合规审计服务确认降级方案是否符合《金融数据输出安全管理规范》第5.2条;③向架构委员会推送含链路追踪ID(trace-id: tr-8a9f2c1e)的根因分析报告。最终定位为新引入的PDF水印组件未适配ARM64架构导致CPU争用,修复后2小时内完成全量热更新。

组织协同机制重构

建立跨职能“交付健康度看板”,集成Jira需求状态、SonarQube技术债趋势、Prometheus SLO达成率、合规扫描平台漏洞计数四大维度。每周站会强制要求安全工程师解读最新CVE影响矩阵,运维负责人同步基础设施变更窗口,产品经理确认业务功能验收用例覆盖率——所有动作均关联至同一Git Commit Hash,实现责任原子化追溯。

技术债偿还的量化锚点

将每次审核发现的问题(如等保测评中指出的“密码修改未强制旧密码验证”)登记为Jira技术债任务,并绑定到对应微服务的Pipeline Stage。当该服务下一次CD成功执行时,自动关闭关联债务项,同时生成《合规改进闭环证明》PDF存档至区块链存证平台,哈希值同步写入监管报送系统。

这种以审核结论为输入、以交付质量为输出的正向循环,使该平台后续版本平均合规准备周期缩短63%,2023年Q4累计完成17次生产变更且零监管通报。

用实验精神探索 Go 语言边界,分享压测与优化心得。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注