fix: 完成后台基础功能的对接、适配深度思考

This commit is contained in:
乾乾
2025-11-26 08:16:14 +08:00
parent 7f3979e38f
commit d152fea9fc
40 changed files with 789 additions and 94 deletions

View File

@@ -18,4 +18,4 @@ chmod 777 /home/docker/rocketmq/timerwheel/
- **打开目录**: 当前文件夹下输入 cmd 回车
- **执行命令**: docker-compose up -d
- **导入nacos数据库**: [mysql-schema.sql](../mysql-schema.sql)
- **导入nacos命名空间数据**: [nacos_config_export_20251019194635.zip](../nacos/nacos_config_export_20251019194635.zip)
- **导入nacos命名空间数据**: [nacos_config_export_20251126080946.zip](../nacos/nacos_config_export_20251126080946.zip)

View File

@@ -11,7 +11,7 @@
Target Server Version : 80030 (8.0.30)
File Encoding : 65001
Date: 21/11/2025 14:56:07
Date: 26/11/2025 08:05:50
*/
SET NAMES utf8mb4;
@@ -37,7 +37,7 @@ CREATE TABLE `ai_api_key` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 94780244072449 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI API 密钥表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 98364012618753 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI API 密钥表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of ai_api_key
@@ -94,10 +94,11 @@ CREATE TABLE `ai_chat_conversation` (
`model` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '模型标识',
`pinned` bit(1) NOT NULL COMMENT '是否置顶',
`pinned_time` datetime NULL DEFAULT NULL COMMENT '置顶时间',
`token_usage` bigint NOT NULL DEFAULT 0 COMMENT '总使用量',
`system_message` varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '角色设定',
`temperature` double NOT NULL COMMENT '温度参数',
`max_tokens` int NOT NULL COMMENT '单条回复的最大 Token 数量',
`max_contexts` int NOT NULL COMMENT '上下文的最大 Message 数量',
`max_contexts` int NOT NULL DEFAULT 50 COMMENT '上下文的最大 Message 数量',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建人',
`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
`updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '更新人',
@@ -105,7 +106,7 @@ CREATE TABLE `ai_chat_conversation` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NULL DEFAULT NULL COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98061603300353 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天对话表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99658689418753 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天对话表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of ai_chat_conversation
@@ -137,7 +138,7 @@ CREATE TABLE `ai_chat_message` (
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_msg_type`(`msg_type` ASC) USING BTREE COMMENT '消息内容类型索引'
) ENGINE = InnoDB AUTO_INCREMENT = 97988513358850 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天消息表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99659553445378 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天消息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of ai_chat_message
@@ -168,7 +169,7 @@ CREATE TABLE `ai_chat_role` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 93210018275841 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天角色表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 98376230626305 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 聊天角色表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of ai_chat_role
@@ -204,7 +205,7 @@ CREATE TABLE `ai_image` (
`deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '是否删除',
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 97776948470785 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 绘画表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99652444100098 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 绘画表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of ai_image
@@ -335,6 +336,7 @@ CREATE TABLE `ai_model` (
`status` tinyint NOT NULL COMMENT '状态',
`public_status` bit(1) NOT NULL DEFAULT b'1' COMMENT '公开、私有',
`temperature` double NULL DEFAULT NULL COMMENT '温度参数',
`supports_reasoning` tinyint NOT NULL DEFAULT 0 COMMENT '是否支持上下文',
`max_tokens` int NULL DEFAULT NULL COMMENT '单条回复的最大 Token 数量',
`max_contexts` int NULL DEFAULT NULL COMMENT '上下文的最大 Message 数量',
`creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '创建者',
@@ -347,7 +349,7 @@ CREATE TABLE `ai_model` (
INDEX `idx_user_id`(`user_id` ASC) USING BTREE,
INDEX `idx_public_status`(`public_status` ASC) USING BTREE,
INDEX `idx_user_public`(`user_id` ASC, `public_status` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 93210576118273 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 模型表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 98365405127681 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 模型表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of ai_model
@@ -370,7 +372,7 @@ CREATE TABLE `ai_model_usage_record` (
`tenant_id` bigint NOT NULL DEFAULT 1 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_user_model`(`user_id` ASC, `model_id` ASC) USING BTREE COMMENT '用户-模型唯一索引'
) ENGINE = InnoDB AUTO_INCREMENT = 98017718297601 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 公开模型使用记录表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99658790082049 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 公开模型使用记录表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of ai_model_usage_record
@@ -436,13 +438,13 @@ CREATE TABLE `ai_platform` (
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '租户编号',
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uk_platform`(`platform` ASC, `deleted` ASC) USING BTREE COMMENT '平台代码唯一索引'
) ENGINE = InnoDB AUTO_INCREMENT = 19 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 平台配置表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 20 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 平台配置表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of ai_platform
-- ----------------------------
INSERT INTO `ai_platform` VALUES (1, 'Moonshot', '月之暗灭', 'Moonshot (KIMI)', 'moonshot-v1-8k, moonshot-v1-32k, moonshot-v1-128k', 'https://platform.moonshot.cn/docs', '请前往 Moonshot 官网查看可用模型列表', 1, 0, '', '2025-11-11 01:19:48', '', '2025-11-11 02:33:25', b'0', 1);
INSERT INTO `ai_platform` VALUES (2, 'DeepSeek', 'DeepSeek', 'DeepSeek', 'deepseek-chat, deepseek-coder, deepseek-reasoner', 'https://api-docs.deepseek.com/zh-cn/guides/reasoning_model', '请前往 DeepSeek 官网查看可用模型列表', 2, 0, '', '2025-11-11 01:19:48', '', '2025-11-11 03:05:33', b'0', 1);
INSERT INTO `ai_platform` VALUES (2, 'DeepSeek', 'DeepSeek', 'DeepSeek', 'deepseek-chat, deepseek-coder, deepseek-reasoner, deepskke, d, deep, deep-s, deepseek-, deepseek-re', 'https://api-docs.deepseek.com/zh-cn/guides/reasoning_model', '请前往 DeepSeek 官网查看可用模型列表', 2, 0, '', '2025-11-11 01:19:48', '', '2025-11-11 03:05:33', b'0', 1);
INSERT INTO `ai_platform` VALUES (3, 'YiYan', '文心一言', 'Baidu (文心一言)', 'ernie-bot-4, ernie-bot-turbo, ernie-bot', 'https://cloud.baidu.com/doc/WENXINWORKSHOP/index.html', '请前往百度智能云官网查看可用模型列表', 13, 0, '', '2025-11-11 01:19:48', '', '2025-11-12 11:15:21', b'0', 1);
INSERT INTO `ai_platform` VALUES (4, 'TongYi', '通义千问', 'Alibaba (通义千问)', 'qwen-turbo, qwen-plus, qwen-max, qwen-long', 'https://help.aliyun.com/zh/dashscope/developer-reference/model-square', '请前往阿里云官网查看可用模型列表', 4, 0, '', '2025-11-11 01:19:48', '', '2025-11-11 02:33:25', b'0', 1);
INSERT INTO `ai_platform` VALUES (5, 'HunYuan', '混元', 'Tencent (混元)', 'hunyuan-lite, hunyuan-standard, hunyuan-pro', 'https://cloud.tencent.com/document/product/1729', '请前往腾讯云官网查看可用模型列表', 5, 0, '', '2025-11-11 01:19:48', '', '2025-11-11 02:33:25', b'0', 1);
@@ -459,6 +461,7 @@ INSERT INTO `ai_platform` VALUES (15, 'Anthropic', 'Anthropic', 'Anthropic', 'cl
INSERT INTO `ai_platform` VALUES (16, 'Google', 'Google', 'Google', 'gemini-pro, gemini-pro-vision, gemini-1.5-pro', 'https://ai.google.dev/models', '请前往 Google AI 官网查看可用模型列表', 16, 0, '', '2025-11-11 01:19:48', '', '2025-11-11 02:33:25', b'0', 1);
INSERT INTO `ai_platform` VALUES (17, 'Ollama', 'Ollama', 'Ollama', 'llama2, mistral, neural-chat, dolphin-mixtral', 'https://ollama.ai/library', '请前往 Ollama 官网查看可用模型列表', 17, 0, '', '2025-11-11 01:19:48', '', '2025-11-11 02:33:25', b'0', 1);
INSERT INTO `ai_platform` VALUES (18, 'OpenRouter', 'OpenRouter', 'OpenRouter', 'openai/gpt-3.5-turbo, openai/gpt-4, anthropic/claude-3-opus', 'https://openrouter.ai/docs', '请前往 OpenRouter 官网查看可用模型列表', 3, 0, '', '2025-11-12 04:16:46', '', '2025-11-12 11:15:13', b'0', 1);
INSERT INTO `ai_platform` VALUES (19, 'Gemini', 'Gemini', 'Google (Gemini)', 'gemini-1.5-pro, gemini-1.5-flash', 'https://developers.google.cn/ai/build', '请前往 Google AI Build 查看可用模型列表', 10, 0, '', '2025-11-21 00:00:00', '', '2025-11-21 00:00:00', b'0', 1);
-- ----------------------------
-- Table structure for ai_tool
@@ -518,7 +521,7 @@ CREATE TABLE `ai_video` (
INDEX `idx_status`(`status` ASC) USING BTREE,
INDEX `idx_task_id`(`task_id` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98017718297602 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 视频生成表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99652406351361 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'AI 视频生成表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of ai_video
@@ -681,7 +684,7 @@ CREATE TABLE `base_employee` (
-- ----------------------------
-- Records of base_employee
-- ----------------------------
INSERT INTO `base_employee` VALUES (1, 0, b'1', 1458051094994223104, 61170828529941, 1451532773234835456, NULL, '2439646234', 0, '', '20', '10', b'1', 1451549146992345088, '2021-11-09 10:54:44', 2, '2024-10-08 15:31:36', 0, 0, 1);
INSERT INTO `base_employee` VALUES (1, 0, b'1', 1458051094994223104, 61170828529941, NULL, NULL, '2439646234', 0, '', '20', '10', b'1', 1451549146992345088, '2021-11-09 10:54:44', 2, '2024-10-08 15:31:36', 0, 0, 1);
-- ----------------------------
-- Table structure for base_employee_org_rel
@@ -1636,7 +1639,7 @@ INSERT INTO `def_interface_property` VALUES (250660012290998281, 250025856074776
DROP TABLE IF EXISTS `def_login_log`;
CREATE TABLE `def_login_log` (
`id` bigint NOT NULL COMMENT '主键',
`tenant_id` bigint NULL DEFAULT NULL COMMENT '所属企业',
`tenant_id` bigint NULL DEFAULT 1 COMMENT '所属企业',
`employee_id` bigint NULL DEFAULT NULL COMMENT '登录员工',
`user_id` bigint NULL DEFAULT NULL COMMENT '登录用户',
`request_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '' COMMENT '登录IP',
@@ -1784,6 +1787,7 @@ INSERT INTO `def_resource` VALUES (5, 1, 'im:moment', '朋友圈管理', '20', 1
INSERT INTO `def_resource` VALUES (6, 1, 'im:contact', '联系人 / 好友管理', '20', 1, '01', '联系人、好友关系、申请记录等', '/im/contact', 'page/ImContact', '', 'AddressBook', b'0', b'0', b'1', 60, '', b'0', b'1', NULL, NULL, NULL, '/1/', 1, '{}', 1, '2021-12-12 12:12:12', 1, '2021-12-12 12:12:12', 0);
INSERT INTO `def_resource` VALUES (7, 1, 'im:config', 'IM 配置', '20', 1, '01', 'im_config 配置、系统参数与高级设置', '/im/config', 'page/ImConfig', '', 'Settings', b'0', b'0', b'1', 70, '', b'0', b'1', NULL, NULL, NULL, '/1/', 1, '{}', 1, '2021-12-12 12:12:12', 1, '2021-12-12 12:12:12', 0);
INSERT INTO `def_resource` VALUES (8, 1, 'ai:model', 'AI 能力中心', '20', 1, '01', '配置 AI 平台与模型,查看调用情况', '/ai/model', 'page/AiModel', '', 'Robot', b'0', b'0', b'1', 80, '', b'0', b'1', NULL, NULL, NULL, '/1/', 1, '{}', 1, '2021-12-12 12:12:12', 1, '2021-12-12 12:12:12', 0);
INSERT INTO `def_resource` VALUES (9, 1, 'im:active', '活跃用户', '20', 1, '01', '按时间范围查看活跃用户列表', '/im/active', 'page/ActiveUsers', '', 'Users', b'0', b'0', b'1', 20, '', b'0', b'1', NULL, NULL, NULL, '/1/', 1, '{}', 1, '2021-12-12 12:12:12', 1, '2021-12-12 12:12:12', 0);
INSERT INTO `def_resource` VALUES (138191972908138500, 3, 'demo:multiple:view3', '多级-隐藏菜单3', '20', 138191972908138497, '01', '', '/multiple/view3', 'demo/test/index', NULL, '', b'1', b'0', b'1', 30, '', b'0', b'1', NULL, NULL, NULL, '/138191972908138497/', 1, '{}', 2, '2021-12-12 12:12:12', 2, '2024-03-05 22:45:51', 0);
INSERT INTO `def_resource` VALUES (138191972908138501, 3, 'demo:multiple:menu4', '下级是隐藏菜单', '20', 138191972908138497, '01', '', '/multiple/menu4', 'demo/test/index', NULL, '', b'0', b'0', b'1', 40, '', b'0', b'1', NULL, NULL, NULL, '/138191972908138497/', 1, '{}', 2, '2021-12-12 12:12:12', 2, '2024-03-05 22:47:07', 0);
INSERT INTO `def_resource` VALUES (138191972908138502, 3, 'demo:multiple:menu4:view1', '多级-隐藏菜单-视图1', '20', 138191972908138501, '01', '', '/multiple/menu4/view1', 'demo/test/index', NULL, '', b'1', b'0', b'1', 1, '', b'0', b'1', NULL, NULL, NULL, '/138191972908138497/138191972908138501/', 2, '{\"currentActiveMenu\":\"/multiple/menu4\"}', 2, '2021-12-12 12:12:12', 2, '2024-05-15 23:08:31', 0);
@@ -2281,8 +2285,8 @@ CREATE TABLE `def_user` (
-- ----------------------------
-- Records of def_user
-- ----------------------------
INSERT INTO `def_user` VALUES (61170828519936, 2, '15147891644', 'HuLa小管家', '', '022', NULL, NULL, '', NULL, b'0', '', '', '1', b'1', '', '2025-08-11 11:11:03.139', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"120.231.232.41\", \"createIpDetail\": null, \"updateIpDetail\": null}', '2025-11-17 09:42:12', 4, NULL, 'a4d5c225e6709ba025272a31c7e90e0121d5e5ba16695afe0b61370bedb677d0', 'Dawn', '2025-07-07 15:27:02', 1, '2025-03-27 04:23:08', NULL, '2025-07-16 12:26:15', 0, 1);
INSERT INTO `def_user` VALUES (61170828519937, 2, '13275346112', 'Dawn', '2439646234@qq.com', 'https://cdn.hulaspark.com/avatar/2439646234/6ec99d37b8ba1296c325d2d36b46a14d.webp', NULL, NULL, '', NULL, b'0', '', '', '1', b'1', '', '2025-08-11 11:11:03.189', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"121.35.47.142\", \"createIpDetail\": null, \"updateIpDetail\": {\"ip\": \"121.35.47.142\", \"isp\": \"电信\", \"area\": \"\", \"city\": \"深圳\", \"isp_id\": \"100017\", \"region\": \"广东\", \"city_id\": \"440300\", \"country\": \"中国\", \"region_id\": \"440000\", \"country_id\": \"CN\"}}', NULL, 0, NULL, 'a4d5c225e6709ba025272a31c7e90e0121d5e5ba16695afe0b61370bedb677d0', 'Dawn', '2025-11-20 19:26:45', 1, '2025-03-27 04:23:08', NULL, '2025-11-20 23:47:36', 0, 1);
INSERT INTO `def_user` VALUES (61170828519936, 2, '15147891644', 'HuLa小管家', '', '022', NULL, NULL, '', NULL, b'0', '', '', '1', b'1', '', '2025-08-11 11:11:03.139', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"120.231.232.41\", \"createIpDetail\": null, \"updateIpDetail\": null}', '2025-11-25 12:09:10', 5, NULL, 'b6351c5c1e5c172aca2ba36c00598a4b77db41f2240f38a9ed5f6b0ed98e4251', 'HuLa小管家', '2025-07-07 15:27:02', 1, '2025-03-27 04:23:08', NULL, '2025-07-16 12:26:15', 0, 1);
INSERT INTO `def_user` VALUES (61170828519937, 2, '13275346112', 'Dawn', '2439646234@qq.com', 'https://cdn.hulaspark.com/avatar/2439646234/6ec99d37b8ba1296c325d2d36b46a14d.webp', NULL, NULL, '', NULL, b'0', '', '', '1', b'1', '', '2025-08-11 11:11:03.189', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"116.24.67.61\", \"createIpDetail\": null, \"updateIpDetail\": {\"ip\": \"116.24.67.61\", \"isp\": \"电信\", \"area\": \"\", \"city\": \"深圳\", \"isp_id\": \"100017\", \"region\": \"广东\", \"city_id\": \"440300\", \"country\": \"中国\", \"region_id\": \"440000\", \"country_id\": \"CN\"}}', NULL, 0, NULL, 'ab6ec4358efb64760e5f248c1f9130d03aaa1856f9c03e4dc5fa63fb38a796b6', 'Dawn', '2025-11-26 05:10:02', 1, '2025-03-27 04:23:08', NULL, '2025-11-26 07:32:15', 0, 1);
-- ----------------------------
-- Table structure for def_user_application
@@ -2358,7 +2362,7 @@ CREATE TABLE `extend_interface_log` (
-- Records of extend_interface_log
-- ----------------------------
INSERT INTO `extend_interface_log` VALUES (66567882983426, 244439130119864323, '阿里短信', 0, 1, '2025-08-26 16:37:01', '2025-08-26 16:37:00', NULL, '2025-08-26 16:37:00', NULL, 0, 0);
INSERT INTO `extend_interface_log` VALUES (655249535051914248, 244881451621810192, '腾讯邮件', 1163, 62, '2025-11-21 14:50:31', '2025-07-16 18:41:01', NULL, '2025-07-16 18:41:01', NULL, 0, 0);
INSERT INTO `extend_interface_log` VALUES (655249535051914248, 244881451621810192, '腾讯邮件', 1334, 62, '2025-11-26 04:10:51', '2025-07-16 18:41:01', NULL, '2025-07-16 18:41:01', NULL, 0, 0);
-- ----------------------------
-- Table structure for extend_interface_logging
@@ -2572,7 +2576,7 @@ CREATE TABLE `worker_node` (
`created` timestamp NULL DEFAULT NULL COMMENT '创建时间',
`is_del` tinyint(1) NOT NULL DEFAULT 0,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 1011 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DB;WorkerID Assigner for UID Generator' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 1036 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DB;WorkerID Assigner for UID Generator' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of worker_node

View File

@@ -11,7 +11,7 @@
Target Server Version : 80030 (8.0.30)
File Encoding : 65001
Date: 21/11/2025 14:56:13
Date: 26/11/2025 08:05:58
*/
SET NAMES utf8mb4;
@@ -151,7 +151,7 @@ CREATE TABLE `im_contact` (
INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
INDEX `idx_update_time`(`update_time` ASC) USING BTREE,
INDEX `idx_contact_room_uid_hide`(`room_id` ASC, `uid` ASC, `hide` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 69082079603081 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会话列表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 69082079607468 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '会话列表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_contact
@@ -324,7 +324,7 @@ CREATE TABLE `im_group_member` (
INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
INDEX `idx_update_time`(`update_time` ASC) USING BTREE,
INDEX `idx_group_member_uid_isdel_groupid`(`uid` ASC, `is_del` ASC, `group_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98068775162882 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '群成员表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99719888100866 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '群成员表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_group_member
@@ -387,7 +387,7 @@ CREATE TABLE `im_message` (
INDEX `idx_from_uid`(`from_uid` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
INDEX `idx_update_time`(`update_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98069286867969 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消息表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99741140639233 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_message
@@ -417,7 +417,7 @@ CREATE TABLE `im_message_mark` (
INDEX `idx_uid`(`uid` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
INDEX `idx_update_time`(`update_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 96691856460801 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消息标记表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99551872670721 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '消息标记表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_message_mark
@@ -449,7 +449,7 @@ CREATE TABLE `im_notice` (
INDEX `idx_receiver_type`(`receiver_id` ASC, `event_type` ASC) USING BTREE,
INDEX `idx_sender`(`sender_id` ASC) USING BTREE,
INDEX `idx_related`(`apply_id` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98015092265987 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '统一通知表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99540803902467 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin COMMENT = '统一通知表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_notice
@@ -499,12 +499,12 @@ CREATE TABLE `im_room` (
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
INDEX `idx_update_time`(`update_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98068775162883 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '房间表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99719888100867 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '房间表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_room
-- ----------------------------
INSERT INTO `im_room` VALUES (1, 1, 1, '2025-11-21 14:52:20.024', 98069286867968, NULL, '2024-07-10 11:17:15.521', '2025-11-21 06:52:20.138', 1, 1, NULL, 0);
INSERT INTO `im_room` VALUES (1, 1, 1, '2025-11-26 05:35:41.099', 99741140639232, NULL, '2024-07-10 11:17:15.521', '2025-11-25 21:35:41.134', 1, 1, NULL, 0);
-- ----------------------------
-- Table structure for im_room_friend
@@ -529,7 +529,7 @@ CREATE TABLE `im_room_friend` (
INDEX `idx_room_id`(`room_id` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
INDEX `idx_update_time`(`update_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98068775162884 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '单聊房间表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99719888100868 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '单聊房间表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_room_friend
@@ -628,7 +628,6 @@ CREATE TABLE `im_user` (
`resume` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '个人简介',
`create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
`password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '用户密码',
`create_by` bigint NOT NULL DEFAULT 1 COMMENT '创建者',
`update_by` bigint NULL DEFAULT NULL COMMENT '更新者',
`is_del` tinyint(1) NOT NULL DEFAULT 0 COMMENT '是否删除',
@@ -641,13 +640,13 @@ CREATE TABLE `im_user` (
INDEX `idx_update_time`(`update_time` ASC) USING BTREE,
INDEX `idx_active_status_last_opt_time`(`last_opt_time` ASC) USING BTREE,
INDEX `account_UNIQUE`(`account` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98068775162881 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99719888100865 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_user
-- ----------------------------
INSERT INTO `im_user` VALUES (1, 61170828519936, 2, 'HuLa小管家', '022', '', 'bot', NULL, '', 0, '2025-07-07 15:27:01.711', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"120.231.232.41\", \"createIpDetail\": {\"ip\": \"206.237.119.215\", \"isp\": \"\", \"area\": \"\", \"city\": \"\", \"isp_id\": \"\", \"region\": \"\", \"city_id\": \"\", \"country\": \"美国\", \"region_id\": \"\", \"country_id\": \"US\"}, \"updateIpDetail\": {\"ip\": \"120.231.232.41\", \"isp\": \"移动\", \"area\": \"\", \"city\": \"\", \"isp_id\": \"100025\", \"region\": \"广东\", \"city_id\": \"\", \"country\": \"中国\", \"region_id\": \"440000\", \"country_id\": \"CN\"}}', 6, 0, '', '2025-03-27 04:23:08.393', '2025-11-14 08:39:22.859', 'k.23772439646234', 0, NULL, 0, '2025-05-09 18:24:37.089', 0, 1, 10);
INSERT INTO `im_user` VALUES (10937855681024, 61170828519937, 3, 'Dawn', 'https://cdn.hulaspark.com/avatar/2439646234/97320189485dca88dcc7a70054445a56.webp', '2439646234@qq.com', '13275346112', NULL, '', 23, '2025-07-30 15:31:57.651', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"121.35.47.142\", \"createIpDetail\": null, \"updateIpDetail\": {\"ip\": \"121.35.47.142\", \"isp\": \"电信\", \"area\": \"\", \"city\": \"深圳\", \"isp_id\": \"100017\", \"region\": \"广东\", \"city_id\": \"440300\", \"country\": \"中国\", \"region_id\": \"440000\", \"country_id\": \"CN\"}}', 6, 0, '', '2025-03-27 04:23:08.393', '2025-11-20 11:10:32.943', 'k.2439646234', 0, NULL, 0, '2025-09-20 21:35:31.415', 0, 1, 10);
INSERT INTO `im_user` VALUES (1, 61170828519936, 2, 'HuLa小管家', '022', '', 'bot', NULL, '', 0, '2025-07-07 15:27:01.711', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"116.24.67.61\", \"createIpDetail\": null, \"updateIpDetail\": {\"ip\": \"116.24.67.61\", \"isp\": \"电信\", \"area\": \"\", \"city\": \"深圳\", \"isp_id\": \"100017\", \"region\": \"广东\", \"city_id\": \"440300\", \"country\": \"中国\", \"region_id\": \"440000\", \"country_id\": \"CN\"}}', 6, 0, '', '2025-03-27 04:23:08.393', '2025-11-24 18:51:22.015', 0, NULL, 0, '2025-05-09 18:24:37.089', 0, 1, 10);
INSERT INTO `im_user` VALUES (10937855681024, 61170828519937, 3, 'Dawn', 'https://cdn.hulaspark.com/avatar/2439646234/97320189485dca88dcc7a70054445a56.webp', '2439646234@qq.com', '13275346112', NULL, '', 23, '2025-07-30 15:31:57.651', '{\"createIp\": \"206.237.119.215\", \"updateIp\": \"116.24.67.61\", \"createIpDetail\": null, \"updateIpDetail\": {\"ip\": \"116.24.67.61\", \"isp\": \"电信\", \"area\": \"\", \"city\": \"深圳\", \"isp_id\": \"100017\", \"region\": \"广东\", \"city_id\": \"440300\", \"country\": \"中国\", \"region_id\": \"440000\", \"country_id\": \"CN\"}}', 6, 0, '', '2025-03-27 04:23:08.393', '2025-11-25 20:33:08.979', 0, NULL, 0, '2025-09-20 21:35:31.415', 0, 1, 10);
-- ----------------------------
-- Table structure for im_user_apply
@@ -676,7 +675,7 @@ CREATE TABLE `im_user_apply` (
INDEX `idx_target_id`(`target_id` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
INDEX `idx_update_time`(`update_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98015092265985 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户申请表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99540803902465 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户申请表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_user_apply
@@ -703,7 +702,7 @@ CREATE TABLE `im_user_backpack` (
INDEX `idx_uid`(`uid` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
INDEX `idx_update_time`(`update_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98068775162891 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户背包表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99719888100875 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户背包表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_user_backpack
@@ -746,7 +745,7 @@ CREATE TABLE `im_user_emoji` (
`update_by` bigint NULL DEFAULT NULL COMMENT '更新者',
PRIMARY KEY (`id`) USING BTREE,
INDEX `IDX_USER_EMOJIS_UID`(`uid` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 97749185974785 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表情包' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99458897533953 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表情包' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_user_emoji
@@ -779,7 +778,7 @@ CREATE TABLE `im_user_friend` (
INDEX `idx_uid_friend_uid`(`uid` ASC, `friend_uid` ASC) USING BTREE,
INDEX `idx_create_time`(`create_time` ASC) USING BTREE,
INDEX `idx_update_time`(`update_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98068775162886 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户联系人表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99719888100870 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户联系人表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of im_user_friend
@@ -957,7 +956,7 @@ CREATE TABLE `secure_invoke_record` (
`is_del` tinyint NOT NULL DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`) USING BTREE,
INDEX `idx_next_retry_time`(`next_retry_time` ASC) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 98069286867970 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '本地消息表' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 99741140639234 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '本地消息表' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of secure_invoke_record
@@ -976,7 +975,7 @@ CREATE TABLE `worker_node` (
`modified` timestamp NULL DEFAULT NULL COMMENT '修改时间',
`created` timestamp NULL DEFAULT NULL COMMENT '创建时间',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 235 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DB;WorkerID Assigner for UID Generator' ROW_FORMAT = Dynamic;
) ENGINE = InnoDB AUTO_INCREMENT = 240 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = 'DB;WorkerID Assigner for UID Generator' ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of worker_node

View File

@@ -18,6 +18,10 @@ import com.luohuo.flex.ai.core.model.HunYuanChatModel;
import com.luohuo.flex.ai.core.model.MidjourneyApi;
import com.luohuo.flex.ai.core.model.SunoApi;
import com.luohuo.flex.ai.core.model.XingHuoChatModel;
import com.luohuo.flex.ai.core.model.google.GeminiChatModel;
import org.springframework.ai.openai.OpenAiChatModel;
import org.springframework.ai.openai.OpenAiChatOptions;
import org.springframework.ai.openai.api.OpenAiApi;
import com.luohuo.flex.ai.core.model.openrouter.OpenRouterApiConstants;
import com.luohuo.flex.ai.core.model.openrouter.OpenRouterChatModel;
import com.luohuo.flex.ai.core.model.silicon.SiliconFlowApiConstants;
@@ -305,6 +309,24 @@ public class AiAutoConfiguration {
return new SunoApi(hulaAiProperties.getSuno().getBaseUrl());
}
@Bean
@ConditionalOnProperty(value = "luohuo.ai.gemini.enable", havingValue = "true")
public GeminiChatModel geminiChatClient(HulaAiProperties hulaAiProperties) {
HulaAiProperties.GeminiProperties properties = hulaAiProperties.getGemini();
String baseUrl = StrUtil.isNotEmpty(properties.getBaseUrl()) ? properties.getBaseUrl() : "https://generativelanguage.googleapis.com/v1beta/openai";
OpenAiChatModel openAiChatModel = OpenAiChatModel.builder()
.openAiApi(OpenAiApi.builder().baseUrl(baseUrl).apiKey(properties.getApiKey()).build())
.defaultOptions(OpenAiChatOptions.builder()
.model(properties.getModel())
.temperature(properties.getTemperature())
.maxTokens(properties.getMaxTokens())
.topP(properties.getTopP())
.build())
.toolCallingManager(getToolCallingManager())
.build();
return new GeminiChatModel(openAiChatModel);
}
// ========== RAG 相关 ==========
@Bean

View File

@@ -61,6 +61,9 @@ public class HulaAiProperties {
@SuppressWarnings("SpellCheckingInspection")
private OpenRouterProperties openrouter;
@SuppressWarnings("SpellCheckingInspection")
private GeminiProperties gemini;
@Data
public static class DeepSeekProperties {
@@ -174,4 +177,18 @@ public class HulaAiProperties {
private Double topP;
}
@Data
public static class GeminiProperties {
private String enable;
private String apiKey;
private String baseUrl;
private String model;
private Double temperature;
private Integer maxTokens;
private Double topP;
}
}

View File

@@ -52,6 +52,9 @@ public class AiChatConversationRespVO implements VO {
@Schema(description = "上下文的最大 Message 数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "10")
private Integer maxContexts;
@Schema(description = "会话累计 Token 用量", example = "12345")
private Integer tokenUsage;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;

View File

@@ -33,10 +33,7 @@ public class AiChatConversationUpdateMyReqVO {
@Schema(description = "温度参数", example = "0.8")
private Double temperature;
@Schema(description = "单条回复的最大 Token ", example = "4096")
private Integer maxTokens;
@Schema(description = "上下文的最大 Message 数量", example = "10")
private Integer maxContexts;
@Schema(description = "会话累计 Token ", example = "12345")
private Integer tokenUsage;
}

View File

@@ -20,4 +20,6 @@ public class AiChatMessageSendReqVO {
@Schema(description = "是否携带上下文", example = "true")
private Boolean useContext;
@Schema(description = "是否开启深度思考模式", example = "false")
private Boolean reasoningEnabled = false;
}

View File

@@ -58,6 +58,9 @@ public class AiApiKeyBalanceRespVO {
@Schema(description = "总余额", example = "110.00")
private BigDecimal totalBalance;
@Schema(description = "使用总量(按平台返回的单位,可能为金额或额度)", example = "12.34")
private BigDecimal usageTotal;
@Schema(description = "赠送余额", example = "10.00")
private BigDecimal grantedBalance;

View File

@@ -50,6 +50,9 @@ public class AiModelRespVO {
@Schema(description = "上下文的最大 Message 数量", example = "8192")
private Integer maxContexts;
@Schema(description = "是否支持深度思考模式", example = "true")
private Boolean supportsReasoning;
@Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
private LocalDateTime createTime;

View File

@@ -53,6 +53,9 @@ public class AiModelSaveReqVO {
@Schema(description = "上下文的最大 Message 数量", example = "8192")
private Integer maxContexts;
@Schema(description = "是否支持深度思考模式", example = "false")
private Boolean supportsReasoning;
@Schema(description = "是否公开0-公开1-私有", example = "0")
private Integer publicStatus;

View File

@@ -150,6 +150,7 @@ public class AiModelFactoryImpl implements AiModelFactory {
case OPENAI -> buildOpenAiChatModel(apiKey, url);
case AZURE_OPENAI -> buildAzureOpenAiChatModel(apiKey, url);
case OLLAMA -> buildOllamaChatModel(url);
case GEMINI -> buildGeminiChatModel(apiKey, url);
default -> throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
};
}
@@ -188,6 +189,8 @@ public class AiModelFactoryImpl implements AiModelFactory {
return SpringUtil.getBean(AzureOpenAiChatModel.class);
case OLLAMA:
return SpringUtil.getBean(OllamaChatModel.class);
case GEMINI:
return SpringUtil.getBean(com.luohuo.flex.ai.core.model.google.GeminiChatModel.class);
default:
throw new IllegalArgumentException(StrUtil.format("未知平台({})", platform));
}
@@ -469,6 +472,12 @@ public class AiModelFactoryImpl implements AiModelFactory {
return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build();
}
private ChatModel buildGeminiChatModel(String apiKey, String url) {
String baseUrl = StrUtil.blankToDefault(url, "https://generativelanguage.googleapis.com/v1beta/openai");
OpenAiApi openAiApi = OpenAiApi.builder().baseUrl(baseUrl).apiKey(apiKey).build();
return OpenAiChatModel.builder().openAiApi(openAiApi).toolCallingManager(getToolCallingManager()).build();
}
/**
* 构建 OpenRouter 聊天模型
*/

View File

@@ -0,0 +1,82 @@
package com.luohuo.flex.ai.core.model.google;
import lombok.Data;
import org.springframework.http.MediaType;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestClient;
import java.util.List;
import java.util.Map;
public class GeminiApi {
private final RestClient client;
private final String apiKey;
private final String baseUrl;
public GeminiApi(String baseUrl, String apiKey) {
this.client = RestClient.builder().baseUrl(baseUrl).build();
this.apiKey = apiKey;
this.baseUrl = baseUrl;
}
public GenerateResponse generateContent(String model, GenerateRequest request) {
String path = "/models/" + model + ":generateContent";
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("key", apiKey);
return client.post()
.uri(builder -> builder.path(path).queryParams(params).build())
.contentType(MediaType.APPLICATION_JSON)
.body(request)
.retrieve()
.body(GenerateResponse.class);
}
@Data
public static class GenerateRequest {
private List<Content> contents;
private GenerationConfig generationConfig;
private SystemInstruction systemInstruction;
}
@Data
public static class SystemInstruction {
private List<Part> parts;
}
@Data
public static class Content {
private String role;
private List<Part> parts;
}
@Data
public static class Part {
private String text;
private InlineData inlineData;
private Map<String, Object> fileData;
}
@Data
public static class InlineData {
private String mimeType;
private String data;
}
@Data
public static class GenerationConfig {
private Double temperature;
private Integer maxOutputTokens;
private Double topP;
}
@Data
public static class GenerateResponse {
private List<Candidate> candidates;
}
@Data
public static class Candidate {
private Content content;
}
}

View File

@@ -0,0 +1,30 @@
package com.luohuo.flex.ai.core.model.google;
import lombok.RequiredArgsConstructor;
import org.springframework.ai.chat.model.ChatModel;
import org.springframework.ai.chat.model.ChatResponse;
import org.springframework.ai.chat.model.StreamingChatModel;
import org.springframework.ai.chat.prompt.ChatOptions;
import org.springframework.ai.chat.prompt.Prompt;
import org.springframework.ai.openai.OpenAiChatModel;
import reactor.core.publisher.Flux;
@RequiredArgsConstructor
public class GeminiChatModel implements ChatModel, StreamingChatModel {
private final OpenAiChatModel delegate;
@Override
public ChatResponse call(Prompt prompt) {
return delegate.call(prompt);
}
@Override
public Flux<ChatResponse> stream(Prompt prompt) {
return delegate.stream(prompt);
}
@Override
public ChatOptions getDefaultOptions() {
return delegate.getDefaultOptions();
}
}

View File

@@ -98,6 +98,11 @@ public class AiChatConversationDO extends BaseDO {
* 上下文的最大 Message 数量
*/
private Integer maxContexts;
/**
* 会话累计 Token 用量
*/
private Integer tokenUsage;
/**
* 租户id
*/

View File

@@ -107,4 +107,9 @@ public class AiModelDO extends BaseDO {
*/
private Integer maxContexts;
/**
* 是否支持深度思考模式
*/
private Boolean supportsReasoning;
}

View File

@@ -53,6 +53,8 @@ public enum AiPlatformEnum {
SUNO("Suno","Suno"), // Suno AI
GEMINI("Gemini","Gemini"),
;
/**

View File

@@ -77,4 +77,7 @@ public interface ErrorCodeConstants {
// ========== AI 使用限制 1-040-012-000 ==========
ErrorCode AI_USAGE_LIMIT_EXCEEDED = new ErrorCode(1_040_012_000, "AI使用次数已用完");
// ========== 会话 Token 限制 1-040-013-000 ==========
ErrorCode CHAT_TOKEN_BUDGET_EXCEEDED = new ErrorCode(1_040_013_000, "本会话的 Token 预算已用完,请新建会话或更换模型");
}

View File

@@ -71,7 +71,8 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
// 2. 创建 AiChatConversationDO 聊天对话
AiChatConversationDO conversation = new AiChatConversationDO().setUserId(userId).setPinned(false)
.setModelId(model.getId()).setModel(model.getModel())
.setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts());
.setTemperature(model.getTemperature()).setMaxTokens(model.getMaxTokens()).setMaxContexts(model.getMaxContexts())
.setTokenUsage(0);
if (role != null) {
conversation.setTitle(role.getName()).setRoleId(role.getId()).setSystemMessage(role.getSystemMessage());
} else {
@@ -108,6 +109,9 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
// 2. 更新对话信息
AiChatConversationDO updateObj = BeanUtils.toBean(updateReqVO, AiChatConversationDO.class);
// 禁止会话层直接修改 maxTokens / maxContexts统一由模型驱动
updateObj.setMaxTokens(null);
updateObj.setMaxContexts(null);
if (Boolean.TRUE.equals(updateReqVO.getPinned())) {
updateObj.setPinnedTime(LocalDateTime.now());
}
@@ -117,8 +121,10 @@ public class AiChatConversationServiceImpl implements AiChatConversationService
updateObj.setSystemMessage(role.getSystemMessage());
}
if (model != null) {
updateObj.setModelId(model.getId());
updateObj.setModelId(model.getId());
updateObj.setModel(model.getModel());
updateObj.setMaxTokens(model.getMaxTokens());
updateObj.setMaxContexts(model.getMaxContexts());
}
chatConversationMapper.updateById(updateObj);
}

View File

@@ -4,9 +4,9 @@ import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjUtil;
import cn.hutool.core.util.StrUtil;
import com.luohuo.basic.context.ContextUtil;
import com.luohuo.basic.tenant.core.aop.TenantIgnore;
import com.luohuo.flex.ai.common.pojo.PageResult;
import com.luohuo.flex.ai.controller.chat.vo.conversation.AiDelReqVO;
import com.luohuo.flex.ai.controller.chat.vo.conversation.AiChatConversationUpdateMyReqVO;
import com.luohuo.flex.ai.controller.chat.vo.message.AiChatMessagePageReqVO;
import com.luohuo.flex.ai.controller.chat.vo.message.AiChatMessageRespVO;
import com.luohuo.flex.ai.controller.chat.vo.message.AiChatMessageSendReqVO;
@@ -61,6 +61,7 @@ import static com.luohuo.flex.ai.common.pojo.CommonResult.error;
import static com.luohuo.basic.base.R.success;
import static com.luohuo.flex.ai.enums.ErrorCodeConstants.CHAT_CONVERSATION_NOT_EXISTS;
import static com.luohuo.flex.ai.enums.ErrorCodeConstants.CHAT_MESSAGE_NOT_EXIST;
import static com.luohuo.flex.ai.enums.ErrorCodeConstants.CHAT_PROMPT_TOO_LONG;
import static com.luohuo.flex.ai.utils.ServiceExceptionUtil.exception;
import static com.luohuo.basic.utils.collection.CollectionUtils.convertList;
@@ -110,6 +111,12 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
// 1.2 校验模型
AiModelDO model = modalService.validateModel(conversation.getModelId());
// 1.2.1 会话 Token 预算校验:累计 tokenUsage 超过模型 maxTokens 则拒绝
if (model.getMaxTokens() != null && conversation.getTokenUsage() != null
&& conversation.getTokenUsage() >= model.getMaxTokens()) {
throw exception(ErrorCodeConstants.CHAT_TOKEN_BUDGET_EXCEEDED);
}
// 1.3 检查并扣减模型使用次数
modelUsageService.checkAndDeductUsage(userId, model);
@@ -135,6 +142,14 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
// 3.3 更新响应内容
String newContent = chatResponse.getResult().getOutput().getText();
chatMessageMapper.updateById(new AiChatMessageDO().setId(assistantMessage.getId()).setContent(newContent));
// 用量统计prompt + completion
int promptTokens = estimateContextTokens(conversation, historyMessages, sendReqVO.getContent());
int completionTokens = estimateTokens(newContent);
int newTotal = (conversation.getTokenUsage() == null ? 0 : conversation.getTokenUsage()) + promptTokens + completionTokens;
AiChatConversationUpdateMyReqVO updateReq = new AiChatConversationUpdateMyReqVO();
updateReq.setId(conversation.getId());
updateReq.setTokenUsage(newTotal);
chatConversationService.updateChatConversationMy(updateReq, userId);
// 3.4 响应结果
Map<Long, AiKnowledgeDocumentDO> documentMap = knowledgeDocumentService.getKnowledgeDocumentMap(
convertSet(knowledgeSegments, AiKnowledgeSegmentSearchRespBO::getDocumentId));
@@ -168,6 +183,12 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
StreamingChatModel chatModel = modalService.getChatModel(model.getId());
// 1.2.1 会话 Token 预算校验:累计 tokenUsage 超过模型 maxTokens 则拒绝
if (model.getMaxTokens() != null && conversation.getTokenUsage() != null
&& conversation.getTokenUsage() >= model.getMaxTokens()) {
return Flux.just(error(ErrorCodeConstants.CHAT_TOKEN_BUDGET_EXCEEDED));
}
// 2. 知识库找回
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments = recallKnowledgeSegment(sendReqVO.getContent(),
conversation);
@@ -204,33 +225,48 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
// 响应结果
String newContent = chunk.getResult() != null ? chunk.getResult().getOutput().getText() : null;
newContent = StrUtil.nullToDefault(newContent, "");
contentBuffer.append(newContent);
String answerChunk = stripReasoningTags(newContent);
contentBuffer.append(answerChunk);
// 提取推理内容
String newReasoningContent = null;
if (chunk.getMetadata() != null && chunk.getMetadata().containsKey("reasoning_content")) {
newReasoningContent = String.valueOf(chunk.getMetadata().get("reasoning_content"));
reasoningBuffer.append(newReasoningContent);
} else {
String extracted = extractReasoningFromText(newContent);
if (StrUtil.isNotEmpty(extracted)) {
newReasoningContent = extracted;
reasoningBuffer.append(extracted);
}
}
return success(new AiChatMessageSendRespVO()
.setSend(BeanUtils.toBean(userMessage, AiChatMessageSendRespVO.Message.class))
.setReceive(BeanUtils.toBean(assistantMessage, AiChatMessageSendRespVO.Message.class)
.setContent(newContent)
.setReasoningContent(newReasoningContent)
.setSegments(segments)));
.setContent(answerChunk)
.setReasoningContent(newReasoningContent)
.setSegments(segments)));
}).doOnComplete(() -> {
// 手动设置租户信息(因为 Flux 异步会切换线程,导致 ThreadLocal 丢失)
try {
ContextUtil.setIgnore(true);
String content = contentBuffer.toString();
String content = stripReasoningTags(contentBuffer.toString());
String reasoningContent = reasoningBuffer.toString();
AiChatMessageDO updateMessage = new AiChatMessageDO().setId(assistantMessage.getId()).setContent(content);
if (StrUtil.isNotEmpty(reasoningContent)) {
updateMessage.setReasoningContent(reasoningContent);
}
chatMessageMapper.updateById(updateMessage);
// 用量统计prompt + completion流式完成时
int promptTokens = estimateContextTokens(conversation, historyMessages, sendReqVO.getContent());
int completionTokens = estimateTokens(content);
int newTotal = (conversation.getTokenUsage() == null ? 0 : conversation.getTokenUsage()) + promptTokens + completionTokens;
AiChatConversationUpdateMyReqVO streamUpdateReq = new AiChatConversationUpdateMyReqVO();
streamUpdateReq.setId(conversation.getId());
streamUpdateReq.setTokenUsage(newTotal);
chatConversationService.updateChatConversationMy(streamUpdateReq, conversation.getUserId());
} finally {
ContextUtil.setIgnore(false);
}
@@ -261,11 +297,27 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
private Prompt buildPrompt(AiChatConversationDO conversation, List<AiChatMessageDO> messages,
List<AiKnowledgeSegmentSearchRespBO> knowledgeSegments,
AiModelDO model, AiChatMessageSendReqVO sendReqVO) {
// 上下文条数限制:携带上下文时,历史问答对已达上限则阻止继续对话
if (Boolean.TRUE.equals(sendReqVO.getUseContext())) {
int pairs = countContextPairs(messages);
if (conversation.getMaxContexts() != null && pairs >= conversation.getMaxContexts()) {
throw exception(CHAT_PROMPT_TOO_LONG);
}
}
List<Message> chatMessages = new ArrayList<>();
// 1.1 System Context 角色设定
if (StrUtil.isNotBlank(conversation.getSystemMessage())) {
chatMessages.add(new SystemMessage(conversation.getSystemMessage()));
}
// 1.1.1 深度思考系统提示(仅对支持的模型生效)
if (sendReqVO.getReasoningEnabled() && isReasoningSupported(model)) {
int thinkingBudget = 1024;
chatMessages.add(new SystemMessage(
"你是深度思考助手。先在‘思考过程’中进行详细推理,再给出最终答案。请保持推理与答案清晰分离。" +
"将推理内容使用 <thinking> 与 </thinking> 包裹,将最终答案使用 <answer> 与 </answer> 包裹。" +
StrUtil.format("思考阶段最多使用 {} token。", thinkingBudget)
));
}
// 1.2 历史 history message 历史消息
List<AiChatMessageDO> contextMessages = filterContextMessages(messages, conversation, sendReqVO);
@@ -300,6 +352,94 @@ public class AiChatMessageServiceImpl implements AiChatMessageService {
return new Prompt(chatMessages, chatOptions);
}
private boolean isReasoningSupported(AiModelDO model) {
return Boolean.TRUE.equals(model.getSupportsReasoning());
}
private int estimateContextTokens(AiChatConversationDO conversation, List<AiChatMessageDO> allMessages, String currentContent) {
int total = 0;
AiChatMessageSendReqVO ctxReq = new AiChatMessageSendReqVO();
ctxReq.setUseContext(true);
List<AiChatMessageDO> contextMessages = filterContextMessages(allMessages, conversation, ctxReq);
for (AiChatMessageDO m : contextMessages) {
total += estimateTokens(m.getContent());
if (StrUtil.isNotEmpty(m.getReasoningContent())) {
total += estimateTokens(m.getReasoningContent());
}
}
if (StrUtil.isNotEmpty(currentContent)) {
total += estimateTokens(currentContent);
}
return total;
}
private int countContextPairs(List<AiChatMessageDO> messages) {
int pairs = 0;
for (int i = messages.size() - 1; i >= 0; i--) {
AiChatMessageDO assistantMessage = CollUtil.get(messages, i);
if (assistantMessage == null || assistantMessage.getReplyId() == null) {
continue;
}
AiChatMessageDO userMessage = CollUtil.get(messages, i - 1);
if (userMessage == null
|| ObjUtil.notEqual(assistantMessage.getReplyId(), userMessage.getId())
|| StrUtil.isEmpty(assistantMessage.getContent())) {
continue;
}
pairs++;
i--; // 跳过已配对的 user
}
return pairs;
}
private int estimateTokens(String text) {
if (StrUtil.isEmpty(text)) return 0;
String ascii = text.replaceAll("[^\\x00-\\x7F]", "");
int nonAsciiCount = text.length() - ascii.length();
String[] words = ascii.trim().split("\\s+");
int asciiTokens = 0;
for (String w : words) {
if (StrUtil.isEmpty(w)) continue;
asciiTokens += (int) Math.ceil(w.length() / 4.0);
}
return asciiTokens + nonAsciiCount;
}
private String extractReasoningFromText(String text) {
if (StrUtil.isEmpty(text)) return null;
StringBuilder sb = new StringBuilder();
int start = 0;
while (true) {
int s = text.indexOf("<thinking>", start);
if (s < 0) break;
int e = text.indexOf("</thinking>", s + 10);
if (e < 0) break;
sb.append(text, s + 10, e);
start = e + 11;
}
if (sb.length() > 0) return sb.toString();
start = 0;
while (true) {
int s = text.indexOf("<reasoning>", start);
if (s < 0) break;
int e = text.indexOf("</reasoning>", s + 11);
if (e < 0) break;
sb.append(text, s + 11, e);
start = e + 12;
}
return sb.length() > 0 ? sb.toString() : null;
}
private String stripReasoningTags(String text) {
if (StrUtil.isEmpty(text)) return text;
String t = text;
t = t.replaceAll("<thinking>[\\s\\S]*?</thinking>", "");
t = t.replaceAll("<reasoning>[\\s\\S]*?</reasoning>", "");
t = t.replaceAll("<answer>", "");
t = t.replaceAll("</answer>", "");
return t;
}
/**
* 从历史消息中,获得倒序的 n 组消息作为消息上下文
* <p>

View File

@@ -442,30 +442,32 @@ public class AiApiKeyServiceImpl implements AiApiKeyService {
if (data != null && data.get("limit") != null && !(data.get("limit") instanceof cn.hutool.json.JSONNull)) {
limit = new BigDecimal(String.valueOf(data.get("limit")));
}
if (data != null && data.get("usage") != null && !(data.get("usage") instanceof cn.hutool.json.JSONNull)) {
usage = new BigDecimal(String.valueOf(data.get("usage")));
}
if (data != null && data.get("usage") != null && !(data.get("usage") instanceof cn.hutool.json.JSONNull)) {
usage = new BigDecimal(String.valueOf(data.get("usage")));
}
BigDecimal remaining = null;
if (limit != null) {
remaining = usage != null ? limit.subtract(usage) : limit;
}
if (remaining != null) {
balanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()
.currency("USD")
.totalBalance(remaining)
.available(true)
.build());
totalBalance = remaining;
} else {
balanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()
.currency("USD")
.totalBalance(BigDecimal.ZERO)
.grantedBalance(BigDecimal.ZERO)
.toppedUpBalance(BigDecimal.ZERO)
.available(Boolean.TRUE.equals(freeTier))
.build());
totalBalance = BigDecimal.ZERO;
}
if (remaining != null) {
balanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()
.currency("USD")
.totalBalance(remaining)
.usageTotal(usage)
.available(true)
.build());
totalBalance = remaining;
} else {
balanceInfos.add(AiApiKeyBalanceRespVO.BalanceInfo.builder()
.currency("USD")
.totalBalance(BigDecimal.ZERO)
.grantedBalance(BigDecimal.ZERO)
.toppedUpBalance(BigDecimal.ZERO)
.usageTotal(BigDecimal.ZERO)
.available(Boolean.TRUE.equals(freeTier))
.build());
totalBalance = BigDecimal.ZERO;
}
}
return AiApiKeyBalanceRespVO.builder()

View File

@@ -64,6 +64,9 @@ public class AiUtils {
case GITEE_AI: // 复用 OpenAI 客户端
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build();
case GEMINI:
return OpenAiChatOptions.builder().model(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build();
case AZURE_OPENAI:
return AzureOpenAiChatOptions.builder().deploymentName(model).temperature(temperature).maxTokens(maxTokens)
.toolNames(toolNames).toolContext(toolContext).build();

View File

@@ -1,6 +1,7 @@
package com.luohuo.flex.base.mapper.system;
import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
import com.luohuo.flex.base.service.system.dto.LoginCountDTO;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;
import com.luohuo.basic.base.mapper.SuperMapper;
@@ -29,4 +30,8 @@ public interface DefLoginLogMapper extends SuperMapper<DefLoginLog> {
* @return 是否成功
*/
Long clearLog(@Param("clearBeforeTime") LocalDateTime clearBeforeTime, @Param("idList") List<Long> idList);
Long selectUserCountWithMinLogins(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end, @Param("minTimes") Integer minTimes);
List<LoginCountDTO> selectLoginCountBetween(@Param("start") LocalDateTime start, @Param("end") LocalDateTime end, @Param("limit") Integer limit);
}

View File

@@ -2,8 +2,10 @@ package com.luohuo.flex.base.service.system;
import com.luohuo.basic.base.service.SuperService;
import com.luohuo.flex.base.entity.system.DefLoginLog;
import com.luohuo.flex.base.service.system.dto.LoginCountDTO;
import java.time.LocalDateTime;
import java.util.List;
/**
* <p>
@@ -23,4 +25,8 @@ public interface DefLoginLogService extends SuperService<Long, DefLoginLog> {
* @return 是否成功
*/
boolean clearLog(LocalDateTime clearBeforeTime, Integer clearBeforeNum);
List<LoginCountDTO> getLoginRank(LocalDateTime start, LocalDateTime end, Integer limit);
Long countUsersWithMinLogins(LocalDateTime start, LocalDateTime end, Integer minTimes);
}

View File

@@ -0,0 +1,9 @@
package com.luohuo.flex.base.service.system.dto;
import lombok.Data;
@Data
public class LoginCountDTO {
private Long uid;
private Long total;
}

View File

@@ -17,9 +17,12 @@ import com.luohuo.flex.base.entity.tenant.DefUser;
import com.luohuo.flex.base.manager.system.DefLoginLogManager;
import com.luohuo.flex.base.manager.tenant.DefUserManager;
import com.luohuo.flex.base.service.system.DefLoginLogService;
import com.luohuo.flex.base.service.system.dto.LoginCountDTO;
import com.luohuo.flex.base.mapper.system.DefLoginLogMapper;
import com.luohuo.flex.base.vo.save.system.DefLoginLogSaveVO;
import java.time.LocalDateTime;
import java.util.List;
import java.util.function.Supplier;
import java.util.stream.Stream;
@@ -45,6 +48,7 @@ public class DefLoginLogServiceImpl extends SuperServiceImpl<DefLoginLogManager,
"Android", "Linux", "Mac OS X", "Ubuntu", "Windows 10", "Windows 8", "Windows 7", "Windows XP", "Windows Vista"
);
private final DefUserManager defUserManager;
private final DefLoginLogMapper defLoginLogMapper;
private static String simplifyOperatingSystem(String operatingSystem) {
return OPERATING_SYSTEM.get().parallel().filter(b -> StrUtil.containsIgnoreCase(operatingSystem, b)).findAny().orElse(operatingSystem);
@@ -95,4 +99,14 @@ public class DefLoginLogServiceImpl extends SuperServiceImpl<DefLoginLogManager,
public boolean clearLog(LocalDateTime clearBeforeTime, Integer clearBeforeNum) {
return superManager.clearLog(clearBeforeTime, clearBeforeNum) > 0;
}
@Override
public List<LoginCountDTO> getLoginRank(LocalDateTime start, LocalDateTime end, Integer limit) {
return defLoginLogMapper.selectLoginCountBetween(start, end, limit);
}
@Override
public Long countUsersWithMinLogins(LocalDateTime start, LocalDateTime end, Integer minTimes) {
return defLoginLogMapper.selectUserCountWithMinLogins(start, end, minTimes);
}
}

View File

@@ -17,4 +17,41 @@
</trim>
</delete>
<select id="selectUserCountWithMinLogins" resultType="long">
SELECT COUNT(*) FROM (
SELECT employee_id AS uid, COUNT(*) AS total
FROM def_login_log
<trim prefix="WHERE" prefixOverrides="AND | OR">
AND status = '01'
<if test="start != null">
AND login_date <![CDATA[ >= ]]> #{start}
</if>
<if test="end != null">
AND login_date <![CDATA[ <= ]]> #{end}
</if>
</trim>
GROUP BY employee_id
HAVING COUNT(*) <![CDATA[ >= ]]> #{minTimes}
) t
</select>
<select id="selectLoginCountBetween" resultType="com.luohuo.flex.base.service.system.dto.LoginCountDTO">
SELECT employee_id AS uid, COUNT(*) AS total
FROM def_login_log
<trim prefix="WHERE" prefixOverrides="AND | OR">
AND status = '01'
<if test="start != null">
AND login_date <![CDATA[ >= ]]> #{start}
</if>
<if test="end != null">
AND login_date <![CDATA[ <= ]]> #{end}
</if>
</trim>
GROUP BY employee_id
ORDER BY total DESC
<if test="limit != null">
LIMIT #{limit}
</if>
</select>
</mapper>

View File

@@ -1,6 +1,7 @@
package com.luohuo.flex.controller.system;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.luohuo.flex.base.service.system.dto.LoginCountDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
@@ -8,6 +9,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@@ -25,6 +27,7 @@ import com.luohuo.flex.base.vo.save.system.DefLoginLogSaveVO;
import com.luohuo.flex.base.vo.update.system.DefLoginLogUpdateVO;
import java.time.LocalDateTime;
import java.util.List;
/**
@@ -88,4 +91,40 @@ public class BaseLoginLogController extends SuperController<DefLoginLogService,
return success(superService.clearLog(clearBeforeTime, clearBeforeNum));
}
@GetMapping("/stats/login-rank")
@Operation(summary = "登录次数排行榜status=01")
public R<List<LoginCountDTO>> getLoginRank(
@RequestParam(value = "start", required = false) LocalDateTime start,
@RequestParam(value = "end", required = false) LocalDateTime end,
@RequestParam(value = "rangeDays", required = false) Integer rangeDays,
@RequestParam(value = "limit", required = false) Integer limit
) {
LocalDateTime now = LocalDateTime.now();
if (start == null || end == null) {
int days = rangeDays != null ? rangeDays : 30;
start = now.minusDays(days);
end = now;
}
if (limit == null) limit = 50;
return success(superService.getLoginRank(start, end, limit));
}
@GetMapping("/stats/user-count")
@Operation(summary = "登录次数达到阈值的用户数量status=01")
public R<Long> countUsersWithMinLogins(
@RequestParam(value = "start", required = false) LocalDateTime start,
@RequestParam(value = "end", required = false) LocalDateTime end,
@RequestParam(value = "rangeDays", required = false) Integer rangeDays,
@RequestParam(value = "minTimes", required = false) Integer minTimes
) {
LocalDateTime now = LocalDateTime.now();
if (start == null || end == null) {
int days = rangeDays != null ? rangeDays : 30;
start = now.minusDays(days);
end = now;
}
if (minTimes == null) minTimes = 3;
return success(superService.countUsersWithMinLogins(start, end, minTimes));
}
}

View File

@@ -0,0 +1,9 @@
package com.luohuo.flex.im.core.admin.dto;
import lombok.Data;
@Data
public class LoginCountDTO {
private Long employeeId;
private Long total;
}

View File

@@ -1,6 +1,11 @@
package com.luohuo.flex.im.core.admin.service;
import com.luohuo.flex.im.domain.vo.resp.admin.ActiveUserResp;
import com.luohuo.flex.im.domain.vo.resp.admin.AdminStatsResp;
import com.luohuo.flex.im.domain.vo.resp.admin.LoginRankResp;
import java.time.LocalDateTime;
import java.util.List;
/**
* 后台管理统计服务
@@ -13,4 +18,8 @@ public interface AdminStatsService {
* @return 统计数据
*/
AdminStatsResp getHomeStats();
List<LoginRankResp> getLoginRank(LocalDateTime start, LocalDateTime end, Integer limit);
List<ActiveUserResp> getActiveUsers(LocalDateTime start, LocalDateTime end, Integer limit);
}

View File

@@ -5,16 +5,27 @@ import com.luohuo.flex.im.core.admin.service.AdminStatsService;
import com.luohuo.flex.im.core.chat.dao.RoomGroupDao;
import com.luohuo.flex.im.core.user.dao.BlackDao;
import com.luohuo.flex.im.core.user.dao.UserDao;
import com.luohuo.flex.im.core.user.service.cache.UserCache;
import com.luohuo.flex.im.domain.entity.Black;
import com.luohuo.flex.im.domain.entity.RoomGroup;
import com.luohuo.flex.im.domain.entity.User;
import com.luohuo.flex.im.domain.vo.resp.admin.ActiveUserResp;
import com.luohuo.flex.im.domain.vo.resp.admin.AdminStatsResp;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import java.util.Comparator;
import com.luohuo.flex.im.api.BaseLoginLogApi;
import com.luohuo.flex.im.domain.vo.resp.admin.LoginRankResp;
/**
* 后台管理统计服务实现
@@ -26,15 +37,15 @@ public class AdminStatsServiceImpl implements AdminStatsService {
@Resource
private UserDao userDao;
@Resource
private RoomGroupDao roomGroupDao;
@Autowired
private UserCache userCache;
@Resource
private BlackDao blackDao;
@Override
public AdminStatsResp getHomeStats() {
public AdminStatsResp getHomeStats() {
// 1. 统计今日活跃用户数
LocalDateTime todayStart = LocalDateTime.now().with(LocalTime.MIN);
LocalDateTime todayEnd = LocalDateTime.now().with(LocalTime.MAX);
@@ -63,7 +74,7 @@ public class AdminStatsServiceImpl implements AdminStatsService {
LocalDateTime weekStart = LocalDateTime.now().minusDays(LocalDateTime.now().getDayOfWeek().getValue() - 1).with(LocalTime.MIN);
LambdaQueryWrapper<Black> weekBlackWrapper = new LambdaQueryWrapper<>();
weekBlackWrapper.between(Black::getCreateTime, weekStart, todayEnd);
Integer weekNewBlack = Math.toIntExact(blackDao.count(weekBlackWrapper));
Integer weekNewBlack = Math.toIntExact(blackDao.count(weekBlackWrapper));
// 4. 构建黑名单统计
AdminStatsResp.BlackStats blackStats = AdminStatsResp.BlackStats.builder()
@@ -73,20 +84,93 @@ public class AdminStatsServiceImpl implements AdminStatsService {
.build();
// 5. AI 统计(假数据,需要对接 AI 服务)
AdminStatsResp.AiStats aiStats = AdminStatsResp.AiStats.builder()
.todayCalls(0)
.weekCalls(0)
.activeModels(0)
.build();
AdminStatsResp.AiStats aiStats = AdminStatsResp.AiStats.builder()
.todayCalls(0)
.weekCalls(0)
.activeModels(0)
.build();
// 6. 构建响应
return AdminStatsResp.builder()
.todayActiveUser(todayActiveUser)
.totalGroup(totalGroup)
.blackCount(blackCount)
.aiCallToday(0) // TODO: 对接 AI 服务
.blackStats(blackStats)
.aiStats(aiStats)
.build();
}
// 6. 最近一个月登录≥3次的用户总数
LocalDateTime monthStart = LocalDateTime.now().minusDays(30).with(LocalTime.MIN);
Long month3plus = baseLoginLogApi.countUsersWithMinLogins(monthStart, todayEnd, 3).getData();
// 7. 构建响应
return AdminStatsResp.builder()
.todayActiveUser(todayActiveUser)
.totalGroup(totalGroup)
.blackCount(blackCount)
.aiCallToday(0) // TODO: 对接 AI 服务
.blackStats(blackStats)
.aiStats(aiStats)
.monthlyLogin3PlusUserCount(month3plus != null ? month3plus.intValue() : 0)
.build();
}
@Resource
private BaseLoginLogApi baseLoginLogApi;
@Override
public List<LoginRankResp> getLoginRank(LocalDateTime start, LocalDateTime end, Integer limit) {
List<BaseLoginLogApi.LoginRankDTO> list = baseLoginLogApi.getLoginRank(start, end, limit).getData();
Map<Long, User> userMap = list.stream()
.map(dto -> dto.uid)
.distinct()
.map(userDao::getById)
.filter(u -> u != null)
.collect(Collectors.toMap(User::getId, u -> u));
return list.stream().map(dto -> {
User u = userMap.get(dto.uid);
return LoginRankResp.builder()
.userId(dto.uid)
.username(u != null ? u.getAccount() : null)
.nickName(u != null ? u.getName() : null)
.total(dto.total)
.build();
}).collect(Collectors.toList());
}
@Override
public List<ActiveUserResp> getActiveUsers(LocalDateTime start, LocalDateTime end, Integer limit) {
LocalDateTime s = start != null ? start : LocalDateTime.now().minusDays(30).with(LocalTime.MIN);
LocalDateTime e = end != null ? end : LocalDateTime.now().with(LocalTime.MAX);
int lim = limit != null ? limit : 200;
List<BaseLoginLogApi.LoginRankDTO> ranks = baseLoginLogApi.getLoginRank(s, e, lim).getData();
List<User> list = userCache.getBatch(ranks.stream().map(item -> item.uid).collect(Collectors.toList())).values().stream().toList();
Map<Long, Long> countMap = ranks == null ? Collections.emptyMap() : ranks.stream().collect(Collectors.toMap(r -> r.uid, r -> r.total, (a, b) -> a));
List<ActiveUserResp> res = list.stream().map(u -> {
String ip = null;
String location = null;
String isp = null;
var info = u.getIpInfo();
if (info != null) {
ip = info.getUpdateIp();
var detail = info.getUpdateIpDetail();
if (detail != null) {
if (ip == null) ip = detail.getIp();
String country = detail.getCountry();
String region = detail.getRegion();
String city = detail.getCity();
isp = detail.getIsp();
StringBuilder sb = new StringBuilder();
if (country != null && !country.isEmpty()) sb.append(country);
if (region != null && !region.isEmpty()) sb.append(region);
if (city != null && !city.isEmpty()) sb.append(city);
location = sb.length() > 0 ? sb.toString() : null;
}
}
ActiveUserResp resp = new ActiveUserResp();
resp.setUsername(u.getAccount());
resp.setNickName(u.getName());
resp.setAvatar(u.getAvatar());
resp.setLastOptTime(u.getLastOptTime());
resp.setIp(ip);
resp.setLocation(location);
resp.setIsp(isp);
resp.setLoginTimes(countMap.getOrDefault(u.getId(), 0L).intValue());
return resp;
}).collect(Collectors.toList());
res.sort(Comparator.comparing(ActiveUserResp::getLoginTimes, Comparator.nullsFirst(Integer::compareTo)).reversed());
return res;
}
}

View File

@@ -3,14 +3,21 @@ package com.luohuo.flex.im.controller.admin;
import com.luohuo.basic.base.R;
import com.luohuo.flex.im.core.admin.service.AdminStatsService;
import com.luohuo.flex.im.domain.vo.resp.admin.AdminStatsResp;
import com.luohuo.flex.im.domain.vo.resp.admin.LoginRankResp;
import com.luohuo.flex.im.domain.vo.resp.admin.ActiveUserResp;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
/**
* 后台管理统计接口
* @author 乾乾
@@ -29,4 +36,40 @@ public class AdminStatsController {
public R<AdminStatsResp> getHomeStats() {
return R.success(adminStatsService.getHomeStats());
}
@GetMapping("/login-rank")
@Operation(summary = "用户登录次数排行榜")
public R<List<LoginRankResp>> getLoginRank(
@RequestParam(value = "start", required = false) LocalDateTime start,
@RequestParam(value = "end", required = false) LocalDateTime end,
@RequestParam(value = "rangeDays", required = false) Integer rangeDays,
@RequestParam(value = "limit", required = false) Integer limit
) {
LocalDateTime now = LocalDateTime.now();
if (start == null || end == null) {
int days = rangeDays != null ? rangeDays : 30;
start = now.minusDays(days).with(LocalTime.MIN);
end = now.with(LocalTime.MAX);
}
if (limit == null) limit = 50;
return R.success(adminStatsService.getLoginRank(start, end, limit));
}
@GetMapping("/active-users")
@Operation(summary = "活跃用户列表(按时间范围)")
public R<List<ActiveUserResp>> getActiveUsers(
@RequestParam(value = "start", required = false) LocalDateTime start,
@RequestParam(value = "end", required = false) LocalDateTime end,
@RequestParam(value = "rangeDays", required = false) Integer rangeDays,
@RequestParam(value = "limit", required = false) Integer limit
) {
LocalDateTime now = LocalDateTime.now();
if (start == null || end == null) {
int days = rangeDays != null ? rangeDays : 30;
start = now.minusDays(days).with(LocalTime.MIN);
end = now.with(LocalTime.MAX);
}
if (limit == null) limit = 200;
return R.success(adminStatsService.getActiveUsers(start, end, limit));
}
}

View File

@@ -0,0 +1,25 @@
package com.luohuo.flex.im.domain.vo.resp.admin;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "活跃用户")
public class ActiveUserResp {
private String username;
private String nickName;
private String avatar;
private LocalDateTime lastOptTime;
private String ip;
private String location;
private String isp;
private Integer loginTimes;
}

View File

@@ -35,6 +35,9 @@ public class AdminStatsResp {
@Schema(description = "AI 统计")
private AiStats aiStats;
@Schema(description = "最近一个月登录≥3次的用户总数")
private Integer monthlyLogin3PlusUserCount;
@Data
@Builder
@AllArgsConstructor

View File

@@ -0,0 +1,27 @@
package com.luohuo.flex.im.domain.vo.resp.admin;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Schema(description = "用户登录次数排行榜项")
public class LoginRankResp {
@Schema(description = "用户ID")
private Long userId;
@Schema(description = "用户名")
private String username;
@Schema(description = "昵称")
private String nickName;
@Schema(description = "登录总次数")
private Long total;
}

View File

@@ -0,0 +1,31 @@
package com.luohuo.flex.im.api;
import com.luohuo.basic.base.R;
import io.swagger.v3.oas.annotations.Operation;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.time.LocalDateTime;
import java.util.List;
@FeignClient(name = "${luohuo.feign.base-server:luohuo-base-server}", path = "/baseLoginLog")
public interface BaseLoginLogApi {
@GetMapping("/stats/login-rank")
@Operation(summary = "登录次数排行榜status=01")
R<List<LoginRankDTO>> getLoginRank(@RequestParam("start") LocalDateTime start,
@RequestParam("end") LocalDateTime end,
@RequestParam("limit") Integer limit);
@GetMapping("/stats/user-count")
@Operation(summary = "登录次数达到阈值的用户数量status=01")
R<Long> countUsersWithMinLogins(@RequestParam("start") LocalDateTime start,
@RequestParam("end") LocalDateTime end,
@RequestParam("minTimes") Integer minTimes);
class LoginRankDTO {
public Long uid;
public Long total;
}
}

View File

@@ -7,6 +7,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import com.luohuo.basic.database.mybatis.conditions.Wraps;
import com.luohuo.basic.jackson.JsonUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.luohuo.basic.utils.BeanPlusUtil;
import com.luohuo.basic.utils.CollHelper;
import com.luohuo.basic.utils.StrPool;
@@ -41,6 +42,7 @@ public class ResourceBiz {
private final DefResourceService defResourceService;
private final DefApplicationService defApplicationService;
private final BaseRoleService baseRoleService;
private final ObjectMapper objectMapper;
/**
* 是否所有的子都是视图
@@ -205,7 +207,11 @@ public class ResourceBiz {
log.debug("level={}, label={}", level, item.getName());
RouterMeta meta = null;
if (StrUtil.isNotEmpty(item.getMetaJson()) && !StrPool.BRACE.equals(item.getMetaJson())) {
meta = JsonUtil.parse(item.getMetaJson(), RouterMeta.class);
try {
meta = JsonUtil.parse(item.getMetaJson(), RouterMeta.class);
} catch (Exception ignored) {
try { meta = objectMapper.readValue(item.getMetaJson(), RouterMeta.class); } catch (Exception ignored2) {}
}
}
if (meta == null && item.getMeta() != null) {
meta = item.getMeta();
@@ -255,7 +261,11 @@ public class ResourceBiz {
log.debug("level={}, label={}", level, item.getName());
RouterMeta meta = null;
if (StrUtil.isNotEmpty(item.getMetaJson()) && !StrPool.BRACE.equals(item.getMetaJson())) {
meta = JsonUtil.parse(item.getMetaJson(), RouterMeta.class);
try {
meta = JsonUtil.parse(item.getMetaJson(), RouterMeta.class);
} catch (Exception ignored) {
try { meta = objectMapper.readValue(item.getMetaJson(), RouterMeta.class); } catch (Exception ignored2) {}
}
}
if (meta == null && item.getMeta() != null) {
meta = item.getMeta();
@@ -336,7 +346,11 @@ public class ResourceBiz {
RouterMeta meta = null;
if (item.getMeta() == null) {
if (StrUtil.isNotEmpty(item.getMetaJson()) && !StrPool.BRACE.equals(item.getMetaJson())) {
meta = JsonUtil.parse(item.getMetaJson(), RouterMeta.class);
try {
meta = JsonUtil.parse(item.getMetaJson(), RouterMeta.class);
} catch (Exception ignored) {
try { meta = objectMapper.readValue(item.getMetaJson(), RouterMeta.class); } catch (Exception ignored2) {}
}
}
if (meta == null && item.getMeta() != null) {
meta = item.getMeta();

View File

@@ -21,6 +21,6 @@ public class HeartbeatProcessor implements MessageProcessor {
@Override
public void process(WebSocketSession session, Long uid, WSBaseReq req) {
log.info("收到用户 {} 的心跳", uid);
// log.info("收到用户 {} 的心跳", uid);
}
}