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 仅在明确支持的旧浏览器启用。

官方延伸阅读 ​

本站内容仅供学习与参考