1. 新增在线协同文档功能 v 1
This commit is contained in:
144
sql/dm/doc_management.sql
Normal file
144
sql/dm/doc_management.sql
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
Yudao Database Transfer Tool
|
||||||
|
|
||||||
|
Source Server Type : MySQL
|
||||||
|
|
||||||
|
Target Server Type : DM8
|
||||||
|
|
||||||
|
Date: 2025-08-29 10:32:27
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for infra_doc_file
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE infra_doc_file (
|
||||||
|
id bigint NOT NULL PRIMARY KEY,
|
||||||
|
title varchar(255) NOT NULL,
|
||||||
|
file_id bigint DEFAULT NULL NULL,
|
||||||
|
file_type varchar(10) NOT NULL,
|
||||||
|
space_type smallint DEFAULT '1' NOT NULL,
|
||||||
|
description varchar(500) DEFAULT NULL NULL,
|
||||||
|
latest_version_id bigint DEFAULT NULL NULL,
|
||||||
|
owner_user_id bigint NOT NULL,
|
||||||
|
status smallint DEFAULT '1' NOT NULL,
|
||||||
|
creator varchar(64) DEFAULT '' NULL,
|
||||||
|
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updater varchar(64) DEFAULT '' NULL,
|
||||||
|
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
deleted bit DEFAULT '0' NOT NULL,
|
||||||
|
tenant_id bigint DEFAULT '0' NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_infra_doc_file_01 ON infra_doc_file (owner_user_id);
|
||||||
|
CREATE INDEX idx_infra_doc_file_02 ON infra_doc_file (space_type);
|
||||||
|
CREATE INDEX idx_infra_doc_file_03 ON infra_doc_file (file_type);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN infra_doc_file.id IS '文档编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.title IS '文档标题';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.file_id IS '文件编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.file_type IS '文件类型(docx/xlsx/pptx/pdf)';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.space_type IS '空间类型(1-个人空间 2-团队空间)';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.description IS '文档描述';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.latest_version_id IS '最新版本编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.owner_user_id IS '所有者用户编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.status IS '状态(0-禁用 1-启用)';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.creator IS '创建者';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.create_time IS '创建时间';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.updater IS '更新者';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.update_time IS '更新时间';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.deleted IS '是否删除';
|
||||||
|
COMMENT ON COLUMN infra_doc_file.tenant_id IS '租户编号';
|
||||||
|
COMMENT ON TABLE infra_doc_file IS '在线文档表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for infra_doc_file_version
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE infra_doc_file_version (
|
||||||
|
id bigint NOT NULL PRIMARY KEY,
|
||||||
|
doc_file_id bigint NOT NULL,
|
||||||
|
version_no varchar(50) NOT NULL,
|
||||||
|
file_id bigint NOT NULL,
|
||||||
|
change_description varchar(500) DEFAULT NULL NULL,
|
||||||
|
creator varchar(64) DEFAULT '' NULL,
|
||||||
|
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updater varchar(64) DEFAULT '' NULL,
|
||||||
|
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
deleted bit DEFAULT '0' NOT NULL,
|
||||||
|
tenant_id bigint DEFAULT '0' NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_infra_doc_file_version_01 ON infra_doc_file_version (doc_file_id);
|
||||||
|
CREATE INDEX idx_infra_doc_file_version_02 ON infra_doc_file_version (version_no);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.id IS '版本编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.doc_file_id IS '文档编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.version_no IS '版本号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.file_id IS '文件编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.change_description IS '变更说明';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.creator IS '创建者';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.create_time IS '创建时间';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.updater IS '更新者';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.update_time IS '更新时间';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.deleted IS '是否删除';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_version.tenant_id IS '租户编号';
|
||||||
|
COMMENT ON TABLE infra_doc_file_version IS '文档版本表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for infra_doc_file_permission
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE infra_doc_file_permission (
|
||||||
|
id bigint NOT NULL PRIMARY KEY,
|
||||||
|
doc_file_id bigint NOT NULL,
|
||||||
|
role_id bigint NOT NULL,
|
||||||
|
permission_type smallint DEFAULT '1' NOT NULL,
|
||||||
|
expire_time datetime DEFAULT NULL NULL,
|
||||||
|
creator varchar(64) DEFAULT '' NULL,
|
||||||
|
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
updater varchar(64) DEFAULT '' NULL,
|
||||||
|
update_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
deleted bit DEFAULT '0' NOT NULL,
|
||||||
|
tenant_id bigint DEFAULT '0' NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_infra_doc_file_permission_01 ON infra_doc_file_permission (role_id);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.id IS '权限编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.doc_file_id IS '文档编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.role_id IS '角色编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.permission_type IS '权限类型(1-只读 2-编辑 3-管理)';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.expire_time IS '过期时间';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.creator IS '创建者';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.create_time IS '创建时间';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.updater IS '更新者';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.update_time IS '更新时间';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.deleted IS '是否删除';
|
||||||
|
COMMENT ON COLUMN infra_doc_file_permission.tenant_id IS '租户编号';
|
||||||
|
COMMENT ON TABLE infra_doc_file_permission IS '文档权限表';
|
||||||
|
|
||||||
|
-- ----------------------------
|
||||||
|
-- Table structure for infra_doc_edit_history
|
||||||
|
-- ----------------------------
|
||||||
|
CREATE TABLE infra_doc_edit_history (
|
||||||
|
id bigint NOT NULL PRIMARY KEY,
|
||||||
|
doc_file_id bigint NOT NULL,
|
||||||
|
user_id bigint NOT NULL,
|
||||||
|
user_name varchar(100) NOT NULL,
|
||||||
|
edit_type smallint DEFAULT '1' NOT NULL,
|
||||||
|
description varchar(500) DEFAULT NULL NULL,
|
||||||
|
create_time datetime DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||||
|
tenant_id bigint DEFAULT '0' NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_infra_doc_edit_history_01 ON infra_doc_edit_history (doc_file_id);
|
||||||
|
CREATE INDEX idx_infra_doc_edit_history_02 ON infra_doc_edit_history (user_id);
|
||||||
|
|
||||||
|
COMMENT ON COLUMN infra_doc_edit_history.id IS '历史编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_edit_history.doc_file_id IS '文档编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_edit_history.user_id IS '编辑用户编号';
|
||||||
|
COMMENT ON COLUMN infra_doc_edit_history.user_name IS '编辑用户名称';
|
||||||
|
COMMENT ON COLUMN infra_doc_edit_history.edit_type IS '编辑类型(1-创建 2-编辑 3-删除 4-重命名)';
|
||||||
|
COMMENT ON COLUMN infra_doc_edit_history.description IS '操作描述';
|
||||||
|
COMMENT ON COLUMN infra_doc_edit_history.create_time IS '创建时间';
|
||||||
|
COMMENT ON COLUMN infra_doc_edit_history.tenant_id IS '租户编号';
|
||||||
|
COMMENT ON TABLE infra_doc_edit_history IS '文档编辑历史表';
|
||||||
39
sql/dm/doc_management_menu.sql
Normal file
39
sql/dm/doc_management_menu.sql
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
-- 在线文档管理功能菜单数据(DM 格式)
|
||||||
|
-- 说明:达梦脚本使用与 patch.sql 相同的精简列形式,省略布尔列与时间列,使用默认值。
|
||||||
|
|
||||||
|
-- 清理旧数据,保持可重复执行
|
||||||
|
DELETE FROM system_menu WHERE id IN (6000,6001,6002,600101,600102,600103,600104,600105,600106,600107,600108,600109,600110);
|
||||||
|
|
||||||
|
-- 顶级目录(父级假定已存在 id=2 的“基础设施/Infra”或同级目录)
|
||||||
|
INSERT INTO system_menu (
|
||||||
|
id, name, permission, type, sort, parent_id,
|
||||||
|
path, icon, component, status, component_name
|
||||||
|
) VALUES (
|
||||||
|
6000, '在线文档', '', 1, 15, 2,
|
||||||
|
'doc', 'fa:file-text-o', '', 0, NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 文档管理主页面
|
||||||
|
INSERT INTO system_menu (
|
||||||
|
id, name, permission, type, sort, parent_id,
|
||||||
|
path, icon, component, status, component_name
|
||||||
|
) VALUES (
|
||||||
|
6001, '文档管理', 'infra:doc:query', 2, 1, 6000,
|
||||||
|
'doc-file', 'fa:file-text', 'infra/doc/index', 0, 'DocFile'
|
||||||
|
);
|
||||||
|
|
||||||
|
-- 按钮权限(操作项)
|
||||||
|
INSERT INTO system_menu (
|
||||||
|
id, name, permission, type, sort, parent_id,
|
||||||
|
path, icon, component, status
|
||||||
|
) VALUES
|
||||||
|
(600101,'文档查询','infra:doc:query',3,1,6001,'','','',0),
|
||||||
|
(600102,'文档创建','infra:doc:create',3,2,6001,'','','',0),
|
||||||
|
(600103,'文档更新','infra:doc:update',3,3,6001,'','','',0),
|
||||||
|
(600104,'文档删除','infra:doc:delete',3,4,6001,'','','',0),
|
||||||
|
(600105,'文档导出','infra:doc:export',3,5,6001,'','','',0),
|
||||||
|
(600106,'文档上传','infra:doc:upload',3,6,6001,'','','',0),
|
||||||
|
(600107,'文档编辑','infra:doc:edit',3,7,6001,'','','',0),
|
||||||
|
(600108,'文档预览','infra:doc:preview',3,8,6001,'','','',0),
|
||||||
|
(600109,'文档下载','infra:doc:download',3,9,6001,'','','',0),
|
||||||
|
(600110,'权限管理','infra:doc:permission',3,10,6001,'','','',0);
|
||||||
@@ -155,7 +155,7 @@ INSERT INTO system_menu(
|
|||||||
path, icon, component, status, component_name
|
path, icon, component, status, component_name
|
||||||
)
|
)
|
||||||
VALUES (
|
VALUES (
|
||||||
'1953701540574969857', '系统序列号管理', '', 2, 0, ${table.parentMenuId},
|
'1953701540574969857', '系统序列号管理', '', 2, 0, 1,
|
||||||
'sequence', '', 'system/sequence/index', 0, 'Sequence'
|
'sequence', '', 'system/sequence/index', 0, 'Sequence'
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -259,4 +259,4 @@ VALUES
|
|||||||
(5021, 1, '固定值', 'FIXED', 'system_sequence_detail_rule_type', 0, 'primary', '', '固定字符串值', 'admin', SYSDATE, 'admin', SYSDATE, 0),
|
(5021, 1, '固定值', 'FIXED', 'system_sequence_detail_rule_type', 0, 'primary', '', '固定字符串值', 'admin', SYSDATE, 'admin', SYSDATE, 0),
|
||||||
(5022, 2, '日期格式', 'DATE', 'system_sequence_detail_rule_type', 0, 'success', '', '日期格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0),
|
(5022, 2, '日期格式', 'DATE', 'system_sequence_detail_rule_type', 0, 'success', '', '日期格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0),
|
||||||
(5023, 3, '数字格式', 'NUMBER', 'system_sequence_detail_rule_type', 0, 'info', '', '数字格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0),
|
(5023, 3, '数字格式', 'NUMBER', 'system_sequence_detail_rule_type', 0, 'info', '', '数字格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0),
|
||||||
(5024, 4, '自定义格式', 'CUSTOM', 'system_sequence_detail_rule_type', 0, 'warning', '', '自定义格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0); (5024, 4, '自定义格式', 'CUSTOM', 'system_sequence_detail_rule_type', 0, 'warning', '', '自定义格式规则', 'admin', NOW(), 'admin', NOW(), b'0');
|
(5024, 4, '自定义格式', 'CUSTOM', 'system_sequence_detail_rule_type', 0, 'warning', '', '自定义格式规则', 'admin', SYSDATE, 'admin', SYSDATE, 0);
|
||||||
|
|||||||
75
sql/mysql/doc_management.sql
Normal file
75
sql/mysql/doc_management.sql
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
-- 在线文档管理功能相关表结构
|
||||||
|
|
||||||
|
-- 在线文档表
|
||||||
|
CREATE TABLE `infra_doc_file` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '文档编号',
|
||||||
|
`title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文档标题',
|
||||||
|
`file_id` bigint DEFAULT NULL COMMENT '文件编号',
|
||||||
|
`file_type` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '文件类型(docx/xlsx/pptx)',
|
||||||
|
`space_type` tinyint NOT NULL DEFAULT '1' COMMENT '空间类型(1-个人空间 2-团队空间)',
|
||||||
|
`description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '文档描述',
|
||||||
|
`latest_version_id` bigint DEFAULT NULL COMMENT '最新版本编号',
|
||||||
|
`owner_user_id` bigint NOT NULL COMMENT '所有者用户编号',
|
||||||
|
`status` tinyint NOT NULL DEFAULT '1' COMMENT '状态(0-禁用 1-启用)',
|
||||||
|
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||||
|
`tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
KEY `idx_owner_user_id` (`owner_user_id`) USING BTREE,
|
||||||
|
KEY `idx_space_type` (`space_type`) USING BTREE,
|
||||||
|
KEY `idx_file_type` (`file_type`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='在线文档表';
|
||||||
|
|
||||||
|
-- 文档版本表
|
||||||
|
CREATE TABLE `infra_doc_file_version` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '版本编号',
|
||||||
|
`doc_file_id` bigint NOT NULL COMMENT '文档编号',
|
||||||
|
`version_no` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '版本号',
|
||||||
|
`file_id` bigint NOT NULL COMMENT '文件编号',
|
||||||
|
`change_description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '变更说明',
|
||||||
|
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||||
|
`tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
KEY `idx_doc_file_id` (`doc_file_id`) USING BTREE,
|
||||||
|
KEY `idx_version_no` (`version_no`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文档版本表';
|
||||||
|
|
||||||
|
-- 文档权限表
|
||||||
|
CREATE TABLE `infra_doc_file_permission` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '权限编号',
|
||||||
|
`doc_file_id` bigint NOT NULL COMMENT '文档编号',
|
||||||
|
`role_id` bigint NOT NULL COMMENT '角色编号',
|
||||||
|
`permission_type` tinyint NOT NULL DEFAULT '1' COMMENT '权限类型(1-只读 2-编辑 3-管理)',
|
||||||
|
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
|
||||||
|
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '创建者',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT '' COMMENT '更新者',
|
||||||
|
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||||
|
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
|
||||||
|
`tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
UNIQUE KEY `uk_doc_role` (`doc_file_id`,`role_id`,`deleted`) USING BTREE,
|
||||||
|
KEY `idx_role_id` (`role_id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文档权限表';
|
||||||
|
|
||||||
|
-- 文档编辑历史表
|
||||||
|
CREATE TABLE `infra_doc_edit_history` (
|
||||||
|
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '历史编号',
|
||||||
|
`doc_file_id` bigint NOT NULL COMMENT '文档编号',
|
||||||
|
`user_id` bigint NOT NULL COMMENT '编辑用户编号',
|
||||||
|
`user_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '编辑用户名称',
|
||||||
|
`edit_type` tinyint NOT NULL DEFAULT '1' COMMENT '编辑类型(1-创建 2-编辑 3-删除 4-重命名)',
|
||||||
|
`description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '操作描述',
|
||||||
|
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||||
|
`tenant_id` bigint NOT NULL DEFAULT '0' COMMENT '租户编号',
|
||||||
|
PRIMARY KEY (`id`) USING BTREE,
|
||||||
|
KEY `idx_doc_file_id` (`doc_file_id`) USING BTREE,
|
||||||
|
KEY `idx_user_id` (`user_id`) USING BTREE
|
||||||
|
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='文档编辑历史表';
|
||||||
24
sql/mysql/doc_management_menu.sql
Normal file
24
sql/mysql/doc_management_menu.sql
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
-- 在线文档管理功能菜单数据
|
||||||
|
|
||||||
|
-- 菜单 SQL
|
||||||
|
-- 约定:6000 顶级目录;6001 子菜单;60010x 按钮;6002 编辑器隐藏子页面
|
||||||
|
DELETE FROM system_menu WHERE id IN (6000,6001,6002,600101,600102,600103,600104,600105,600106,600107,600108,600109,600110);
|
||||||
|
|
||||||
|
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
|
||||||
|
VALUES (6000, '在线文档', '', 1, 15, 2, 'doc', 'fa:file-text-o', '', 0, true, true, true, 'admin', NOW(), '', NOW(), false);
|
||||||
|
|
||||||
|
INSERT INTO system_menu (id, name, permission, type, sort, parent_id, path, icon, component, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted)
|
||||||
|
VALUES (6001, '文档管理', 'infra:doc:query', 2, 1, 6000, 'doc-file', 'fa:file-text', 'infra/doc/index', 0, true, true, true, 'admin', NOW(), '', NOW(), false);
|
||||||
|
|
||||||
|
-- 文档管理的操作权限
|
||||||
|
INSERT INTO system_menu (id,name, permission, type, sort, parent_id, path, icon, component, status, visible, keep_alive, always_show, creator, create_time, updater, update_time, deleted) VALUES
|
||||||
|
(600101,'文档查询','infra:doc:query',3,1,6001,'','','',0,true,true,true,'admin',NOW(),'admin',NOW(),false),
|
||||||
|
(600102,'文档创建','infra:doc:create',3,2,6001,'','','',0,true,true,true,'admin',NOW(),'admin',NOW(),false),
|
||||||
|
(600103,'文档更新','infra:doc:update',3,3,6001,'','','',0,true,true,true,'admin',NOW(),'admin',NOW(),false),
|
||||||
|
(600104,'文档删除','infra:doc:delete',3,4,6001,'','','',0,true,true,true,'admin',NOW(),'admin',NOW(),false),
|
||||||
|
(600105,'文档导出','infra:doc:export',3,5,6001,'','','',0,true,true,true,'admin',NOW(),'admin',NOW(),false),
|
||||||
|
(600106,'文档上传','infra:doc:upload',3,6,6001,'','','',0,true,true,true,'admin',NOW(),'admin',NOW(),false),
|
||||||
|
(600107,'文档编辑','infra:doc:edit',3,7,6001,'','','',0,true,true,true,'admin',NOW(),'admin',NOW(),false),
|
||||||
|
(600108,'文档预览','infra:doc:preview',3,8,6001,'','','',0,true,true,true,'admin',NOW(),'admin',NOW(),false),
|
||||||
|
(600109,'文档下载','infra:doc:download',3,9,6001,'','','',0,true,true,true,'admin',NOW(),'admin',NOW(),false),
|
||||||
|
(600110,'权限管理','infra:doc:permission',3,10,6001,'','','',0,true,true,true,'admin',NOW(),'admin',NOW(),false);
|
||||||
@@ -86,4 +86,5 @@ public interface ErrorCodeConstants {
|
|||||||
ErrorCode STANDARD_NAME_NOT_EXISTS = new ErrorCode(1_002_030_000, "数据命名与简写标准不存在");
|
ErrorCode STANDARD_NAME_NOT_EXISTS = new ErrorCode(1_002_030_000, "数据命名与简写标准不存在");
|
||||||
|
|
||||||
ErrorCode STD_ABBR_NOT_EXISTS = new ErrorCode(1_002_030_001, "字段名 {} 不存在命名规范定义,请核对字段定义");
|
ErrorCode STD_ABBR_NOT_EXISTS = new ErrorCode(1_002_030_001, "字段名 {} 不存在命名规范定义,请核对字段定义");
|
||||||
|
ErrorCode DOC_NOT_EXISTS = new ErrorCode(1_002_031_000, "文档不存在");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,12 @@
|
|||||||
<version>${revision}</version>
|
<version>${revision}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
<artifactId>yudao-module-system-api</artifactId>
|
||||||
|
<version>${revision}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!-- 业务组件 -->
|
<!-- 业务组件 -->
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>cn.iocoder.cloud</groupId>
|
<groupId>cn.iocoder.cloud</groupId>
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.config;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.module.infra.websocket.DocWebSocketHandler;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||||
|
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||||
|
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WebSocket 配置
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSocket
|
||||||
|
public class WebSocketConfig implements WebSocketConfigurer {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private DocWebSocketHandler docWebSocketHandler;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
|
||||||
|
// 注册文档协作WebSocket处理器
|
||||||
|
registry.addHandler(docWebSocketHandler, "/doc-collaboration")
|
||||||
|
.setAllowedOrigins("*") // 允许跨域,生产环境应该限制域名
|
||||||
|
.withSockJS(); // 启用SockJS支持
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.controller.admin.doc;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
|
||||||
|
import cn.iocoder.yudao.framework.security.core.LoginUser;
|
||||||
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
|
import cn.iocoder.yudao.framework.tenant.core.aop.TenantIgnore;
|
||||||
|
import cn.iocoder.yudao.module.infra.controller.admin.doc.vo.*;
|
||||||
|
import cn.iocoder.yudao.module.infra.convert.doc.DocFileConvert;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFilePermissionDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFileVersionDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.service.doc.DocFileService;
|
||||||
|
import cn.iocoder.yudao.module.infra.service.file.FileService;
|
||||||
|
import cn.hutool.json.JSONUtil;
|
||||||
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.annotation.security.PermitAll;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
|
||||||
|
|
||||||
|
@Tag(name = "管理后台 - 在线文档")
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/infra/doc-file")
|
||||||
|
@Validated
|
||||||
|
@Slf4j
|
||||||
|
public class DocFileController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private DocFileService docFileService;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private FileService fileService;
|
||||||
|
|
||||||
|
@PostMapping("/create")
|
||||||
|
@Operation(summary = "创建在线文档")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:create')")
|
||||||
|
public CommonResult<Long> createDocFile(@Valid @RequestBody DocFileCreateReqVO createReqVO) {
|
||||||
|
return success(docFileService.createDocFile(createReqVO));
|
||||||
|
}
|
||||||
|
|
||||||
|
@PutMapping("/update")
|
||||||
|
@Operation(summary = "修改在线文档")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:update')")
|
||||||
|
public CommonResult<Boolean> updateDocFile(@Valid @RequestBody DocFileUpdateReqVO updateReqVO) {
|
||||||
|
docFileService.updateDocFile(updateReqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/delete")
|
||||||
|
@Operation(summary = "删除在线文档")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:delete')")
|
||||||
|
public CommonResult<Boolean> deleteDocFile(@RequestParam("id") Long id) {
|
||||||
|
docFileService.deleteDocFile(id);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/get")
|
||||||
|
@Operation(summary = "获得在线文档")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:query')")
|
||||||
|
public CommonResult<DocFileRespVO> getDocFile(@RequestParam("id") Long id) {
|
||||||
|
DocFileRespVO doc = docFileService.getDocFileWithFileInfo(id);
|
||||||
|
return success(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/page")
|
||||||
|
@Operation(summary = "获得在线文档分页")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:query')")
|
||||||
|
public CommonResult<PageResult<DocFileRespVO>> getDocFilePage(@Valid DocFilePageReqVO pageReqVO) {
|
||||||
|
PageResult<DocFileRespVO> page = docFileService.getDocFilePageWithFileInfo(pageReqVO);
|
||||||
|
return success(page);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/upload")
|
||||||
|
@Operation(summary = "上传文档文件")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:upload')")
|
||||||
|
public CommonResult<Long> upload(@RequestPart("file") MultipartFile file,
|
||||||
|
@RequestParam("title") String title,
|
||||||
|
@RequestParam("spaceType") Integer spaceType,
|
||||||
|
@RequestParam(value = "description", required = false) String description) {
|
||||||
|
return success(docFileService.uploadDocFile(file, title, spaceType, description));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/editor-config")
|
||||||
|
@Operation(summary = "获取 OnlyOffice 编辑器配置")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:edit')")
|
||||||
|
public CommonResult<DocEditorConfigRespVO> getEditorConfig(@RequestParam("id") Long id) {
|
||||||
|
return success(docFileService.getEditorConfig(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/file-content")
|
||||||
|
@Operation(summary = "获取文档文件内容")
|
||||||
|
@PermitAll
|
||||||
|
public void getFileContent(@RequestParam("fileId") Long fileId,
|
||||||
|
@RequestParam("token") String token,
|
||||||
|
HttpServletResponse response) {
|
||||||
|
docFileService.getDocFileContent(fileId, token, response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/callback")
|
||||||
|
@Operation(summary = "OnlyOffice 回调接口")
|
||||||
|
@PermitAll
|
||||||
|
@TenantIgnore
|
||||||
|
public Map<String,Object> callback(@RequestParam("docFileId") Long docFileId,
|
||||||
|
@RequestParam(value = "userId", required = false) Long triggerUserId,
|
||||||
|
@RequestBody Map<String,Object> body,
|
||||||
|
HttpServletRequest request) {
|
||||||
|
try {
|
||||||
|
log.info("=== OnlyOffice 回调接口被调用 ===");
|
||||||
|
log.info("请求URL: {}", request.getRequestURL());
|
||||||
|
log.info("请求URI: {}", request.getRequestURI());
|
||||||
|
log.info("请求方法: {}", request.getMethod());
|
||||||
|
log.info("Content-Type: {}", request.getContentType());
|
||||||
|
log.info("User-Agent: {}", request.getHeader("User-Agent"));
|
||||||
|
log.info("客户端IP: {}", getClientIpAddress(request));
|
||||||
|
log.info("OnlyOffice 回调请求: docFileId={}, triggerUserId={}, body={}", docFileId, triggerUserId, body);
|
||||||
|
|
||||||
|
// 如果传入了triggerUserId,设置当前请求的登录用户为该用户
|
||||||
|
if (triggerUserId != null) {
|
||||||
|
LoginUser mockUser = new LoginUser();
|
||||||
|
mockUser.setId(triggerUserId);
|
||||||
|
mockUser.setUserType(UserTypeEnum.ADMIN.getValue());
|
||||||
|
mockUser.setTenantId(1L); // 默认租户
|
||||||
|
SecurityFrameworkUtils.setLoginUser(mockUser, request);
|
||||||
|
log.info("已设置登录用户上下文: userId={}", triggerUserId);
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer status = (Integer) body.get("status");
|
||||||
|
String downloadUrl = (String) body.get("url");
|
||||||
|
|
||||||
|
// 扩展保存策略:更多状态下进行保存
|
||||||
|
boolean shouldSave = false;
|
||||||
|
String saveReason = "";
|
||||||
|
|
||||||
|
if (status != null) {
|
||||||
|
switch (status) {
|
||||||
|
case 1:
|
||||||
|
// 文档正在编辑中 - 记录活动但不保存
|
||||||
|
log.info("文档编辑中: docFileId={}, users={}", docFileId, body.get("users"));
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
// 文档已准备保存(用户主动保存或停止编辑)- 立即保存
|
||||||
|
shouldSave = true;
|
||||||
|
saveReason = "用户主动保存";
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
// 保存时出错 - 记录错误但不重试保存,避免循环
|
||||||
|
log.warn("OnlyOffice保存出错: docFileId={}, url={}", docFileId, downloadUrl);
|
||||||
|
break;
|
||||||
|
case 4:
|
||||||
|
// 文档关闭且无错误 - 只在有实际内容时保存
|
||||||
|
if (downloadUrl != null && !downloadUrl.trim().isEmpty()) {
|
||||||
|
shouldSave = true;
|
||||||
|
saveReason = "文档关闭保存";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 6:
|
||||||
|
// 强制保存(通常是协作冲突解决) - 立即保存
|
||||||
|
shouldSave = true;
|
||||||
|
saveReason = "协作保存";
|
||||||
|
break;
|
||||||
|
case 7:
|
||||||
|
// 保存时出错但继续编辑 - 记录错误但不重试
|
||||||
|
log.warn("OnlyOffice编辑中保存出错: docFileId={}, url={}", docFileId, downloadUrl);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.info("OnlyOffice未处理的回调状态: docFileId={}, status={}", docFileId, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldSave) {
|
||||||
|
if (downloadUrl == null || downloadUrl.trim().isEmpty()) {
|
||||||
|
log.error("OnlyOffice 回调 URL 为空: docFileId={}, status={}, reason={}", docFileId, status, saveReason);
|
||||||
|
return Map.of("error", 1, "message", "下载URL为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("开始保存文档: docFileId={}, triggerUserId={}, status={}, reason={}, url={}",
|
||||||
|
docFileId, triggerUserId, status, saveReason, downloadUrl);
|
||||||
|
docFileService.saveDocumentContent(docFileId, downloadUrl, saveReason, triggerUserId);
|
||||||
|
log.info("文档保存成功: docFileId={}, triggerUserId={}, reason={}", docFileId, triggerUserId, saveReason);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按 OnlyOffice 协议返回 {error:0}
|
||||||
|
return Map.of("error", 0);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("OnlyOffice 回调处理失败: docFileId={}, body={}", docFileId, body, e);
|
||||||
|
return Map.of("error", 1, "message", e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------------- 权限 -------------------
|
||||||
|
|
||||||
|
@PostMapping("/permission/set")
|
||||||
|
@Operation(summary = "设置文档权限")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:permission')")
|
||||||
|
public CommonResult<Boolean> setPermission(@Valid @RequestBody DocFilePermissionReqVO permissionReqVO) {
|
||||||
|
docFileService.setDocFilePermission(permissionReqVO);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/permission/list")
|
||||||
|
@Operation(summary = "获得文档权限列表")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:permission')")
|
||||||
|
public CommonResult<List<DocFilePermissionRespVO>> getPermissionList(@RequestParam("docFileId") Long docFileId) {
|
||||||
|
List<DocFilePermissionDO> list = docFileService.getDocFilePermissions(docFileId);
|
||||||
|
return success(DocFileConvert.INSTANCE.convertPermissionList(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/permission/delete")
|
||||||
|
@Operation(summary = "删除文档权限")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:permission')")
|
||||||
|
public CommonResult<Boolean> deletePermission(@RequestParam("docFileId") Long docFileId,
|
||||||
|
@RequestParam("roleId") Long roleId) {
|
||||||
|
docFileService.deleteDocFilePermission(docFileId, roleId);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 版本管理 ==========
|
||||||
|
|
||||||
|
@GetMapping("/version/list")
|
||||||
|
@Operation(summary = "获取文档版本列表")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:query')")
|
||||||
|
public CommonResult<List<DocFileVersionRespVO>> getVersionList(@RequestParam("docFileId") Long docFileId) {
|
||||||
|
List<DocFileVersionDO> list = docFileService.getDocFileVersions(docFileId);
|
||||||
|
List<DocFileVersionRespVO> result = DocFileConvert.INSTANCE.convertVersionList(list);
|
||||||
|
|
||||||
|
// 填充文件信息
|
||||||
|
for (DocFileVersionRespVO vo : result) {
|
||||||
|
if (vo.getFileId() != null) {
|
||||||
|
try {
|
||||||
|
var fileInfo = fileService.getActiveFileById(vo.getFileId());
|
||||||
|
if (fileInfo != null) {
|
||||||
|
vo.setFileName(fileInfo.getName());
|
||||||
|
vo.setFileSize(fileInfo.getSize() != null ? fileInfo.getSize().longValue() : 0L);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取版本文件信息失败: versionId={}, fileId={}", vo.getId(), vo.getFileId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/version/restore")
|
||||||
|
@Operation(summary = "恢复到指定版本")
|
||||||
|
@PreAuthorize("@ss.hasPermission('infra:doc:update')")
|
||||||
|
public CommonResult<Boolean> restoreToVersion(@RequestParam("docFileId") Long docFileId,
|
||||||
|
@RequestParam("versionId") Long versionId) {
|
||||||
|
docFileService.restoreDocFileToVersion(docFileId, versionId);
|
||||||
|
return success(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端真实IP地址
|
||||||
|
*/
|
||||||
|
private String getClientIpAddress(HttpServletRequest request) {
|
||||||
|
String ip = request.getHeader("X-Forwarded-For");
|
||||||
|
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getHeader("Proxy-Client-IP");
|
||||||
|
}
|
||||||
|
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getHeader("WL-Proxy-Client-IP");
|
||||||
|
}
|
||||||
|
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getHeader("HTTP_CLIENT_IP");
|
||||||
|
}
|
||||||
|
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getHeader("HTTP_X_FORWARDED_FOR");
|
||||||
|
}
|
||||||
|
if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
|
||||||
|
ip = request.getRemoteAddr();
|
||||||
|
}
|
||||||
|
return ip;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.controller.admin.doc.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - OnlyOffice 编辑器配置 Response VO")
|
||||||
|
@Data
|
||||||
|
public class DocEditorConfigRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "文档类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "text")
|
||||||
|
private String documentType;
|
||||||
|
|
||||||
|
@Schema(description = "文档配置", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private DocumentConfig document;
|
||||||
|
|
||||||
|
@Schema(description = "编辑器配置", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private EditorConfig editorConfig;
|
||||||
|
|
||||||
|
@Schema(description = "文档高度", requiredMode = Schema.RequiredMode.REQUIRED, example = "100%")
|
||||||
|
private String height;
|
||||||
|
|
||||||
|
@Schema(description = "令牌", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private String token;
|
||||||
|
|
||||||
|
@Schema(description = "OnlyOffice服务器地址", requiredMode = Schema.RequiredMode.REQUIRED, example = "http://localhost:8085")
|
||||||
|
private String documentServerUrl;
|
||||||
|
|
||||||
|
@Schema(description = "文档配置")
|
||||||
|
@Data
|
||||||
|
public static class DocumentConfig {
|
||||||
|
@Schema(description = "文件类型", example = "docx")
|
||||||
|
private String fileType;
|
||||||
|
|
||||||
|
@Schema(description = "文档key", example = "doc123")
|
||||||
|
private String key;
|
||||||
|
|
||||||
|
@Schema(description = "文档标题", example = "技术文档")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "文档地址")
|
||||||
|
private String url;
|
||||||
|
|
||||||
|
@Schema(description = "权限配置")
|
||||||
|
private Permissions permissions;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema(description = "权限配置")
|
||||||
|
@Data
|
||||||
|
public static class Permissions {
|
||||||
|
@Schema(description = "是否可编辑", example = "true")
|
||||||
|
private Boolean edit;
|
||||||
|
|
||||||
|
@Schema(description = "是否可下载", example = "true")
|
||||||
|
private Boolean download;
|
||||||
|
|
||||||
|
@Schema(description = "是否可打印", example = "true")
|
||||||
|
private Boolean print;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema(description = "编辑器配置")
|
||||||
|
@Data
|
||||||
|
public static class EditorConfig {
|
||||||
|
@Schema(description = "回调地址")
|
||||||
|
private String callbackUrl;
|
||||||
|
|
||||||
|
@Schema(description = "语言", example = "zh-CN")
|
||||||
|
private String lang;
|
||||||
|
|
||||||
|
@Schema(description = "模式", example = "edit")
|
||||||
|
private String mode;
|
||||||
|
|
||||||
|
@Schema(description = "用户配置")
|
||||||
|
private User user;
|
||||||
|
|
||||||
|
@Schema(description = "协作配置")
|
||||||
|
private CoEditing coEditing;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema(description = "用户配置")
|
||||||
|
@Data
|
||||||
|
public static class User {
|
||||||
|
@Schema(description = "用户ID", example = "1")
|
||||||
|
private String id;
|
||||||
|
|
||||||
|
@Schema(description = "用户名", example = "admin")
|
||||||
|
private String name;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Schema(description = "协作配置")
|
||||||
|
@Data
|
||||||
|
public static class CoEditing {
|
||||||
|
@Schema(description = "协作模式", example = "fast")
|
||||||
|
private String mode;
|
||||||
|
|
||||||
|
@Schema(description = "是否允许更改", example = "true")
|
||||||
|
private Boolean change;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.controller.admin.doc.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotEmpty;
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 在线文档创建 Request VO")
|
||||||
|
@Data
|
||||||
|
public class DocFileCreateReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "文档标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "技术文档")
|
||||||
|
@NotEmpty(message = "文档标题不能为空")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "文件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "docx")
|
||||||
|
@NotEmpty(message = "文件类型不能为空")
|
||||||
|
private String fileType;
|
||||||
|
|
||||||
|
@Schema(description = "空间类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
@NotNull(message = "空间类型不能为空")
|
||||||
|
private Integer spaceType;
|
||||||
|
|
||||||
|
@Schema(description = "文档描述", example = "这是一个技术文档")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "文件编号", example = "1")
|
||||||
|
private Long fileId;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.controller.admin.doc.vo;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.PageParam;
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.EqualsAndHashCode;
|
||||||
|
import lombok.ToString;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 在线文档分页 Request VO")
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
public class DocFilePageReqVO extends PageParam {
|
||||||
|
|
||||||
|
@Schema(description = "文档标题", example = "技术文档")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "文件类型", example = "docx")
|
||||||
|
private String fileType;
|
||||||
|
|
||||||
|
@Schema(description = "空间类型", example = "1")
|
||||||
|
private Integer spaceType;
|
||||||
|
|
||||||
|
@Schema(description = "状态", example = "1")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
|
||||||
|
private LocalDateTime[] createTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.controller.admin.doc.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 文档权限 Request VO")
|
||||||
|
@Data
|
||||||
|
public class DocFilePermissionReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
|
@NotNull(message = "文档编号不能为空")
|
||||||
|
private Long docFileId;
|
||||||
|
|
||||||
|
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
@NotNull(message = "角色编号不能为空")
|
||||||
|
private Long roleId;
|
||||||
|
|
||||||
|
@Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
@NotNull(message = "权限类型不能为空")
|
||||||
|
private Integer permissionType;
|
||||||
|
|
||||||
|
@Schema(description = "过期时间", example = "2024-12-31 23:59:59")
|
||||||
|
private LocalDateTime expireTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.controller.admin.doc.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 文档权限 Response VO")
|
||||||
|
@Data
|
||||||
|
public class DocFilePermissionRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "权限编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
|
private Long docFileId;
|
||||||
|
|
||||||
|
@Schema(description = "角色编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Long roleId;
|
||||||
|
|
||||||
|
@Schema(description = "角色名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "管理员")
|
||||||
|
private String roleName;
|
||||||
|
|
||||||
|
@Schema(description = "权限类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Integer permissionType;
|
||||||
|
|
||||||
|
@Schema(description = "过期时间", example = "2024-12-31 23:59:59")
|
||||||
|
private LocalDateTime expireTime;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.controller.admin.doc.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 在线文档 Response VO")
|
||||||
|
@Data
|
||||||
|
public class DocFileRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "文档标题", requiredMode = Schema.RequiredMode.REQUIRED, example = "技术文档")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "文件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "2048")
|
||||||
|
private Long fileId;
|
||||||
|
|
||||||
|
@Schema(description = "文件类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "docx")
|
||||||
|
private String fileType;
|
||||||
|
|
||||||
|
@Schema(description = "空间类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Integer spaceType;
|
||||||
|
|
||||||
|
@Schema(description = "文档描述", example = "这是一个技术文档")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "最新版本编号", example = "3072")
|
||||||
|
private Long latestVersionId;
|
||||||
|
|
||||||
|
@Schema(description = "所有者用户编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Long ownerUserId;
|
||||||
|
|
||||||
|
@Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
// 以下字段从 fileService 动态获取
|
||||||
|
@Schema(description = "文件名称", example = "document.docx")
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
@Schema(description = "文件大小", example = "1024")
|
||||||
|
private Long fileSize;
|
||||||
|
|
||||||
|
@Schema(description = "文件访问地址", example = "http://127.0.0.1:48080/admin-api/infra/file/4/get/37e56010ecbee472cdd821ac073b8")
|
||||||
|
private String fileUrl;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.controller.admin.doc.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 在线文档更新 Request VO")
|
||||||
|
@Data
|
||||||
|
public class DocFileUpdateReqVO {
|
||||||
|
|
||||||
|
@Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
|
||||||
|
@NotNull(message = "文档编号不能为空")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "文档标题", example = "技术文档")
|
||||||
|
private String title;
|
||||||
|
|
||||||
|
@Schema(description = "文档描述", example = "这是一个技术文档")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.controller.admin.doc.vo;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 文档版本 Response VO")
|
||||||
|
@Data
|
||||||
|
public class DocFileVersionRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "版本编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "文档编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Long docFileId;
|
||||||
|
|
||||||
|
@Schema(description = "版本号", requiredMode = Schema.RequiredMode.REQUIRED, example = "v1")
|
||||||
|
private String versionNo;
|
||||||
|
|
||||||
|
@Schema(description = "文件编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
|
||||||
|
private Long fileId;
|
||||||
|
|
||||||
|
@Schema(description = "变更说明", example = "修改文档内容")
|
||||||
|
private String changeDescription;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
|
||||||
|
@Schema(description = "创建者", example = "1")
|
||||||
|
private String creator;
|
||||||
|
|
||||||
|
// 额外字段,从文件服务获取
|
||||||
|
@Schema(description = "文件名称", example = "document.docx")
|
||||||
|
private String fileName;
|
||||||
|
|
||||||
|
@Schema(description = "文件大小", example = "1024")
|
||||||
|
private Long fileSize;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.convert.doc;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
|
import cn.iocoder.yudao.module.infra.controller.admin.doc.vo.*;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFileDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFilePermissionDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFileVersionDO;
|
||||||
|
import org.mapstruct.Mapper;
|
||||||
|
import org.mapstruct.factory.Mappers;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线文档 Convert
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DocFileConvert {
|
||||||
|
|
||||||
|
DocFileConvert INSTANCE = Mappers.getMapper(DocFileConvert.class);
|
||||||
|
|
||||||
|
DocFileDO convert(DocFileCreateReqVO bean);
|
||||||
|
|
||||||
|
DocFileRespVO convert(DocFileDO bean);
|
||||||
|
|
||||||
|
List<DocFileRespVO> convertList(List<DocFileDO> list);
|
||||||
|
|
||||||
|
PageResult<DocFileRespVO> convertPage(PageResult<DocFileDO> page);
|
||||||
|
|
||||||
|
DocFilePermissionDO convert(DocFilePermissionReqVO bean);
|
||||||
|
|
||||||
|
DocFilePermissionRespVO convert(DocFilePermissionDO bean);
|
||||||
|
|
||||||
|
List<DocFilePermissionRespVO> convertPermissionList(List<DocFilePermissionDO> list);
|
||||||
|
|
||||||
|
DocFileVersionRespVO convert(DocFileVersionDO bean);
|
||||||
|
|
||||||
|
List<DocFileVersionRespVO> convertVersionList(List<DocFileVersionDO> list);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.dal.dataobject.doc;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档编辑历史 DO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@TableName("infra_doc_edit_history")
|
||||||
|
@KeySequence("infra_doc_edit_history_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||||
|
@Data
|
||||||
|
@ToString
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DocEditHistoryDO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 历史编号
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
private Long id;
|
||||||
|
/**
|
||||||
|
* 文档编号
|
||||||
|
*/
|
||||||
|
private Long docFileId;
|
||||||
|
/**
|
||||||
|
* 编辑用户编号
|
||||||
|
*/
|
||||||
|
private Long userId;
|
||||||
|
/**
|
||||||
|
* 编辑用户名称
|
||||||
|
*/
|
||||||
|
private String userName;
|
||||||
|
/**
|
||||||
|
* 编辑类型
|
||||||
|
*
|
||||||
|
* 枚举 {@link cn.iocoder.yudao.module.infra.enums.doc.DocEditTypeEnum}
|
||||||
|
*/
|
||||||
|
private Integer editType;
|
||||||
|
/**
|
||||||
|
* 操作描述
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
/**
|
||||||
|
* 创建时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
/**
|
||||||
|
* 租户编号
|
||||||
|
*/
|
||||||
|
private Long tenantId;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.dal.dataobject.doc;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线文档 DO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@TableName("infra_doc_file")
|
||||||
|
@KeySequence("infra_doc_file_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DocFileDO extends BaseDO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档编号
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
private Long id;
|
||||||
|
/**
|
||||||
|
* 文档标题
|
||||||
|
*/
|
||||||
|
private String title;
|
||||||
|
/**
|
||||||
|
* 文件编号
|
||||||
|
*/
|
||||||
|
private Long fileId;
|
||||||
|
/**
|
||||||
|
* 文件类型
|
||||||
|
*
|
||||||
|
* 枚举 {@link cn.iocoder.yudao.module.infra.enums.doc.DocFileTypeEnum}
|
||||||
|
*/
|
||||||
|
private String fileType;
|
||||||
|
/**
|
||||||
|
* 空间类型
|
||||||
|
*
|
||||||
|
* 枚举 {@link cn.iocoder.yudao.module.infra.enums.doc.DocSpaceTypeEnum}
|
||||||
|
*/
|
||||||
|
private Integer spaceType;
|
||||||
|
/**
|
||||||
|
* 文档描述
|
||||||
|
*/
|
||||||
|
private String description;
|
||||||
|
/**
|
||||||
|
* 最新版本编号
|
||||||
|
*/
|
||||||
|
private Long latestVersionId;
|
||||||
|
/**
|
||||||
|
* 所有者用户编号
|
||||||
|
*/
|
||||||
|
private Long ownerUserId;
|
||||||
|
/**
|
||||||
|
* 状态
|
||||||
|
*
|
||||||
|
* 枚举 {@link cn.iocoder.yudao.framework.common.enums.CommonStatusEnum}
|
||||||
|
*/
|
||||||
|
private Integer status;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.dal.dataobject.doc;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档权限 DO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@TableName("infra_doc_file_permission")
|
||||||
|
@KeySequence("infra_doc_file_permission_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DocFilePermissionDO extends BaseDO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 权限编号
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
private Long id;
|
||||||
|
/**
|
||||||
|
* 文档编号
|
||||||
|
*/
|
||||||
|
private Long docFileId;
|
||||||
|
/**
|
||||||
|
* 角色编号
|
||||||
|
*/
|
||||||
|
private Long roleId;
|
||||||
|
/**
|
||||||
|
* 权限类型
|
||||||
|
*
|
||||||
|
* 枚举 {@link cn.iocoder.yudao.module.infra.enums.doc.DocPermissionTypeEnum}
|
||||||
|
*/
|
||||||
|
private Integer permissionType;
|
||||||
|
/**
|
||||||
|
* 过期时间
|
||||||
|
*/
|
||||||
|
private LocalDateTime expireTime;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.dal.dataobject.doc;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
|
||||||
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.KeySequence;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableName;
|
||||||
|
import lombok.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档版本 DO
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@TableName("infra_doc_file_version")
|
||||||
|
@KeySequence("infra_doc_file_version_seq") // 用于 Oracle、PostgreSQL、Kingbase、DB2、H2 数据库的主键自增。如果是 MySQL 等数据库,可不写。
|
||||||
|
@Data
|
||||||
|
@EqualsAndHashCode(callSuper = true)
|
||||||
|
@ToString(callSuper = true)
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DocFileVersionDO extends BaseDO {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 版本编号
|
||||||
|
*/
|
||||||
|
@TableId(type = IdType.ASSIGN_ID)
|
||||||
|
private Long id;
|
||||||
|
/**
|
||||||
|
* 文档编号
|
||||||
|
*/
|
||||||
|
private Long docFileId;
|
||||||
|
/**
|
||||||
|
* 版本号
|
||||||
|
*/
|
||||||
|
private String versionNo;
|
||||||
|
/**
|
||||||
|
* 文件编号
|
||||||
|
*/
|
||||||
|
private Long fileId;
|
||||||
|
/**
|
||||||
|
* 变更说明
|
||||||
|
*/
|
||||||
|
private String changeDescription;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.dal.mysql.doc;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocEditHistoryDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档编辑历史 Mapper
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DocEditHistoryMapper extends BaseMapperX<DocEditHistoryDO> {
|
||||||
|
|
||||||
|
default List<DocEditHistoryDO> selectListByDocFileId(Long docFileId) {
|
||||||
|
return selectList(new LambdaQueryWrapperX<DocEditHistoryDO>()
|
||||||
|
.eq(DocEditHistoryDO::getDocFileId, docFileId)
|
||||||
|
.orderByDesc(DocEditHistoryDO::getCreateTime));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.dal.mysql.doc;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
|
import cn.iocoder.yudao.module.infra.controller.admin.doc.vo.DocFilePageReqVO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFileDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线文档 Mapper
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DocFileMapper extends BaseMapperX<DocFileDO> {
|
||||||
|
|
||||||
|
default PageResult<DocFileDO> selectPage(DocFilePageReqVO reqVO) {
|
||||||
|
return selectPage(reqVO, new LambdaQueryWrapperX<DocFileDO>()
|
||||||
|
.likeIfPresent(DocFileDO::getTitle, reqVO.getTitle())
|
||||||
|
.eqIfPresent(DocFileDO::getFileType, reqVO.getFileType())
|
||||||
|
.eqIfPresent(DocFileDO::getSpaceType, reqVO.getSpaceType())
|
||||||
|
.betweenIfPresent(DocFileDO::getCreateTime, reqVO.getCreateTime())
|
||||||
|
.eqIfPresent(DocFileDO::getStatus, reqVO.getStatus())
|
||||||
|
.orderByDesc(DocFileDO::getId));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.dal.mysql.doc;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFilePermissionDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档权限 Mapper
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DocFilePermissionMapper extends BaseMapperX<DocFilePermissionDO> {
|
||||||
|
|
||||||
|
default List<DocFilePermissionDO> selectListByDocFileId(Long docFileId) {
|
||||||
|
return selectList(new LambdaQueryWrapperX<DocFilePermissionDO>()
|
||||||
|
.eq(DocFilePermissionDO::getDocFileId, docFileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
default DocFilePermissionDO selectByDocFileIdAndRoleId(Long docFileId, Long roleId) {
|
||||||
|
return selectOne(new LambdaQueryWrapperX<DocFilePermissionDO>()
|
||||||
|
.eq(DocFilePermissionDO::getDocFileId, docFileId)
|
||||||
|
.eq(DocFilePermissionDO::getRoleId, roleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<DocFilePermissionDO> selectListByRoleId(Long roleId) {
|
||||||
|
return selectList(new LambdaQueryWrapperX<DocFilePermissionDO>()
|
||||||
|
.eq(DocFilePermissionDO::getRoleId, roleId));
|
||||||
|
}
|
||||||
|
|
||||||
|
default List<DocFilePermissionDO> selectListByRoleIds(List<Long> roleIds) {
|
||||||
|
return selectList(new LambdaQueryWrapperX<DocFilePermissionDO>()
|
||||||
|
.in(DocFilePermissionDO::getRoleId, roleIds));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.dal.mysql.doc;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
|
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFileVersionDO;
|
||||||
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档版本 Mapper
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Mapper
|
||||||
|
public interface DocFileVersionMapper extends BaseMapperX<DocFileVersionDO> {
|
||||||
|
|
||||||
|
default List<DocFileVersionDO> selectListByDocFileId(Long docFileId) {
|
||||||
|
return selectList(new LambdaQueryWrapperX<DocFileVersionDO>()
|
||||||
|
.eq(DocFileVersionDO::getDocFileId, docFileId)
|
||||||
|
.orderByDesc(DocFileVersionDO::getId));
|
||||||
|
}
|
||||||
|
|
||||||
|
default DocFileVersionDO selectLatestByDocFileId(Long docFileId) {
|
||||||
|
return selectOne(new LambdaQueryWrapperX<DocFileVersionDO>()
|
||||||
|
.eq(DocFileVersionDO::getDocFileId, docFileId)
|
||||||
|
.orderByDesc(DocFileVersionDO::getId)
|
||||||
|
.last("LIMIT 1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
default Long selectCountByDocFileId(Long docFileId) {
|
||||||
|
return selectCount(new LambdaQueryWrapperX<DocFileVersionDO>()
|
||||||
|
.eq(DocFileVersionDO::getDocFileId, docFileId));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.enums.doc;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档编辑类型枚举
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum DocEditTypeEnum {
|
||||||
|
|
||||||
|
CREATE(1, "创建"),
|
||||||
|
EDIT(2, "编辑"),
|
||||||
|
DELETE(3, "删除"),
|
||||||
|
RENAME(4, "重命名");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
private final Integer type;
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.enums.doc;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档文件类型枚举
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum DocFileTypeEnum {
|
||||||
|
|
||||||
|
DOCX("docx", "Word文档"),
|
||||||
|
XLSX("xlsx", "Excel表格"),
|
||||||
|
PPTX("pptx", "PowerPoint演示文稿"),
|
||||||
|
PDF("pdf", "PDF文档");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
private final String type;
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.enums.doc;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档权限类型枚举
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum DocPermissionTypeEnum {
|
||||||
|
|
||||||
|
READ(1, "只读"),
|
||||||
|
EDIT(2, "编辑"),
|
||||||
|
MANAGE(3, "管理");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
private final Integer type;
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.enums.doc;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档空间类型枚举
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum DocSpaceTypeEnum {
|
||||||
|
|
||||||
|
PERSONAL(1, "个人空间"),
|
||||||
|
TEAM(2, "团队空间");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 类型
|
||||||
|
*/
|
||||||
|
private final Integer type;
|
||||||
|
/**
|
||||||
|
* 描述
|
||||||
|
*/
|
||||||
|
private final String description;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
package cn.iocoder.yudao.module.infra.framework.rpc.config;
|
package cn.iocoder.yudao.module.infra.framework.rpc.config;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
|
||||||
|
import cn.iocoder.yudao.module.system.api.permission.RoleApi;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
@Configuration(value = "infraRpcConfiguration", proxyBeanMethods = false)
|
@Configuration(value = "infraRpcConfiguration", proxyBeanMethods = false)
|
||||||
@EnableFeignClients()
|
@EnableFeignClients(clients = {PermissionApi.class, RoleApi.class, AdminUserApi.class})
|
||||||
public class RpcConfiguration {
|
public class RpcConfiguration {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,6 +38,9 @@ public class SecurityConfiguration {
|
|||||||
.requestMatchers(adminSeverContextPath + "/**").permitAll();
|
.requestMatchers(adminSeverContextPath + "/**").permitAll();
|
||||||
// 文件读取
|
// 文件读取
|
||||||
registry.requestMatchers(buildAdminApi("/infra/file/*/get/**")).permitAll();
|
registry.requestMatchers(buildAdminApi("/infra/file/*/get/**")).permitAll();
|
||||||
|
// 在线文档相关接口
|
||||||
|
registry.requestMatchers(buildAdminApi("/infra/doc-file/file-content")).permitAll();
|
||||||
|
registry.requestMatchers(buildAdminApi("/infra/doc-file/callback")).permitAll();
|
||||||
|
|
||||||
// TODO 芋艿:这个每个项目都需要重复配置,得捉摸有没通用的方案
|
// TODO 芋艿:这个每个项目都需要重复配置,得捉摸有没通用的方案
|
||||||
// RPC 服务的安全配置
|
// RPC 服务的安全配置
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.service.doc;
|
||||||
|
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
|
import cn.iocoder.yudao.module.infra.controller.admin.doc.vo.*;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFileDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFilePermissionDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFileVersionDO;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线文档 Service 接口
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
public interface DocFileService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 创建在线文档
|
||||||
|
*
|
||||||
|
* @param createReqVO 创建信息
|
||||||
|
* @return 编号
|
||||||
|
*/
|
||||||
|
Long createDocFile(@Valid DocFileCreateReqVO createReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新在线文档
|
||||||
|
*
|
||||||
|
* @param updateReqVO 更新信息
|
||||||
|
*/
|
||||||
|
void updateDocFile(@Valid DocFileUpdateReqVO updateReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除在线文档
|
||||||
|
*
|
||||||
|
* @param id 编号
|
||||||
|
*/
|
||||||
|
void deleteDocFile(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得在线文档
|
||||||
|
*
|
||||||
|
* @param id 编号
|
||||||
|
* @return 在线文档
|
||||||
|
*/
|
||||||
|
DocFileDO getDocFile(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得在线文档完整信息(包含文件信息)
|
||||||
|
*
|
||||||
|
* @param id 编号
|
||||||
|
* @return 在线文档完整信息
|
||||||
|
*/
|
||||||
|
DocFileRespVO getDocFileWithFileInfo(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得在线文档分页
|
||||||
|
*
|
||||||
|
* @param pageReqVO 分页查询
|
||||||
|
* @return 在线文档分页
|
||||||
|
*/
|
||||||
|
PageResult<DocFileDO> getDocFilePage(DocFilePageReqVO pageReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获得在线文档分页(包含文件信息)
|
||||||
|
*
|
||||||
|
* @param pageReqVO 分页查询
|
||||||
|
* @return 在线文档分页
|
||||||
|
*/
|
||||||
|
PageResult<DocFileRespVO> getDocFilePageWithFileInfo(DocFilePageReqVO pageReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 上传文档
|
||||||
|
*
|
||||||
|
* @param file 文件
|
||||||
|
* @param title 文档标题
|
||||||
|
* @param spaceType 空间类型
|
||||||
|
* @param description 描述
|
||||||
|
* @return 编号
|
||||||
|
*/
|
||||||
|
Long uploadDocFile(MultipartFile file, String title, Integer spaceType, String description);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取编辑器配置
|
||||||
|
*
|
||||||
|
* @param id 文档编号
|
||||||
|
* @return 编辑器配置
|
||||||
|
*/
|
||||||
|
DocEditorConfigRespVO getEditorConfig(Long id);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档文件内容
|
||||||
|
*
|
||||||
|
* @param fileId 文件编号
|
||||||
|
* @param token JWT令牌
|
||||||
|
* @param response HTTP响应对象
|
||||||
|
*/
|
||||||
|
void getDocFileContent(Long fileId, String token, jakarta.servlet.http.HttpServletResponse response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存文档内容(OnlyOffice回调)
|
||||||
|
*
|
||||||
|
* @param id 文档编号
|
||||||
|
* @param downloadUrl 下载地址
|
||||||
|
* @param changeDescription 变更说明
|
||||||
|
* @param triggerUserId 触发保存的用户ID(可选,为空时使用系统用户)
|
||||||
|
*/
|
||||||
|
void saveDocumentContent(Long id, String downloadUrl, String changeDescription, Long triggerUserId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 保存文档内容(OnlyOffice回调) - 兼容旧版本
|
||||||
|
*
|
||||||
|
* @param id 文档编号
|
||||||
|
* @param downloadUrl 下载地址
|
||||||
|
* @param changeDescription 变更说明
|
||||||
|
*/
|
||||||
|
default void saveDocumentContent(Long id, String downloadUrl, String changeDescription) {
|
||||||
|
saveDocumentContent(id, downloadUrl, changeDescription, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置文档权限
|
||||||
|
*
|
||||||
|
* @param permissionReqVO 权限信息
|
||||||
|
*/
|
||||||
|
void setDocFilePermission(@Valid DocFilePermissionReqVO permissionReqVO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档权限列表
|
||||||
|
*
|
||||||
|
* @param docFileId 文档编号
|
||||||
|
* @return 权限列表
|
||||||
|
*/
|
||||||
|
List<DocFilePermissionDO> getDocFilePermissions(Long docFileId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除文档权限
|
||||||
|
*
|
||||||
|
* @param docFileId 文档编号
|
||||||
|
* @param roleId 角色编号
|
||||||
|
*/
|
||||||
|
void deleteDocFilePermission(Long docFileId, Long roleId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查用户是否有文档权限
|
||||||
|
*
|
||||||
|
* @param docFileId 文档编号
|
||||||
|
* @param userId 用户编号
|
||||||
|
* @param permissionType 权限类型
|
||||||
|
* @return 是否有权限
|
||||||
|
*/
|
||||||
|
boolean hasPermission(Long docFileId, Long userId, Integer permissionType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取文档版本历史
|
||||||
|
*
|
||||||
|
* @param docFileId 文档编号
|
||||||
|
* @return 版本列表
|
||||||
|
*/
|
||||||
|
List<DocFileVersionDO> getDocFileVersions(Long docFileId);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 恢复到指定版本
|
||||||
|
*
|
||||||
|
* @param docFileId 文档编号
|
||||||
|
* @param versionId 版本编号
|
||||||
|
*/
|
||||||
|
void restoreDocFileToVersion(Long docFileId, Long versionId);
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,849 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.service.doc.impl;
|
||||||
|
|
||||||
|
import cn.hutool.core.io.IoUtil;
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import cn.hutool.http.HttpUtil;
|
||||||
|
import cn.hutool.jwt.JWT;
|
||||||
|
import cn.iocoder.yudao.framework.common.pojo.PageResult;
|
||||||
|
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
|
||||||
|
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
|
||||||
|
import cn.iocoder.yudao.module.infra.controller.admin.doc.vo.*;
|
||||||
|
import cn.iocoder.yudao.module.infra.convert.doc.DocFileConvert;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocEditHistoryDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFileDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFilePermissionDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.doc.DocFileVersionDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.dataobject.file.FileDO;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.mysql.doc.DocEditHistoryMapper;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.mysql.doc.DocFileMapper;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.mysql.doc.DocFilePermissionMapper;
|
||||||
|
import cn.iocoder.yudao.module.infra.dal.mysql.doc.DocFileVersionMapper;
|
||||||
|
import cn.iocoder.yudao.module.infra.enums.doc.DocPermissionTypeEnum;
|
||||||
|
import cn.iocoder.yudao.module.infra.service.doc.DocFileService;
|
||||||
|
import cn.iocoder.yudao.module.infra.service.file.FileService;
|
||||||
|
import cn.iocoder.yudao.module.infra.websocket.DocWebSocketHandler;
|
||||||
|
import cn.iocoder.yudao.module.system.api.permission.PermissionApi;
|
||||||
|
import cn.iocoder.yudao.module.system.api.permission.RoleApi;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
|
||||||
|
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.http.HttpServletResponse;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
|
||||||
|
import static cn.iocoder.yudao.module.infra.enums.ErrorCodeConstants.DOC_NOT_EXISTS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线文档 Service 实现类
|
||||||
|
*/
|
||||||
|
@Service
|
||||||
|
@Slf4j
|
||||||
|
public class DocFileServiceImpl implements DocFileService {
|
||||||
|
|
||||||
|
// OnlyOffice支持的文件类型定义
|
||||||
|
private static final String TEXT_DOCUMENT_EXTENSIONS = "doc,docx,odt,rtf,txt,html,htm,mht,pdf,djvu,fb2,epub,xps";
|
||||||
|
private static final String SPREADSHEET_EXTENSIONS = "xls,xlsx,ods,csv";
|
||||||
|
private static final String PRESENTATION_EXTENSIONS = "ppt,pptx,odp";
|
||||||
|
|
||||||
|
// 文档类型常量
|
||||||
|
private static final String DOCUMENT_TYPE_TEXT = "text";
|
||||||
|
private static final String DOCUMENT_TYPE_SPREADSHEET = "spreadsheet";
|
||||||
|
private static final String DOCUMENT_TYPE_PRESENTATION = "presentation";
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private DocFileMapper docFileMapper;
|
||||||
|
@Resource
|
||||||
|
private DocFileVersionMapper docFileVersionMapper;
|
||||||
|
@Resource
|
||||||
|
private DocFilePermissionMapper docFilePermissionMapper;
|
||||||
|
@Resource
|
||||||
|
private DocEditHistoryMapper docEditHistoryMapper;
|
||||||
|
@Resource
|
||||||
|
private FileService fileService;
|
||||||
|
@Resource
|
||||||
|
private PermissionApi permissionApi;
|
||||||
|
@Resource
|
||||||
|
private RoleApi roleApi;
|
||||||
|
@Resource
|
||||||
|
private AdminUserApi adminUserApi;
|
||||||
|
|
||||||
|
@Value("${onlyoffice.base-url:http://localhost:8085}")
|
||||||
|
private String onlyOfficeBaseUrl;
|
||||||
|
@Value("${onlyoffice.callback-base-url:http://host.docker.internal:48080}")
|
||||||
|
private String onlyOfficeCallbackBaseUrl;
|
||||||
|
@Value("${onlyoffice.jwt-secret:P@ssword25}")
|
||||||
|
private String onlyOfficeJwtSecret;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据文件扩展名获取OnlyOffice文档类型
|
||||||
|
* @param fileExtension 文件扩展名
|
||||||
|
* @return OnlyOffice文档类型
|
||||||
|
*/
|
||||||
|
private String getDocumentType(String fileExtension) {
|
||||||
|
if (StrUtil.isEmpty(fileExtension)) {
|
||||||
|
return DOCUMENT_TYPE_TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ext = fileExtension.toLowerCase();
|
||||||
|
|
||||||
|
// 文本文档类型
|
||||||
|
if (TEXT_DOCUMENT_EXTENSIONS.contains(ext)) {
|
||||||
|
return DOCUMENT_TYPE_TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 电子表格类型
|
||||||
|
if (SPREADSHEET_EXTENSIONS.contains(ext)) {
|
||||||
|
return DOCUMENT_TYPE_SPREADSHEET;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 演示文稿类型
|
||||||
|
if (PRESENTATION_EXTENSIONS.contains(ext)) {
|
||||||
|
return DOCUMENT_TYPE_PRESENTATION;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认返回text类型
|
||||||
|
return DOCUMENT_TYPE_TEXT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取默认的文件扩展名,基于请求中的文档类型或文件类型
|
||||||
|
* @param requestedFileType 请求的文件类型
|
||||||
|
* @return 默认的文件扩展名
|
||||||
|
*/
|
||||||
|
private String getDefaultFileExtension(String requestedFileType) {
|
||||||
|
if (StrUtil.isNotEmpty(requestedFileType)) {
|
||||||
|
return requestedFileType.toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认返回docx(Word文档)
|
||||||
|
return "docx";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证文件类型是否被OnlyOffice支持
|
||||||
|
* @param fileExtension 文件扩展名
|
||||||
|
* @return 是否支持
|
||||||
|
*/
|
||||||
|
private boolean isSupportedFileType(String fileExtension) {
|
||||||
|
if (StrUtil.isEmpty(fileExtension)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
String ext = fileExtension.toLowerCase();
|
||||||
|
return TEXT_DOCUMENT_EXTENSIONS.contains(ext) ||
|
||||||
|
SPREADSHEET_EXTENSIONS.contains(ext) ||
|
||||||
|
PRESENTATION_EXTENSIONS.contains(ext);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Long createDocFile(@Valid DocFileCreateReqVO createReqVO) {
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
DocFileDO doc = BeanUtils.toBean(createReqVO, DocFileDO.class);
|
||||||
|
doc.setOwnerUserId(userId);
|
||||||
|
doc.setStatus(1);
|
||||||
|
|
||||||
|
// 如果提供了文件信息,使用上传的文件;否则创建空文档
|
||||||
|
if (createReqVO.getFileId() != null) {
|
||||||
|
// 使用已上传的文件
|
||||||
|
doc.setFileId(createReqVO.getFileId());
|
||||||
|
// 从文件类型后缀推断,如果没有提供 fileType
|
||||||
|
if (StrUtil.isEmpty(createReqVO.getFileType())) {
|
||||||
|
try {
|
||||||
|
var fileInfo = fileService.getActiveFileById(createReqVO.getFileId());
|
||||||
|
if (fileInfo != null && StrUtil.isNotEmpty(fileInfo.getName())) {
|
||||||
|
String fileName = fileInfo.getName();
|
||||||
|
if (fileName.contains(".")) {
|
||||||
|
doc.setFileType(fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 如果获取文件信息失败,使用默认值
|
||||||
|
doc.setFileType(getDefaultFileExtension(createReqVO.getFileType()));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
doc.setFileType(createReqVO.getFileType());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 创建空文档
|
||||||
|
doc.setFileId(null);
|
||||||
|
doc.setFileType(getDefaultFileExtension(createReqVO.getFileType()));
|
||||||
|
}
|
||||||
|
|
||||||
|
docFileMapper.insert(doc);
|
||||||
|
|
||||||
|
// 创建初始版本记录
|
||||||
|
DocFileVersionDO version = DocFileVersionDO.builder()
|
||||||
|
.docFileId(doc.getId())
|
||||||
|
.versionNo("v1")
|
||||||
|
.fileId(createReqVO.getFileId()) // 如果是新建文档,这个为null,从上传创建则有值
|
||||||
|
.changeDescription("创建文档")
|
||||||
|
.build();
|
||||||
|
docFileVersionMapper.insert(version);
|
||||||
|
doc.setLatestVersionId(version.getId());
|
||||||
|
docFileMapper.updateById(doc);
|
||||||
|
|
||||||
|
// 添加创建历史
|
||||||
|
docEditHistoryMapper.insert(DocEditHistoryDO.builder()
|
||||||
|
.docFileId(doc.getId()).userId(userId).userName(String.valueOf(userId))
|
||||||
|
.editType(1).description("创建文档").build());
|
||||||
|
|
||||||
|
// 给创建者的角色分配管理权限
|
||||||
|
Set<Long> userRoleIds = permissionApi.getUserRoleIdListByUserId(userId).getCheckedData();
|
||||||
|
for (Long roleId : userRoleIds) {
|
||||||
|
DocFilePermissionDO permission = DocFilePermissionDO.builder()
|
||||||
|
.docFileId(doc.getId())
|
||||||
|
.roleId(roleId)
|
||||||
|
.permissionType(DocPermissionTypeEnum.MANAGE.getType())
|
||||||
|
.build();
|
||||||
|
// 检查是否已存在权限记录,避免重复插入
|
||||||
|
DocFilePermissionDO exists = docFilePermissionMapper.selectByDocFileIdAndRoleId(doc.getId(), roleId);
|
||||||
|
if (exists == null) {
|
||||||
|
docFilePermissionMapper.insert(permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return doc.getId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDocFile(@Valid DocFileUpdateReqVO updateReqVO) {
|
||||||
|
DocFileDO exists = docFileMapper.selectById(updateReqVO.getId());
|
||||||
|
if (exists == null) {
|
||||||
|
throw exception(DOC_NOT_EXISTS);
|
||||||
|
}
|
||||||
|
if (StrUtil.isNotEmpty(updateReqVO.getTitle())) {
|
||||||
|
exists.setTitle(updateReqVO.getTitle());
|
||||||
|
}
|
||||||
|
if (updateReqVO.getDescription() != null) {
|
||||||
|
exists.setDescription(updateReqVO.getDescription());
|
||||||
|
}
|
||||||
|
docFileMapper.updateById(exists);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteDocFile(Long id) {
|
||||||
|
DocFileDO exists = docFileMapper.selectById(id);
|
||||||
|
if (exists == null) {
|
||||||
|
throw exception(DOC_NOT_EXISTS);
|
||||||
|
}
|
||||||
|
docFileMapper.deleteById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocFileDO getDocFile(Long id) {
|
||||||
|
return docFileMapper.selectById(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocFileRespVO getDocFileWithFileInfo(Long id) {
|
||||||
|
DocFileDO doc = docFileMapper.selectById(id);
|
||||||
|
if (doc == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return buildDocFileRespVO(doc);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<DocFileDO> getDocFilePage(DocFilePageReqVO pageReqVO) {
|
||||||
|
return docFileMapper.selectPage(pageReqVO);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public PageResult<DocFileRespVO> getDocFilePageWithFileInfo(DocFilePageReqVO pageReqVO) {
|
||||||
|
PageResult<DocFileDO> page = docFileMapper.selectPage(pageReqVO);
|
||||||
|
return new PageResult<>(
|
||||||
|
page.getList().stream().map(this::buildDocFileRespVO).collect(java.util.stream.Collectors.toList()),
|
||||||
|
page.getTotal()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建包含文件信息的 DocFileRespVO
|
||||||
|
*/
|
||||||
|
private DocFileRespVO buildDocFileRespVO(DocFileDO doc) {
|
||||||
|
DocFileRespVO respVO = DocFileConvert.INSTANCE.convert(doc);
|
||||||
|
|
||||||
|
// 从 fileService 获取文件信息
|
||||||
|
if (doc.getFileId() != null) {
|
||||||
|
try {
|
||||||
|
var fileInfo = fileService.getActiveFileById(doc.getFileId());
|
||||||
|
if (fileInfo != null) {
|
||||||
|
respVO.setFileName(fileInfo.getName());
|
||||||
|
respVO.setFileSize(fileInfo.getSize().longValue());
|
||||||
|
|
||||||
|
// 生成文件访问的JWT token
|
||||||
|
String fileToken = "";
|
||||||
|
if (StrUtil.isNotBlank(onlyOfficeJwtSecret)) {
|
||||||
|
// 创建文件访问token,包含fileId和过期时间(1小时后过期)
|
||||||
|
long expTime = System.currentTimeMillis() / 1000 + 3600; // 1小时后过期
|
||||||
|
fileToken = JWT.create()
|
||||||
|
.setPayload("fileId", doc.getFileId())
|
||||||
|
.setPayload("exp", expTime)
|
||||||
|
.setPayload("iat", System.currentTimeMillis() / 1000)
|
||||||
|
.setKey(onlyOfficeJwtSecret.getBytes())
|
||||||
|
.sign();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用新的接口生成文件URL,包含JWT token
|
||||||
|
String fileUrl = StrUtil.removeSuffix(onlyOfficeCallbackBaseUrl, "/") + "/admin-api/infra/doc-file/file-content?fileId=" + doc.getFileId();
|
||||||
|
if (StrUtil.isNotBlank(fileToken)) {
|
||||||
|
fileUrl += "&token=" + fileToken;
|
||||||
|
}
|
||||||
|
respVO.setFileUrl(fileUrl);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取文件信息失败: docId={}, fileId={}", doc.getId(), doc.getFileId(), e);
|
||||||
|
// 设置默认值
|
||||||
|
respVO.setFileName(doc.getTitle() + "." + doc.getFileType());
|
||||||
|
respVO.setFileSize(0L);
|
||||||
|
respVO.setFileUrl("");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 空文档
|
||||||
|
respVO.setFileName(doc.getTitle() + "." + doc.getFileType());
|
||||||
|
respVO.setFileSize(0L);
|
||||||
|
respVO.setFileUrl("");
|
||||||
|
}
|
||||||
|
|
||||||
|
return respVO;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public Long uploadDocFile(MultipartFile file, String title, Integer spaceType, String description) {
|
||||||
|
try (InputStream is = file.getInputStream()) {
|
||||||
|
byte[] content = IoUtil.readBytes(is);
|
||||||
|
String fileName = file.getOriginalFilename();
|
||||||
|
String ext = fileName != null && fileName.contains(".") ?
|
||||||
|
fileName.substring(fileName.lastIndexOf('.') + 1).toLowerCase() :
|
||||||
|
getDefaultFileExtension(null);
|
||||||
|
|
||||||
|
// 验证文件类型是否支持
|
||||||
|
if (!isSupportedFileType(ext)) {
|
||||||
|
log.warn("不支持的文件类型: {}, 文件名: {}", ext, fileName);
|
||||||
|
throw new RuntimeException("不支持的文件类型: " + ext + "。支持的文件类型包括:" +
|
||||||
|
TEXT_DOCUMENT_EXTENSIONS + "," + SPREADSHEET_EXTENSIONS + "," + PRESENTATION_EXTENSIONS);
|
||||||
|
}
|
||||||
|
|
||||||
|
String name = title + "." + ext;
|
||||||
|
var saved = fileService.createFileWhitReturn(content, name, "document", file.getContentType(), false);
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
DocFileDO doc = DocFileDO.builder()
|
||||||
|
.title(title)
|
||||||
|
.fileId(saved.getId())
|
||||||
|
.fileType(ext)
|
||||||
|
.spaceType(spaceType)
|
||||||
|
.description(description)
|
||||||
|
.ownerUserId(userId)
|
||||||
|
.status(1)
|
||||||
|
.build();
|
||||||
|
docFileMapper.insert(doc);
|
||||||
|
// 版本
|
||||||
|
DocFileVersionDO version = DocFileVersionDO.builder()
|
||||||
|
.docFileId(doc.getId())
|
||||||
|
.versionNo("v1")
|
||||||
|
.fileId(saved.getId())
|
||||||
|
.build();
|
||||||
|
docFileVersionMapper.insert(version);
|
||||||
|
doc.setLatestVersionId(version.getId());
|
||||||
|
docFileMapper.updateById(doc);
|
||||||
|
|
||||||
|
// 给上传者的所有角色分配管理权限
|
||||||
|
Set<Long> userRoleIds = permissionApi.getUserRoleIdListByUserId(userId).getCheckedData();
|
||||||
|
for (Long roleId : userRoleIds) {
|
||||||
|
DocFilePermissionDO permission = DocFilePermissionDO.builder()
|
||||||
|
.docFileId(doc.getId())
|
||||||
|
.roleId(roleId)
|
||||||
|
.permissionType(DocPermissionTypeEnum.MANAGE.getType())
|
||||||
|
.build();
|
||||||
|
// 检查是否已存在权限记录,避免重复插入
|
||||||
|
DocFilePermissionDO exists = docFilePermissionMapper.selectByDocFileIdAndRoleId(doc.getId(), roleId);
|
||||||
|
if (exists == null) {
|
||||||
|
docFilePermissionMapper.insert(permission);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
docEditHistoryMapper.insert(DocEditHistoryDO.builder()
|
||||||
|
.docFileId(doc.getId()).userId(userId).userName(String.valueOf(userId))
|
||||||
|
.editType(1).description("上传文档").build());
|
||||||
|
return doc.getId();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public DocEditorConfigRespVO getEditorConfig(Long id) {
|
||||||
|
DocFileDO doc = docFileMapper.selectById(id);
|
||||||
|
if (doc == null) {
|
||||||
|
throw exception(DOC_NOT_EXISTS);
|
||||||
|
}
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
DocEditorConfigRespVO resp = new DocEditorConfigRespVO();
|
||||||
|
|
||||||
|
// 根据文件类型动态设置文档类型
|
||||||
|
String documentType = getDocumentType(doc.getFileType());
|
||||||
|
resp.setDocumentType(documentType);
|
||||||
|
|
||||||
|
DocEditorConfigRespVO.DocumentConfig document = new DocEditorConfigRespVO.DocumentConfig();
|
||||||
|
document.setFileType(doc.getFileType());
|
||||||
|
|
||||||
|
// 关键修复:使用基于文档版本的稳定key策略
|
||||||
|
// 只有当文档内容真正发生版本变更时,key才会改变
|
||||||
|
String documentKey;
|
||||||
|
if (doc.getLatestVersionId() != null) {
|
||||||
|
// 使用文档ID + 最新版本ID,确保同一版本在编辑期间key保持不变
|
||||||
|
documentKey = doc.getId() + "_v" + doc.getLatestVersionId();
|
||||||
|
} else {
|
||||||
|
// 如果没有版本信息,使用文档ID + 创建时间(不会变化)
|
||||||
|
documentKey = doc.getId() + "_init_" + doc.getCreateTime().toEpochSecond(java.time.ZoneOffset.UTC);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.setKey(documentKey);
|
||||||
|
document.setTitle(doc.getTitle());
|
||||||
|
|
||||||
|
log.info("生成文档key: docId={}, key={}, latestVersionId={}, userId={}",
|
||||||
|
doc.getId(), documentKey, doc.getLatestVersionId(), userId);
|
||||||
|
|
||||||
|
// 使用新的接口生成文件 URL
|
||||||
|
String fileUrl = "";
|
||||||
|
if (doc.getFileId() != null) {
|
||||||
|
// 生成文件访问的JWT token
|
||||||
|
String fileToken = "";
|
||||||
|
if (StrUtil.isNotBlank(onlyOfficeJwtSecret)) {
|
||||||
|
// 创建文件访问token,包含fileId和过期时间(1小时后过期)
|
||||||
|
long expTime = System.currentTimeMillis() / 1000 + 3600; // 1小时后过期
|
||||||
|
fileToken = JWT.create()
|
||||||
|
.setPayload("fileId", doc.getFileId())
|
||||||
|
.setPayload("exp", expTime)
|
||||||
|
.setPayload("iat", System.currentTimeMillis() / 1000)
|
||||||
|
.setKey(onlyOfficeJwtSecret.getBytes())
|
||||||
|
.sign();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成通过新接口访问文件的URL,包含JWT token
|
||||||
|
fileUrl = StrUtil.removeSuffix(onlyOfficeCallbackBaseUrl, "/") + "/admin-api/infra/doc-file/file-content?fileId=" + doc.getFileId();
|
||||||
|
if (StrUtil.isNotBlank(fileToken)) {
|
||||||
|
fileUrl += "&token=" + fileToken;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.setUrl(fileUrl);
|
||||||
|
|
||||||
|
DocEditorConfigRespVO.Permissions permissions = new DocEditorConfigRespVO.Permissions();
|
||||||
|
permissions.setEdit(hasPermission(id, userId, DocPermissionTypeEnum.EDIT.getType()) || hasPermission(id, userId, DocPermissionTypeEnum.MANAGE.getType()));
|
||||||
|
permissions.setDownload(true);
|
||||||
|
permissions.setPrint(true);
|
||||||
|
document.setPermissions(permissions);
|
||||||
|
resp.setDocument(document);
|
||||||
|
|
||||||
|
DocEditorConfigRespVO.EditorConfig editorConfig = new DocEditorConfigRespVO.EditorConfig();
|
||||||
|
// 在回调URL中添加触发用户ID,确保回调时能准确记录操作用户
|
||||||
|
String callbackUrl = StrUtil.removeSuffix(onlyOfficeCallbackBaseUrl, "/") +
|
||||||
|
"/admin-api/infra/doc-file/callback?docFileId=" + id + "&userId=" + userId;
|
||||||
|
editorConfig.setCallbackUrl(callbackUrl);
|
||||||
|
editorConfig.setLang("zh-CN");
|
||||||
|
editorConfig.setMode(permissions.getEdit() ? "edit" : "view");
|
||||||
|
|
||||||
|
// 重要:设置正确的用户信息以支持多人协作
|
||||||
|
DocEditorConfigRespVO.User user = new DocEditorConfigRespVO.User();
|
||||||
|
user.setId(String.valueOf(userId));
|
||||||
|
|
||||||
|
// 获取真实用户姓名
|
||||||
|
String userName = "User" + userId; // 默认值
|
||||||
|
try {
|
||||||
|
AdminUserRespDTO userInfo = adminUserApi.getUser(userId).getCheckedData();
|
||||||
|
if (userInfo != null && StrUtil.isNotBlank(userInfo.getNickname())) {
|
||||||
|
userName = userInfo.getNickname();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取用户姓名失败,使用默认值,用户ID: {}", userId, e);
|
||||||
|
}
|
||||||
|
user.setName(userName);
|
||||||
|
editorConfig.setUser(user);
|
||||||
|
|
||||||
|
// 添加协作配置
|
||||||
|
DocEditorConfigRespVO.CoEditing coEditing = new DocEditorConfigRespVO.CoEditing();
|
||||||
|
coEditing.setMode("fast"); // 快速协作模式
|
||||||
|
coEditing.setChange(true); // 允许实时变更
|
||||||
|
editorConfig.setCoEditing(coEditing);
|
||||||
|
|
||||||
|
resp.setEditorConfig(editorConfig);
|
||||||
|
resp.setHeight("100%");
|
||||||
|
resp.setDocumentServerUrl(onlyOfficeBaseUrl);
|
||||||
|
|
||||||
|
// 生成 token(可选 JWT)
|
||||||
|
if (StrUtil.isNotBlank(onlyOfficeJwtSecret)) {
|
||||||
|
// OnlyOffice 标准 JWT payload 格式
|
||||||
|
String token = JWT.create()
|
||||||
|
.setPayload("document", resp.getDocument())
|
||||||
|
.setPayload("documentType", resp.getDocumentType())
|
||||||
|
.setPayload("editorConfig", resp.getEditorConfig())
|
||||||
|
.setPayload("height", resp.getHeight())
|
||||||
|
.setKey(onlyOfficeJwtSecret.getBytes())
|
||||||
|
.sign();
|
||||||
|
resp.setToken(token);
|
||||||
|
} else {
|
||||||
|
// 如果没有 JWT secret,不设置 token,让 OnlyOffice 在无验证模式工作
|
||||||
|
resp.setToken(null);
|
||||||
|
}
|
||||||
|
return resp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void getDocFileContent(Long fileId, String token, HttpServletResponse response) {
|
||||||
|
try {
|
||||||
|
// 验证JWT token
|
||||||
|
if (StrUtil.isBlank(token)) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证JWT签名和有效性
|
||||||
|
if (StrUtil.isNotBlank(onlyOfficeJwtSecret)) {
|
||||||
|
try {
|
||||||
|
JWT jwt = JWT.of(token).setKey(onlyOfficeJwtSecret.getBytes());
|
||||||
|
if (!jwt.verify()) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证token中的fileId是否匹配
|
||||||
|
Object tokenFileId = jwt.getPayload("fileId");
|
||||||
|
if (tokenFileId == null || !fileId.equals(Long.valueOf(tokenFileId.toString()))) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证token是否过期(可选,如果token中包含过期时间)
|
||||||
|
Object expTime = jwt.getPayload("exp");
|
||||||
|
if (expTime != null) {
|
||||||
|
long expTimestamp = Long.parseLong(expTime.toString());
|
||||||
|
if (System.currentTimeMillis() / 1000 > expTimestamp) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("JWT验证失败: token={}", token, e);
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件信息
|
||||||
|
FileDO file = fileService.getActiveFileById(fileId);
|
||||||
|
if (file == null) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取文件内容
|
||||||
|
byte[] content = fileService.getFileContent(fileId);
|
||||||
|
if (content == null) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_NOT_FOUND);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
response.setContentType(file.getType() != null ? file.getType() : "application/octet-stream");
|
||||||
|
response.setContentLength(content.length);
|
||||||
|
response.setHeader("Content-Disposition", "inline; filename=\"" + file.getName() + "\"");
|
||||||
|
// 添加跨域头,允许 OnlyOffice 访问
|
||||||
|
response.setHeader("Access-Control-Allow-Origin", "*");
|
||||||
|
response.setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
|
||||||
|
response.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With");
|
||||||
|
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
|
response.setHeader("Pragma", "no-cache");
|
||||||
|
response.setHeader("Expires", "0");
|
||||||
|
|
||||||
|
// 写入响应
|
||||||
|
try (OutputStream os = response.getOutputStream()) {
|
||||||
|
os.write(content);
|
||||||
|
os.flush();
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("获取文档文件内容失败: fileId={}", fileId, e);
|
||||||
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void saveDocumentContent(Long id, String downloadUrl, String changeDescription, Long triggerUserId) {
|
||||||
|
DocFileDO doc = docFileMapper.selectById(id);
|
||||||
|
if (doc == null) {
|
||||||
|
throw exception(DOC_NOT_EXISTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前登录用户ID(在回调场景下,已经在Controller中设置了正确的用户上下文)
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
if (userId == null) {
|
||||||
|
// 兜底:使用系统默认用户ID(1表示系统用户)
|
||||||
|
userId = 1L;
|
||||||
|
log.warn("无法获取登录用户ID,使用系统用户ID作为默认值: {}", userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("开始保存 OnlyOffice 文档: docId={}, userId={}, downloadUrl={}, changeDescription={}",
|
||||||
|
id, userId, downloadUrl, changeDescription);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 验证下载 URL 格式
|
||||||
|
if (StrUtil.isBlank(downloadUrl) || !downloadUrl.startsWith("http")) {
|
||||||
|
log.error("无效的下载 URL: docId={}, url={}", id, downloadUrl);
|
||||||
|
throw new RuntimeException("无效的下载 URL: " + downloadUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 下载 OnlyOffice 提供的最新文件内容
|
||||||
|
byte[] newContent;
|
||||||
|
try {
|
||||||
|
log.info("开始下载文档内容: url={}", downloadUrl);
|
||||||
|
newContent = HttpUtil.downloadBytes(downloadUrl); // 30秒超时
|
||||||
|
log.info("成功下载文档内容: size={} bytes", newContent.length);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("下载文档内容失败: url={}", downloadUrl, e);
|
||||||
|
throw new RuntimeException("下载文档内容失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newContent == null || newContent.length == 0) {
|
||||||
|
log.error("下载的文档内容为空: url={}", downloadUrl);
|
||||||
|
throw new RuntimeException("下载的文档内容为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 优化:检查内容是否与当前版本相同,避免创建重复版本
|
||||||
|
boolean contentChanged = true;
|
||||||
|
if (doc.getFileId() != null) {
|
||||||
|
try {
|
||||||
|
FileDO currentFile = fileService.getActiveFileById(doc.getFileId());
|
||||||
|
if (currentFile != null) {
|
||||||
|
// 获取当前文件的内容
|
||||||
|
byte[] currentContent = fileService.getFileContent(doc.getFileId());
|
||||||
|
if (currentContent != null && currentContent.length == newContent.length) {
|
||||||
|
// 简单的内容比较,如果完全相同则跳过版本创建
|
||||||
|
contentChanged = !java.util.Arrays.equals(currentContent, newContent);
|
||||||
|
log.info("内容比较结果: docId={}, contentChanged={}, oldSize={}, newSize={}",
|
||||||
|
id, contentChanged, currentContent.length, newContent.length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("无法比较文档内容,将继续创建新版本: docId={}", id, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!contentChanged) {
|
||||||
|
log.info("文档内容未发生变化,跳过版本创建: docId={}", id);
|
||||||
|
// 通过WebSocket通知,但不创建新版本
|
||||||
|
DocWebSocketHandler.notifyDocSaveStatus(String.valueOf(id), "success",
|
||||||
|
"文档已保存(内容未变更)");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重新上传生成新的文件 (保持扩展名)
|
||||||
|
String ext = doc.getFileType();
|
||||||
|
String newName = doc.getTitle() + "-" + System.currentTimeMillis() + "." + ext;
|
||||||
|
|
||||||
|
try {
|
||||||
|
var saved = fileService.createFileWhitReturn(newContent, newName, "document/onlyoffice", null, false);
|
||||||
|
log.info("成功保存新文件版本: docId={}, fileId={}", id, saved.getId());
|
||||||
|
|
||||||
|
// 获取当前文档的版本数量以生成正确的版本号
|
||||||
|
Long versionCount = docFileVersionMapper.selectCountByDocFileId(id);
|
||||||
|
String versionNo = "v" + (versionCount + 1);
|
||||||
|
|
||||||
|
DocFileVersionDO version = DocFileVersionDO.builder()
|
||||||
|
.docFileId(id)
|
||||||
|
.versionNo(versionNo)
|
||||||
|
.fileId(saved.getId())
|
||||||
|
.changeDescription(StrUtil.emptyToDefault(changeDescription, "自动保存"))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
// 手动设置创建者和更新者,解决回调场景下用户ID为空的问题
|
||||||
|
version.setCreator(userId.toString());
|
||||||
|
version.setUpdater(userId.toString());
|
||||||
|
version.setCreateTime(LocalDateTime.now());
|
||||||
|
version.setUpdateTime(LocalDateTime.now());
|
||||||
|
|
||||||
|
docFileVersionMapper.insert(version);
|
||||||
|
|
||||||
|
// 更新主表指向最新文件
|
||||||
|
doc.setLatestVersionId(version.getId());
|
||||||
|
doc.setFileId(saved.getId());
|
||||||
|
docFileMapper.updateById(doc);
|
||||||
|
|
||||||
|
// 记录历史 - 获取真实用户名
|
||||||
|
String userName = "系统用户"; // 默认值
|
||||||
|
if (userId > 1) { // 非系统用户时获取真实用户名
|
||||||
|
try {
|
||||||
|
AdminUserRespDTO userInfo = adminUserApi.getUser(userId).getCheckedData();
|
||||||
|
if (userInfo != null && StrUtil.isNotBlank(userInfo.getNickname())) {
|
||||||
|
userName = userInfo.getNickname();
|
||||||
|
} else {
|
||||||
|
userName = "User" + userId;
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("获取用户姓名失败,使用默认值,用户ID: {}", userId, e);
|
||||||
|
userName = "User" + userId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DocEditHistoryDO editHistory = DocEditHistoryDO.builder()
|
||||||
|
.docFileId(id)
|
||||||
|
.userId(userId)
|
||||||
|
.userName(userName)
|
||||||
|
.editType(2) // 编辑类型:编辑
|
||||||
|
.description("OnlyOffice回调保存: " + changeDescription)
|
||||||
|
.build();
|
||||||
|
docEditHistoryMapper.insert(editHistory);
|
||||||
|
|
||||||
|
log.info("文档保存完成: docId={}, versionId={}, versionNo={}", id, version.getId(), versionNo);
|
||||||
|
|
||||||
|
// 通过WebSocket通知所有协作用户保存成功
|
||||||
|
DocWebSocketHandler.notifyDocSaveStatus(String.valueOf(id), "success",
|
||||||
|
"文档保存成功: " + versionNo + " - " + changeDescription);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存文件版本失败: docId={}", id, e);
|
||||||
|
|
||||||
|
// 通过WebSocket通知保存失败
|
||||||
|
DocWebSocketHandler.notifyDocSaveStatus(String.valueOf(id), "error",
|
||||||
|
"文档保存失败: " + e.getMessage());
|
||||||
|
|
||||||
|
throw new RuntimeException("保存文件版本失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("保存 OnlyOffice 文档失败 docId={} url={}", id, downloadUrl, e);
|
||||||
|
|
||||||
|
// 通过WebSocket通知保存失败
|
||||||
|
DocWebSocketHandler.notifyDocSaveStatus(String.valueOf(id), "error",
|
||||||
|
"文档保存失败: " + e.getMessage());
|
||||||
|
|
||||||
|
throw new RuntimeException("保存文档失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setDocFilePermission(@Valid DocFilePermissionReqVO permissionReqVO) {
|
||||||
|
DocFilePermissionDO exists = docFilePermissionMapper.selectByDocFileIdAndRoleId(permissionReqVO.getDocFileId(), permissionReqVO.getRoleId());
|
||||||
|
if (exists == null) {
|
||||||
|
exists = BeanUtils.toBean(permissionReqVO, DocFilePermissionDO.class);
|
||||||
|
docFilePermissionMapper.insert(exists);
|
||||||
|
} else {
|
||||||
|
exists.setPermissionType(permissionReqVO.getPermissionType());
|
||||||
|
exists.setExpireTime(permissionReqVO.getExpireTime());
|
||||||
|
docFilePermissionMapper.updateById(exists);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DocFilePermissionDO> getDocFilePermissions(Long docFileId) {
|
||||||
|
return docFilePermissionMapper.selectListByDocFileId(docFileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void deleteDocFilePermission(Long docFileId, Long roleId) {
|
||||||
|
DocFilePermissionDO exists = docFilePermissionMapper.selectByDocFileIdAndRoleId(docFileId, roleId);
|
||||||
|
if (exists != null) {
|
||||||
|
docFilePermissionMapper.deleteById(exists.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean hasPermission(Long docFileId, Long userId, Integer permissionType) {
|
||||||
|
// 1. 超级管理员拥有所有权限
|
||||||
|
Set<Long> userRoleIds = permissionApi.getUserRoleIdListByUserId(userId).getCheckedData();
|
||||||
|
|
||||||
|
if (roleApi.hasAnySuperAdmin(userRoleIds).getCheckedData()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查是否为文档拥有者
|
||||||
|
DocFileDO docFile = docFileMapper.selectById(docFileId);
|
||||||
|
if (docFile != null && userId.equals(docFile.getOwnerUserId())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 检查用户角色权限
|
||||||
|
List<DocFilePermissionDO> rolePermissions = docFilePermissionMapper.selectListByRoleIds(new ArrayList<>(userRoleIds));
|
||||||
|
|
||||||
|
for (DocFilePermissionDO permission : rolePermissions) {
|
||||||
|
if (Objects.equals(permission.getDocFileId(), docFileId)) {
|
||||||
|
// 检查是否过期
|
||||||
|
if (permission.getExpireTime() != null && permission.getExpireTime().isBefore(LocalDateTime.now())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 管理权限拥有所有权限
|
||||||
|
if (permission.getPermissionType().equals(DocPermissionTypeEnum.MANAGE.getType())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查具体权限类型
|
||||||
|
if (permission.getPermissionType().equals(permissionType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<DocFileVersionDO> getDocFileVersions(Long docFileId) {
|
||||||
|
return docFileVersionMapper.selectListByDocFileId(docFileId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void restoreDocFileToVersion(Long docFileId, Long versionId) {
|
||||||
|
DocFileDO doc = docFileMapper.selectById(docFileId);
|
||||||
|
if (doc == null) {
|
||||||
|
throw exception(DOC_NOT_EXISTS);
|
||||||
|
}
|
||||||
|
|
||||||
|
DocFileVersionDO version = docFileVersionMapper.selectById(versionId);
|
||||||
|
if (version == null || !version.getDocFileId().equals(docFileId)) {
|
||||||
|
throw new RuntimeException("版本不存在或不属于该文档");
|
||||||
|
}
|
||||||
|
|
||||||
|
Long userId = SecurityFrameworkUtils.getLoginUserId();
|
||||||
|
|
||||||
|
// 创建新版本记录(恢复操作也是一个新版本)
|
||||||
|
Long versionCount = docFileVersionMapper.selectCountByDocFileId(docFileId);
|
||||||
|
String newVersionNo = "v" + (versionCount + 1);
|
||||||
|
|
||||||
|
DocFileVersionDO newVersion = DocFileVersionDO.builder()
|
||||||
|
.docFileId(docFileId)
|
||||||
|
.versionNo(newVersionNo)
|
||||||
|
.fileId(version.getFileId()) // 使用被恢复版本的文件ID
|
||||||
|
.changeDescription("恢复到版本 " + version.getVersionNo())
|
||||||
|
.build();
|
||||||
|
docFileVersionMapper.insert(newVersion);
|
||||||
|
|
||||||
|
// 更新主表
|
||||||
|
doc.setLatestVersionId(newVersion.getId());
|
||||||
|
doc.setFileId(version.getFileId());
|
||||||
|
docFileMapper.updateById(doc);
|
||||||
|
|
||||||
|
// 记录历史
|
||||||
|
docEditHistoryMapper.insert(DocEditHistoryDO.builder()
|
||||||
|
.docFileId(docFileId).userId(userId).userName(String.valueOf(userId))
|
||||||
|
.editType(2).description("恢复到版本 " + version.getVersionNo())
|
||||||
|
.build());
|
||||||
|
|
||||||
|
log.info("文档版本恢复完成: docId={}, 恢复到版本={}, 新版本={}",
|
||||||
|
docFileId, version.getVersionNo(), newVersionNo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -86,6 +86,14 @@ public interface FileService {
|
|||||||
*/
|
*/
|
||||||
byte[] getFileContent(Long configId, String path) throws Exception;
|
byte[] getFileContent(Long configId, String path) throws Exception;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认从主配置中根据 FileId 读取文件内容
|
||||||
|
*
|
||||||
|
* @param fileId 文件编号
|
||||||
|
* @return 文件内容
|
||||||
|
*/
|
||||||
|
byte[] getFileContent(Long fileId) throws Exception;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 根据 fileId 查询生效中的 FileDO
|
* 根据 fileId 查询生效中的 FileDO
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -271,6 +271,14 @@ public class FileServiceImpl implements FileService {
|
|||||||
Assert.notNull(client, "客户端({}) 不能为空", configId);
|
Assert.notNull(client, "客户端({}) 不能为空", configId);
|
||||||
return client.getContent(path);
|
return client.getContent(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public byte[] getFileContent(Long fileId) throws Exception {
|
||||||
|
FileClient masterFileClient = fileConfigService.getMasterFileClient();
|
||||||
|
FileDO fileDO = fileMapper.selectById(fileId);
|
||||||
|
return masterFileClient.getContent(fileDO.getPath());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public FileDO getActiveFileById(Long fileId) {
|
public FileDO getActiveFileById(Long fileId) {
|
||||||
// 由于 FileDO 没有状态字段,直接查主键即为生效中的文件
|
// 由于 FileDO 没有状态字段,直接查主键即为生效中的文件
|
||||||
|
|||||||
@@ -0,0 +1,191 @@
|
|||||||
|
package cn.iocoder.yudao.module.infra.websocket;
|
||||||
|
|
||||||
|
import cn.hutool.core.util.StrUtil;
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.web.socket.*;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 文档协作WebSocket处理器
|
||||||
|
*
|
||||||
|
* @author 芋道源码
|
||||||
|
*/
|
||||||
|
@Component
|
||||||
|
@Slf4j
|
||||||
|
public class DocWebSocketHandler implements WebSocketHandler {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储文档ID -> WebSocket会话映射
|
||||||
|
*/
|
||||||
|
private static final Map<String, Map<String, WebSocketSession>> DOC_SESSIONS = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存储会话ID -> 文档ID映射
|
||||||
|
*/
|
||||||
|
private static final Map<String, String> SESSION_TO_DOC = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
|
||||||
|
String docId = getDocIdFromSession(session);
|
||||||
|
if (StrUtil.isNotBlank(docId)) {
|
||||||
|
// 将会话添加到对应文档的会话组
|
||||||
|
DOC_SESSIONS.computeIfAbsent(docId, k -> new ConcurrentHashMap<>())
|
||||||
|
.put(session.getId(), session);
|
||||||
|
SESSION_TO_DOC.put(session.getId(), docId);
|
||||||
|
|
||||||
|
log.info("文档协作WebSocket连接建立: docId={}, sessionId={}", docId, session.getId());
|
||||||
|
|
||||||
|
// 向客户端发送连接成功消息
|
||||||
|
sendMessage(session, Map.of(
|
||||||
|
"type", "connected",
|
||||||
|
"docId", docId,
|
||||||
|
"message", "文档协作连接已建立"
|
||||||
|
));
|
||||||
|
|
||||||
|
// 通知其他用户有新用户加入
|
||||||
|
broadcastToDoc(docId, Map.of(
|
||||||
|
"type", "user_joined",
|
||||||
|
"sessionId", session.getId(),
|
||||||
|
"message", "有新用户加入协作"
|
||||||
|
), session.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleMessage(WebSocketSession session, WebSocketMessage<?> message) throws Exception {
|
||||||
|
try {
|
||||||
|
String payload = message.getPayload().toString();
|
||||||
|
Map<String, Object> data = JSON.parseObject(payload, Map.class);
|
||||||
|
String type = (String) data.get("type");
|
||||||
|
String docId = SESSION_TO_DOC.get(session.getId());
|
||||||
|
|
||||||
|
log.debug("收到WebSocket消息: docId={}, sessionId={}, type={}", docId, session.getId(), type);
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "heartbeat":
|
||||||
|
// 心跳保持连接
|
||||||
|
sendMessage(session, Map.of("type", "heartbeat_ack"));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.debug("WebSocket消息: docId={}, type={}", docId, type);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("处理WebSocket消息失败: sessionId={}", session.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
|
||||||
|
log.error("WebSocket传输错误: sessionId={}", session.getId(), exception);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception {
|
||||||
|
String docId = SESSION_TO_DOC.remove(session.getId());
|
||||||
|
if (StrUtil.isNotBlank(docId)) {
|
||||||
|
Map<String, WebSocketSession> docSessions = DOC_SESSIONS.get(docId);
|
||||||
|
if (docSessions != null) {
|
||||||
|
docSessions.remove(session.getId());
|
||||||
|
if (docSessions.isEmpty()) {
|
||||||
|
DOC_SESSIONS.remove(docId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知其他用户有用户离开
|
||||||
|
broadcastToDoc(docId, Map.of(
|
||||||
|
"type", "user_left",
|
||||||
|
"sessionId", session.getId(),
|
||||||
|
"message", "有用户离开协作"
|
||||||
|
), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.info("文档协作WebSocket连接关闭: docId={}, sessionId={}, status={}",
|
||||||
|
docId, session.getId(), closeStatus);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean supportsPartialMessages() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从会话中获取文档ID
|
||||||
|
*/
|
||||||
|
private String getDocIdFromSession(WebSocketSession session) {
|
||||||
|
String query = session.getUri().getQuery();
|
||||||
|
if (StrUtil.isNotBlank(query)) {
|
||||||
|
String[] params = query.split("&");
|
||||||
|
for (String param : params) {
|
||||||
|
if (param.startsWith("docId=")) {
|
||||||
|
return param.substring(6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向指定会话发送消息
|
||||||
|
*/
|
||||||
|
private void sendMessage(WebSocketSession session, Object message) {
|
||||||
|
try {
|
||||||
|
if (session.isOpen()) {
|
||||||
|
session.sendMessage(new TextMessage(JSON.toJSONString(message)));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("发送WebSocket消息失败: sessionId={}", session.getId(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向文档的所有会话广播消息
|
||||||
|
*/
|
||||||
|
private void broadcastToDoc(String docId, Object message, String excludeSessionId) {
|
||||||
|
Map<String, WebSocketSession> docSessions = DOC_SESSIONS.get(docId);
|
||||||
|
if (docSessions != null) {
|
||||||
|
docSessions.forEach((sessionId, session) -> {
|
||||||
|
if (!sessionId.equals(excludeSessionId)) {
|
||||||
|
sendMessage(session, message);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 静态方法:向指定文档的所有会话发送保存状态通知
|
||||||
|
*/
|
||||||
|
public static void notifyDocSaveStatus(String docId, String status, String message) {
|
||||||
|
Map<String, WebSocketSession> docSessions = DOC_SESSIONS.get(docId);
|
||||||
|
if (docSessions != null) {
|
||||||
|
Map<String, Object> notification = Map.of(
|
||||||
|
"type", "save_status",
|
||||||
|
"status", status,
|
||||||
|
"message", message,
|
||||||
|
"timestamp", System.currentTimeMillis()
|
||||||
|
);
|
||||||
|
|
||||||
|
docSessions.values().forEach(session -> {
|
||||||
|
try {
|
||||||
|
if (session.isOpen()) {
|
||||||
|
session.sendMessage(new TextMessage(JSON.toJSONString(notification)));
|
||||||
|
}
|
||||||
|
} catch (IOException e) {
|
||||||
|
log.error("发送保存状态通知失败: sessionId={}", session.getId(), e);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取指定文档的在线用户数
|
||||||
|
*/
|
||||||
|
public static int getOnlineUserCount(String docId) {
|
||||||
|
Map<String, WebSocketSession> docSessions = DOC_SESSIONS.get(docId);
|
||||||
|
return docSessions != null ? docSessions.size() : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -186,7 +186,17 @@ yudao:
|
|||||||
tenant: # 多租户相关配置项
|
tenant: # 多租户相关配置项
|
||||||
enable: true
|
enable: true
|
||||||
ignore-urls:
|
ignore-urls:
|
||||||
|
- /admin-api/infra/doc-file/file-content
|
||||||
|
- /admin-api/infra/doc-file/callback* # OnlyOffice回调接口
|
||||||
ignore-tables:
|
ignore-tables:
|
||||||
- infra_std_name
|
- infra_std_name
|
||||||
|
- infra_doc_file
|
||||||
|
- infra_doc_file_version
|
||||||
|
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
|
# OnlyOffice 配置
|
||||||
|
onlyoffice:
|
||||||
|
base-url: http://172.16.46.63:30085 # OnlyOffice 服务地址
|
||||||
|
callback-base-url: http://172.16.46.62:30081 # 应用服务回调地址,OnlyOffice需要能访问到
|
||||||
|
jwt-secret: P@ssword25 # JWT 密钥
|
||||||
|
|||||||
@@ -24,4 +24,9 @@ public interface PermissionApi extends PermissionCommonApi {
|
|||||||
@Parameter(name = "roleIds", description = "角色编号集合", example = "1,2", required = true)
|
@Parameter(name = "roleIds", description = "角色编号集合", example = "1,2", required = true)
|
||||||
CommonResult<Set<Long>> getUserRoleIdListByRoleIds(@RequestParam("roleIds") Collection<Long> roleIds);
|
CommonResult<Set<Long>> getUserRoleIdListByRoleIds(@RequestParam("roleIds") Collection<Long> roleIds);
|
||||||
|
|
||||||
|
@GetMapping(PREFIX + "/user-role-id-list-by-user-id")
|
||||||
|
@Operation(summary = "获得用户拥有的角色编号集合")
|
||||||
|
@Parameter(name = "userId", description = "用户编号", example = "1", required = true)
|
||||||
|
CommonResult<Set<Long>> getUserRoleIdListByUserId(@RequestParam("userId") Long userId);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -22,4 +22,9 @@ public interface RoleApi {
|
|||||||
@Parameter(name = "ids", description = "角色编号数组", example = "1,2", required = true)
|
@Parameter(name = "ids", description = "角色编号数组", example = "1,2", required = true)
|
||||||
CommonResult<Boolean> validRoleList(@RequestParam("ids") Collection<Long> ids);
|
CommonResult<Boolean> validRoleList(@RequestParam("ids") Collection<Long> ids);
|
||||||
|
|
||||||
|
@GetMapping(PREFIX + "/has-any-super-admin")
|
||||||
|
@Operation(summary = "判断角色列表中是否有超级管理员")
|
||||||
|
@Parameter(name = "roleIds", description = "角色编号集合", example = "1,2", required = true)
|
||||||
|
CommonResult<Boolean> hasAnySuperAdmin(@RequestParam("roleIds") Collection<Long> roleIds);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,11 @@ public class PermissionApiImpl implements PermissionApi {
|
|||||||
return success(permissionService.getUserRoleIdListByRoleId(roleIds));
|
return success(permissionService.getUserRoleIdListByRoleId(roleIds));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommonResult<Set<Long>> getUserRoleIdListByUserId(Long userId) {
|
||||||
|
return success(permissionService.getUserRoleIdListByUserIdFromCache(userId));
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CommonResult<Boolean> hasAnyPermissions(Long userId, String... permissions) {
|
public CommonResult<Boolean> hasAnyPermissions(Long userId, String... permissions) {
|
||||||
return success(permissionService.hasAnyPermissions(userId, permissions));
|
return success(permissionService.hasAnyPermissions(userId, permissions));
|
||||||
|
|||||||
@@ -22,4 +22,9 @@ public class RoleApiImpl implements RoleApi {
|
|||||||
roleService.validateRoleList(ids);
|
roleService.validateRoleList(ids);
|
||||||
return success(true);
|
return success(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public CommonResult<Boolean> hasAnySuperAdmin(Collection<Long> roleIds) {
|
||||||
|
return success(roleService.hasAnySuperAdmin(roleIds));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -301,6 +301,9 @@ yudao:
|
|||||||
enable: true
|
enable: true
|
||||||
ignore-urls:
|
ignore-urls:
|
||||||
- /jmreport/* # 积木报表,无法携带租户编号
|
- /jmreport/* # 积木报表,无法携带租户编号
|
||||||
|
- /admin-api/infra/doc-file/file-content
|
||||||
|
- /admin-api/infra/doc-file/callback # OnlyOffice回调接口
|
||||||
|
- /admin-api/infra/doc-file/callback?* # OnlyOffice回调接口(带参数)
|
||||||
ignore-visit-urls:
|
ignore-visit-urls:
|
||||||
- /admin-api/system/user/profile/**
|
- /admin-api/system/user/profile/**
|
||||||
- /admin-api/system/auth/**
|
- /admin-api/system/auth/**
|
||||||
@@ -344,6 +347,13 @@ yudao:
|
|||||||
customer: E77DF18BE109F454A5CD319E44BF5177
|
customer: E77DF18BE109F454A5CD319E44BF5177
|
||||||
|
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
|
# OnlyOffice 配置
|
||||||
|
onlyoffice:
|
||||||
|
base-url: http://172.16.46.63:30085 # OnlyOffice 服务地址
|
||||||
|
callback-base-url: http://172.16.46.62:30081 # 应用服务回调地址,OnlyOffice需要能访问到
|
||||||
|
jwt-secret: P@ssword25 # JWT 密钥
|
||||||
|
|
||||||
# 插件配置 TODO 芋艿:【IOT】需要处理下
|
# 插件配置 TODO 芋艿:【IOT】需要处理下
|
||||||
pf4j:
|
pf4j:
|
||||||
pluginsDir: /Users/anhaohao/code/gitee/ruoyi-vue-pro/plugins # 插件目录
|
pluginsDir: /Users/anhaohao/code/gitee/ruoyi-vue-pro/plugins # 插件目录
|
||||||
Reference in New Issue
Block a user