From 1ed31a4f49135642260c4b0d1eef6d88d2dbde04 Mon Sep 17 00:00:00 2001 From: chenbowen Date: Mon, 1 Sep 2025 10:01:35 +0800 Subject: [PATCH] =?UTF-8?q?1.=20=E6=96=B0=E5=A2=9E=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E5=8D=8F=E5=90=8C=E6=96=87=E6=A1=A3=E5=8A=9F=E8=83=BD=20v=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- sql/dm/doc_management.sql | 144 +++ sql/dm/doc_management_menu.sql | 39 + sql/dm/patch.sql | 4 +- sql/mysql/doc_management.sql | 75 ++ sql/mysql/doc_management_menu.sql | 24 + .../infra/enums/ErrorCodeConstants.java | 1 + .../yudao-module-infra-server/pom.xml | 6 + .../module/infra/config/WebSocketConfig.java | 30 + .../admin/doc/DocFileController.java | 288 ++++++ .../admin/doc/vo/DocEditorConfigRespVO.java | 99 ++ .../admin/doc/vo/DocFileCreateReqVO.java | 31 + .../admin/doc/vo/DocFilePageReqVO.java | 36 + .../admin/doc/vo/DocFilePermissionReqVO.java | 28 + .../admin/doc/vo/DocFilePermissionRespVO.java | 33 + .../admin/doc/vo/DocFileRespVO.java | 52 ++ .../admin/doc/vo/DocFileUpdateReqVO.java | 22 + .../admin/doc/vo/DocFileVersionRespVO.java | 40 + .../infra/convert/doc/DocFileConvert.java | 41 + .../dal/dataobject/doc/DocEditHistoryDO.java | 61 ++ .../infra/dal/dataobject/doc/DocFileDO.java | 69 ++ .../dataobject/doc/DocFilePermissionDO.java | 51 ++ .../dal/dataobject/doc/DocFileVersionDO.java | 47 + .../dal/mysql/doc/DocEditHistoryMapper.java | 24 + .../infra/dal/mysql/doc/DocFileMapper.java | 28 + .../mysql/doc/DocFilePermissionMapper.java | 39 + .../dal/mysql/doc/DocFileVersionMapper.java | 36 + .../infra/enums/doc/DocEditTypeEnum.java | 29 + .../infra/enums/doc/DocFileTypeEnum.java | 29 + .../enums/doc/DocPermissionTypeEnum.java | 28 + .../infra/enums/doc/DocSpaceTypeEnum.java | 27 + .../rpc/config/RpcConfiguration.java | 5 +- .../config/SecurityConfiguration.java | 3 + .../infra/service/doc/DocFileService.java | 172 ++++ .../service/doc/impl/DocFileServiceImpl.java | 849 ++++++++++++++++++ .../infra/service/file/FileService.java | 8 + .../infra/service/file/FileServiceImpl.java | 8 + .../infra/websocket/DocWebSocketHandler.java | 191 ++++ .../src/main/resources/application.yaml | 10 + .../system/api/permission/PermissionApi.java | 5 + .../module/system/api/permission/RoleApi.java | 5 + .../api/permission/PermissionApiImpl.java | 5 + .../system/api/permission/RoleApiImpl.java | 5 + .../src/main/resources/application.yaml | 10 + 43 files changed, 2734 insertions(+), 3 deletions(-) create mode 100644 sql/dm/doc_management.sql create mode 100644 sql/dm/doc_management_menu.sql create mode 100644 sql/mysql/doc_management.sql create mode 100644 sql/mysql/doc_management_menu.sql create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/config/WebSocketConfig.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/DocFileController.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocEditorConfigRespVO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileCreateReqVO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePageReqVO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePermissionReqVO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePermissionRespVO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileRespVO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileUpdateReqVO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileVersionRespVO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/convert/doc/DocFileConvert.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocEditHistoryDO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFileDO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFilePermissionDO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFileVersionDO.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocEditHistoryMapper.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFileMapper.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFilePermissionMapper.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFileVersionMapper.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocEditTypeEnum.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocFileTypeEnum.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocPermissionTypeEnum.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocSpaceTypeEnum.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/doc/DocFileService.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/doc/impl/DocFileServiceImpl.java create mode 100644 yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/websocket/DocWebSocketHandler.java diff --git a/sql/dm/doc_management.sql b/sql/dm/doc_management.sql new file mode 100644 index 00000000..5dbde549 --- /dev/null +++ b/sql/dm/doc_management.sql @@ -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 '文档编辑历史表'; diff --git a/sql/dm/doc_management_menu.sql b/sql/dm/doc_management_menu.sql new file mode 100644 index 00000000..ec698c8b --- /dev/null +++ b/sql/dm/doc_management_menu.sql @@ -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); diff --git a/sql/dm/patch.sql b/sql/dm/patch.sql index d97f7849..2a130011 100644 --- a/sql/dm/patch.sql +++ b/sql/dm/patch.sql @@ -155,7 +155,7 @@ INSERT INTO system_menu( path, icon, component, status, component_name ) VALUES ( - '1953701540574969857', '系统序列号管理', '', 2, 0, ${table.parentMenuId}, + '1953701540574969857', '系统序列号管理', '', 2, 0, 1, '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), (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), - (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); diff --git a/sql/mysql/doc_management.sql b/sql/mysql/doc_management.sql new file mode 100644 index 00000000..c3be7477 --- /dev/null +++ b/sql/mysql/doc_management.sql @@ -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='文档编辑历史表'; diff --git a/sql/mysql/doc_management_menu.sql b/sql/mysql/doc_management_menu.sql new file mode 100644 index 00000000..808a54ff --- /dev/null +++ b/sql/mysql/doc_management_menu.sql @@ -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); diff --git a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java index d920c75f..3d306bac 100644 --- a/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java +++ b/yudao-module-infra/yudao-module-infra-api/src/main/java/cn/iocoder/yudao/module/infra/enums/ErrorCodeConstants.java @@ -86,4 +86,5 @@ public interface ErrorCodeConstants { ErrorCode STANDARD_NAME_NOT_EXISTS = new ErrorCode(1_002_030_000, "数据命名与简写标准不存在"); ErrorCode STD_ABBR_NOT_EXISTS = new ErrorCode(1_002_030_001, "字段名 {} 不存在命名规范定义,请核对字段定义"); + ErrorCode DOC_NOT_EXISTS = new ErrorCode(1_002_031_000, "文档不存在"); } diff --git a/yudao-module-infra/yudao-module-infra-server/pom.xml b/yudao-module-infra/yudao-module-infra-server/pom.xml index face0f7b..1cce5b0c 100644 --- a/yudao-module-infra/yudao-module-infra-server/pom.xml +++ b/yudao-module-infra/yudao-module-infra-server/pom.xml @@ -32,6 +32,12 @@ ${revision} + + cn.iocoder.cloud + yudao-module-system-api + ${revision} + + cn.iocoder.cloud diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/config/WebSocketConfig.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/config/WebSocketConfig.java new file mode 100644 index 00000000..b3184c9d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/config/WebSocketConfig.java @@ -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支持 + } +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/DocFileController.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/DocFileController.java new file mode 100644 index 00000000..5f9fa990 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/DocFileController.java @@ -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 createDocFile(@Valid @RequestBody DocFileCreateReqVO createReqVO) { + return success(docFileService.createDocFile(createReqVO)); + } + + @PutMapping("/update") + @Operation(summary = "修改在线文档") + @PreAuthorize("@ss.hasPermission('infra:doc:update')") + public CommonResult updateDocFile(@Valid @RequestBody DocFileUpdateReqVO updateReqVO) { + docFileService.updateDocFile(updateReqVO); + return success(true); + } + + @DeleteMapping("/delete") + @Operation(summary = "删除在线文档") + @PreAuthorize("@ss.hasPermission('infra:doc:delete')") + public CommonResult deleteDocFile(@RequestParam("id") Long id) { + docFileService.deleteDocFile(id); + return success(true); + } + + @GetMapping("/get") + @Operation(summary = "获得在线文档") + @PreAuthorize("@ss.hasPermission('infra:doc:query')") + public CommonResult 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> getDocFilePage(@Valid DocFilePageReqVO pageReqVO) { + PageResult page = docFileService.getDocFilePageWithFileInfo(pageReqVO); + return success(page); + } + + @PostMapping("/upload") + @Operation(summary = "上传文档文件") + @PreAuthorize("@ss.hasPermission('infra:doc:upload')") + public CommonResult 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 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 callback(@RequestParam("docFileId") Long docFileId, + @RequestParam(value = "userId", required = false) Long triggerUserId, + @RequestBody Map 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 setPermission(@Valid @RequestBody DocFilePermissionReqVO permissionReqVO) { + docFileService.setDocFilePermission(permissionReqVO); + return success(true); + } + + @GetMapping("/permission/list") + @Operation(summary = "获得文档权限列表") + @PreAuthorize("@ss.hasPermission('infra:doc:permission')") + public CommonResult> getPermissionList(@RequestParam("docFileId") Long docFileId) { + List list = docFileService.getDocFilePermissions(docFileId); + return success(DocFileConvert.INSTANCE.convertPermissionList(list)); + } + + @DeleteMapping("/permission/delete") + @Operation(summary = "删除文档权限") + @PreAuthorize("@ss.hasPermission('infra:doc:permission')") + public CommonResult 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> getVersionList(@RequestParam("docFileId") Long docFileId) { + List list = docFileService.getDocFileVersions(docFileId); + List 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 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; + } +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocEditorConfigRespVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocEditorConfigRespVO.java new file mode 100644 index 00000000..80cf67ec --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocEditorConfigRespVO.java @@ -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; + } + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileCreateReqVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileCreateReqVO.java new file mode 100644 index 00000000..739b8018 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileCreateReqVO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePageReqVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePageReqVO.java new file mode 100644 index 00000000..5f172729 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePageReqVO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePermissionReqVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePermissionReqVO.java new file mode 100644 index 00000000..a36bb489 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePermissionReqVO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePermissionRespVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePermissionRespVO.java new file mode 100644 index 00000000..ec28f497 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFilePermissionRespVO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileRespVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileRespVO.java new file mode 100644 index 00000000..cb6adb07 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileRespVO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileUpdateReqVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileUpdateReqVO.java new file mode 100644 index 00000000..5ea74b26 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileUpdateReqVO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileVersionRespVO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileVersionRespVO.java new file mode 100644 index 00000000..f35067e2 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/controller/admin/doc/vo/DocFileVersionRespVO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/convert/doc/DocFileConvert.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/convert/doc/DocFileConvert.java new file mode 100644 index 00000000..714e242e --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/convert/doc/DocFileConvert.java @@ -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 convertList(List list); + + PageResult convertPage(PageResult page); + + DocFilePermissionDO convert(DocFilePermissionReqVO bean); + + DocFilePermissionRespVO convert(DocFilePermissionDO bean); + + List convertPermissionList(List list); + + DocFileVersionRespVO convert(DocFileVersionDO bean); + + List convertVersionList(List list); + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocEditHistoryDO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocEditHistoryDO.java new file mode 100644 index 00000000..1c9ecf3d --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocEditHistoryDO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFileDO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFileDO.java new file mode 100644 index 00000000..cec6edf0 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFileDO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFilePermissionDO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFilePermissionDO.java new file mode 100644 index 00000000..02cf6ab3 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFilePermissionDO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFileVersionDO.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFileVersionDO.java new file mode 100644 index 00000000..c8249c09 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/dataobject/doc/DocFileVersionDO.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocEditHistoryMapper.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocEditHistoryMapper.java new file mode 100644 index 00000000..97410608 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocEditHistoryMapper.java @@ -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 { + + default List selectListByDocFileId(Long docFileId) { + return selectList(new LambdaQueryWrapperX() + .eq(DocEditHistoryDO::getDocFileId, docFileId) + .orderByDesc(DocEditHistoryDO::getCreateTime)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFileMapper.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFileMapper.java new file mode 100644 index 00000000..7be76cc5 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFileMapper.java @@ -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 { + + default PageResult selectPage(DocFilePageReqVO reqVO) { + return selectPage(reqVO, new LambdaQueryWrapperX() + .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)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFilePermissionMapper.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFilePermissionMapper.java new file mode 100644 index 00000000..8523239a --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFilePermissionMapper.java @@ -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 { + + default List selectListByDocFileId(Long docFileId) { + return selectList(new LambdaQueryWrapperX() + .eq(DocFilePermissionDO::getDocFileId, docFileId)); + } + + default DocFilePermissionDO selectByDocFileIdAndRoleId(Long docFileId, Long roleId) { + return selectOne(new LambdaQueryWrapperX() + .eq(DocFilePermissionDO::getDocFileId, docFileId) + .eq(DocFilePermissionDO::getRoleId, roleId)); + } + + default List selectListByRoleId(Long roleId) { + return selectList(new LambdaQueryWrapperX() + .eq(DocFilePermissionDO::getRoleId, roleId)); + } + + default List selectListByRoleIds(List roleIds) { + return selectList(new LambdaQueryWrapperX() + .in(DocFilePermissionDO::getRoleId, roleIds)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFileVersionMapper.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFileVersionMapper.java new file mode 100644 index 00000000..682f9259 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/dal/mysql/doc/DocFileVersionMapper.java @@ -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 { + + default List selectListByDocFileId(Long docFileId) { + return selectList(new LambdaQueryWrapperX() + .eq(DocFileVersionDO::getDocFileId, docFileId) + .orderByDesc(DocFileVersionDO::getId)); + } + + default DocFileVersionDO selectLatestByDocFileId(Long docFileId) { + return selectOne(new LambdaQueryWrapperX() + .eq(DocFileVersionDO::getDocFileId, docFileId) + .orderByDesc(DocFileVersionDO::getId) + .last("LIMIT 1")); + } + + default Long selectCountByDocFileId(Long docFileId) { + return selectCount(new LambdaQueryWrapperX() + .eq(DocFileVersionDO::getDocFileId, docFileId)); + } + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocEditTypeEnum.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocEditTypeEnum.java new file mode 100644 index 00000000..499d5397 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocEditTypeEnum.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocFileTypeEnum.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocFileTypeEnum.java new file mode 100644 index 00000000..1ad59f26 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocFileTypeEnum.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocPermissionTypeEnum.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocPermissionTypeEnum.java new file mode 100644 index 00000000..b73e2553 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocPermissionTypeEnum.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocSpaceTypeEnum.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocSpaceTypeEnum.java new file mode 100644 index 00000000..e23a6a21 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/enums/doc/DocSpaceTypeEnum.java @@ -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; + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/rpc/config/RpcConfiguration.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/rpc/config/RpcConfiguration.java index eab0f724..e45d5af1 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/rpc/config/RpcConfiguration.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/rpc/config/RpcConfiguration.java @@ -1,9 +1,12 @@ 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.context.annotation.Configuration; @Configuration(value = "infraRpcConfiguration", proxyBeanMethods = false) -@EnableFeignClients() +@EnableFeignClients(clients = {PermissionApi.class, RoleApi.class, AdminUserApi.class}) public class RpcConfiguration { } diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java index f6533609..925127de 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/framework/security/config/SecurityConfiguration.java @@ -38,6 +38,9 @@ public class SecurityConfiguration { .requestMatchers(adminSeverContextPath + "/**").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 芋艿:这个每个项目都需要重复配置,得捉摸有没通用的方案 // RPC 服务的安全配置 diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/doc/DocFileService.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/doc/DocFileService.java new file mode 100644 index 00000000..8e60eebc --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/doc/DocFileService.java @@ -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 getDocFilePage(DocFilePageReqVO pageReqVO); + + /** + * 获得在线文档分页(包含文件信息) + * + * @param pageReqVO 分页查询 + * @return 在线文档分页 + */ + PageResult 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 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 getDocFileVersions(Long docFileId); + + /** + * 恢复到指定版本 + * + * @param docFileId 文档编号 + * @param versionId 版本编号 + */ + void restoreDocFileToVersion(Long docFileId, Long versionId); + +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/doc/impl/DocFileServiceImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/doc/impl/DocFileServiceImpl.java new file mode 100644 index 00000000..ec514671 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/doc/impl/DocFileServiceImpl.java @@ -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 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 getDocFilePage(DocFilePageReqVO pageReqVO) { + return docFileMapper.selectPage(pageReqVO); + } + + @Override + public PageResult getDocFilePageWithFileInfo(DocFilePageReqVO pageReqVO) { + PageResult 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 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 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 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 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 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); + } +} + diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java index d84fee6d..318156ba 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileService.java @@ -86,6 +86,14 @@ public interface FileService { */ byte[] getFileContent(Long configId, String path) throws Exception; + /** + * 默认从主配置中根据 FileId 读取文件内容 + * + * @param fileId 文件编号 + * @return 文件内容 + */ + byte[] getFileContent(Long fileId) throws Exception; + /** * 根据 fileId 查询生效中的 FileDO * diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java index 70069904..4d6363ab 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/service/file/FileServiceImpl.java @@ -271,6 +271,14 @@ public class FileServiceImpl implements FileService { Assert.notNull(client, "客户端({}) 不能为空", configId); 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 public FileDO getActiveFileById(Long fileId) { // 由于 FileDO 没有状态字段,直接查主键即为生效中的文件 diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/websocket/DocWebSocketHandler.java b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/websocket/DocWebSocketHandler.java new file mode 100644 index 00000000..86f679c0 --- /dev/null +++ b/yudao-module-infra/yudao-module-infra-server/src/main/java/cn/iocoder/yudao/module/infra/websocket/DocWebSocketHandler.java @@ -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> DOC_SESSIONS = new ConcurrentHashMap<>(); + + /** + * 存储会话ID -> 文档ID映射 + */ + private static final Map 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 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 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 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 docSessions = DOC_SESSIONS.get(docId); + if (docSessions != null) { + Map 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 docSessions = DOC_SESSIONS.get(docId); + return docSessions != null ? docSessions.size() : 0; + } +} diff --git a/yudao-module-infra/yudao-module-infra-server/src/main/resources/application.yaml b/yudao-module-infra/yudao-module-infra-server/src/main/resources/application.yaml index 115dfd3f..66242b35 100644 --- a/yudao-module-infra/yudao-module-infra-server/src/main/resources/application.yaml +++ b/yudao-module-infra/yudao-module-infra-server/src/main/resources/application.yaml @@ -186,7 +186,17 @@ yudao: tenant: # 多租户相关配置项 enable: true ignore-urls: + - /admin-api/infra/doc-file/file-content + - /admin-api/infra/doc-file/callback* # OnlyOffice回调接口 ignore-tables: - infra_std_name + - infra_doc_file + - infra_doc_file_version 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 密钥 diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java index 45927f91..ffb5b1b0 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApi.java @@ -24,4 +24,9 @@ public interface PermissionApi extends PermissionCommonApi { @Parameter(name = "roleIds", description = "角色编号集合", example = "1,2", required = true) CommonResult> getUserRoleIdListByRoleIds(@RequestParam("roleIds") Collection roleIds); + @GetMapping(PREFIX + "/user-role-id-list-by-user-id") + @Operation(summary = "获得用户拥有的角色编号集合") + @Parameter(name = "userId", description = "用户编号", example = "1", required = true) + CommonResult> getUserRoleIdListByUserId(@RequestParam("userId") Long userId); + } \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApi.java b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApi.java index 33c6fa23..a1676f6c 100644 --- a/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApi.java +++ b/yudao-module-system/yudao-module-system-api/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApi.java @@ -22,4 +22,9 @@ public interface RoleApi { @Parameter(name = "ids", description = "角色编号数组", example = "1,2", required = true) CommonResult validRoleList(@RequestParam("ids") Collection ids); + @GetMapping(PREFIX + "/has-any-super-admin") + @Operation(summary = "判断角色列表中是否有超级管理员") + @Parameter(name = "roleIds", description = "角色编号集合", example = "1,2", required = true) + CommonResult hasAnySuperAdmin(@RequestParam("roleIds") Collection roleIds); + } \ No newline at end of file diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java index 7cfa7f3f..0327e02a 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/permission/PermissionApiImpl.java @@ -26,6 +26,11 @@ public class PermissionApiImpl implements PermissionApi { return success(permissionService.getUserRoleIdListByRoleId(roleIds)); } + @Override + public CommonResult> getUserRoleIdListByUserId(Long userId) { + return success(permissionService.getUserRoleIdListByUserIdFromCache(userId)); + } + @Override public CommonResult hasAnyPermissions(Long userId, String... permissions) { return success(permissionService.hasAnyPermissions(userId, permissions)); diff --git a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApiImpl.java b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApiImpl.java index f7f5dd98..55e4e188 100644 --- a/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApiImpl.java +++ b/yudao-module-system/yudao-module-system-server/src/main/java/cn/iocoder/yudao/module/system/api/permission/RoleApiImpl.java @@ -22,4 +22,9 @@ public class RoleApiImpl implements RoleApi { roleService.validateRoleList(ids); return success(true); } + + @Override + public CommonResult hasAnySuperAdmin(Collection roleIds) { + return success(roleService.hasAnySuperAdmin(roleIds)); + } } diff --git a/yudao-server/src/main/resources/application.yaml b/yudao-server/src/main/resources/application.yaml index 5e5a67d5..97236661 100644 --- a/yudao-server/src/main/resources/application.yaml +++ b/yudao-server/src/main/resources/application.yaml @@ -301,6 +301,9 @@ yudao: enable: true ignore-urls: - /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: - /admin-api/system/user/profile/** - /admin-api/system/auth/** @@ -344,6 +347,13 @@ yudao: customer: E77DF18BE109F454A5CD319E44BF5177 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】需要处理下 pf4j: pluginsDir: /Users/anhaohao/code/gitee/ruoyi-vue-pro/plugins # 插件目录 \ No newline at end of file