DocumentViewPicker+FileIO:3DGS质检CSV保存授权与公式注入阻断【鸿蒙心迹】
团队做完一批 3DGS 采集帧的人工质检,通常还会让人把判定结果导出去。看起来不过是四行 CSV:帧号、文件名、结论、备注。可真正准备给运营或者外部协作者时,麻烦反而来了。第一,用户保存的文件应该落在哪里,应用不能假设拥有任意公共目录的写权限。第二,备注里如果包含以等号开头的公式样式文本,交给电子表格打开时可能被解释为公式。第三,保存对话框刚出现不代表字节已经写入,更不等于对方打开文件后内容正确。
我把这几个边界放进 ExportGuard,任务编号 EX-1042,只处理四条模拟质检记录;其中一条备注以 =HYPERLINK 开头。它是应用内导出流程示例,不调用真实 3DGS 重建 SDK,也不声称完成任何设备上的空间重建。目标是让“生成数据”“用户选保存位置”“获得 URI”“写入字节”“完成提示”成为五个可以审计的独立阶段。

一、CSV不是普通拼接,先把内容风险关在文件外
CSV 最容易犯的错,是直接把数组 join(','),认为双引号只在文本包含逗号时才有用。实际项目中的备注可能带有换行、双引号,甚至来自外部标注;若没有转义,表格读入时会把一行拆成多个单元格。另一个坑是以 = + - @ 起始的单元格。部分表格软件可能把它当表达式处理,所以不能只在界面上隐藏或者把前缀删掉,应该在导出边界做有意识的“按文本解释”处理。
CSV 还有编码上的现实问题:文本编辑器通常能识别 UTF-8,部分电子表格工具却会依据地区设置猜测字符集和分隔符。如果外部协作者在不同系统上打开同一文件,字段错位、中文乱码和公式误解释可能一起出现。工程验收应说明文件采用的编码、行结束符以及预期导入方式,而不是只要生成了扩展名 .csv 就宣布完成。
这里采用保守策略:全部字段加双引号、字段内部的双引号翻倍;先去掉控制字符中不需要的零字节,再对首个非空白字符属于公式触发符号的内容加前导单引号。这样能够降低通用电子表格自动解释的风险,但不是能覆盖所有办公软件和本地化导入方式的安全证明。高风险数据还应把导入软件、编码和默认公式策略纳入验收。
interface AuditRow {
frame: string;
file: string;
verdict: string;
note: string;
}
const sample: AuditRow[] = [
{ frame: 'F001', file: 'frame_001.jpg', verdict: '通过', note: '纹理稳定' },
{ frame: 'F002', file: 'frame_002.jpg', verdict: '复核', note: '=HYPERLINK("url")' },
{ frame: 'F003', file: 'frame_003.jpg', verdict: '通过', note: '曝光正常' },
{ frame: 'F004', file: 'frame_004.jpg', verdict: '通过', note: '遮挡可接受' }
];
function csvCell(input: string): string {
const clean = input.replace(/\u0000/g, '');
const asText = /^[\s]*[=+\-@]/.test(clean) ? "'" + clean : clean;
return '"' + asText.replace(/"/g, '""') + '"';
}
function buildCsv(rows: AuditRow[]): string {
const header = ['帧号', '文件名', '结论', '备注'].map(csvCell).join(',');
const body = rows.map((r: AuditRow) =>
[r.frame, r.file, r.verdict, r.note].map(csvCell).join(','));
return [header, ...body].join('\r\n') + '\r\n';
}
这个函数有两个刻意的限制。其一,单引号前缀是面向表格查看的兼容性方案,不是所有 CSV 解析器都会按同一规则还原字符串;如需无损往返,应另外提供 JSON 或约定一套严格的导入契约。其二,示例四条记录是固定模拟输入,没有验证图片来源、帧连续性或真实重建质量,不能把“导出成功”当成“质检合格”。
二、用 DocumentViewPicker 让用户决定文件归宿
不要把 downloads/ 之类的硬编码路径当成三方应用可写的公共目录。DocumentViewPicker 的价值,是由系统交互把保存目标 URI 交回给应用,开发者再在授权范围内访问这个目标。保存器的返回值是用户选择的入口,不是应用对整个目录的永久授权。用户取消操作、能力不支持或者 URI 访问失败,都应该正常退出并给出合适提示。
下面是 Stage 模型页面的简化调用。getHostContext() 需要转换并确认是 UIAbilityContext;实际项目还应与所选 SDK 的 DocumentSaveOptions 定义核对文件名和后缀配置。我们只申请一个文件名,不预设用户的目录,不尝试扫描系统其他路径。
import { common } from '@kit.AbilityKit';
import { picker } from '@kit.CoreFileKit';
// 位于 ExportPage 的一个 async 成员方法内
private async selectCsvTarget(): Promise<string | null> {
const context = this.getUIContext().getHostContext() as common.UIAbilityContext;
if (!context) return null;
const documentPicker = new picker.DocumentViewPicker(context);
try {
const picked = await documentPicker.save({
newFileNames: ['gs_quality_1042.csv'],
fileSuffixChoices: ['CSV|.csv']
});
return picked.length > 0 ? picked[0] : null;
} catch (e) {
// 包括用户取消;业务上视为未获得可写目标。
return null;
}
}
我更愿意把流程状态明确设计为 PREPARED → PICKER_PENDING → WRITING → COMPLETED,异常进入 FAILED,取消进入 CANCELLED。画面中展示的“等待保存位置”就对应 PICKER_PENDING:四条内容在内存中已经编码,但尚无理由告诉用户“导出完成”。系统对话框显示期间,页面也不应再次发起第二次保存流程,否则回调可能相互覆盖。
三、字节写完才确认;失败时别留下模糊结果
文件读写是另一道边界。得到 URI 后,调用 FileIO openSync 获得文件描述符,用 writeSync 将 UTF-8 字节写入。一次写操作的返回值是已写入字节数,不应想当然把一次调用当成全部写完;文件描述符必须在 finally 中关闭。为兼容常用表格工具,这里在 UTF-8 内容前加 BOM,但它是否合适取决于对方的系统与导入链路。
import { fileIo } from '@kit.CoreFileKit';
import { util } from '@kit.ArkTS';
// 需要在拿到 DocumentViewPicker 返回的 URI 后执行。
function writeCsvToUri(uri: string, csv: string): number {
const bytes = new util.TextEncoder().encodeInto('\uFEFF' + csv);
const file = fileIo.openSync(uri,
fileIo.OpenMode.WRITE_ONLY | fileIo.OpenMode.TRUNC);
let offset = 0;
try {
while (offset < bytes.byteLength) {
const rest = bytes.slice(offset);
const count = fileIo.writeSync(file.fd, rest.buffer);
if (count <= 0) throw new Error('CSV写入未继续推进');
offset += count;
}
fileIo.fsyncSync(file.fd);
return offset;
} finally {
fileIo.closeSync(file.fd);
}
}
fsyncSync 可以用于表达应用要求写回的意图,但不能据此扩展成所有介质、文件提供方和突然断电情形下的绝对持久化承诺。尤其是公共保存 URI 可能对应不同底层提供者,能力是否支持必须在实际目标环境验证。更严格的“原子替换”需要先在应用沙箱完整生成,再按目标提供方支持的协议保存;不能简单把沙箱 rename 规则照搬到用户选定的 URI 上。

示例截图展示的任务 EX-1042、文件 gs_quality_1042.csv、四条记录和一条公式样式备注,只是静态演示材料。HiLog 用“csvReady rows=4 escaped=1”和“waiting for picker”提醒开发者分开记录编码与保存阶段,并没有声称真实保存对话框已被拉起、文件真的落盘。生产版本不能把 URI 或用户完整备注随意输出到日志,尤其不能把外部导出的私人路径传播给分析后台。
如果是长时间导出任务,还要记录当前请求令牌,避免用户离开页面后继续弹出成功提示。对于导出失败,界面应清楚说明“保存未完成”;如果目标文件已经创建但只写入了部分内容,需要向用户提供重新导出和检查文件的方法,而不能悄悄当作成功。系统 Picker 的授权解决不了业务原子性,CSV 转义也解决不了访问控制,职责边界必须逐条落实。

验收时我建议把四种输入放进回归样本:正常中文、带英文逗号的备注、带双引号的备注、以公式触发符号开头的备注。检查表头字段数、文件编码、单元格原文和取消后 UI 状态,同时记录不同电子表格软件的解析差异。手机端还要覆盖用户取消、选错目录、存储空间不足、写入中断以及页面后台切换。没有跑这些检查前,文章只能称为可参考的工程方案,不能当成一次已完成的系统集成验收。
这件事把导出做成可回放的业务事件也有价值:记录任务 ID、样例行数、生成摘要和最后一个完成阶段,而非记录完整行内容。在产品排错中,我们希望回答“用户是否取消了 Picker”“是否拿到 URI”“写入是否达到预期字节数”,不希望靠捕获用户私有文件路径来定位问题。对于资料敏感的质检单,日志最小化与 CSV 公式防注入一样重要,后者保护接收者,前者保护导出者。
这件事最后其实不是“怎么生成 CSV”。真正重要的是,把数据解释权、目标访问权和写入完成权拆成三个确认点:业务生成可信的导出内容;系统交互只赋予本次选中的保存目标;写入与释放流程再决定能不能展示完成。对 3DGS 质检如此,对图片批量审核、教学作业报告和普通运营名单也一样。把边界收紧,才有资格让用户放心把文件交出去。
技术与素材边界:本篇未调用空间重建 SDK,所有质检内容是人工构造的四行样例;未做 DevEco 编译、真实 DocumentViewPicker 选择或实体存储写入验证。三张独立 PNG 是示意图,不是真机、实测日志或官方审核凭证。
更多推荐




所有评论(0)