前言
在日常运维中,我们遇到过这样一个问题:致远 OA 中积累了大量不再需要的文件,只在系统界面里逐个处理效率很低,而直接进入服务器删除文件,又很容易因为逻辑文件名、数据库记录和磁盘物理文件之间的关系不清楚而误操作。
因此,这项工作的第一步不是写工具,而是手动查询数据库,把目录、文件记录和服务器物理文件之间的关系逐层确认。等删除逻辑经过实际数据验证后,再将整个过程封装成 Windows Server 桌面工具,提高重复操作的效率和可控性。
本文涉及数据库记录和服务器文件删除。所有操作都应在获得授权的环境中进行,并在生产操作前完成数据库及文件备份。不同致远 OA 版本的表结构可能不同,文中的字段关系需要结合实际环境核对。

一、从文件夹名称定位目录 ID
最开始只知道 OA 界面中显示的文件夹名称,例如“产品承认书”。第一步是在 DOC_RESOURCES 中确认它对应的目录记录:
SELECT
ID,
FR_NAME AS 目录名称,
PARENT_FR_ID AS 父目录ID,
LOGICAL_PATH AS 完整路径
FROM DOC_RESOURCES
WHERE FR_NAME = N'产品承认书'
AND IS_FOLDER = 1;
这里有几个重要字段:
ID:当前资源或目录的逻辑 ID。PARENT_FR_ID:父目录 ID,用于建立目录层级。IS_FOLDER:是否为文件夹,值为1时表示目录。LOGICAL_PATH:OA 内部保存的逻辑路径。
这条 SQL 很快暴露了第一个风险:同名文件夹可能出现在多个位置。只按 FR_NAME 查询,会把所有同名目录都找出来。如果继续批量删除,它们下面的文件可能一起被处理。
后续工具因此增加了多级目录查询。例如输入:
文控中心/PCB图纸
程序会先把“PCB图纸”作为目标目录,再通过 PARENT_FR_ID 向上校验它的父目录必须是“文控中心”,从而避免误匹配其他位置的同名文件夹。
二、先查询直属文件,再扩展到全部子目录
拿到文件夹 ID 后,可以先查询它的直属内容:
SELECT
ID,
FR_NAME AS 名称,
IS_FOLDER AS 是否文件夹,
PARENT_FR_ID AS 父目录ID,
FR_SIZE AS 文件大小,
CREATE_TIME AS 创建时间,
LAST_UPDATE AS 最后修改时间
FROM DOC_RESOURCES
WHERE PARENT_FR_ID = 451823763545606013
ORDER BY IS_FOLDER DESC, FR_NAME;
这条 SQL 与 OA 当前目录列表比较接近:先显示文件夹,再按名称显示文件。但它只能查询一层。如果目标目录下面还有多层子目录,子目录中的文件不会被查出来。
为此,需要使用递归 CTE:
WITH FolderRoots AS (
SELECT
ID, FR_NAME, PARENT_FR_ID, IS_FOLDER, SOURCE_ID, FR_SIZE
FROM DOC_RESOURCES
WHERE ID = @目标文件夹ID
),
ResourceTree AS (
SELECT
ID, FR_NAME, PARENT_FR_ID, IS_FOLDER, SOURCE_ID, FR_SIZE
FROM FolderRoots
UNION ALL
SELECT
child.ID,
child.FR_NAME,
child.PARENT_FR_ID,
child.IS_FOLDER,
child.SOURCE_ID,
child.FR_SIZE
FROM DOC_RESOURCES AS child
INNER JOIN ResourceTree AS parent
ON child.PARENT_FR_ID = parent.ID
)
SELECT
ID,
FR_NAME,
SOURCE_ID,
FR_SIZE
FROM ResourceTree
WHERE IS_FOLDER = 0
OPTION (MAXRECURSION 32767);
这样就能从目标目录开始,递归遍历所有子文件夹,最终只返回文件记录。
开发过程中曾出现过两次典型错误:新增物理文件查询时忘记把 SOURCE_ID 放进递归结果;新增大小统计时又忘记传递 FR_SIZE。外层查询引用不到字段后,SQL Server 会报“列名无效”。递归 CTE 每一层的字段数量、顺序和类型必须完全对应,这是扩展查询时需要特别注意的地方。
三、从逻辑文件找到物理文件 ID
DOC_RESOURCES.ID 是 OA 资源记录的逻辑 ID,并不是服务器磁盘上的文件名。继续检查表结构后,我们确认了以下映射:
DOC_RESOURCES.SOURCE_ID -> CTP_FILE.ID
于是可以关联 CTP_FILE 查询物理文件 ID 和源文件大小:
SELECT
resource.ID AS 文件ID,
resource.FR_NAME AS 文件名称,
fileInfo.ID AS 物理文件ID,
COALESCE(fileInfo.FILE_SIZE, resource.FR_SIZE, 0) AS 文件大小
FROM DOC_RESOURCES AS resource
LEFT JOIN CTP_FILE AS fileInfo
ON fileInfo.ID = resource.SOURCE_ID
WHERE resource.ID = @文件ID;
这里最终在工具界面保留了三个最有用的字段:文件 ID、文件名称、物理文件 ID。文件大小作为隐藏字段用于生成删除统计,不占用列表显示空间。
四、确认服务器上的两份物理数据
数据库关系明确后,还需要到 OA 服务器上核对实际文件。我们的环境中有两处需要处理:
upload 根目录
└─ 年
└─ 月
└─ 日
└─ 物理文件ID
officetrans 根目录
└─ 年月日
└─ 物理文件ID文件夹
upload 中保存源文件,文件名就是物理文件 ID。officetrans 中则是同名的物理 ID 文件夹,里面保存 OA 预览或转换产生的数据。
最初只删除 upload 源文件时,OA 中仍可能通过 officetrans 内容打开或显示文件。因此,一次完整清理需要处理三个位置:
- 删除
upload下匹配物理 ID 的源文件。 - 递归删除
officetrans日期目录下匹配物理 ID 的文件夹。 - 删除
DOC_RESOURCES中对应的 OA 显示记录。
工具没有删除 CTP_FILE 记录。是否需要清理该表及其关联数据,应根据具体 OA 版本、外键关系和厂商建议单独评估,不能只凭字段名称直接操作。
五、从验证脚本到桌面工具
手动 SQL 可以验证逻辑,但面对几百甚至几千个文件时,人工复制 ID、查找目录和核对结果的成本很高,也容易遗漏。因此,在逻辑稳定后,我使用 .NET 8 和 Windows Forms 开发了一个可以直接在 Windows Server 上运行的工具。
工具的操作流程如下:
连接 OA 数据库
↓
输入单级或多级目录路径
↓
递归查询所有文件及物理 ID
↓
单选、多选或选择全部结果
↓
二次确认删除范围
↓
清理 upload 和 officetrans
↓
分批删除 DOC_RESOURCES 记录
↓
生成 CSV 明细并推送企业微信
数据库账号、密码、企业微信 Webhook 和自定义存储路径都只保存在当前进程内存中,不写入本地配置文件。工具默认提供常用路径,也允许其他部署环境分别配置自己的 upload 和 officetrans 根目录。
六、批量删除遇到的 SQL Server 参数限制
早期版本将所有文件 ID 都放入一条参数化 SQL:
DELETE FROM DOC_RESOURCES
WHERE IS_FOLDER = 0
AND ID IN (@id0, @id1, @id2, ...);
一千多个文件可以正常执行,但增加到几千个后开始失败。原因是 SQL Server 单条命令最多只能接受大约 2100 个参数。
解决方式是将 ID 每 500 条分成一批,但所有批次仍放在同一个事务中:
await using var transaction = await connection.BeginTransactionAsync();
foreach (var batch in resourceIds.Chunk(500))
{
// 为当前批次生成参数并执行 DELETE。
}
await transaction.CommitAsync();
这样既避开参数数量上限,又能保证数据库操作的原子性:全部批次成功才提交,任何一批失败都回滚数据库事务。
需要注意,磁盘文件删除无法像数据库事务一样自动回滚。因此工具会先进行路径范围校验和二次确认,失败时也会明确提示立即核对服务器文件。生产环境中仍应依靠备份保证可恢复性。
七、路径安全比删除代码更重要
删除文件本身只需要一行代码,真正重要的是确保目标路径不会越界。工具对路径做了几层限制:
upload和officetrans必须分别配置,不能是同一个目录。- 配置的目录必须真实存在。
- 目录末级名称必须分别是
upload和officetrans。 - 每一个待删除路径都先转换为绝对路径。
- 绝对路径必须位于对应的已配置根目录之下。
- 删除前显示文件数量和影响范围,并要求二次确认。
这些限制会牺牲一点配置自由度,但对于运行在生产服务器上的删除工具,这种约束是必要的。
八、删除结果通过企业微信留痕
文件数量较少时,文本消息就能记录结果;一次删除几千个文件时,文本会超过企业微信消息长度限制。因此工具会在删除成功后生成 UTF-8 CSV 文档,上传给企业微信群机器人。
CSV 中包含:
- 查询文件夹路径。
- 删除文件总数。
- OA 数据库记录数。
- 源文件总大小。
- OA 服务器名称和操作时间。
- 每个文件的文件 ID、文件名称、物理文件 ID 和文件大小。
源文件总大小优先使用 CTP_FILE.FILE_SIZE 汇总,并明确备注:officetrans 被删除空间未计入该总大小。临时 CSV 上传完成后会立即从服务器删除。
企业微信文件上传还有一个实际兼容问题:普通的 .NET MultipartFormDataContent 请求曾返回 44001 empty media data。最终按照企业微信接口要求构造包含 filename、filelength 和明确 boundary 的 multipart 请求体后,文件上传和群消息发送才都返回 errcode = 0。
九、最终得到的不只是一个删除按钮
这个工具最初只是为了解决重复手工操作,但在开发过程中逐步补齐了真正影响生产可用性的细节:
- 递归查询多层目录。
- 用多级路径消除同名文件夹歧义。
- 映射逻辑文件 ID 与物理文件 ID。
- 同时清理源文件和预览转换目录。
- 支持单选、多选和全部删除。
- 分批处理数千条数据库记录。
- 自定义不同服务器的存储路径。
- 统计文件大小并生成完整删除明细。
- 通过企业微信推送操作结果。
回顾整个过程,最关键的并不是一开始就写出完整程序,而是先用 SQL 和实际磁盘数据逐步验证每一层关系。只有目录层级、物理 ID、存储路径和数据库记录都对得上,自动化工具才有可靠的基础。
对于这类涉及生产数据的运维工具,效率应该建立在可验证、可审计和可恢复之上。先确认逻辑,再做自动化,最终得到的工具才真正有价值。
项目地址
- GitHub 仓库:dengchuanfu/seeyon-file-purge
- 最新版本:Releases
本文中的工具和 SQL 仅供已授权的运维、测试和数据清理场景使用。请根据实际版本核对表结构,并在生产环境操作前完成备份。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/qq_42259469/article/details/165120086




