Skip to content

文件、下载与通知

Aras 中的文件不是某个网页目录下的静态资源。业务 Item 通常关联 File Item,而物理内容由 Vault Server 和 Vault 管理。上传、下载和通知都应保留登录用户权限、文件元数据与审计链。

适用版本: File / Vault / Notification 模型以 R31 为主;浏览器文件选择、下载和邮件 helper 需由目标 Release adapter 实现。

执行层: 客户端选择与反馈、服务端授权与关联、Vault 物理存储;数据库层不直接写系统容器。

主要依据: R31 原厂手册 Unit 16-18、平台 Vault 文档,以及本项目提供的上传、下载、邮件和通知素材。

文件模型

标注:Stable

一个完整的文件操作至少涉及:

  1. 用户或服务端选择物理文件。
  2. Vault 接收内容并创建 File Item。
  3. File 作为属性或关系连接到业务容器 Item。
  4. 失败时清理未关联文件,或由受控的 orphan cleanup 处理。
  5. 下载前再次执行业务 Item、File 与 Vault 的权限检查。

R31 原厂配置手册把 File、Container、Vault 与 Replication 分成独立主题。不要把它们简化成 <a href="File/...">

客户端上传:把版本差异隔离起来

标注:Classic client compatibility

不同 Release 的文件选择对象和 Vault API 可能不同,因此业务 Method 只调用 adapter:

js
async function attachSelectedFile(fileAdapter, parentItem) {
  if (!fileAdapter || !parentItem) {
    throw new Error("缺少文件适配器或父 Item。");
  }

  const selected = await fileAdapter.selectOne();
  if (!selected) {
    return null;
  }

  const allowedExtensions = new Set(["pdf", "docx", "xlsx"]);
  const extension = selected.name.split(".").pop().toLowerCase();

  if (!allowedExtensions.has(extension)) {
    throw new Error("不支持此文件类型。");
  }

  if (selected.size > 20 * 1024 * 1024) {
    throw new Error("文件超过允许的大小。");
  }

  return fileAdapter.uploadAndAttach({
    parentItem,
    propertyName: "attachment_id",
    file: selected,
  });
}

uploadAndAttach 必须由目标 Release 的受支持 API 实现,并保证:

  • 物理文件上传成功后才返回 File ID;
  • 父 Item 关联失败时不会静默留下孤儿;
  • 错误对象来自真实上传结果,而不是引用另一个未定义变量;
  • 文件名只作为元数据,不参与服务器路径拼接;
  • MIME、扩展名、大小和恶意文件检测在服务端再次执行。

不要只创建 File Item

单独 newItem("File", "add") 并不能证明物理内容已经写入正确 Vault,也不能证明 File 已连接到业务容器。上传测试必须重新查询关系,并实际下载校验内容。

下载文件

标注:Stable(权限原则) + 版本化 API

业务代码应传递 File Item ID,让 adapter 使用当前会话和 Vault 下载能力生成受控下载;不要拼静态相对路径。

js
async function downloadAttachment(fileAdapter, fileId) {
  if (!/^[0-9a-f]{32}$/i.test(fileId || "")) {
    throw new Error("File ID 格式无效。");
  }

  await fileAdapter.download(fileId);
}

adapter 的服务端必须确认:调用者能读取承载该 File 的业务 Item;仅能 Get File Item 并不一定等同于能读取所有业务附件。

File/Vault 故障诊断

标注:Stable(诊断流程)

非管理员上传失败时,按下面顺序定位:

  1. 记录 HTTP 状态、Aras error detail、File Item 和容器 Item 类型。
  2. 用同一个普通用户确认容器 Item 的 Get/Update 权限。
  3. 检查 File 属性或 RelationshipType、默认 Permission 和目标 Vault 配置。
  4. 检查 Vault Server 日志、写入路径、服务账号和磁盘空间。
  5. 对比管理员与普通用户产生的 AML 请求,不直接对比数据库行。
  6. 在 LDE 重现并通过配置或受支持 package 修复。

禁止直插系统容器

不要向 SystemFileContainer 等系统表写固定 ID,也不要复制另一个数据库的 Permission ID。这样的“修复”绕过元数据、权限和包依赖,重复执行还可能制造主键冲突。

优先使用配置通知

标注:Stable

生命周期或工作流上的固定通知,优先创建 E-Mail Message,再把它配置到 Lifecycle state、transition 或 Workflow Activity。好处是:

  • 收件 Identity 与流程配置可审查;
  • 模板和变量不埋在 C# 中;
  • 环境迁移可以进入 Package;
  • 测试可以固定输入和输出。

代码发送只用于配置无法表达的动态收件人、动态附件或外部系统整合。

服务端发送邮件的安全结构

标注:Stable(结构) + 版本化邮件 API

csharp
Innovator inn = this.getInnovator();

Item template = inn.newItem("EMail Message", "get");
template.setAttribute("select", "id,name");
template.setProperty("name", "Example Review Completed");
template = template.apply();

if (template.isError())
{
    return inn.newError("邮件模板查询失败,请联系管理员并提供操作时间。");
}

if (template.getItemCount() != 1)
{
    return inn.newError("邮件模板必须唯一。");
}

Item recipient = inn.newItem("Identity", "get");
recipient.setAttribute("select", "id,name");
recipient.setProperty("name", "Example Reviewers");
recipient = recipient.apply();

if (recipient.isError() || recipient.getItemCount() != 1)
{
    return inn.newError("收件 Identity 无效。");
}

// 由目标 Release 的 mailAdapter 使用已验证的公开或受支持接口发送。
return mailAdapter.SendTemplate(template, recipient, this);

mailAdapter 是项目边界。不要在一份 Method 里混用多个互斥邮件 API,也不要用未定义的数组、循环索引和模板变量。

邮件内容

  • 对主题和纯文本内容做长度限制。
  • 如果模板允许 HTML,只让受信模板产生 HTML;用户输入必须编码。
  • 附件只能来自经授权的 File Item,不接受任意服务器磁盘路径。
  • 发送失败要返回明确错误或进入可重试队列,不能只写 Console

页面通知

标注:Classic client compatibility

把成功、警告和错误统一封装,避免每个 Method 自己寻找窗口:

js
function notifyUser(level, message) {
  const text = String(message ?? "");
  const arasApi = top.aras;

  if (level === "success") {
    return arasApi.AlertSuccess(text);
  }

  if (level === "warning") {
    return arasApi.AlertWarning(text);
  }

  return arasApi.AlertError(text);
}

如果目标 Release 有 context notification API,可由 adapter 映射成非模态消息;业务代码不应直接操作通知组件 DOM。

复制到剪贴板

标注:Stable(Web API)

js
async function copyText(text) {
  const value = String(text ?? "");
  if (!value) {
    throw new Error("没有可复制的内容。");
  }

  if (!navigator.clipboard?.writeText) {
    throw new Error("当前浏览器不支持安全 Clipboard API。");
  }

  await navigator.clipboard.writeText(value);
}

copyText("Example text")
  .then(() => notifyUser("success", "已复制到剪贴板。"))
  .catch((error) => notifyUser("error", error.message));

Clipboard API 通常要求 HTTPS 或安全上下文。旧的 document.execCommand("copy") 只能作为有测试的兼容 fallback,并应及时移除临时 DOM。

为什么不要这样做

原始模式标注风险
下载硬编码的相对音乐文件Reject与 Vault 无关,绕过业务授权和文件元数据
直接插入系统 File Container 表Reject固定 ID、权限错配、系统数据损坏
上传失败时读取未定义的结果变量Reject真正错误被新的 JavaScript 异常覆盖
只创建 File 不关联容器Reject产生孤儿 File,用户也无法从业务 Item 找到它
从任意服务器路径发送附件Reject文件泄露与路径访问风险
提升到高权限 Identity 后忘记释放Reject后续代码在超出预期的权限下运行
execCommand("copy") 且不清理 textareaClassic client compatibility旧 API、焦点干扰、DOM 泄漏

验证清单

  • [ ] 普通用户完成上传、关联、保存、重新打开和下载全链路测试。
  • [ ] 取消保存、上传超时和关联失败不会留下不可控孤儿。
  • [ ] 文件类型、大小和内容检测在服务端执行。
  • [ ] 下载重新验证业务 Item 权限。
  • [ ] 邮件模板、Identity 和 File 都通过 ID/唯一查询定位。
  • [ ] HTML 正文不会把用户输入直接当作 markup。
  • [ ] Clipboard fallback 仅在明确支持的旧浏览器启用。

官方延伸阅读

本站内容仅供学习与参考