文件、下载与通知
Aras 中的文件不是某个网页目录下的静态资源。业务 Item 通常关联 File Item,而物理内容由 Vault Server 和 Vault 管理。上传、下载和通知都应保留登录用户权限、文件元数据与审计链。
适用版本: File / Vault / Notification 模型以 R31 为主;浏览器文件选择、下载和邮件 helper 需由目标 Release adapter 实现。
执行层: 客户端选择与反馈、服务端授权与关联、Vault 物理存储;数据库层不直接写系统容器。
主要依据: R31 原厂手册 Unit 16-18、平台 Vault 文档,以及本项目提供的上传、下载、邮件和通知素材。
文件模型
标注:Stable
一个完整的文件操作至少涉及:
- 用户或服务端选择物理文件。
- Vault 接收内容并创建
FileItem。 File作为属性或关系连接到业务容器 Item。- 失败时清理未关联文件,或由受控的 orphan cleanup 处理。
- 下载前再次执行业务 Item、File 与 Vault 的权限检查。
R31 原厂配置手册把 File、Container、Vault 与 Replication 分成独立主题。不要把它们简化成 <a href="File/...">。
客户端上传:把版本差异隔离起来
标注:Classic client compatibility
不同 Release 的文件选择对象和 Vault API 可能不同,因此业务 Method 只调用 adapter:
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 下载能力生成受控下载;不要拼静态相对路径。
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(诊断流程)
非管理员上传失败时,按下面顺序定位:
- 记录 HTTP 状态、Aras error detail、File Item 和容器 Item 类型。
- 用同一个普通用户确认容器 Item 的 Get/Update 权限。
- 检查 File 属性或 RelationshipType、默认 Permission 和目标 Vault 配置。
- 检查 Vault Server 日志、写入路径、服务账号和磁盘空间。
- 对比管理员与普通用户产生的 AML 请求,不直接对比数据库行。
- 在 LDE 重现并通过配置或受支持 package 修复。
禁止直插系统容器
不要向 SystemFileContainer 等系统表写固定 ID,也不要复制另一个数据库的 Permission ID。这样的“修复”绕过元数据、权限和包依赖,重复执行还可能制造主键冲突。
优先使用配置通知
标注:Stable
生命周期或工作流上的固定通知,优先创建 E-Mail Message,再把它配置到 Lifecycle state、transition 或 Workflow Activity。好处是:
- 收件 Identity 与流程配置可审查;
- 模板和变量不埋在 C# 中;
- 环境迁移可以进入 Package;
- 测试可以固定输入和输出。
代码发送只用于配置无法表达的动态收件人、动态附件或外部系统整合。
服务端发送邮件的安全结构
标注:Stable(结构) + 版本化邮件 API
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 自己寻找窗口:
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)
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") 且不清理 textarea | Classic client compatibility | 旧 API、焦点干扰、DOM 泄漏 |
验证清单
- [ ] 普通用户完成上传、关联、保存、重新打开和下载全链路测试。
- [ ] 取消保存、上传超时和关联失败不会留下不可控孤儿。
- [ ] 文件类型、大小和内容检测在服务端执行。
- [ ] 下载重新验证业务 Item 权限。
- [ ] 邮件模板、Identity 和 File 都通过 ID/唯一查询定位。
- [ ] HTML 正文不会把用户输入直接当作 markup。
- [ ] Clipboard fallback 仅在明确支持的旧浏览器启用。
