From 16abb964a59920f2961bce5365304619cb76040c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E5=BC=A0?= <2091066548@qq.com> Date: Sun, 21 Sep 2025 00:27:58 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix=EF=BC=9A=E9=97=A8=E6=88=B7=E8=AE=BE?= =?UTF-8?q?=E8=AE=A1=E9=83=A8=E5=88=86=E6=90=AC=E8=BF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../online-design/portal/DesignEditor.vue | 539 +++++++++++ .../aiol/online-design/portal/Preview.vue | 224 +++++ .../portal/components/ComponentLibrary.vue | 328 +++++++ .../portal/components/DesignCanvas.vue | 831 +++++++++++++++++ .../portal/components/PropertyEditor.vue | 854 ++++++++++++++++++ .../design-components/ButtonComponent.vue | 232 +++++ .../design-components/CarouselComponent.vue | 230 +++++ .../design-components/ImageComponent.vue | 211 +++++ .../design-components/TextComponent.vue | 223 +++++ .../components/design-components/index.ts | 143 +++ .../views/aiol/online-design/portal/index.vue | 519 +++++++++++ .../src/views/aiol/online-design/test.vue | 135 +++ 12 files changed, 4469 insertions(+) create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/DesignEditor.vue create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/Preview.vue create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/components/ComponentLibrary.vue create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/components/DesignCanvas.vue create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/components/PropertyEditor.vue create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/ButtonComponent.vue create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/CarouselComponent.vue create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/ImageComponent.vue create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/TextComponent.vue create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/index.ts create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/portal/index.vue create mode 100644 jeecgboot-vue3/src/views/aiol/online-design/test.vue diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/DesignEditor.vue b/jeecgboot-vue3/src/views/aiol/online-design/portal/DesignEditor.vue new file mode 100644 index 00000000..54e8d9e7 --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/DesignEditor.vue @@ -0,0 +1,539 @@ + + + + + + + + + + 返回 + + + {{ pageTitle }} + + + + + + + + 预览 + + + + + + 保存 + + + + + + + + + + + + + + 组件库 + + + + + + + 组件库 + + + + + + + + + + + + + + + + + + + 属性设置 + + + + + + + + + + + + + + + diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/Preview.vue b/jeecgboot-vue3/src/views/aiol/online-design/portal/Preview.vue new file mode 100644 index 00000000..18298681 --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/Preview.vue @@ -0,0 +1,224 @@ + + + + + + + + + + 返回 + + + {{ pageTitle }} - 预览 + + + + + + + + 编辑 + + + + + + 刷新 + + + + + + + + + + + + + + + + + + + + + + diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/components/ComponentLibrary.vue b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/ComponentLibrary.vue new file mode 100644 index 00000000..fb923ce3 --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/ComponentLibrary.vue @@ -0,0 +1,328 @@ + + + + 组件库 + + + + + + + + 基础组件 + + + + + + + + + + 轮播图 + 图片轮播展示 + + + + + + + + + + + + + 文本 + 文字内容展示 + + + + + + + + + + + + + 图片 + 单张图片展示 + + + + + + + + + + + + + 按钮 + 交互按钮 + + + + + + + + + + + + + 布局组件 + + + + + + + + + + 容器 + 内容容器 + + + + + + + + + + 分栏 + 多列布局 + + + + + + + + + + + diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/components/DesignCanvas.vue b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/DesignCanvas.vue new file mode 100644 index 00000000..ec8abc7c --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/DesignCanvas.vue @@ -0,0 +1,831 @@ + + + + + + 设计画布 + + + + + + 网格 + + + + + + {{ Math.round(zoomLevel * 100) }}% + + + + + + + + + + + + 清空 + + + + + + + + + + + + + + + + + + 从左侧组件库拖拽组件到网格中开始设计 + 支持网格布局和组件缩放 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/components/PropertyEditor.vue b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/PropertyEditor.vue new file mode 100644 index 00000000..27698735 --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/PropertyEditor.vue @@ -0,0 +1,854 @@ + + + + 属性编辑 + + + + + + + + + 请选择一个组件来编辑属性 + + + + + + + + + 基本信息 + + + 组件类型 + + + + 组件ID + + + + + + + + + + 轮播设置 + + + 自动播放 + + + + 显示指示点 + + + + 显示箭头 + + + + + + + + 图片管理 + + + + + + + + + + + + + + + + + + + + 点击上传图片 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 添加图片 + + + + + + + + + + + 文本内容 + + + 标签类型 + + 段落 (p) + 标题1 (h1) + 标题2 (h2) + 标题3 (h3) + 标题4 (h4) + 行内文本 (span) + + + + 文本内容 + + + + + + + + + + + 图片设置 + + + 图片 + + + + + + + + + + + + + + + + + 点击上传图片 + 支持 JPG、PNG、GIF 格式 + + + + + + + + + + + + + + 选择文件 + + + + + 替代文本 + + + + 显示方式 + + 覆盖 + 包含 + 填充 + 原始 + 缩小 + + + + + + + + + + + 按钮设置 + + + 按钮文字 + + + + 按钮类型 + + 主要按钮 + 默认按钮 + 虚线按钮 + 文本按钮 + 链接按钮 + + + + 按钮大小 + + 大 + 中 + 小 + + + + + + + + + + 样式设置 + + + 宽度 + + + + 高度 + + + + 外边距 + + + + 内边距 + + + + + + + 字体大小 + + + + 字体颜色 + + + + 文本对齐 + + 左对齐 + 居中 + 右对齐 + 两端对齐 + + + + + + + + + + + + diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/ButtonComponent.vue b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/ButtonComponent.vue new file mode 100644 index 00000000..9f32918c --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/ButtonComponent.vue @@ -0,0 +1,232 @@ + + + + + + + {{ text }} + + + + + + + 按钮组件 + 请在右侧属性面板设置按钮文字 + + + + + + + + diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/CarouselComponent.vue b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/CarouselComponent.vue new file mode 100644 index 00000000..c2bbae0e --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/CarouselComponent.vue @@ -0,0 +1,230 @@ + + + + + + + + {{ image.title }} + {{ image.description }} + + + + + + + + + + 轮播图组件 + 请在右侧属性面板添加图片 + + + + + + + + diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/ImageComponent.vue b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/ImageComponent.vue new file mode 100644 index 00000000..484029e5 --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/ImageComponent.vue @@ -0,0 +1,211 @@ + + + + + + + + + + 图片组件 + 请在右侧属性面板设置图片URL + + + + + + + + + diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/TextComponent.vue b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/TextComponent.vue new file mode 100644 index 00000000..7a0ca7af --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/TextComponent.vue @@ -0,0 +1,223 @@ + + + + + + + + + 文本组件 + 请在右侧属性面板编辑文本内容 + + + + + + + + diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/index.ts b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/index.ts new file mode 100644 index 00000000..ef68bda5 --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/components/design-components/index.ts @@ -0,0 +1,143 @@ +import { App } from 'vue'; +import CarouselComponent from './CarouselComponent.vue'; +import TextComponent from './TextComponent.vue'; +import ImageComponent from './ImageComponent.vue'; +import ButtonComponent from './ButtonComponent.vue'; + +// 组件映射表 +export const componentMap = { + carousel: CarouselComponent, + text: TextComponent, + image: ImageComponent, + button: ButtonComponent, +}; + +// 获取组件类型 +export function getComponentType(type: string) { + return componentMap[type] || 'div'; +} + +// 注册所有设计组件 +export function registerDesignComponents(app: App) { + Object.entries(componentMap).forEach(([name, component]) => { + app.component(`Design${name.charAt(0).toUpperCase() + name.slice(1)}Component`, component); + }); +} + +// 导出所有组件 +export { + CarouselComponent, + TextComponent, + ImageComponent, + ButtonComponent, +}; + +// 组件配置信息 +export const componentConfigs = { + carousel: { + name: '轮播图', + icon: 'ion:images-outline', + description: '图片轮播展示', + category: 'basic', + defaultProps: { + autoplay: true, + dots: true, + arrows: false, + effect: 'scrollx', + dotPosition: 'bottom', + }, + defaultData: { + images: [ + { + url: 'https://via.placeholder.com/800x300/4CAF50/white?text=Slide+1', + alt: 'Slide 1' + }, + { + url: 'https://via.placeholder.com/800x300/2196F3/white?text=Slide+2', + alt: 'Slide 2' + }, + ], + }, + defaultStyle: { + width: '100%', + height: '300px', + marginBottom: '20px', + }, + }, + text: { + name: '文本', + icon: 'ion:text-outline', + description: '文字内容展示', + category: 'basic', + defaultProps: { + tag: 'p', + }, + defaultData: { + content: '这是一段文本内容,点击右侧属性面板可以编辑。', + }, + defaultStyle: { + fontSize: '16px', + color: '#333', + lineHeight: '1.6', + marginBottom: '16px', + }, + }, + image: { + name: '图片', + icon: 'ion:image-outline', + description: '单张图片展示', + category: 'basic', + defaultProps: { + alt: '图片', + objectFit: 'cover', + }, + defaultData: { + src: 'https://via.placeholder.com/400x200/FF9800/white?text=Image', + }, + defaultStyle: { + width: '400px', + height: '200px', + marginBottom: '16px', + }, + }, + button: { + name: '按钮', + icon: 'ion:radio-button-on-outline', + description: '交互按钮', + category: 'basic', + defaultProps: { + type: 'primary', + size: 'middle', + shape: 'default', + }, + defaultData: { + text: '按钮文字', + }, + defaultStyle: { + marginBottom: '16px', + }, + }, +}; + +// 根据类型获取组件配置 +export function getComponentConfig(type: string) { + return componentConfigs[type]; +} + +// 创建新组件实例 +export function createComponentInstance(type: string) { + const config = getComponentConfig(type); + if (!config) { + throw new Error(`Unknown component type: ${type}`); + } + + const id = `${type}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; + + return { + id, + type, + props: { ...config.defaultProps }, + data: { ...config.defaultData }, + style: { ...config.defaultStyle }, + }; +} diff --git a/jeecgboot-vue3/src/views/aiol/online-design/portal/index.vue b/jeecgboot-vue3/src/views/aiol/online-design/portal/index.vue new file mode 100644 index 00000000..7c313377 --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/portal/index.vue @@ -0,0 +1,519 @@ + + + + + + + 门户设计 + 创建和管理您的门户页面设计 + + + + + + + + 新建门户 + + + + + + + + + + + + + + + + 新建门户 + + + + + + + + + + {{ portal.name }} + + + + + + + + + + 设计 + + + + + + 预览 + + + + + + + + {{ portal.name }} + + + {{ portal.status === 1 ? '已发布' : '草稿' }} + + {{ formatTime(portal.updateTime) }} + + + + + + + + + + + + + + 编辑设计 + + + + 预览 + + + + 复制 + + + + + 删除 + + + + + + + + + + + + + + + 还没有门户设计 + 点击"新建门户"开始创建您的第一个门户页面 + + + + + + 新建门户 + + + + + + + + + + diff --git a/jeecgboot-vue3/src/views/aiol/online-design/test.vue b/jeecgboot-vue3/src/views/aiol/online-design/test.vue new file mode 100644 index 00000000..f8d27dff --- /dev/null +++ b/jeecgboot-vue3/src/views/aiol/online-design/test.vue @@ -0,0 +1,135 @@ + + + 在线设计模块测试页面 + + + 组件测试 + + + + 轮播图组件 + + + + + + 文本组件 + + + + + + + 图片组件 + + + + + + 按钮组件 + + + + + + + + 功能测试 + 请访问 /online-design/portal 测试完整的在线设计功能。 + + + + + + + From 55296465066b79e8f36fbb047817b23d619da4eb Mon Sep 17 00:00:00 2001 From: GoCo Date: Sun, 21 Sep 2025 16:25:15 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E8=A1=A5=E5=85=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/AiolCommentController.java | 29 +++++++ .../controller/AiolDiscussionController.java | 82 +++++++++++++++--- .../controller/AiolHomeworkController.java | 83 +++++++++++++++++-- .../aiol/dto/AiolDiscussionSaveDTO.java | 21 +++++ .../modules/aiol/dto/AiolHomeworkSaveDTO.java | 4 - .../aiol/dto/DiscussionWithSectionDTO.java | 24 ++++++ .../aiol/dto/HomeworkWithDetailsDTO.java | 29 +++++++ .../modules/aiol/entity/AiolHomework.java | 4 + .../aiol/service/IAiolEntityLinkService.java | 19 +++++ .../impl/AiolEntityLinkServiceImpl.java | 28 +++++++ .../src/views/aiol/AiolHomework.data.ts | 33 +++++--- 11 files changed, 324 insertions(+), 32 deletions(-) create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/AiolDiscussionSaveDTO.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/DiscussionWithSectionDTO.java create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/HomeworkWithDetailsDTO.java diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolCommentController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolCommentController.java index 1f6a263f..63386e01 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolCommentController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolCommentController.java @@ -365,4 +365,33 @@ public class AiolCommentController extends JeecgController topComment(@PathVariable("commentId") String commentId) { + try { + // 1. 查询评论是否存在 + AiolComment comment = aiolCommentService.getById(commentId); + if (comment == null) { + return Result.error("评论不存在"); + } + + // 2. 设置置顶状态 + comment.setIzTop(1); + + boolean updated = aiolCommentService.updateById(comment); + if (!updated) { + return Result.error("置顶失败"); + } + + log.info("评论置顶成功: commentId={}", commentId); + return Result.OK("置顶成功!"); + + } catch (Exception e) { + log.error("评论置顶失败: commentId={}, error={}", commentId, e.getMessage(), e); + return Result.error("置顶失败: " + e.getMessage()); + } + } } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolDiscussionController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolDiscussionController.java index 4c7ba046..f4bf3704 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolDiscussionController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolDiscussionController.java @@ -16,8 +16,12 @@ import org.jeecg.common.system.query.QueryRuleEnum; import org.jeecg.common.util.oConvertUtils; import org.jeecg.config.shiro.IgnoreAuth; import org.jeecg.modules.aiol.entity.AiolDiscussion; +import org.jeecg.modules.aiol.dto.DiscussionWithSectionDTO; +import org.jeecg.modules.aiol.dto.AiolDiscussionSaveDTO; import org.jeecg.modules.aiol.service.IAiolDiscussionService; import org.jeecg.modules.aiol.service.IAiolEntityLinkService; +import org.jeecg.modules.aiol.service.IAiolCourseSectionService; +import org.jeecg.modules.aiol.entity.AiolCourseSection; import org.jeecg.modules.aiol.constant.EntityLinkConst; import org.jeecg.common.system.util.JwtUtil; import org.jeecg.common.system.vo.LoginUser; @@ -61,6 +65,8 @@ public class AiolDiscussionController extends JeecgController add(@RequestBody AiolDiscussion aiolDiscussion, - @RequestParam(value = "sectionId", required = false) String sectionId) { + public Result add(@RequestBody AiolDiscussionSaveDTO aiolDiscussionSaveDTO) { try { + // 创建讨论实体 + AiolDiscussion aiolDiscussion = new AiolDiscussion(); + aiolDiscussion.setTitle(aiolDiscussionSaveDTO.getTitle()); + aiolDiscussion.setDescription(aiolDiscussionSaveDTO.getDescription()); + aiolDiscussion.setCourseId(aiolDiscussionSaveDTO.getCourseId()); + // 保存讨论记录 aiolDiscussionService.save(aiolDiscussion); // 如果传入了sectionId,创建与章节的关联关系 + String sectionId = aiolDiscussionSaveDTO.getSectionId(); if (sectionId != null && !sectionId.trim().isEmpty()) { aiolEntityLinkService.save( EntityLinkConst.SourceType.COURSE_SECTION, @@ -115,7 +126,7 @@ public class AiolDiscussionController extends JeecgController> queryCourseDiscussions(HttpServletRequest request, @RequestParam(value = "courseId") String courseId) { + public Result> queryCourseDiscussions(HttpServletRequest request, @RequestParam(value = "courseId") String courseId) { try { - // 2. 查询课程下的讨论列表 + // 1. 查询课程下的讨论列表 QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("course_id", courseId) .orderByDesc("create_time"); List discussionList = aiolDiscussionService.list(queryWrapper); - return Result.OK(discussionList); + // 2. 转换为包含章节信息的DTO列表 + List resultList = discussionList.stream() + .map(this::convertToDiscussionWithSection) + .collect(Collectors.toList()); + + return Result.OK(resultList); } catch (Exception e) { - log.error("查询用户讨论列表失败: error={}", e.getMessage(), e); - return Result.error("查询用户讨论列表失败: " + e.getMessage()); + log.error("查询课程讨论列表失败: error={}", e.getMessage(), e); + return Result.error("查询课程讨论列表失败: " + e.getMessage()); } } + /** + * 将AiolDiscussion转换为包含章节信息的DTO + * @param discussion 讨论实体 + * @return 包含章节ID的DTO + */ + private DiscussionWithSectionDTO convertToDiscussionWithSection(AiolDiscussion discussion) { + DiscussionWithSectionDTO dto = new DiscussionWithSectionDTO(); + + // 复制基本属性 + dto.setId(discussion.getId()); + dto.setTitle(discussion.getTitle()); + dto.setDescription(discussion.getDescription()); + dto.setCourseId(discussion.getCourseId()); + dto.setCreateBy(discussion.getCreateBy()); + dto.setCreateTime(discussion.getCreateTime()); + dto.setUpdateBy(discussion.getUpdateBy()); + dto.setUpdateTime(discussion.getUpdateTime()); + + try { + // 查询关联的章节ID + String sectionId = aiolEntityLinkService.listSourceId( + EntityLinkConst.TargetType.DISCUSSION, + discussion.getId(), + EntityLinkConst.SourceType.COURSE_SECTION + ); + + if (sectionId != null) { + dto.setSectionId(sectionId); + + // 查询章节名称 + AiolCourseSection section = aiolCourseSectionService.getById(sectionId); + if (section != null && section.getName() != null) { + dto.setSectionName(section.getName()); + } + } + + } catch (Exception e) { + log.error("查询讨论关联章节失败: discussionId={}, error={}", discussion.getId(), e.getMessage(), e); + // 即使查询失败,也返回基本的讨论信息 + } + + return dto; + } + } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolHomeworkController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolHomeworkController.java index 8e53d37e..30ceb9c6 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolHomeworkController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolHomeworkController.java @@ -22,10 +22,15 @@ import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.util.oConvertUtils; import org.jeecg.modules.aiol.dto.AiolHomeworkSaveDTO; import org.jeecg.modules.aiol.dto.StudentSubmitHomework; +import org.jeecg.modules.aiol.dto.HomeworkWithDetailsDTO; import org.jeecg.modules.aiol.entity.AiolHomework; import org.jeecg.modules.aiol.entity.AiolHomeworkSubmit; +import org.jeecg.modules.aiol.entity.AiolClass; +import org.jeecg.modules.aiol.entity.AiolCourseSection; import org.jeecg.modules.aiol.service.IAiolHomeworkService; import org.jeecg.modules.aiol.service.IAiolEntityLinkService; +import org.jeecg.modules.aiol.service.IAiolClassService; +import org.jeecg.modules.aiol.service.IAiolCourseSectionService; import org.jeecg.modules.aiol.constant.EntityLinkConst; import org.jeecg.modules.aiol.mapper.AiolClassStudentMapper; import org.jeecg.modules.aiol.mapper.AiolCourseSignupMapper; @@ -162,14 +167,17 @@ public class AiolHomeworkController extends JeecgController queryById(@RequestParam(name = "id", required = true) String id) { + public Result queryById(@RequestParam(name = "id", required = true) String id) { AiolHomework aiolHomework = aiolHomeworkService.getById(id); if (aiolHomework == null) { return Result.error("未找到对应数据"); } - return Result.OK(aiolHomework); + + // 转换为包含详情的DTO + HomeworkWithDetailsDTO result = convertToHomeworkWithDetails(aiolHomework); + return Result.OK(result); } /** @@ -212,6 +220,12 @@ public class AiolHomeworkController extends JeecgController> list(@PathVariable String courseId) { @@ -219,9 +233,9 @@ public class AiolHomeworkController extends JeecgController> teacherList(@RequestParam(value = "courseId") String courseId, HttpServletRequest request) { + public Result> teacherList(@RequestParam(value = "courseId") String courseId, HttpServletRequest request) { try { QueryWrapper queryWrapper = new QueryWrapper<>(); queryWrapper.eq("course_id", courseId) @@ -229,7 +243,12 @@ public class AiolHomeworkController extends JeecgController homeworkList = aiolHomeworkService.list(queryWrapper); - return Result.OK(homeworkList); + // 转换为包含详情的DTO列表 + List resultList = homeworkList.stream() + .map(this::convertToHomeworkWithDetails) + .collect(Collectors.toList()); + + return Result.OK(resultList); } catch (Exception e) { log.error("查询教师作业列表失败: error={}", e.getMessage(), e); @@ -422,4 +441,56 @@ public class AiolHomeworkController extends JeecgController classNames = new ArrayList<>(); + String classId = homework.getClassId(); + if (classId != null && !classId.trim().isEmpty()) { + String[] classIds = classId.split(","); + for (String singleClassId : classIds) { + if (singleClassId != null && !singleClassId.trim().isEmpty()) { + singleClassId = singleClassId.trim(); + AiolClass aiolClass = aiolClassService.getById(singleClassId); + if (aiolClass != null && aiolClass.getName() != null) { + classNames.add(aiolClass.getName()); + } + } + } + } + dto.setClassNames(classNames); + + // 2. 查询章节信息 + List sectionIds = entityLinkBizService.listSourceIds( + EntityLinkConst.TargetType.HOMEWORK, + homework.getId(), + EntityLinkConst.SourceType.COURSE_SECTION + ); + + if (!sectionIds.isEmpty()) { + String sectionId = sectionIds.get(0); // 取第一个章节ID + dto.setSectionId(sectionId); + + AiolCourseSection section = aiolCourseSectionService.getById(sectionId); + if (section != null && section.getName() != null) { + dto.setSectionTitle(section.getName()); + } + } + + } catch (Exception e) { + log.error("转换作业详情失败: homeworkId={}, error={}", homework.getId(), e.getMessage(), e); + // 即使转换失败,也返回基本的作业信息 + } + + return dto; + } } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/AiolDiscussionSaveDTO.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/AiolDiscussionSaveDTO.java new file mode 100644 index 00000000..6ae94d20 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/AiolDiscussionSaveDTO.java @@ -0,0 +1,21 @@ +package org.jeecg.modules.aiol.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.jeecg.modules.aiol.entity.AiolDiscussion; + +/** + * @Description: 讨论保存DTO + * @Author: jeecg-boot + * @Date: 2025-01-16 + * @Version: V1.0 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "讨论保存DTO") +public class AiolDiscussionSaveDTO extends AiolDiscussion { + + @Schema(description = "关联的章节ID") + private String sectionId; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/AiolHomeworkSaveDTO.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/AiolHomeworkSaveDTO.java index 1569b617..77408e02 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/AiolHomeworkSaveDTO.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/AiolHomeworkSaveDTO.java @@ -11,10 +11,6 @@ import org.jeecg.modules.aiol.entity.AiolHomework; @Data @Schema(description = "作业保存DTO") public class AiolHomeworkSaveDTO extends AiolHomework { - - @Schema(description = "班级ID,多个用逗号分割") - private String classId; - @Schema(description = "章节ID") private String sectionId; } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/DiscussionWithSectionDTO.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/DiscussionWithSectionDTO.java new file mode 100644 index 00000000..92010af6 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/DiscussionWithSectionDTO.java @@ -0,0 +1,24 @@ +package org.jeecg.modules.aiol.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.jeecg.modules.aiol.entity.AiolDiscussion; + +/** + * @Description: 讨论详情DTO + * @Author: jeecg-boot + * @Date: 2025-01-16 + * @Version: V1.0 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "讨论详情") +public class DiscussionWithSectionDTO extends AiolDiscussion { + + @Schema(description = "关联的章节ID") + private String sectionId; + + @Schema(description = "章节名称") + private String sectionName; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/HomeworkWithDetailsDTO.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/HomeworkWithDetailsDTO.java new file mode 100644 index 00000000..fa7420a1 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/HomeworkWithDetailsDTO.java @@ -0,0 +1,29 @@ +package org.jeecg.modules.aiol.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.jeecg.modules.aiol.entity.AiolHomework; + +import java.util.List; + +/** + * @Description: 作业详情DTO + * @Author: jeecg-boot + * @Date: 2025-01-16 + * @Version: V1.0 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "作业详情") +public class HomeworkWithDetailsDTO extends AiolHomework { + + @Schema(description = "班级名称列表") + private List classNames; + + @Schema(description = "章节ID") + private String sectionId; + + @Schema(description = "章节标题") + private String sectionTitle; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/entity/AiolHomework.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/entity/AiolHomework.java index d7952bc7..bf99f7e7 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/entity/AiolHomework.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/entity/AiolHomework.java @@ -41,6 +41,10 @@ public class AiolHomework implements Serializable { @Excel(name = "所属课程id", width = 15) @Schema(description = "所属课程id") private java.lang.String courseId; + /**班级id*/ + @Excel(name = "班级id", width = 15) + @Schema(description = "班级id") + private java.lang.String classId; /**标题*/ @Excel(name = "标题", width = 15) @Schema(description = "标题") diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/IAiolEntityLinkService.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/IAiolEntityLinkService.java index e7f5efc4..8b27cb68 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/IAiolEntityLinkService.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/IAiolEntityLinkService.java @@ -21,6 +21,15 @@ public interface IAiolEntityLinkService extends IService { */ List listTargetIds(String sourceType, String sourceId, String targetType); + /** + * 根据主体与内容类型查询绑定的 target_id + * @param sourceType 主体类型 + * @param sourceId 主体ID + * @param targetType 内容类型 + * @return target_id + */ + String listTargetId(String sourceType, String sourceId, String targetType); + /** * 根据内容与主体类型查询绑定的 source_id 列表 * @param targetType 内容类型 @@ -30,6 +39,16 @@ public interface IAiolEntityLinkService extends IService { */ List listSourceIds(String targetType, String targetId, String sourceType); + /** + * 根据内容与主体类型查询绑定的 source_id + * @param targetType 内容类型 + * @param targetId 内容ID + * @param sourceType 主体类型 + * @return source_id + */ + String listSourceId(String targetType, String targetId, String sourceType); + + /** * 保存主体与内容类型的绑定关系 * @param sourceType 主体类型 diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/impl/AiolEntityLinkServiceImpl.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/impl/AiolEntityLinkServiceImpl.java index 581b276d..1d636381 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/impl/AiolEntityLinkServiceImpl.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/service/impl/AiolEntityLinkServiceImpl.java @@ -54,4 +54,32 @@ public class AiolEntityLinkServiceImpl extends ServiceImpl qw = new LambdaQueryWrapper<>(); + qw.eq(AiolEntityLink::getTargetType, targetType) + .eq(AiolEntityLink::getTargetId, targetId) + .eq(AiolEntityLink::getSourceType, sourceType) + .select(AiolEntityLink::getSourceId) + .last("LIMIT 1"); + return this.list(qw).stream() + .map(AiolEntityLink::getSourceId) + .findFirst() + .orElse(null); + } + + @Override + public String listTargetId(String sourceType, String sourceId, String targetType) { + LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); + qw.eq(AiolEntityLink::getSourceType, sourceType) + .eq(AiolEntityLink::getSourceId, sourceId) + .eq(AiolEntityLink::getTargetType, targetType) + .select(AiolEntityLink::getTargetId) + .last("LIMIT 1"); + return this.list(qw).stream() + .map(AiolEntityLink::getTargetId) + .findFirst() + .orElse(null); + } } diff --git a/jeecgboot-vue3/src/views/aiol/AiolHomework.data.ts b/jeecgboot-vue3/src/views/aiol/AiolHomework.data.ts index 1cebd1aa..93a8ebab 100644 --- a/jeecgboot-vue3/src/views/aiol/AiolHomework.data.ts +++ b/jeecgboot-vue3/src/views/aiol/AiolHomework.data.ts @@ -10,6 +10,11 @@ export const columns: BasicColumn[] = [ align:"center", dataIndex: 'courseId' }, + { + title: '班级id', + align:"center", + dataIndex: 'classId' + }, { title: '标题', align:"center", @@ -76,6 +81,11 @@ export const formSchema: FormSchema[] = [ field: 'courseId', component: 'Input', }, + { + label: '班级id', + field: 'classId', + component: 'Input', + }, { label: '标题', field: 'title', @@ -161,17 +171,18 @@ export const formSchema: FormSchema[] = [ // 高级查询数据 export const superQuerySchema = { courseId: {title: '所属课程id',order: 0,view: 'text', type: 'string',}, - title: {title: '标题',order: 1,view: 'text', type: 'string',}, - description: {title: '说明',order: 2,view: 'umeditor', type: 'string',}, - attachment: {title: '附件',order: 3,view: 'file', type: 'string',}, - maxScore: {title: '满分',order: 4,view: 'number', type: 'number',}, - passScore: {title: '及格分数',order: 5,view: 'number', type: 'number',}, - startTime: {title: '开始时间',order: 6,view: 'datetime', type: 'string',}, - endTime: {title: '结束时间',order: 7,view: 'datetime', type: 'string',}, - status: {title: '状态',order: 8,view: 'number', type: 'number',dictCode: 'course_status',}, - allowMakeup: {title: '是否允许补交',order: 9,view: 'number', type: 'number',}, - makeupTime: {title: '补交截止时间',order: 10,view: 'datetime', type: 'string',}, - notifyTime: {title: '作业通知时间',order: 11,view: 'number', type: 'number',}, + classId: {title: '班级id',order: 1,view: 'text', type: 'string',}, + title: {title: '标题',order: 2,view: 'text', type: 'string',}, + description: {title: '说明',order: 3,view: 'umeditor', type: 'string',}, + attachment: {title: '附件',order: 4,view: 'file', type: 'string',}, + maxScore: {title: '满分',order: 5,view: 'number', type: 'number',}, + passScore: {title: '及格分数',order: 6,view: 'number', type: 'number',}, + startTime: {title: '开始时间',order: 7,view: 'datetime', type: 'string',}, + endTime: {title: '结束时间',order: 8,view: 'datetime', type: 'string',}, + status: {title: '状态',order: 9,view: 'number', type: 'number',dictCode: 'course_status',}, + allowMakeup: {title: '是否允许补交',order: 10,view: 'number', type: 'number',}, + makeupTime: {title: '补交截止时间',order: 11,view: 'datetime', type: 'string',}, + notifyTime: {title: '作业通知时间',order: 12,view: 'number', type: 'number',}, }; /** From 59cd0049e2c4cdb5cf78fa977d0fc83a7e89b7c3 Mon Sep 17 00:00:00 2001 From: GoCo Date: Mon, 22 Sep 2025 08:44:15 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=F0=9F=8E=B8=20=E5=9C=A8=E7=BA=BF?= =?UTF-8?q?=E4=BA=A4=E6=B5=81=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../aiol/controller/AiolChatController.java | 528 ++++++++++++++++-- .../aiol/dto/ChatWithUnreadCountDTO.java | 21 + .../modules/aiol/entity/AiolChatMember.java | 2 +- 3 files changed, 517 insertions(+), 34 deletions(-) create mode 100644 jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/ChatWithUnreadCountDTO.java diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolChatController.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolChatController.java index 4199302f..c92d743a 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolChatController.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/controller/AiolChatController.java @@ -24,10 +24,13 @@ import org.jeecg.modules.aiol.entity.AiolChatMember; import org.jeecg.modules.aiol.entity.AiolChatMessage; import org.jeecg.modules.aiol.entity.AiolClass; import org.jeecg.modules.aiol.entity.AiolClassStudent; +import org.jeecg.modules.aiol.dto.ChatWithUnreadCountDTO; import org.jeecg.modules.aiol.mapper.AiolChatMemberMapper; import org.jeecg.modules.aiol.mapper.AiolChatMessageMapper; import org.jeecg.modules.aiol.mapper.AiolClassMapper; import org.jeecg.modules.aiol.mapper.AiolClassStudentMapper; +import org.jeecg.modules.system.mapper.SysUserRoleMapper; +import org.jeecg.modules.aiol.constant.RoleConst; import org.jeecg.modules.aiol.service.IAiolChatService; import org.jeecg.modules.system.entity.SysUser; import org.jeecg.modules.system.mapper.SysUserMapper; @@ -85,6 +88,9 @@ public class AiolChatController extends JeecgController> queryMyChatList(HttpServletRequest request) { + public Result> queryMyChatList(HttpServletRequest request) { try { // 1. 从token获取当前用户信息 String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); @@ -235,7 +241,7 @@ public class AiolChatController extends JeecgController memberWrapper = new QueryWrapper<>(); memberWrapper.eq("user_id", sysUser.getId()); List chatMembers = aiolChatMemberMapper.selectList(memberWrapper); @@ -256,37 +262,16 @@ public class AiolChatController extends JeecgController chatList = aiolChatService.list(chatWrapper); - // 5. 处理私聊类型的会话,获取对方用户信息 + // 5. 转换为包含未读消息数的DTO列表 + List resultList = new java.util.ArrayList<>(); + for (AiolChat chat : chatList) { - if (chat.getType() != null && chat.getType() == 0) { - // 私聊类型,需要获取对方用户信息 - try { - // 查询该会话的成员,排除当前用户 - QueryWrapper otherMemberWrapper = new QueryWrapper<>(); - otherMemberWrapper.eq("chat_id", chat.getId()) - .ne("user_id", sysUser.getId()); - List otherMembers = aiolChatMemberMapper.selectList(otherMemberWrapper); - - if (!otherMembers.isEmpty()) { - // 获取对方用户ID - String otherUserId = otherMembers.get(0).getUserId(); - - // 查询对方用户信息 - SysUser otherUser = sysUserMapper.selectById(otherUserId); - if (otherUser != null) { - // 替换会话的name和avatar为对方用户信息 - chat.setName(otherUser.getRealname()); - chat.setAvatar(otherUser.getAvatar()); - } - } - } catch (Exception e) { - log.warn("获取私聊对方用户信息失败: chatId={}, error={}", chat.getId(), e.getMessage()); - } - } + ChatWithUnreadCountDTO chatDTO = convertToChatWithUnreadCount(chat, chatMembers, sysUser.getId()); + resultList.add(chatDTO); } - log.info("用户 {} 查询到 {} 个会话", username, chatList.size()); - return Result.OK(chatList); + log.info("用户 {} 查询到 {} 个会话", username, resultList.size()); + return Result.OK(resultList); } catch (Exception e) { log.error("查询用户会话列表失败: {}", e.getMessage(), e); @@ -300,7 +285,7 @@ public class AiolChatController extends JeecgController>> queryChatMembers(@PathVariable(value = "chatId") String chatId) { try { @@ -321,7 +306,26 @@ public class AiolChatController extends JeecgController userList = sysUserMapper.selectByIds(userIds); - // 4. 构建返回结果 + // 4. 查询所有用户的教师角色身份 + Map teacherStatusMap = new java.util.HashMap<>(); + if (!userIds.isEmpty()) { + try { + QueryWrapper roleWrapper = new QueryWrapper<>(); + roleWrapper.eq("role_id", RoleConst.TEACHER_ROLE_ID) + .in("user_id", userIds); + + List teacherRoleList = sysUserRoleMapper.selectList(roleWrapper); + + // 构建教师身份映射 + for (org.jeecg.modules.system.entity.SysUserRole userRole : teacherRoleList) { + teacherStatusMap.put(userRole.getUserId(), true); + } + } catch (Exception e) { + log.warn("查询教师角色身份失败: error={}", e.getMessage()); + } + } + + // 5. 构建返回结果 List> result = new java.util.ArrayList<>(); for (SysUser user : userList) { Map memberInfo = new java.util.HashMap<>(); @@ -329,6 +333,10 @@ public class AiolChatController extends JeecgController muteAll(@PathVariable(value = "chatId") String chatId) { + try { + return updateChatSetting(chatId, "iz_all_muted", 1, "开启全员禁言"); + } catch (Exception e) { + log.error("开启全员禁言失败: chatId={}, error={}", chatId, e.getMessage(), e); + return Result.error("开启全员禁言失败: " + e.getMessage()); + } + } + + /** + * 关闭全员禁言 + * + * @param chatId 会话ID + * @return + */ + @AutoLog(value = "会话-关闭全员禁言") + @Operation(summary = "关闭全员禁言", description = "关闭群聊的全员禁言功能,设置iz_all_muted字段为0") + @PostMapping(value = "/{chatId}/unmute_all") + public Result unmuteAll(@PathVariable(value = "chatId") String chatId) { + try { + return updateChatSetting(chatId, "iz_all_muted", 0, "关闭全员禁言"); + } catch (Exception e) { + log.error("关闭全员禁言失败: chatId={}, error={}", chatId, e.getMessage(), e); + return Result.error("关闭全员禁言失败: " + e.getMessage()); + } + } + + /** + * 开启显示教师标签 + * + * @param chatId 会话ID + * @return + */ + @AutoLog(value = "会话-开启显示教师标签") + @Operation(summary = "开启显示教师标签", description = "开启群聊中显示教师标签功能,设置show_label字段为1") + @PostMapping(value = "/{chatId}/show_label") + public Result showLabel(@PathVariable(value = "chatId") String chatId) { + try { + return updateChatSetting(chatId, "show_label", 1, "开启显示教师标签"); + } catch (Exception e) { + log.error("开启显示教师标签失败: chatId={}, error={}", chatId, e.getMessage(), e); + return Result.error("开启显示教师标签失败: " + e.getMessage()); + } + } + + /** + * 关闭显示教师标签 + * + * @param chatId 会话ID + * @return + */ + @AutoLog(value = "会话-关闭显示教师标签") + @Operation(summary = "关闭显示教师标签", description = "关闭群聊中显示教师标签功能,设置show_label字段为0") + @PostMapping(value = "/{chatId}/hide_label") + public Result hideLabel(@PathVariable(value = "chatId") String chatId) { + try { + return updateChatSetting(chatId, "show_label", 0, "关闭显示教师标签"); + } catch (Exception e) { + log.error("关闭显示教师标签失败: chatId={}, error={}", chatId, e.getMessage(), e); + return Result.error("关闭显示教师标签失败: " + e.getMessage()); + } + } + + /** + * 通用更新会话设置的方法 + * + * @param chatId 会话ID + * @param fieldName 字段名 + * @param value 字段值 + * @param operationName 操作名称 + * @return + */ + private Result updateChatSetting(String chatId, String fieldName, Integer value, String operationName) { + // 1. 查询会话是否存在 + AiolChat chat = aiolChatService.getById(chatId); + if (chat == null) { + return Result.error("会话不存在"); + } + + // 2. 根据字段名设置相应的值 + if ("iz_all_muted".equals(fieldName)) { + chat.setIzAllMuted(value); + } else if ("show_label".equals(fieldName)) { + chat.setShowLabel(value); + } else { + return Result.error("无效的字段名"); + } + + // 3. 更新数据库 + boolean updated = aiolChatService.updateById(chat); + if (!updated) { + return Result.error(operationName + "失败"); + } + + log.info("{}成功: chatId={}, {}={}", operationName, chatId, fieldName, value); + return Result.OK(operationName + "成功!"); + } + + /** + * 禁言群聊成员 + * + * @param chatId 会话ID + * @param userId 用户ID + * @return + */ + @AutoLog(value = "会话-禁言群聊成员") + @Operation(summary = "禁言群聊成员", description = "禁言指定群聊成员,设置iz_muted字段为1") + @PostMapping(value = "/{chatId}/mute_member/{userId}") + public Result muteMember(@PathVariable(value = "chatId") String chatId, + @PathVariable(value = "userId") String userId) { + try { + return updateMemberSetting(chatId, userId, "iz_muted", 1, "禁言群聊成员"); + } catch (Exception e) { + log.error("禁言群聊成员失败: chatId={}, userId={}, error={}", chatId, userId, e.getMessage(), e); + return Result.error("禁言群聊成员失败: " + e.getMessage()); + } + } + + /** + * 解除禁言群聊成员 + * + * @param chatId 会话ID + * @param userId 用户ID + * @return + */ + @AutoLog(value = "会话-解除禁言群聊成员") + @Operation(summary = "解除禁言群聊成员", description = "解除指定群聊成员的禁言状态,设置iz_muted字段为0") + @PostMapping(value = "/{chatId}/unmute_member/{userId}") + public Result unmuteMember(@PathVariable(value = "chatId") String chatId, + @PathVariable(value = "userId") String userId) { + try { + return updateMemberSetting(chatId, userId, "iz_muted", 0, "解除禁言群聊成员"); + } catch (Exception e) { + log.error("解除禁言群聊成员失败: chatId={}, userId={}, error={}", chatId, userId, e.getMessage(), e); + return Result.error("解除禁言群聊成员失败: " + e.getMessage()); + } + } + + /** + * 开启免打扰 + * + * @param chatId 会话ID + * @param userId 用户ID + * @return + */ + @AutoLog(value = "会话-开启免打扰") + @Operation(summary = "开启免打扰", description = "为指定用户开启群聊免打扰功能,设置iz_not_disturb字段为1") + @PostMapping(value = "/{chatId}/enable_not_disturb/{userId}") + public Result enableNotDisturb(@PathVariable(value = "chatId") String chatId, + @PathVariable(value = "userId") String userId) { + try { + return updateMemberSetting(chatId, userId, "iz_not_disturb", 1, "开启免打扰"); + } catch (Exception e) { + log.error("开启免打扰失败: chatId={}, userId={}, error={}", chatId, userId, e.getMessage(), e); + return Result.error("开启免打扰失败: " + e.getMessage()); + } + } + + /** + * 关闭免打扰 + * + * @param chatId 会话ID + * @param userId 用户ID + * @return + */ + @AutoLog(value = "会话-关闭免打扰") + @Operation(summary = "关闭免打扰", description = "为指定用户关闭群聊免打扰功能,设置iz_not_disturb字段为0") + @PostMapping(value = "/{chatId}/disable_not_disturb/{userId}") + public Result disableNotDisturb(@PathVariable(value = "chatId") String chatId, + @PathVariable(value = "userId") String userId) { + try { + return updateMemberSetting(chatId, userId, "iz_not_disturb", 0, "关闭免打扰"); + } catch (Exception e) { + log.error("关闭免打扰失败: chatId={}, userId={}, error={}", chatId, userId, e.getMessage(), e); + return Result.error("关闭免打扰失败: " + e.getMessage()); + } + } + + /** + * 通用更新群聊成员设置的方法 + * + * @param chatId 会话ID + * @param userId 用户ID + * @param fieldName 字段名 + * @param value 字段值 + * @param operationName 操作名称 + * @return + */ + private Result updateMemberSetting(String chatId, String userId, String fieldName, Integer value, String operationName) { + // 1. 查询群聊成员是否存在 + QueryWrapper memberWrapper = new QueryWrapper<>(); + memberWrapper.eq("chat_id", chatId).eq("user_id", userId); + AiolChatMember chatMember = aiolChatMemberMapper.selectOne(memberWrapper); + + if (chatMember == null) { + return Result.error("该用户不是群聊成员或会话不存在"); + } + + // 2. 根据字段名设置相应的值 + if ("iz_muted".equals(fieldName)) { + chatMember.setIzMuted(value); + } else if ("iz_not_disturb".equals(fieldName)) { + chatMember.setIzNotDisturb(value); + } else { + return Result.error("无效的字段名"); + } + + // 3. 更新数据库 + boolean updated = aiolChatMemberMapper.updateById(chatMember) > 0; + if (!updated) { + return Result.error(operationName + "失败"); + } + + log.info("{}成功: chatId={}, userId={}, {}={}", operationName, chatId, userId, fieldName, value); + return Result.OK(operationName + "成功!"); + } + + /** + * 将AiolChat转换为包含未读消息数的DTO + * @param chat 会话实体 + * @param chatMembers 会话成员列表 + * @param userId 当前用户ID + * @return 包含未读消息数的DTO + */ + private ChatWithUnreadCountDTO convertToChatWithUnreadCount(AiolChat chat, List chatMembers, String userId) { + ChatWithUnreadCountDTO chatDTO = new ChatWithUnreadCountDTO(); + + // 复制基本属性 + chatDTO.setId(chat.getId()); + chatDTO.setType(chat.getType()); + chatDTO.setName(chat.getName()); + chatDTO.setAvatar(chat.getAvatar()); + chatDTO.setRefId(chat.getRefId()); + chatDTO.setIzAllMuted(chat.getIzAllMuted()); + chatDTO.setShowLabel(chat.getShowLabel()); + chatDTO.setCreateBy(chat.getCreateBy()); + chatDTO.setCreateTime(chat.getCreateTime()); + chatDTO.setUpdateBy(chat.getUpdateBy()); + chatDTO.setUpdateTime(chat.getUpdateTime()); + + try { + // 1. 计算未读消息数 + AiolChatMember currentUserMember = chatMembers.stream() + .filter(member -> member.getChatId().equals(chat.getId()) && member.getUserId().equals(userId)) + .findFirst() + .orElse(null); + + int unreadCount = 0; + if (currentUserMember != null) { + String lastReadMsgId = currentUserMember.getLastReadMsgId(); + + // 查询该会话中last_read_msg_id之后的消息数量 + QueryWrapper messageWrapper = new QueryWrapper<>(); + messageWrapper.eq("chat_id", chat.getId()); + + if (lastReadMsgId != null && !lastReadMsgId.trim().isEmpty()) { + // 如果有最后读取的消息ID,查询该消息之后的消息 + QueryWrapper lastReadMsgWrapper = new QueryWrapper<>(); + lastReadMsgWrapper.eq("chat_id", chat.getId()).eq("id", lastReadMsgId); + AiolChatMessage lastReadMsg = aiolChatMessageMapper.selectOne(lastReadMsgWrapper); + + if (lastReadMsg != null) { + // 查询创建时间晚于最后读取消息的消息数量 + messageWrapper.gt("create_time", lastReadMsg.getCreateTime()); + } + } + + Long count = aiolChatMessageMapper.selectCount(messageWrapper); + unreadCount = count != null ? count.intValue() : 0; + } + + chatDTO.setUnreadCount(unreadCount); + + // 2. 处理私聊类型的会话,获取对方用户信息 + if (chat.getType() != null && chat.getType() == 0) { + // 私聊类型,需要获取对方用户信息 + try { + // 查询该会话的成员,排除当前用户 + QueryWrapper otherMemberWrapper = new QueryWrapper<>(); + otherMemberWrapper.eq("chat_id", chat.getId()) + .ne("user_id", userId); + List otherMembers = aiolChatMemberMapper.selectList(otherMemberWrapper); + + if (!otherMembers.isEmpty()) { + // 获取对方用户ID + String otherUserId = otherMembers.get(0).getUserId(); + + // 查询对方用户信息 + SysUser otherUser = sysUserMapper.selectById(otherUserId); + if (otherUser != null) { + // 替换会话的name和avatar为对方用户信息 + chatDTO.setName(otherUser.getRealname()); + chatDTO.setAvatar(otherUser.getAvatar()); + } + } + } catch (Exception e) { + log.warn("获取私聊对方用户信息失败: chatId={}, error={}", chat.getId(), e.getMessage()); + } + } + + } catch (Exception e) { + log.error("转换会话信息失败: chatId={}, error={}", chat.getId(), e.getMessage(), e); + // 即使转换失败,也返回基本的会话信息 + chatDTO.setUnreadCount(0); + } + + return chatDTO; + } + + /** + * 更新用户最后读取的消息ID + * + * @param chatId 会话ID + * @param messageId 消息ID + * @param request HTTP请求对象 + * @return + */ + @AutoLog(value = "会话-更新最后读取消息ID") + @Operation(summary = "更新最后读取消息ID", description = "更新当前用户在指定会话中的最后读取消息ID") + @PostMapping(value = "/{chatId}/update_last_read/{messageId}") + public Result updateLastReadMsgId(@PathVariable(value = "chatId") String chatId, + @PathVariable(value = "messageId") String messageId, + HttpServletRequest request) { + try { + // 1. 从token获取当前用户信息 + String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); + if (token == null || token.trim().isEmpty()) { + return Result.error("用户未登录"); + } + + String username = JwtUtil.getUsername(token); + LoginUser sysUser = sysBaseApi.getUserByName(username); + + if (sysUser == null) { + return Result.error("用户信息不存在"); + } + + // 2. 查询群聊成员是否存在 + QueryWrapper memberWrapper = new QueryWrapper<>(); + memberWrapper.eq("chat_id", chatId).eq("user_id", sysUser.getId()); + AiolChatMember chatMember = aiolChatMemberMapper.selectOne(memberWrapper); + + if (chatMember == null) { + return Result.error("该用户不是群聊成员或会话不存在"); + } + + // 3. 验证消息是否存在 + AiolChatMessage message = aiolChatMessageMapper.selectById(messageId); + if (message == null || !message.getChatId().equals(chatId)) { + return Result.error("消息不存在或不属于该会话"); + } + + // 4. 更新最后读取的消息ID + chatMember.setLastReadMsgId(messageId); + boolean updated = aiolChatMemberMapper.updateById(chatMember) > 0; + + if (!updated) { + return Result.error("更新失败"); + } + + log.info("用户 {} 更新会话 {} 的最后读取消息ID为 {}", username, chatId, messageId); + return Result.OK("更新成功!"); + + } catch (Exception e) { + log.error("更新最后读取消息ID失败: chatId={}, messageId={}, error={}", chatId, messageId, e.getMessage(), e); + return Result.error("更新失败: " + e.getMessage()); + } + } + + /** + * 退出群聊 + * + * @param chatId 会话ID + * @param request HTTP请求对象 + * @return + */ + @AutoLog(value = "会话-退出群聊") + @Operation(summary = "退出群聊", description = "当前用户退出指定的群聊会话,从aiol_chat_member表中删除用户记录") + @DeleteMapping(value = "/{chatId}/exit") + public Result exitChat(@PathVariable(value = "chatId") String chatId, + HttpServletRequest request) { + try { + // 1. 从token获取当前用户信息 + String token = request.getHeader(CommonConstant.X_ACCESS_TOKEN); + if (token == null || token.trim().isEmpty()) { + return Result.error("用户未登录"); + } + + String username = JwtUtil.getUsername(token); + LoginUser sysUser = sysBaseApi.getUserByName(username); + + if (sysUser == null) { + return Result.error("用户信息不存在"); + } + + // 2. 验证会话是否存在 + AiolChat chat = aiolChatService.getById(chatId); + if (chat == null) { + return Result.error("会话不存在"); + } + + // 3. 查询用户是否为该会话的成员 + QueryWrapper memberWrapper = new QueryWrapper<>(); + memberWrapper.eq("chat_id", chatId).eq("user_id", sysUser.getId()); + AiolChatMember chatMember = aiolChatMemberMapper.selectOne(memberWrapper); + + if (chatMember == null) { + return Result.error("您不是该会话的成员"); + } + + // 4. 检查是否为会话创建者(可选:不允许创建者退出) + if (chat.getCreateBy() != null && chat.getCreateBy().equals(sysUser.getId())) { + // 可选:如果创建者退出,需要转移创建者权限或不允许退出 + log.warn("会话创建者尝试退出群聊: chatId={}, userId={}", chatId, sysUser.getId()); + // 这里可以选择是否允许创建者退出,或者需要先转移权限 + // return Result.error("群聊创建者不能直接退出,请先转移群主权限"); + } + + // 5. 从会话成员表中删除该用户 + boolean deleted = aiolChatMemberMapper.deleteById(chatMember.getId()) > 0; + + if (!deleted) { + return Result.error("退出群聊失败"); + } + + // 6. 检查会话是否还有其他成员,如果没有则删除会话(可选) + QueryWrapper remainingMemberWrapper = new QueryWrapper<>(); + remainingMemberWrapper.eq("chat_id", chatId); + long remainingMemberCount = aiolChatMemberMapper.selectCount(remainingMemberWrapper); + + if (remainingMemberCount == 0) { + // 如果没有其他成员了,删除会话 + aiolChatService.removeById(chatId); + log.info("会话 {} 无成员,已自动删除", chatId); + } + + log.info("用户 {} 成功退出群聊 {}", username, chatId); + return Result.OK("退出群聊成功!"); + + } catch (Exception e) { + log.error("退出群聊失败: chatId={}, error={}", chatId, e.getMessage(), e); + return Result.error("退出群聊失败: " + e.getMessage()); + } + } } diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/ChatWithUnreadCountDTO.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/ChatWithUnreadCountDTO.java new file mode 100644 index 00000000..21524d25 --- /dev/null +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/dto/ChatWithUnreadCountDTO.java @@ -0,0 +1,21 @@ +package org.jeecg.modules.aiol.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; +import lombok.EqualsAndHashCode; +import org.jeecg.modules.aiol.entity.AiolChat; + +/** + * @Description: 会话详情DTO,包含未读消息数 + * @Author: jeecg-boot + * @Date: 2025-01-16 + * @Version: V1.0 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "会话详情,包含未读消息数") +public class ChatWithUnreadCountDTO extends AiolChat { + + @Schema(description = "未读消息数") + private Integer unreadCount; +} diff --git a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/entity/AiolChatMember.java b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/entity/AiolChatMember.java index 4e442739..27addb30 100644 --- a/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/entity/AiolChatMember.java +++ b/jeecg-boot/jeecg-boot-module/jeecg-module-aiol/src/main/java/org/jeecg/modules/aiol/entity/AiolChatMember.java @@ -60,7 +60,7 @@ public class AiolChatMember implements Serializable { /**最后已读消息id*/ @Excel(name = "最后已读消息id", width = 15) @Schema(description = "最后已读消息id") - private java.lang.Integer lastReadMsgId; + private java.lang.String lastReadMsgId; /**创建人*/ @Schema(description = "创建人") private java.lang.String createBy;
从左侧组件库拖拽组件到网格中开始设计
支持网格布局和组件缩放
请选择一个组件来编辑属性
点击上传图片
支持 JPG、PNG、GIF 格式
按钮组件
请在右侧属性面板设置按钮文字
{{ image.description }}
轮播图组件
请在右侧属性面板添加图片
图片组件
请在右侧属性面板设置图片URL
文本组件
请在右侧属性面板编辑文本内容
创建和管理您的门户页面设计
点击"新建门户"开始创建您的第一个门户页面
请访问 /online-design/portal 测试完整的在线设计功能。