Просмотр исходного кода

feat(backend): AI管家会话管理 - ButlerSession实体/服务/控制器 + 迁移42

- 新增 ButlerSession 实体 (butler_sessions表)
- 新增 ButlerSessionMapper (MyBatis-Plus)
- 新增 ButlerSessionService (CRUD/归档/上下文管理)
- 新增 ButlerController (/api/ai/butler/sessions/* 6端点)
- DatabaseInitializer 迁移42: 创建 butler_sessions 表
- schema.sql 同步更新
Xiaogang Liao 2 месяцев назад
Родитель
Сommit
afa236deee

+ 20 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -4355,6 +4355,26 @@ try {
         } catch (Exception e) {
         } catch (Exception e) {
             log.warn("创建family_relationships表可能已存在: {}", e.getMessage());
             log.warn("创建family_relationships表可能已存在: {}", e.getMessage());
         }
         }
+
+        // 迁移42: 创建butler_sessions表(AI管家会话管理)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS butler_sessions (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "user_id BIGINT NOT NULL COMMENT '用户ID', " +
+                    "title VARCHAR(200) DEFAULT '新会话' COMMENT '会话标题', " +
+                    "conversation_id VARCHAR(100) COMMENT 'Dify对话ID', " +
+                    "context TEXT COMMENT '上下文信息(JSON)', " +
+                    "status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active(进行中)/archived(已归档)', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_user_id (user_id), " +
+                    "INDEX idx_status (status), " +
+                    "INDEX idx_user_status (user_id, status)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI管家会话表'");
+            log.info("已创建butler_sessions表");
+        } catch (Exception e) {
+            log.warn("创建butler_sessions表可能已存在: {}", e.getMessage());
+        }
     }
     }
 
 
     private void insertSysConfigSeed(String key, String value, String desc) {
     private void insertSysConfigSeed(String key, String value, String desc) {

+ 96 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/ButlerController.java

@@ -0,0 +1,96 @@
+package com.etotem.cfc.controller.ai;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.ButlerSession;
+import com.etotem.cfc.service.ButlerSessionService;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.Map;
+
+@Tag(name = "AI管家", description = "AI管家会话管理接口")
+@RestController
+@RequestMapping("/api/ai/butler")
+public class ButlerController {
+
+    @Resource
+    private ButlerSessionService butlerSessionService;
+
+    @Operation(summary = "创建新会话")
+    @PostMapping("/sessions/create")
+    public Result<Map<String, Object>> createSession(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, String> params) {
+        String title = params.getOrDefault("title", "新会话");
+        String context = params.get("context");
+        Long sessionId = butlerSessionService.createSession(userId, title, context);
+        return Result.success(Collections.singletonMap("sessionId", sessionId));
+    }
+
+    @Operation(summary = "更新会话Dify conversationId")
+    @PostMapping("/sessions/update-conversation")
+    public Result<Void> updateConversationId(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        Long sessionId = Long.valueOf(params.get("sessionId").toString());
+        String conversationId = (String) params.get("conversationId");
+        ButlerSession session = butlerSessionService.getSession(sessionId, userId);
+        if (session == null) {
+            return Result.error("会话不存在或无权访问");
+        }
+        butlerSessionService.updateConversationId(sessionId, conversationId);
+        return Result.success(null);
+    }
+
+    @Operation(summary = "归档会话")
+    @PostMapping("/sessions/archive")
+    public Result<Void> archiveSession(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        Long sessionId = Long.valueOf(params.get("sessionId").toString());
+        butlerSessionService.archiveSession(sessionId, userId);
+        return Result.success(null);
+    }
+
+    @Operation(summary = "获取会话列表")
+    @PostMapping("/sessions/list")
+    public Result<Page<ButlerSession>> listSessions(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
+        Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 20;
+        String status = (String) params.get("status");
+        Page<ButlerSession> sessions = butlerSessionService.getSessionsByUser(userId, page, size, status);
+        return Result.success(sessions);
+    }
+
+    @Operation(summary = "获取会话详情")
+    @PostMapping("/sessions/detail")
+    public Result<ButlerSession> sessionDetail(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        Long sessionId = Long.valueOf(params.get("sessionId").toString());
+        ButlerSession session = butlerSessionService.getSession(sessionId, userId);
+        if (session == null) {
+            return Result.error("会话不存在或无权访问");
+        }
+        return Result.success(session);
+    }
+
+    @Operation(summary = "删除会话")
+    @PostMapping("/sessions/delete")
+    public Result<Void> deleteSession(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, Object> params) {
+        Long sessionId = Long.valueOf(params.get("sessionId").toString());
+        boolean deleted = butlerSessionService.deleteSession(sessionId, userId);
+        if (!deleted) {
+            return Result.error("会话不存在或无权删除");
+        }
+        return Result.success(null);
+    }
+}

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/ButlerSession.java

@@ -0,0 +1,31 @@
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("butler_sessions")
+public class ButlerSession implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long userId;            // 用户ID
+
+    private String title;           // 会话标题
+
+    private String conversationId;  // Dify对话ID
+
+    private String context;         // 上下文信息(JSON)
+
+    private String status;          // 状态: active(进行中)/archived(已归档)
+
+    private Date createdAt;         // 创建时间
+
+    private Date updatedAt;         // 更新时间
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/ButlerSessionMapper.java

@@ -0,0 +1,9 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ButlerSession;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface ButlerSessionMapper extends BaseMapper<ButlerSession> {
+}

+ 116 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ButlerSessionService.java

@@ -0,0 +1,116 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.entity.ButlerSession;
+import com.etotem.cfc.mapper.ButlerSessionMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class ButlerSessionService {
+
+    @Resource
+    private ButlerSessionMapper butlerSessionMapper;
+
+    /**
+     * 创建新会话
+     */
+    public Long createSession(Long userId, String title, String context) {
+        ButlerSession session = new ButlerSession();
+        session.setUserId(userId);
+        session.setTitle(title);
+        session.setContext(context);
+        session.setStatus("active");
+        session.setCreatedAt(new Date());
+        session.setUpdatedAt(new Date());
+        butlerSessionMapper.insert(session);
+        return session.getId();
+    }
+
+    /**
+     * 更新会话的Dify conversationId
+     */
+    public void updateConversationId(Long sessionId, String conversationId) {
+        ButlerSession session = butlerSessionMapper.selectById(sessionId);
+        if (session != null) {
+            session.setConversationId(conversationId);
+            session.setUpdatedAt(new Date());
+            butlerSessionMapper.updateById(session);
+        }
+    }
+
+    /**
+     * 更新会话上下文
+     */
+    public void updateContext(Long sessionId, String context) {
+        ButlerSession session = butlerSessionMapper.selectById(sessionId);
+        if (session != null) {
+            session.setContext(context);
+            session.setUpdatedAt(new Date());
+            butlerSessionMapper.updateById(session);
+        }
+    }
+
+    /**
+     * 归档会话
+     */
+    public void archiveSession(Long sessionId, Long userId) {
+        ButlerSession session = butlerSessionMapper.selectById(sessionId);
+        if (session != null && session.getUserId().equals(userId)) {
+            session.setStatus("archived");
+            session.setUpdatedAt(new Date());
+            butlerSessionMapper.updateById(session);
+        }
+    }
+
+    /**
+     * 获取用户的活跃会话列表
+     */
+    public List<ButlerSession> getActiveSessions(Long userId) {
+        return butlerSessionMapper.selectList(new LambdaQueryWrapper<ButlerSession>()
+                .eq(ButlerSession::getUserId, userId)
+                .eq(ButlerSession::getStatus, "active")
+                .orderByDesc(ButlerSession::getUpdatedAt));
+    }
+
+    /**
+     * 分页获取用户的会话列表
+     */
+    public Page<ButlerSession> getSessionsByUser(Long userId, Integer page, Integer size, String status) {
+        Page<ButlerSession> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<ButlerSession> wrapper = new LambdaQueryWrapper<ButlerSession>()
+                .eq(ButlerSession::getUserId, userId)
+                .orderByDesc(ButlerSession::getUpdatedAt);
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(ButlerSession::getStatus, status);
+        }
+        return butlerSessionMapper.selectPage(pageParam, wrapper);
+    }
+
+    /**
+     * 根据ID获取会话(校验归属)
+     */
+    public ButlerSession getSession(Long sessionId, Long userId) {
+        ButlerSession session = butlerSessionMapper.selectById(sessionId);
+        if (session != null && session.getUserId().equals(userId)) {
+            return session;
+        }
+        return null;
+    }
+
+    /**
+     * 删除会话
+     */
+    public boolean deleteSession(Long sessionId, Long userId) {
+        ButlerSession session = butlerSessionMapper.selectById(sessionId);
+        if (session != null && session.getUserId().equals(userId)) {
+            butlerSessionMapper.deleteById(sessionId);
+            return true;
+        }
+        return false;
+    }
+}

+ 15 - 0
cfc-backend/src/main/resources/schema.sql

@@ -2297,3 +2297,18 @@ CREATE TABLE IF NOT EXISTS product_purchase_fields (
     INDEX idx_product_id (product_id),
     INDEX idx_product_id (product_id),
     UNIQUE KEY uk_product_field (product_id, field_key)
     UNIQUE KEY uk_product_field (product_id, field_key)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品购买信息字段配置';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品购买信息字段配置';
+
+-- AI管家会话表
+CREATE TABLE IF NOT EXISTS butler_sessions (
+    id BIGINT AUTO_INCREMENT PRIMARY KEY,
+    user_id BIGINT NOT NULL COMMENT '用户ID',
+    title VARCHAR(200) DEFAULT '新会话' COMMENT '会话标题',
+    conversation_id VARCHAR(100) COMMENT 'Dify对话ID',
+    context TEXT COMMENT '上下文信息(JSON)',
+    status VARCHAR(20) DEFAULT 'active' COMMENT '状态: active(进行中)/archived(已归档)',
+    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX idx_user_id (user_id),
+    INDEX idx_status (status),
+    INDEX idx_user_status (user_id, status)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='AI管家会话表';