diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/SysOaAiController.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/SysOaAiController.java new file mode 100644 index 0000000..2551c80 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/controller/SysOaAiController.java @@ -0,0 +1,247 @@ +package com.ruoyi.oa.controller; + +import java.util.*; + +import com.ruoyi.common.helper.LoginHelper; +import lombok.RequiredArgsConstructor; +import javax.servlet.http.HttpServletResponse; +import javax.validation.constraints.*; + +import org.springframework.web.bind.annotation.*; +import org.springframework.validation.annotation.Validated; +import com.ruoyi.common.annotation.RepeatSubmit; +import com.ruoyi.common.annotation.Log; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.PageQuery; +import com.ruoyi.common.core.domain.R; +import com.ruoyi.common.core.validate.AddGroup; +import com.ruoyi.common.core.validate.EditGroup; +import com.ruoyi.common.enums.BusinessType; +import com.ruoyi.common.utils.poi.ExcelUtil; +import com.ruoyi.oa.domain.vo.SysOaAiConversationVo; +import com.ruoyi.oa.domain.vo.SysOaAiMessageVo; +import com.ruoyi.oa.domain.bo.SysOaAiConversationBo; +import com.ruoyi.oa.domain.bo.SysOaAiMessageBo; +import com.ruoyi.oa.service.ISysOaAiConversationService; +import com.ruoyi.oa.service.ISysOaAiMessageService; +import com.ruoyi.oa.service.ISysOaAiConfigService; +import com.ruoyi.oa.utils.AiServiceUtil; +import com.ruoyi.common.core.page.TableDataInfo; + +/** + * AI对话管理 + * + * @author ruoyi + * @date 2024-12-19 + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/oa/ai") +public class SysOaAiController extends BaseController { + + private final ISysOaAiConversationService conversationService; + private final ISysOaAiMessageService messageService; + private final ISysOaAiConfigService configService; + private final AiServiceUtil aiServiceUtil; + + /** + * 查询AI对话历史列表 + */ + @GetMapping("/conversation/list") + public TableDataInfo list(SysOaAiConversationBo bo, PageQuery pageQuery) { + bo.setUserId(LoginHelper.getUserId()); + return conversationService.queryPageList(bo, pageQuery); + } + + /** + * 查询我的AI对话历史列表 + */ + @GetMapping("/conversation/my-list") + public R> myConversations() { + List list = conversationService.queryByUserId(LoginHelper.getUserId()); + return R.ok(list); + } + + /** + * 获取AI对话历史详细信息 + * + * @param conversationId 主键 + */ + @GetMapping("/conversation/{conversationId}") + public R getConversationInfo(@NotNull(message = "主键不能为空") @PathVariable Long conversationId) { + return R.ok(conversationService.queryById(conversationId)); + } + + /** + * 新增AI对话历史 + */ + @Log(title = "AI对话历史", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping("/conversation") + public R addConversation(@Validated(AddGroup.class) @RequestBody SysOaAiConversationBo bo) { + bo.setUserId(LoginHelper.getUserId()); + return toAjax(conversationService.insertByBo(bo)); + } + + /** + * 修改AI对话历史 + */ + @Log(title = "AI对话历史", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping("/conversation") + public R editConversation(@Validated(EditGroup.class) @RequestBody SysOaAiConversationBo bo) { + return toAjax(conversationService.updateByBo(bo)); + } + + /** + * 删除AI对话历史 + * + * @param conversationIds 主键串 + */ + @Log(title = "AI对话历史", businessType = BusinessType.DELETE) + @DeleteMapping("/conversation/{conversationIds}") + public R removeConversation(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] conversationIds) { + return toAjax(conversationService.deleteWithValidByIds(Arrays.asList(conversationIds), true)); + } + + /** + * 查询AI对话详情列表 + */ + @GetMapping("/message/list") + public TableDataInfo messageList(SysOaAiMessageBo bo, PageQuery pageQuery) { + return messageService.queryPageList(bo, pageQuery); + } + + /** + * 根据对话ID查询消息列表 + */ + @GetMapping("/message/conversation/{conversationId}") + public R> getMessagesByConversation(@PathVariable Long conversationId) { + List messages = messageService.queryByConversationId(conversationId); + return R.ok(messages); + } + + /** + * 查询对话详情(包含所有消息) + */ + @GetMapping("/conversation/{conversationId}/messages") + public R> getConversationMessages(@PathVariable Long conversationId) { + List messages = messageService.queryByConversationId(conversationId); + return R.ok(messages); + } + + /** + * 获取AI对话详情详细信息 + * + * @param messageId 主键 + */ + @GetMapping("/message/{messageId}") + public R getMessageInfo(@NotNull(message = "主键不能为空") @PathVariable Long messageId) { + return R.ok(messageService.queryById(messageId)); + } + + /** + * 新增AI对话详情 + */ + @Log(title = "AI对话详情", businessType = BusinessType.INSERT) + @RepeatSubmit() + @PostMapping("/message") + public R addMessage(@Validated(AddGroup.class) @RequestBody SysOaAiMessageBo bo) { + return toAjax(messageService.insertByBo(bo)); + } + + /** + * 修改AI对话详情 + */ + @Log(title = "AI对话详情", businessType = BusinessType.UPDATE) + @RepeatSubmit() + @PutMapping("/message") + public R editMessage(@Validated(EditGroup.class) @RequestBody SysOaAiMessageBo bo) { + return toAjax(messageService.updateByBo(bo)); + } + + /** + * 删除AI对话详情 + * + * @param messageIds 主键串 + */ + @Log(title = "AI对话详情", businessType = BusinessType.DELETE) + @DeleteMapping("/message/{messageIds}") + public R removeMessage(@NotEmpty(message = "主键不能为空") + @PathVariable Long[] messageIds) { + return toAjax(messageService.deleteWithValidByIds(Arrays.asList(messageIds), true)); + } + + /** + * 创建新对话 + */ + @PostMapping("/conversation/create") + public R> createConversation(@RequestBody SysOaAiConversationBo bo) { + bo.setUserId(LoginHelper.getUserId()); + boolean success = conversationService.insertByBo(bo); + + if (success) { + // 获取刚创建的对话ID + Map result = new HashMap<>(); + result.put("conversationId", bo.getConversationId()); + result.put("conversationTitle", bo.getConversationTitle()); + result.put("modelName", bo.getModelName()); + + return R.ok(result); + } else { + return R.fail("创建对话失败"); + } + } + + /** + * 结束对话 + */ + @PostMapping("/conversation/{conversationId}/end") + public R endConversation(@PathVariable Long conversationId) { + return toAjax(conversationService.endConversation(conversationId)); + } + + /** + * 发送消息给AI + */ + @PostMapping("/chat") + public R> chat(@RequestParam Long conversationId, @RequestParam String message) { + try { + // 1. 保存用户消息 + messageService.addUserMessage(conversationId, message); + + // 2. 获取对话历史 + List conversationHistory = messageService.queryByConversationId(conversationId); + + // 3. 调用AI服务获取回复(传递对话历史) + String aiResponse = callAiServiceWithHistory(message, conversationHistory); + + // 4. 保存AI回复 + messageService.addAiMessage(conversationId, aiResponse, 0, java.math.BigDecimal.ZERO); + + Map result = new HashMap<>(); + result.put("conversationId", conversationId); + result.put("response", aiResponse); + + return R.ok(result); + } catch (Exception e) { + return R.fail("AI服务调用失败: " + e.getMessage()); + } + } + + /** + * 调用AI服务(带对话历史) + */ + private String callAiServiceWithHistory(String message, List conversationHistory) { + return aiServiceUtil.callDeepSeekWithHistory(message, conversationHistory); + } + + /** + * 调用AI服务(单次对话) + */ + private String callAiService(String message) { + return aiServiceUtil.callDeepSeek(message); + } +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/SysOaAiConfig.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/SysOaAiConfig.java new file mode 100644 index 0000000..cf9d80c --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/SysOaAiConfig.java @@ -0,0 +1,87 @@ +package com.ruoyi.oa.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.io.Serializable; +import java.util.Date; +import java.math.BigDecimal; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * AI配置对象 sys_oa_ai_config + * + * @author ruoyi + * @date 2024-12-19 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_oa_ai_config") +public class SysOaAiConfig extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 配置ID + */ + @TableId(value = "config_id") + private Long configId; + + /** + * 配置名称 + */ + private String configName; + + /** + * API密钥 + */ + private String apiKey; + + /** + * 基础URL + */ + private String baseUrl; + + /** + * 模型名称 + */ + private String modelName; + + /** + * 最大重试次数 + */ + private Integer maxRetries; + + /** + * 温度参数 + */ + private BigDecimal temperature; + + /** + * 最大token数 + */ + private Integer maxTokens; + + /** + * 状态(0:禁用 1:启用) + */ + private Integer status; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/SysOaAiConversation.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/SysOaAiConversation.java new file mode 100644 index 0000000..8be2f29 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/SysOaAiConversation.java @@ -0,0 +1,77 @@ +package com.ruoyi.oa.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.io.Serializable; +import java.util.Date; +import java.math.BigDecimal; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * AI对话历史对象 sys_oa_ai_conversation + * + * @author ruoyi + * @date 2024-12-19 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_oa_ai_conversation") +public class SysOaAiConversation extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 对话ID + */ + @TableId(value = "conversation_id") + private Long conversationId; + + /** + * 对话标题 + */ + private String conversationTitle; + + /** + * 用户ID + */ + private Long userId; + + /** + * AI模型名称 + */ + private String modelName; + + /** + * 对话状态(0:已结束 1:进行中) + */ + private Integer status; + + /** + * 总消耗token数 + */ + private Integer totalTokens; + + /** + * 总费用 + */ + private BigDecimal totalCost; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + + /** + * 更新时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date updateTime; + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/SysOaAiMessage.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/SysOaAiMessage.java new file mode 100644 index 0000000..1502cbe --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/SysOaAiMessage.java @@ -0,0 +1,70 @@ +package com.ruoyi.oa.domain; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import lombok.EqualsAndHashCode; +import java.io.Serializable; +import java.util.Date; +import java.math.BigDecimal; + +import com.fasterxml.jackson.annotation.JsonFormat; +import com.ruoyi.common.core.domain.BaseEntity; +import org.springframework.format.annotation.DateTimeFormat; + +/** + * AI对话详情对象 sys_oa_ai_message + * + * @author ruoyi + * @date 2024-12-19 + */ +@Data +@EqualsAndHashCode(callSuper = true) +@TableName("sys_oa_ai_message") +public class SysOaAiMessage extends BaseEntity { + + private static final long serialVersionUID = 1L; + + /** + * 消息ID + */ + @TableId(value = "message_id") + private Long messageId; + + /** + * 对话ID + */ + private Long conversationId; + + /** + * 角色(user/assistant) + */ + private String role; + + /** + * 消息内容 + */ + private String content; + + /** + * 消耗token数 + */ + private Integer tokens; + + /** + * 费用 + */ + private BigDecimal cost; + + /** + * 消息顺序 + */ + private Integer messageOrder; + + /** + * 创建时间 + */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private Date createTime; + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaAiConfigBo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaAiConfigBo.java new file mode 100644 index 0000000..f1911dd --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaAiConfigBo.java @@ -0,0 +1,72 @@ +package com.ruoyi.oa.domain.bo; + +import com.ruoyi.common.core.validate.AddGroup; +import com.ruoyi.common.core.validate.EditGroup; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import javax.validation.constraints.*; + +import java.util.Date; +import java.math.BigDecimal; + +/** + * AI配置业务对象 sys_oa_ai_config + * + * @author ruoyi + * @date 2024-12-19 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SysOaAiConfigBo extends BaseEntity { + + /** + * 配置ID + */ + @NotNull(message = "配置ID不能为空", groups = { EditGroup.class }) + private Long configId; + + /** + * 配置名称 + */ + @NotBlank(message = "配置名称不能为空", groups = { AddGroup.class, EditGroup.class }) + private String configName; + + /** + * API密钥 + */ + @NotBlank(message = "API密钥不能为空", groups = { AddGroup.class, EditGroup.class }) + private String apiKey; + + /** + * 基础URL + */ + @NotBlank(message = "基础URL不能为空", groups = { AddGroup.class, EditGroup.class }) + private String baseUrl; + + /** + * 模型名称 + */ + private String modelName; + + /** + * 最大重试次数 + */ + private Integer maxRetries; + + /** + * 温度参数 + */ + private BigDecimal temperature; + + /** + * 最大token数 + */ + private Integer maxTokens; + + /** + * 状态(0:禁用 1:启用) + */ + private Integer status; + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaAiConversationBo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaAiConversationBo.java new file mode 100644 index 0000000..a032edf --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaAiConversationBo.java @@ -0,0 +1,61 @@ +package com.ruoyi.oa.domain.bo; + +import com.ruoyi.common.core.validate.AddGroup; +import com.ruoyi.common.core.validate.EditGroup; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import javax.validation.constraints.*; + +import java.util.Date; +import java.math.BigDecimal; + +/** + * AI对话历史业务对象 sys_oa_ai_conversation + * + * @author ruoyi + * @date 2024-12-19 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SysOaAiConversationBo extends BaseEntity { + + /** + * 对话ID + */ + @NotNull(message = "对话ID不能为空", groups = { EditGroup.class }) + private Long conversationId; + + /** + * 对话标题 + */ + @NotBlank(message = "对话标题不能为空", groups = { AddGroup.class, EditGroup.class }) + private String conversationTitle; + + /** + * 用户ID + */ + @NotNull(message = "用户ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long userId; + + /** + * AI模型名称 + */ + private String modelName; + + /** + * 对话状态(0:已结束 1:进行中) + */ + private Integer status; + + /** + * 总消耗token数 + */ + private Integer totalTokens; + + /** + * 总费用 + */ + private BigDecimal totalCost; + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaAiMessageBo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaAiMessageBo.java new file mode 100644 index 0000000..2f63175 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/bo/SysOaAiMessageBo.java @@ -0,0 +1,62 @@ +package com.ruoyi.oa.domain.bo; + +import com.ruoyi.common.core.validate.AddGroup; +import com.ruoyi.common.core.validate.EditGroup; +import com.ruoyi.common.core.domain.BaseEntity; +import lombok.Data; +import lombok.EqualsAndHashCode; +import javax.validation.constraints.*; + +import java.util.Date; +import java.math.BigDecimal; + +/** + * AI对话详情业务对象 sys_oa_ai_message + * + * @author ruoyi + * @date 2024-12-19 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SysOaAiMessageBo extends BaseEntity { + + /** + * 消息ID + */ + @NotNull(message = "消息ID不能为空", groups = { EditGroup.class }) + private Long messageId; + + /** + * 对话ID + */ + @NotNull(message = "对话ID不能为空", groups = { AddGroup.class, EditGroup.class }) + private Long conversationId; + + /** + * 角色(user/assistant) + */ + @NotBlank(message = "角色不能为空", groups = { AddGroup.class, EditGroup.class }) + private String role; + + /** + * 消息内容 + */ + @NotBlank(message = "消息内容不能为空", groups = { AddGroup.class, EditGroup.class }) + private String content; + + /** + * 消耗token数 + */ + private Integer tokens; + + /** + * 费用 + */ + private BigDecimal cost; + + /** + * 消息顺序 + */ + private Integer messageOrder; + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaAiConfigVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaAiConfigVo.java new file mode 100644 index 0000000..0a224fc --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaAiConfigVo.java @@ -0,0 +1,110 @@ +package com.ruoyi.oa.domain.vo; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import com.ruoyi.common.annotation.ExcelDictFormat; +import com.ruoyi.common.convert.ExcelDictConvert; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; +import java.math.BigDecimal; + +/** + * AI配置视图对象 sys_oa_ai_config + * + * @author ruoyi + * @date 2024-12-19 + */ +@Data +@ExcelIgnoreUnannotated +public class SysOaAiConfigVo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 配置ID + */ + @ExcelProperty(value = "配置ID") + private Long configId; + + /** + * 配置名称 + */ + @ExcelProperty(value = "配置名称") + private String configName; + + /** + * API密钥 + */ + @ExcelProperty(value = "API密钥") + private String apiKey; + + /** + * 基础URL + */ + @ExcelProperty(value = "基础URL") + private String baseUrl; + + /** + * 模型名称 + */ + @ExcelProperty(value = "模型名称") + private String modelName; + + /** + * 最大重试次数 + */ + @ExcelProperty(value = "最大重试次数") + private Integer maxRetries; + + /** + * 温度参数 + */ + @ExcelProperty(value = "温度参数") + private BigDecimal temperature; + + /** + * 最大token数 + */ + @ExcelProperty(value = "最大token数") + private Integer maxTokens; + + /** + * 状态(0:禁用 1:启用) + */ + @ExcelProperty(value = "状态", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "ai_config_status") + private Integer status; + + /** + * 创建时间 + */ + @ExcelProperty(value = "创建时间") + private Date createTime; + + /** + * 更新时间 + */ + @ExcelProperty(value = "更新时间") + private Date updateTime; + + /** + * 创建者 + */ + @ExcelProperty(value = "创建者") + private String createBy; + + /** + * 更新者 + */ + @ExcelProperty(value = "更新者") + private String updateBy; + + /** + * 备注 + */ + @ExcelProperty(value = "备注") + private String remark; + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaAiConversationVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaAiConversationVo.java new file mode 100644 index 0000000..6c4a5d8 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaAiConversationVo.java @@ -0,0 +1,98 @@ +package com.ruoyi.oa.domain.vo; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import com.ruoyi.common.annotation.ExcelDictFormat; +import com.ruoyi.common.convert.ExcelDictConvert; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; +import java.math.BigDecimal; + +/** + * AI对话历史视图对象 sys_oa_ai_conversation + * + * @author ruoyi + * @date 2024-12-19 + */ +@Data +@ExcelIgnoreUnannotated +public class SysOaAiConversationVo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 对话ID + */ + @ExcelProperty(value = "对话ID") + private Long conversationId; + + /** + * 对话标题 + */ + @ExcelProperty(value = "对话标题") + private String conversationTitle; + + /** + * 用户ID + */ + @ExcelProperty(value = "用户ID") + private Long userId; + + /** + * AI模型名称 + */ + @ExcelProperty(value = "AI模型名称") + private String modelName; + + /** + * 对话状态(0:已结束 1:进行中) + */ + @ExcelProperty(value = "对话状态", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "ai_conversation_status") + private Integer status; + + /** + * 总消耗token数 + */ + @ExcelProperty(value = "总消耗token数") + private Integer totalTokens; + + /** + * 总费用 + */ + @ExcelProperty(value = "总费用") + private BigDecimal totalCost; + + /** + * 创建时间 + */ + @ExcelProperty(value = "创建时间") + private Date createTime; + + /** + * 更新时间 + */ + @ExcelProperty(value = "更新时间") + private Date updateTime; + + /** + * 创建者 + */ + @ExcelProperty(value = "创建者") + private String createBy; + + /** + * 更新者 + */ + @ExcelProperty(value = "更新者") + private String updateBy; + + /** + * 备注 + */ + @ExcelProperty(value = "备注") + private String remark; + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaAiMessageVo.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaAiMessageVo.java new file mode 100644 index 0000000..b567bfb --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/domain/vo/SysOaAiMessageVo.java @@ -0,0 +1,80 @@ +package com.ruoyi.oa.domain.vo; + +import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; +import com.alibaba.excel.annotation.ExcelProperty; +import com.ruoyi.common.annotation.ExcelDictFormat; +import com.ruoyi.common.convert.ExcelDictConvert; +import lombok.Data; + +import java.io.Serializable; +import java.util.Date; +import java.math.BigDecimal; + +/** + * AI对话详情视图对象 sys_oa_ai_message + * + * @author ruoyi + * @date 2024-12-19 + */ +@Data +@ExcelIgnoreUnannotated +public class SysOaAiMessageVo implements Serializable { + + private static final long serialVersionUID = 1L; + + /** + * 消息ID + */ + @ExcelProperty(value = "消息ID") + private Long messageId; + + /** + * 对话ID + */ + @ExcelProperty(value = "对话ID") + private Long conversationId; + + /** + * 角色(user/assistant) + */ + @ExcelProperty(value = "角色", converter = ExcelDictConvert.class) + @ExcelDictFormat(dictType = "ai_message_role") + private String role; + + /** + * 消息内容 + */ + @ExcelProperty(value = "消息内容") + private String content; + + /** + * 消耗token数 + */ + @ExcelProperty(value = "消耗token数") + private Integer tokens; + + /** + * 费用 + */ + @ExcelProperty(value = "费用") + private BigDecimal cost; + + /** + * 消息顺序 + */ + @ExcelProperty(value = "消息顺序") + private Integer messageOrder; + + /** + * 创建时间 + */ + @ExcelProperty(value = "创建时间") + private Date createTime; + + /** + * 创建者 + */ + @ExcelProperty(value = "创建者") + private String createBy; + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaAiConfigMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaAiConfigMapper.java new file mode 100644 index 0000000..c586e08 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaAiConfigMapper.java @@ -0,0 +1,15 @@ +package com.ruoyi.oa.mapper; + +import com.ruoyi.common.core.mapper.BaseMapperPlus; +import com.ruoyi.oa.domain.SysOaAiConfig; +import com.ruoyi.oa.domain.vo.SysOaAiConfigVo; + +/** + * AI配置Mapper接口 + * + * @author ruoyi + * @date 2024-12-19 + */ +public interface SysOaAiConfigMapper extends BaseMapperPlus { + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaAiConversationMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaAiConversationMapper.java new file mode 100644 index 0000000..b8e9d39 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaAiConversationMapper.java @@ -0,0 +1,15 @@ +package com.ruoyi.oa.mapper; + +import com.ruoyi.common.core.mapper.BaseMapperPlus; +import com.ruoyi.oa.domain.SysOaAiConversation; +import com.ruoyi.oa.domain.vo.SysOaAiConversationVo; + +/** + * AI对话历史Mapper接口 + * + * @author ruoyi + * @date 2024-12-19 + */ +public interface SysOaAiConversationMapper extends BaseMapperPlus { + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaAiMessageMapper.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaAiMessageMapper.java new file mode 100644 index 0000000..94c34a8 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/mapper/SysOaAiMessageMapper.java @@ -0,0 +1,26 @@ +package com.ruoyi.oa.mapper; + +import com.ruoyi.common.core.mapper.BaseMapperPlus; +import com.ruoyi.oa.domain.SysOaAiMessage; +import com.ruoyi.oa.domain.vo.SysOaAiMessageVo; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +/** + * AI对话详情Mapper接口 + * + * @author ruoyi + * @date 2024-12-19 + */ +public interface SysOaAiMessageMapper extends BaseMapperPlus { + + /** + * 根据对话ID查询消息列表 + * + * @param conversationId 对话ID + * @return 消息列表 + */ + List selectMessagesByConversationId(@Param("conversationId") Long conversationId); + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/ISysOaAiConfigService.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/ISysOaAiConfigService.java new file mode 100644 index 0000000..0f6880d --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/ISysOaAiConfigService.java @@ -0,0 +1,54 @@ +package com.ruoyi.oa.service; + +import com.ruoyi.oa.domain.bo.SysOaAiConfigBo; +import com.ruoyi.oa.domain.vo.SysOaAiConfigVo; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.domain.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * AI配置Service接口 + * + * @author ruoyi + * @date 2024-12-19 + */ +public interface ISysOaAiConfigService { + + /** + * 查询AI配置 + */ + SysOaAiConfigVo queryById(Long configId); + + /** + * 查询AI配置列表 + */ + TableDataInfo queryPageList(SysOaAiConfigBo bo, PageQuery pageQuery); + + /** + * 查询AI配置列表 + */ + List queryList(SysOaAiConfigBo bo); + + /** + * 新增AI配置 + */ + Boolean insertByBo(SysOaAiConfigBo bo); + + /** + * 修改AI配置 + */ + Boolean updateByBo(SysOaAiConfigBo bo); + + /** + * 校验并批量删除AI配置信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 获取启用的AI配置 + */ + SysOaAiConfigVo getActiveConfig(); + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/ISysOaAiConversationService.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/ISysOaAiConversationService.java new file mode 100644 index 0000000..5b97624 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/ISysOaAiConversationService.java @@ -0,0 +1,64 @@ +package com.ruoyi.oa.service; + +import com.ruoyi.oa.domain.bo.SysOaAiConversationBo; +import com.ruoyi.oa.domain.vo.SysOaAiConversationVo; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.domain.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * AI对话历史Service接口 + * + * @author ruoyi + * @date 2024-12-19 + */ +public interface ISysOaAiConversationService { + + /** + * 查询AI对话历史 + */ + SysOaAiConversationVo queryById(Long conversationId); + + /** + * 查询AI对话历史列表 + */ + TableDataInfo queryPageList(SysOaAiConversationBo bo, PageQuery pageQuery); + + /** + * 查询AI对话历史列表 + */ + List queryList(SysOaAiConversationBo bo); + + /** + * 新增AI对话历史 + */ + Boolean insertByBo(SysOaAiConversationBo bo); + + /** + * 修改AI对话历史 + */ + Boolean updateByBo(SysOaAiConversationBo bo); + + /** + * 校验并批量删除AI对话历史信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 根据用户ID查询对话列表 + */ + List queryByUserId(Long userId); + + /** + * 创建新对话 + */ + Long createConversation(Long userId, String title); + + /** + * 结束对话 + */ + Boolean endConversation(Long conversationId); + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/ISysOaAiMessageService.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/ISysOaAiMessageService.java new file mode 100644 index 0000000..279f16f --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/ISysOaAiMessageService.java @@ -0,0 +1,64 @@ +package com.ruoyi.oa.service; + +import com.ruoyi.oa.domain.bo.SysOaAiMessageBo; +import com.ruoyi.oa.domain.vo.SysOaAiMessageVo; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.domain.PageQuery; + +import java.util.Collection; +import java.util.List; + +/** + * AI对话详情Service接口 + * + * @author ruoyi + * @date 2024-12-19 + */ +public interface ISysOaAiMessageService { + + /** + * 查询AI对话详情 + */ + SysOaAiMessageVo queryById(Long messageId); + + /** + * 查询AI对话详情列表 + */ + TableDataInfo queryPageList(SysOaAiMessageBo bo, PageQuery pageQuery); + + /** + * 查询AI对话详情列表 + */ + List queryList(SysOaAiMessageBo bo); + + /** + * 新增AI对话详情 + */ + Boolean insertByBo(SysOaAiMessageBo bo); + + /** + * 修改AI对话详情 + */ + Boolean updateByBo(SysOaAiMessageBo bo); + + /** + * 校验并批量删除AI对话详情信息 + */ + Boolean deleteWithValidByIds(Collection ids, Boolean isValid); + + /** + * 根据对话ID查询消息列表 + */ + List queryByConversationId(Long conversationId); + + /** + * 添加用户消息 + */ + Boolean addUserMessage(Long conversationId, String content); + + /** + * 添加AI回复消息 + */ + Boolean addAiMessage(Long conversationId, String content, Integer tokens, java.math.BigDecimal cost); + +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaAiConfigServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaAiConfigServiceImpl.java new file mode 100644 index 0000000..b9d4501 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaAiConfigServiceImpl.java @@ -0,0 +1,127 @@ +package com.ruoyi.oa.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.domain.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import com.ruoyi.oa.domain.bo.SysOaAiConfigBo; +import com.ruoyi.oa.domain.vo.SysOaAiConfigVo; +import com.ruoyi.oa.domain.SysOaAiConfig; +import com.ruoyi.oa.mapper.SysOaAiConfigMapper; +import com.ruoyi.oa.service.ISysOaAiConfigService; + +import java.util.List; +import java.util.Map; +import java.util.Collection; + +/** + * AI配置Service业务层处理 + * + * @author ruoyi + * @date 2024-12-19 + */ +@RequiredArgsConstructor +@Service +public class SysOaAiConfigServiceImpl implements ISysOaAiConfigService { + + private final SysOaAiConfigMapper baseMapper; + + /** + * 查询AI配置 + */ + @Override + public SysOaAiConfigVo queryById(Long configId) { + return baseMapper.selectVoById(configId); + } + + /** + * 查询AI配置列表 + */ + @Override + public TableDataInfo queryPageList(SysOaAiConfigBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询AI配置列表 + */ + @Override + public List queryList(SysOaAiConfigBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(SysOaAiConfigBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.like(StringUtils.isNotBlank(bo.getConfigName()), SysOaAiConfig::getConfigName, bo.getConfigName()); + lqw.eq(StringUtils.isNotBlank(bo.getApiKey()), SysOaAiConfig::getApiKey, bo.getApiKey()); + lqw.eq(StringUtils.isNotBlank(bo.getBaseUrl()), SysOaAiConfig::getBaseUrl, bo.getBaseUrl()); + lqw.eq(StringUtils.isNotBlank(bo.getModelName()), SysOaAiConfig::getModelName, bo.getModelName()); + lqw.eq(bo.getMaxRetries() != null, SysOaAiConfig::getMaxRetries, bo.getMaxRetries()); + lqw.eq(bo.getTemperature() != null, SysOaAiConfig::getTemperature, bo.getTemperature()); + lqw.eq(bo.getMaxTokens() != null, SysOaAiConfig::getMaxTokens, bo.getMaxTokens()); + lqw.eq(bo.getStatus() != null, SysOaAiConfig::getStatus, bo.getStatus()); + return lqw; + } + + /** + * 新增AI配置 + */ + @Override + public Boolean insertByBo(SysOaAiConfigBo bo) { + SysOaAiConfig add = BeanUtil.toBean(bo, SysOaAiConfig.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setConfigId(add.getConfigId()); + } + return flag; + } + + /** + * 修改AI配置 + */ + @Override + public Boolean updateByBo(SysOaAiConfigBo bo) { + SysOaAiConfig update = BeanUtil.toBean(bo, SysOaAiConfig.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(SysOaAiConfig entity) { + // TODO 做一些数据校验,如唯一约束 + } + + /** + * 批量删除AI配置 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 获取启用的AI配置 + */ + @Override + public SysOaAiConfigVo getActiveConfig() { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(SysOaAiConfig::getStatus, 1); // 启用状态 + lqw.last("LIMIT 1"); + return baseMapper.selectVoOne(lqw); + } +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaAiConversationServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaAiConversationServiceImpl.java new file mode 100644 index 0000000..831baca --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaAiConversationServiceImpl.java @@ -0,0 +1,152 @@ +package com.ruoyi.oa.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.domain.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import com.ruoyi.oa.domain.bo.SysOaAiConversationBo; +import com.ruoyi.oa.domain.vo.SysOaAiConversationVo; +import com.ruoyi.oa.domain.SysOaAiConversation; +import com.ruoyi.oa.mapper.SysOaAiConversationMapper; +import com.ruoyi.oa.service.ISysOaAiConversationService; + +import java.util.List; +import java.util.Map; +import java.util.Collection; + +/** + * AI对话历史Service业务层处理 + * + * @author ruoyi + * @date 2024-12-19 + */ +@RequiredArgsConstructor +@Service +public class SysOaAiConversationServiceImpl implements ISysOaAiConversationService { + + private final SysOaAiConversationMapper baseMapper; + + /** + * 查询AI对话历史 + */ + @Override + public SysOaAiConversationVo queryById(Long conversationId) { + return baseMapper.selectVoById(conversationId); + } + + /** + * 查询AI对话历史列表 + */ + @Override + public TableDataInfo queryPageList(SysOaAiConversationBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询AI对话历史列表 + */ + @Override + public List queryList(SysOaAiConversationBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(SysOaAiConversationBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.like(StringUtils.isNotBlank(bo.getConversationTitle()), SysOaAiConversation::getConversationTitle, bo.getConversationTitle()); + lqw.eq(bo.getUserId() != null, SysOaAiConversation::getUserId, bo.getUserId()); + lqw.eq(StringUtils.isNotBlank(bo.getModelName()), SysOaAiConversation::getModelName, bo.getModelName()); + lqw.eq(bo.getStatus() != null, SysOaAiConversation::getStatus, bo.getStatus()); + lqw.eq(bo.getTotalTokens() != null, SysOaAiConversation::getTotalTokens, bo.getTotalTokens()); + lqw.eq(bo.getTotalCost() != null, SysOaAiConversation::getTotalCost, bo.getTotalCost()); + return lqw; + } + + /** + * 新增AI对话历史 + */ + @Override + public Boolean insertByBo(SysOaAiConversationBo bo) { + SysOaAiConversation add = BeanUtil.toBean(bo, SysOaAiConversation.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setConversationId(add.getConversationId()); + } + return flag; + } + + /** + * 修改AI对话历史 + */ + @Override + public Boolean updateByBo(SysOaAiConversationBo bo) { + SysOaAiConversation update = BeanUtil.toBean(bo, SysOaAiConversation.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(SysOaAiConversation entity) { + // TODO 做一些数据校验,如唯一约束 + } + + /** + * 批量删除AI对话历史 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 根据用户ID查询对话列表 + */ + @Override + public List queryByUserId(Long userId) { + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(SysOaAiConversation::getUserId, userId); + lqw.orderByDesc(SysOaAiConversation::getCreateTime); + return baseMapper.selectVoList(lqw); + } + + /** + * 创建新对话 + */ + @Override + public Long createConversation(Long userId, String title) { + SysOaAiConversation conversation = new SysOaAiConversation(); + conversation.setUserId(userId); + conversation.setConversationTitle(title); + conversation.setModelName("deepseek-chat"); + conversation.setStatus(1); // 进行中 + conversation.setTotalTokens(0); + conversation.setTotalCost(java.math.BigDecimal.ZERO); + baseMapper.insert(conversation); + return conversation.getConversationId(); + } + + /** + * 结束对话 + */ + @Override + public Boolean endConversation(Long conversationId) { + SysOaAiConversation conversation = new SysOaAiConversation(); + conversation.setConversationId(conversationId); + conversation.setStatus(0); // 已结束 + return baseMapper.updateById(conversation) > 0; + } +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaAiMessageServiceImpl.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaAiMessageServiceImpl.java new file mode 100644 index 0000000..f84b527 --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/service/impl/SysOaAiMessageServiceImpl.java @@ -0,0 +1,172 @@ +package com.ruoyi.oa.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.core.page.TableDataInfo; +import com.ruoyi.common.core.domain.PageQuery; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import com.ruoyi.oa.domain.bo.SysOaAiMessageBo; +import com.ruoyi.oa.domain.vo.SysOaAiMessageVo; +import com.ruoyi.oa.domain.SysOaAiMessage; +import com.ruoyi.oa.mapper.SysOaAiMessageMapper; +import com.ruoyi.oa.service.ISysOaAiMessageService; + +import java.util.List; +import java.util.Map; +import java.util.Collection; + +/** + * AI对话详情Service业务层处理 + * + * @author ruoyi + * @date 2024-12-19 + */ +@RequiredArgsConstructor +@Service +public class SysOaAiMessageServiceImpl implements ISysOaAiMessageService { + + private final SysOaAiMessageMapper baseMapper; + + /** + * 查询AI对话详情 + */ + @Override + public SysOaAiMessageVo queryById(Long messageId) { + return baseMapper.selectVoById(messageId); + } + + /** + * 查询AI对话详情列表 + */ + @Override + public TableDataInfo queryPageList(SysOaAiMessageBo bo, PageQuery pageQuery) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); + return TableDataInfo.build(result); + } + + /** + * 查询AI对话详情列表 + */ + @Override + public List queryList(SysOaAiMessageBo bo) { + LambdaQueryWrapper lqw = buildQueryWrapper(bo); + return baseMapper.selectVoList(lqw); + } + + private LambdaQueryWrapper buildQueryWrapper(SysOaAiMessageBo bo) { + Map params = bo.getParams(); + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(bo.getConversationId() != null, SysOaAiMessage::getConversationId, bo.getConversationId()); + lqw.eq(StringUtils.isNotBlank(bo.getRole()), SysOaAiMessage::getRole, bo.getRole()); + lqw.like(StringUtils.isNotBlank(bo.getContent()), SysOaAiMessage::getContent, bo.getContent()); + lqw.eq(bo.getTokens() != null, SysOaAiMessage::getTokens, bo.getTokens()); + lqw.eq(bo.getCost() != null, SysOaAiMessage::getCost, bo.getCost()); + lqw.eq(bo.getMessageOrder() != null, SysOaAiMessage::getMessageOrder, bo.getMessageOrder()); + return lqw; + } + + /** + * 新增AI对话详情 + */ + @Override + public Boolean insertByBo(SysOaAiMessageBo bo) { + SysOaAiMessage add = BeanUtil.toBean(bo, SysOaAiMessage.class); + validEntityBeforeSave(add); + boolean flag = baseMapper.insert(add) > 0; + if (flag) { + bo.setMessageId(add.getMessageId()); + } + return flag; + } + + /** + * 修改AI对话详情 + */ + @Override + public Boolean updateByBo(SysOaAiMessageBo bo) { + SysOaAiMessage update = BeanUtil.toBean(bo, SysOaAiMessage.class); + validEntityBeforeSave(update); + return baseMapper.updateById(update) > 0; + } + + /** + * 保存前的数据校验 + */ + private void validEntityBeforeSave(SysOaAiMessage entity) { + // TODO 做一些数据校验,如唯一约束 + } + + /** + * 批量删除AI对话详情 + */ + @Override + public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { + if (isValid) { + // TODO 做一些业务上的校验,判断是否需要校验 + } + return baseMapper.deleteBatchIds(ids) > 0; + } + + /** + * 根据对话ID查询消息列表 + */ + @Override + public List queryByConversationId(Long conversationId) { + return baseMapper.selectMessagesByConversationId(conversationId); + } + + /** + * 添加用户消息 + */ + @Override + public Boolean addUserMessage(Long conversationId, String content) { + SysOaAiMessage message = new SysOaAiMessage(); + message.setConversationId(conversationId); + message.setRole("user"); + message.setContent(content); + message.setTokens(0); + message.setCost(java.math.BigDecimal.ZERO); + + // 获取当前消息顺序 + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(SysOaAiMessage::getConversationId, conversationId); + lqw.orderByDesc(SysOaAiMessage::getMessageOrder); + lqw.last("LIMIT 1"); + SysOaAiMessage lastMessage = baseMapper.selectOne(lqw); + int nextOrder = (lastMessage != null && lastMessage.getMessageOrder() != null) ? + lastMessage.getMessageOrder() + 1 : 1; + message.setMessageOrder(nextOrder); + + return baseMapper.insert(message) > 0; + } + + /** + * 添加AI回复消息 + */ + @Override + public Boolean addAiMessage(Long conversationId, String content, Integer tokens, java.math.BigDecimal cost) { + SysOaAiMessage message = new SysOaAiMessage(); + message.setConversationId(conversationId); + message.setRole("assistant"); + message.setContent(content); + message.setTokens(tokens != null ? tokens : 0); + message.setCost(cost != null ? cost : java.math.BigDecimal.ZERO); + + // 获取当前消息顺序 + LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); + lqw.eq(SysOaAiMessage::getConversationId, conversationId); + lqw.orderByDesc(SysOaAiMessage::getMessageOrder); + lqw.last("LIMIT 1"); + SysOaAiMessage lastMessage = baseMapper.selectOne(lqw); + int nextOrder = (lastMessage != null && lastMessage.getMessageOrder() != null) ? + lastMessage.getMessageOrder() + 1 : 1; + message.setMessageOrder(nextOrder); + + return baseMapper.insert(message) > 0; + } +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/java/com/ruoyi/oa/utils/AiServiceUtil.java b/ruoyi-oa/src/main/java/com/ruoyi/oa/utils/AiServiceUtil.java new file mode 100644 index 0000000..bf3958c --- /dev/null +++ b/ruoyi-oa/src/main/java/com/ruoyi/oa/utils/AiServiceUtil.java @@ -0,0 +1,125 @@ +package com.ruoyi.oa.utils; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.ruoyi.oa.domain.vo.SysOaAiConfigVo; +import com.ruoyi.oa.domain.vo.SysOaAiMessageVo; +import com.ruoyi.oa.service.ISysOaAiConfigService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.*; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestTemplate; + +import java.math.BigDecimal; +import java.util.*; + +/** + * AI服务工具类 + * + * @author ruoyi + * @date 2024-12-19 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class AiServiceUtil { + + private final ISysOaAiConfigService configService; + private final RestTemplate restTemplate; + private final ObjectMapper objectMapper; + + /** + * 调用DeepSeek AI服务(单次对话) + * + * @param message 用户消息 + * @return AI回复 + */ + public String callDeepSeek(String message) { + return callDeepSeekWithHistory(message, null); + } + + /** + * 调用DeepSeek AI服务(连续对话) + * + * @param message 用户消息 + * @param conversationHistory 对话历史 + * @return AI回复 + */ + public String callDeepSeekWithHistory(String message, List conversationHistory) { + try { + // 获取AI配置 + SysOaAiConfigVo config = configService.getActiveConfig(); + if (config == null) { + throw new RuntimeException("未找到可用的AI配置"); + } + + // 构建请求头 + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + config.getApiKey()); + + // 构建请求体 + Map requestBody = new HashMap<>(); + requestBody.put("model", config.getModelName()); + requestBody.put("temperature", config.getTemperature()); + requestBody.put("max_tokens", config.getMaxTokens()); + + List> messages = new ArrayList<>(); + + // 添加对话历史 + if (conversationHistory != null && !conversationHistory.isEmpty()) { + for (SysOaAiMessageVo historyMessage : conversationHistory) { + Map historyMsg = new HashMap<>(); + historyMsg.put("role", historyMessage.getRole()); + historyMsg.put("content", historyMessage.getContent()); + messages.add(historyMsg); + } + } + + // 添加当前用户消息 + Map userMessage = new HashMap<>(); + userMessage.put("role", "user"); + userMessage.put("content", message); + messages.add(userMessage); + + requestBody.put("messages", messages); + + // 发送请求 + HttpEntity> entity = new HttpEntity<>(requestBody, headers); + String url = config.getBaseUrl() + "/chat/completions"; + + log.info("调用DeepSeek API: {}, 消息数量: {}", url, messages.size()); + ResponseEntity response = restTemplate.exchange(url, HttpMethod.POST, entity, String.class); + + if (response.getStatusCode() == HttpStatus.OK) { + JsonNode jsonNode = objectMapper.readTree(response.getBody()); + String aiResponse = jsonNode.path("choices").path(0).path("message").path("content").asText(); + + // 记录token使用情况 + int totalTokens = jsonNode.path("usage").path("total_tokens").asInt(); + log.info("AI回复成功,消耗token: {}", totalTokens); + + return aiResponse; + } else { + log.error("DeepSeek API调用失败: {}", response.getStatusCode()); + throw new RuntimeException("AI服务调用失败"); + } + } catch (Exception e) { + log.error("调用DeepSeek服务异常", e); + throw new RuntimeException("AI服务调用异常: " + e.getMessage()); + } + } + + /** + * 计算费用(示例:每1000个token 0.002美元) + * + * @param tokens token数量 + * @return 费用 + */ + public BigDecimal calculateCost(int tokens) { + // 这里可以根据实际的定价策略计算费用 + BigDecimal costPerToken = new BigDecimal("0.000002"); // 每token 0.000002美元 + return costPerToken.multiply(new BigDecimal(tokens)); + } +} \ No newline at end of file diff --git a/ruoyi-oa/src/main/resources/mapper/oa/SysOaAiMessageMapper.xml b/ruoyi-oa/src/main/resources/mapper/oa/SysOaAiMessageMapper.xml new file mode 100644 index 0000000..531dd16 --- /dev/null +++ b/ruoyi-oa/src/main/resources/mapper/oa/SysOaAiMessageMapper.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + select message_id, conversation_id, role, content, tokens, cost, message_order, create_time, create_by + from sys_oa_ai_message + + + + + \ No newline at end of file