diff --git a/.gitignore b/.gitignore index f8d850d..6f7b3f2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,56 +1,45 @@ -# --- Common --- -.DS_Store -Thumbs.db -Desktop.ini -*.log -*.tmp -*.bak -*.swp - -# --- IDEs & Editors --- +# IDEs .idea/ .vscode/ *.iml -*.ipr *.iws +*.ipr .classpath .project .settings/ .factorypath -# --- Java / Maven (Backend) --- +# OS +.DS_Store +Thumbs.db +ehthumbs.db +Desktop.ini + +# Maven target/ -*.class -*.jar -*.war -*.ear -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar +.mvn/ +mvnw +mvnw.cmd +*.log -# --- Node / Vue (Frontend) --- -node_modules/ -dist/ -dist-ssr/ -coverage/ -*.local -.npm/ -# Logs -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* +# Frontend (Vue/Vite) +ui/node_modules/ +ui/dist/ +ui/npm-debug.log* +ui/yarn-debug.log* +ui/yarn-error.log* +ui/pnpm-debug.log* +ui/.env.local +ui/.env.*.local +ui/.DS_Store +ui/coverage/ +ui/.nyc_output/ -# --- Application Specific --- -# Uploaded files -uploads/ -# Ignore local environment override files if any -.env.local -.env.development.local -.env.test.local -.env.production.local +# Application Logs +logs/ +*.log + +# Temp files +*.tmp +*.bak +*.swp diff --git a/db/init.sql b/db/init.sql index 55a97b0..ffc397b 100644 --- a/db/init.sql +++ b/db/init.sql @@ -11,12 +11,48 @@ Target Server Version : 80040 (8.0.40) File Encoding : 65001 - Date: 20/01/2026 15:33:14 + Date: 22/01/2026 14:58:04 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; +-- ---------------------------- +-- Table structure for ai_conversations +-- ---------------------------- +DROP TABLE IF EXISTS `ai_conversations`; +CREATE TABLE `ai_conversations` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '对话标题', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_user_id`(`user_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 38 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '对话记录表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of ai_conversations +-- ---------------------------- + +-- ---------------------------- +-- Table structure for ai_messages +-- ---------------------------- +DROP TABLE IF EXISTS `ai_messages`; +CREATE TABLE `ai_messages` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `conversation_id` bigint NOT NULL COMMENT '所属对话ID', + `role` enum('user','assistant') CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '消息角色', + `content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '消息内容', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`) USING BTREE, + INDEX `idx_conversation_id`(`conversation_id` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 159 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '对话消息表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of ai_messages +-- ---------------------------- + -- ---------------------------- -- Table structure for sys_menu -- ---------------------------- @@ -107,13 +143,13 @@ CREATE TABLE `sys_user` ( PRIMARY KEY (`id`) USING BTREE, UNIQUE INDEX `uk_username`(`username` ASC) USING BTREE, INDEX `idx_status`(`status` ASC) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统用户表' ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 24 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '系统用户表' ROW_FORMAT = Dynamic; -- ---------------------------- -- Records of sys_user -- ---------------------------- -INSERT INTO `sys_user` VALUES (1, 'hertz', '$2a$10$Gker6.ggCxG3wfZ13rE/Eu7aDnB.DX2JmP6h6vct30RTtBr9.q5Pq', '赫兹', '', '18888888888', 'hertz@hertz.com', 1, 1, '2026-01-19 17:30:21', '2026-01-20 15:33:01'); -INSERT INTO `sys_user` VALUES (2, 'demo', '$2a$10$PSIz9pWXAwXfB32HWSxTjeGhVi0bixsSKxzeX8YAdKnRRXPxJC3Xe', 'demo', '', '13888888888', 'demo@hertz.com', 1, 1, '2026-01-19 17:30:21', '2026-01-20 15:33:04'); +INSERT INTO `sys_user` VALUES (1, 'hertz', '$2a$10$Gker6.ggCxG3wfZ13rE/Eu7aDnB.DX2JmP6h6vct30RTtBr9.q5Pq', '赫兹', '/uploads/avatar/2026/01/20/56b8d363d1c743d6afb749faa0b4f5cb.png', '18888888888', 'hertz@hertz.com', 1, 1, '2026-01-19 17:30:21', '2026-01-22 14:29:46'); +INSERT INTO `sys_user` VALUES (2, 'demo', '$2a$10$PSIz9pWXAwXfB32HWSxTjeGhVi0bixsSKxzeX8YAdKnRRXPxJC3Xe', 'demo', '/uploads/avatar/2026/01/20/e19931fe65664b1ba31bf3db85cc6519.jpg', '13888888888', 'demo@hertz.com', 1, 1, '2026-01-19 17:30:21', '2026-01-20 17:57:47'); -- ---------------------------- -- Table structure for sys_user_role diff --git a/hertz_springboot_ui/README.md b/hertz_springboot_ui/README.md deleted file mode 100644 index 33895ab..0000000 --- a/hertz_springboot_ui/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Vue 3 + TypeScript + Vite - -This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 ` - diff --git a/hertz_springboot_ui/src/views/portal/About.vue b/hertz_springboot_ui/src/views/portal/About.vue deleted file mode 100644 index 901b1b5..0000000 --- a/hertz_springboot_ui/src/views/portal/About.vue +++ /dev/null @@ -1,8 +0,0 @@ - - - \ No newline at end of file diff --git a/hertz_springboot/pom.xml b/pom.xml similarity index 69% rename from hertz_springboot/pom.xml rename to pom.xml index 6c61e00..cb86e90 100644 --- a/hertz_springboot/pom.xml +++ b/pom.xml @@ -12,9 +12,9 @@ com.hertz - hertz-springboot-backend + hertz-springboot 0.0.1-SNAPSHOT - hertz-springboot-backend + hertz-springboot Hertz 权限管理系统后端 @@ -22,8 +22,21 @@ 21 3.5.8 0.12.6 + 1.0.0-M5 + + + + org.springframework.ai + spring-ai-bom + ${spring-ai.version} + pom + import + + + + org.springframework.boot @@ -38,6 +51,15 @@ spring-boot-starter-security + + org.springframework.ai + spring-ai-ollama-spring-boot-starter + + + org.springframework.boot + spring-boot-starter-actuator + + com.baomidou mybatis-plus-spring-boot3-starter @@ -74,6 +96,23 @@ runtime + + org.springframework.boot + spring-boot-starter-data-redis + + + cn.hutool + hutool-all + 5.8.25 + + + + + com.github.oshi + oshi-core + 6.6.5 + + org.springframework.boot spring-boot-starter-test @@ -86,6 +125,17 @@ + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + diff --git a/hertz_springboot/src/main/java/com/hertz/HertzApplication.java b/src/main/java/com/hertz/HertzApplication.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/HertzApplication.java rename to src/main/java/com/hertz/HertzApplication.java diff --git a/src/main/java/com/hertz/ai/controller/AiController.java b/src/main/java/com/hertz/ai/controller/AiController.java new file mode 100644 index 0000000..f96d2e5 --- /dev/null +++ b/src/main/java/com/hertz/ai/controller/AiController.java @@ -0,0 +1,84 @@ +package com.hertz.ai.controller; + +import com.hertz.ai.dto.ChatRequest; +import com.hertz.ai.entity.Conversation; +import com.hertz.ai.entity.Message; +import com.hertz.ai.service.AiService; +import com.hertz.ai.service.ConversationService; +import com.hertz.common.api.ApiResponse; +import com.hertz.security.SecurityUtils; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.*; +import reactor.core.publisher.Flux; + +import java.util.List; + +@RestController +@RequestMapping("/api/ai") +@RequiredArgsConstructor +public class AiController { + + private final AiService aiService; + private final ConversationService conversationService; + + @PostMapping("/chat") + public ApiResponse chat(@RequestBody ChatRequest request) { + if (request.getConversationId() != null) { + conversationService.saveMessage(request.getConversationId(), "user", request.getMessage()); + } + String response = aiService.chat(request.getMessage(), request.getTemperature()); + if (request.getConversationId() != null) { + conversationService.saveMessage(request.getConversationId(), "assistant", response); + } + return ApiResponse.ok(response); + } + + @PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) + public Flux streamChat(@RequestBody ChatRequest request) { + // Frontend handles message persistence + return aiService.streamChat(request.getMessage(), request.getTemperature()); + } + + @PostMapping("/conversations") + public ApiResponse createConversation(@RequestBody Conversation conversation) { + Long userId = SecurityUtils.getCurrentUserId(); + return ApiResponse.ok(conversationService.createConversation(conversation.getTitle(), userId)); + } + + @GetMapping("/conversations") + public ApiResponse> listConversations() { + Long userId = SecurityUtils.getCurrentUserId(); + return ApiResponse.ok(conversationService.getConversations(userId)); + } + + @DeleteMapping("/conversations/{id}") + public ApiResponse deleteConversation(@PathVariable Long id) { + Long userId = SecurityUtils.getCurrentUserId(); + conversationService.deleteConversation(id, userId); + return ApiResponse.ok(); + } + + @PutMapping("/conversations/{id}") + public ApiResponse updateConversation(@PathVariable Long id, @RequestBody Conversation conversation) { + Long userId = SecurityUtils.getCurrentUserId(); + conversationService.updateConversationTitle(id, conversation.getTitle(), userId); + return ApiResponse.ok(); + } + + @GetMapping("/conversations/search") + public ApiResponse> searchConversations(@RequestParam String query) { + Long userId = SecurityUtils.getCurrentUserId(); + return ApiResponse.ok(conversationService.searchConversations(query, userId)); + } + + @PostMapping("/conversations/{id}/messages") + public ApiResponse saveMessage(@PathVariable Long id, @RequestBody Message message) { + return ApiResponse.ok(conversationService.saveMessage(id, message.getRole(), message.getContent())); + } + + @GetMapping("/conversations/{id}/messages") + public ApiResponse> getMessages(@PathVariable Long id) { + return ApiResponse.ok(conversationService.getMessages(id)); + } +} diff --git a/src/main/java/com/hertz/ai/dto/ChatRequest.java b/src/main/java/com/hertz/ai/dto/ChatRequest.java new file mode 100644 index 0000000..3567ed9 --- /dev/null +++ b/src/main/java/com/hertz/ai/dto/ChatRequest.java @@ -0,0 +1,10 @@ +package com.hertz.ai.dto; + +import lombok.Data; + +@Data +public class ChatRequest { + private String message; + private Double temperature; + private Long conversationId; +} diff --git a/src/main/java/com/hertz/ai/entity/Conversation.java b/src/main/java/com/hertz/ai/entity/Conversation.java new file mode 100644 index 0000000..c26cae3 --- /dev/null +++ b/src/main/java/com/hertz/ai/entity/Conversation.java @@ -0,0 +1,18 @@ +package com.hertz.ai.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@TableName("ai_conversations") +public class Conversation { + @TableId(type = IdType.AUTO) + private Long id; + private Long userId; + private String title; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/src/main/java/com/hertz/ai/entity/Message.java b/src/main/java/com/hertz/ai/entity/Message.java new file mode 100644 index 0000000..6417e41 --- /dev/null +++ b/src/main/java/com/hertz/ai/entity/Message.java @@ -0,0 +1,21 @@ +package com.hertz.ai.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import java.time.LocalDateTime; +import lombok.Data; + +@Data +@TableName("ai_messages") +public class Message { + @TableId(type = IdType.AUTO) + private Long id; + private Long conversationId; + /** + * user or assistant + */ + private String role; + private String content; + private LocalDateTime createdAt; +} diff --git a/src/main/java/com/hertz/ai/mapper/ConversationMapper.java b/src/main/java/com/hertz/ai/mapper/ConversationMapper.java new file mode 100644 index 0000000..fe2f731 --- /dev/null +++ b/src/main/java/com/hertz/ai/mapper/ConversationMapper.java @@ -0,0 +1,9 @@ +package com.hertz.ai.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.hertz.ai.entity.Conversation; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface ConversationMapper extends BaseMapper { +} diff --git a/src/main/java/com/hertz/ai/mapper/MessageMapper.java b/src/main/java/com/hertz/ai/mapper/MessageMapper.java new file mode 100644 index 0000000..c0d36ce --- /dev/null +++ b/src/main/java/com/hertz/ai/mapper/MessageMapper.java @@ -0,0 +1,9 @@ +package com.hertz.ai.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.hertz.ai.entity.Message; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface MessageMapper extends BaseMapper { +} diff --git a/src/main/java/com/hertz/ai/service/AiService.java b/src/main/java/com/hertz/ai/service/AiService.java new file mode 100644 index 0000000..f2a9d69 --- /dev/null +++ b/src/main/java/com/hertz/ai/service/AiService.java @@ -0,0 +1,8 @@ +package com.hertz.ai.service; + +import reactor.core.publisher.Flux; + +public interface AiService { + String chat(String message, Double temperature); + Flux streamChat(String message, Double temperature); +} diff --git a/src/main/java/com/hertz/ai/service/ConversationService.java b/src/main/java/com/hertz/ai/service/ConversationService.java new file mode 100644 index 0000000..1bc24c3 --- /dev/null +++ b/src/main/java/com/hertz/ai/service/ConversationService.java @@ -0,0 +1,16 @@ +package com.hertz.ai.service; + +import com.hertz.ai.entity.Conversation; +import com.hertz.ai.entity.Message; +import java.util.List; + +public interface ConversationService { + Conversation createConversation(String title, Long userId); + List getConversations(Long userId); + void deleteConversation(Long id, Long userId); + List searchConversations(String query, Long userId); + Message saveMessage(Long conversationId, String role, String content); + List getMessages(Long conversationId); + Conversation getConversation(Long id); + void updateConversationTitle(Long id, String title, Long userId); +} diff --git a/src/main/java/com/hertz/ai/service/impl/AiServiceImpl.java b/src/main/java/com/hertz/ai/service/impl/AiServiceImpl.java new file mode 100644 index 0000000..d7cdc65 --- /dev/null +++ b/src/main/java/com/hertz/ai/service/impl/AiServiceImpl.java @@ -0,0 +1,71 @@ +package com.hertz.ai.service.impl; + +import com.hertz.ai.service.AiService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.SystemMessage; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.ollama.OllamaChatModel; +import org.springframework.ai.ollama.api.OllamaOptions; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; + +import java.util.List; + +@Service +@Slf4j +@RequiredArgsConstructor +public class AiServiceImpl implements AiService { + + private final OllamaChatModel chatModel; + private static final String SYSTEM_PROMPT = "你是赫兹官网的AI助手。赫兹是一个基于Spring Boot的权限控制管理系统框架,用于构建高性能、可扩展的应用程序。" + + "你可以回答关于赫兹的问题,如:赫兹的架构、性能优化、微服务设计模式等。" + + "如果用户有其他问题,也请尽力回答。"; + + @Override + public String chat(String message, Double temperature) { + long startTime = System.currentTimeMillis(); + try { + log.info("Starting AI chat request. Message length: {}", message.length()); + + OllamaOptions options = OllamaOptions.builder() + .temperature(temperature != null ? temperature : 0.7) + .build(); + + List messages = List.of( + new SystemMessage(SYSTEM_PROMPT), + new UserMessage(message) + ); + + String response = chatModel.call(new Prompt(messages, options)).getResult().getOutput().getContent(); + + log.info("AI chat request completed in {}ms", System.currentTimeMillis() - startTime); + return response; + } catch (Exception e) { + log.error("AI chat request failed", e); + throw new RuntimeException("AI service unavailable: " + e.getMessage()); + } + } + + @Override + public Flux streamChat(String message, Double temperature) { + OllamaOptions options = OllamaOptions.builder() + .temperature(temperature != null ? temperature : 0.7) + .build(); + + List messages = List.of( + new SystemMessage(SYSTEM_PROMPT), + new UserMessage(message) + ); + + return chatModel.stream(new Prompt(messages, options)) + .map(response -> { + String content = response.getResult().getOutput().getContent(); + return content != null ? content : ""; + }) + .doOnError(e -> log.error("Error in AI stream", e)) + .onErrorResume(e -> Flux.just(" [Error: " + e.getMessage() + "]")); + } +} diff --git a/src/main/java/com/hertz/ai/service/impl/ConversationServiceImpl.java b/src/main/java/com/hertz/ai/service/impl/ConversationServiceImpl.java new file mode 100644 index 0000000..5e11851 --- /dev/null +++ b/src/main/java/com/hertz/ai/service/impl/ConversationServiceImpl.java @@ -0,0 +1,106 @@ +package com.hertz.ai.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.hertz.ai.entity.Conversation; +import com.hertz.ai.entity.Message; +import com.hertz.ai.mapper.ConversationMapper; +import com.hertz.ai.mapper.MessageMapper; +import com.hertz.ai.service.ConversationService; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class ConversationServiceImpl implements ConversationService { + + private final ConversationMapper conversationMapper; + private final MessageMapper messageMapper; + + @Override + @Transactional + public Conversation createConversation(String title, Long userId) { + Conversation conversation = new Conversation(); + conversation.setUserId(userId); + conversation.setTitle(title != null && !title.isEmpty() ? title : "New Chat"); + conversation.setCreatedAt(LocalDateTime.now()); + conversation.setUpdatedAt(LocalDateTime.now()); + conversationMapper.insert(conversation); + return conversation; + } + + @Override + public List getConversations(Long userId) { + return conversationMapper.selectList(new LambdaQueryWrapper() + .eq(Conversation::getUserId, userId) + .orderByDesc(Conversation::getUpdatedAt)); + } + + @Override + @Transactional + public void deleteConversation(Long id, Long userId) { + Conversation conversation = conversationMapper.selectById(id); + if (conversation != null && conversation.getUserId().equals(userId)) { + messageMapper.delete(new LambdaQueryWrapper() + .eq(Message::getConversationId, id)); + conversationMapper.deleteById(id); + } else { + throw new RuntimeException("Conversation not found or access denied"); + } + } + + @Override + public List searchConversations(String query, Long userId) { + return conversationMapper.selectList(new LambdaQueryWrapper() + .eq(Conversation::getUserId, userId) + .like(Conversation::getTitle, query) + .orderByDesc(Conversation::getUpdatedAt)); + } + + @Override + @Transactional + public Message saveMessage(Long conversationId, String role, String content) { + Message message = new Message(); + message.setConversationId(conversationId); + message.setRole(role); + message.setContent(content); + message.setCreatedAt(LocalDateTime.now()); + messageMapper.insert(message); + + Conversation conversation = conversationMapper.selectById(conversationId); + if (conversation != null) { + conversation.setUpdatedAt(LocalDateTime.now()); + conversationMapper.updateById(conversation); + } + + return message; + } + + @Override + public List getMessages(Long conversationId) { + return messageMapper.selectList(new LambdaQueryWrapper() + .eq(Message::getConversationId, conversationId) + .orderByAsc(Message::getCreatedAt)); + } + + @Override + public Conversation getConversation(Long id) { + return conversationMapper.selectById(id); + } + + @Override + @Transactional + public void updateConversationTitle(Long id, String title, Long userId) { + Conversation conversation = conversationMapper.selectById(id); + if (conversation != null && conversation.getUserId().equals(userId)) { + conversation.setTitle(title); + conversation.setUpdatedAt(LocalDateTime.now()); + conversationMapper.updateById(conversation); + } else { + throw new RuntimeException("Conversation not found or access denied"); + } + } +} diff --git a/hertz_springboot/src/main/java/com/hertz/common/api/ApiResponse.java b/src/main/java/com/hertz/common/api/ApiResponse.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/common/api/ApiResponse.java rename to src/main/java/com/hertz/common/api/ApiResponse.java diff --git a/hertz_springboot/src/main/java/com/hertz/common/exception/BusinessException.java b/src/main/java/com/hertz/common/exception/BusinessException.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/common/exception/BusinessException.java rename to src/main/java/com/hertz/common/exception/BusinessException.java diff --git a/hertz_springboot/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java b/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java similarity index 97% rename from hertz_springboot/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java rename to src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java index f34d956..5649b79 100644 --- a/hertz_springboot/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/hertz/common/exception/GlobalExceptionHandler.java @@ -54,6 +54,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ApiResponse handleException(Exception e) { + e.printStackTrace(); // 打印堆栈信息到控制台 return ApiResponse.fail(50000, "系统异常"); } } diff --git a/src/main/java/com/hertz/common/filter/RequestLogFilter.java b/src/main/java/com/hertz/common/filter/RequestLogFilter.java new file mode 100644 index 0000000..c431765 --- /dev/null +++ b/src/main/java/com/hertz/common/filter/RequestLogFilter.java @@ -0,0 +1,35 @@ +package com.hertz.common.filter; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +@Slf4j +public class RequestLogFilter extends OncePerRequestFilter { + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + long startTime = System.currentTimeMillis(); + String method = request.getMethod(); + String uri = request.getRequestURI(); + + try { + filterChain.doFilter(request, response); + } finally { + long endTime = System.currentTimeMillis(); + long timeTaken = endTime - startTime; + int status = response.getStatus(); + + log.info("Request: [{}] {} | Status: {} | Time: {}ms", method, uri, status, timeTaken); + } + } +} diff --git a/src/main/java/com/hertz/monitor/controller/MonitorController.java b/src/main/java/com/hertz/monitor/controller/MonitorController.java new file mode 100644 index 0000000..56d42e1 --- /dev/null +++ b/src/main/java/com/hertz/monitor/controller/MonitorController.java @@ -0,0 +1,22 @@ +package com.hertz.monitor.controller; + +import com.hertz.common.api.ApiResponse; +import com.hertz.monitor.dto.MonitorDto; +import com.hertz.monitor.service.MonitorService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/monitor") +@RequiredArgsConstructor +public class MonitorController { + + private final MonitorService monitorService; + + @GetMapping("/server") + public ApiResponse getServerInfo() { + return ApiResponse.ok(monitorService.getServerInfo()); + } +} diff --git a/src/main/java/com/hertz/monitor/dto/MonitorDto.java b/src/main/java/com/hertz/monitor/dto/MonitorDto.java new file mode 100644 index 0000000..dbbe843 --- /dev/null +++ b/src/main/java/com/hertz/monitor/dto/MonitorDto.java @@ -0,0 +1,89 @@ +package com.hertz.monitor.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.List; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class MonitorDto { + private CpuInfo cpu; + private MemInfo mem; + private JvmInfo jvm; + private SysInfo sys; + private List disks; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class CpuInfo { + private int cpuNum; + private double total; + private double sys; + private double used; + private double wait; + private double free; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class MemInfo { + private double total; + private double used; + private double free; + private double usage; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class JvmInfo { + private double total; + private double max; + private double free; + private String version; + private String home; + private double usage; + private String startTime; + private String runTime; + private String inputArgs; + private String name; + private long gcCount; + private long gcTime; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SysInfo { + private String computerName; + private String computerIp; + private String userDir; + private String osName; + private String osArch; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class DiskInfo { + private String dirName; + private String sysTypeName; + private String typeName; + private String total; + private String free; + private String used; + private double usage; + } +} diff --git a/src/main/java/com/hertz/monitor/service/MonitorService.java b/src/main/java/com/hertz/monitor/service/MonitorService.java new file mode 100644 index 0000000..6e0ae81 --- /dev/null +++ b/src/main/java/com/hertz/monitor/service/MonitorService.java @@ -0,0 +1,7 @@ +package com.hertz.monitor.service; + +import com.hertz.monitor.dto.MonitorDto; + +public interface MonitorService { + MonitorDto getServerInfo(); +} diff --git a/src/main/java/com/hertz/monitor/service/impl/MonitorServiceImpl.java b/src/main/java/com/hertz/monitor/service/impl/MonitorServiceImpl.java new file mode 100644 index 0000000..e8de52d --- /dev/null +++ b/src/main/java/com/hertz/monitor/service/impl/MonitorServiceImpl.java @@ -0,0 +1,165 @@ +package com.hertz.monitor.service.impl; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.net.NetUtil; +import cn.hutool.core.util.NumberUtil; +import com.hertz.monitor.dto.MonitorDto; +import com.hertz.monitor.service.MonitorService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import oshi.SystemInfo; +import oshi.hardware.CentralProcessor; +import oshi.hardware.CentralProcessor.TickType; +import oshi.hardware.GlobalMemory; +import oshi.hardware.HardwareAbstractionLayer; +import oshi.software.os.FileSystem; +import oshi.software.os.OSFileStore; +import oshi.software.os.OperatingSystem; +import oshi.util.Util; + +import java.lang.management.GarbageCollectorMXBean; +import java.lang.management.ManagementFactory; +import java.lang.management.RuntimeMXBean; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Properties; + +@Slf4j +@Service +public class MonitorServiceImpl implements MonitorService { + + private final SystemInfo si = new SystemInfo(); + + @Override + public MonitorDto getServerInfo() { + HardwareAbstractionLayer hal = si.getHardware(); + OperatingSystem os = si.getOperatingSystem(); + + return MonitorDto.builder() + .cpu(getCpuInfo(hal.getProcessor())) + .mem(getMemInfo(hal.getMemory())) + .sys(getSysInfo()) + .jvm(getJvmInfo()) + .disks(getDiskInfo(os)) + .build(); + } + + private MonitorDto.CpuInfo getCpuInfo(CentralProcessor processor) { + long[] prevTicks = processor.getSystemCpuLoadTicks(); + Util.sleep(1000); + long[] ticks = processor.getSystemCpuLoadTicks(); + long nice = ticks[TickType.NICE.getIndex()] - prevTicks[TickType.NICE.getIndex()]; + long irq = ticks[TickType.IRQ.getIndex()] - prevTicks[TickType.IRQ.getIndex()]; + long softirq = ticks[TickType.SOFTIRQ.getIndex()] - prevTicks[TickType.SOFTIRQ.getIndex()]; + long steal = ticks[TickType.STEAL.getIndex()] - prevTicks[TickType.STEAL.getIndex()]; + long cSys = ticks[TickType.SYSTEM.getIndex()] - prevTicks[TickType.SYSTEM.getIndex()]; + long user = ticks[TickType.USER.getIndex()] - prevTicks[TickType.USER.getIndex()]; + long iowait = ticks[TickType.IOWAIT.getIndex()] - prevTicks[TickType.IOWAIT.getIndex()]; + long idle = ticks[TickType.IDLE.getIndex()] - prevTicks[TickType.IDLE.getIndex()]; + long totalCpu = user + nice + cSys + idle + iowait + irq + softirq + steal; + + // Prevent division by zero + if (totalCpu == 0) totalCpu = 1; + + return MonitorDto.CpuInfo.builder() + .cpuNum(processor.getLogicalProcessorCount()) + .total(totalCpu) + .sys(NumberUtil.round(cSys * 100.0 / totalCpu, 2).doubleValue()) + .used(NumberUtil.round(user * 100.0 / totalCpu, 2).doubleValue()) + .wait(NumberUtil.round(iowait * 100.0 / totalCpu, 2).doubleValue()) + .free(NumberUtil.round(idle * 100.0 / totalCpu, 2).doubleValue()) + .build(); + } + + private MonitorDto.MemInfo getMemInfo(GlobalMemory memory) { + double total = NumberUtil.div(memory.getTotal(), (1024 * 1024 * 1024), 2); + double free = NumberUtil.div(memory.getAvailable(), (1024 * 1024 * 1024), 2); + double used = NumberUtil.sub(total, free); + double usage = NumberUtil.mul(NumberUtil.div(used, total, 4), 100); + + return MonitorDto.MemInfo.builder() + .total(total) + .used(used) + .free(free) + .usage(usage) + .build(); + } + + private MonitorDto.SysInfo getSysInfo() { + Properties props = System.getProperties(); + return MonitorDto.SysInfo.builder() + .computerName(NetUtil.getLocalHostName()) + .computerIp(NetUtil.getLocalhostStr()) + .osName(props.getProperty("os.name")) + .osArch(props.getProperty("os.arch")) + .userDir(props.getProperty("user.dir")) + .build(); + } + + private MonitorDto.JvmInfo getJvmInfo() { + Properties props = System.getProperties(); + RuntimeMXBean runtime = ManagementFactory.getRuntimeMXBean(); + long time = runtime.getStartTime(); + + // GC Info + long gcCount = 0; + long gcTime = 0; + List gcBeans = ManagementFactory.getGarbageCollectorMXBeans(); + for (GarbageCollectorMXBean gcBean : gcBeans) { + long count = gcBean.getCollectionCount(); + long t = gcBean.getCollectionTime(); + if (count > 0) gcCount += count; + if (t > 0) gcTime += t; + } + + double total = NumberUtil.div(Runtime.getRuntime().totalMemory(), (1024 * 1024), 2); + double max = NumberUtil.div(Runtime.getRuntime().maxMemory(), (1024 * 1024), 2); + double free = NumberUtil.div(Runtime.getRuntime().freeMemory(), (1024 * 1024), 2); + double used = total - free; + double usage = NumberUtil.mul(NumberUtil.div(used, total, 4), 100); + + return MonitorDto.JvmInfo.builder() + .total(total) + .max(max) + .free(free) + .version(props.getProperty("java.version")) + .home(props.getProperty("java.home")) + .name(ManagementFactory.getRuntimeMXBean().getVmName()) + .usage(usage) + .startTime(DateUtil.formatDateTime(new Date(time))) + .runTime(DateUtil.formatBetween(new Date(time), new Date())) + .inputArgs(runtime.getInputArguments().toString()) + .gcCount(gcCount) + .gcTime(gcTime) + .build(); + } + + private List getDiskInfo(OperatingSystem os) { + List list = new ArrayList<>(); + FileSystem fileSystem = os.getFileSystem(); + List fileStores = fileSystem.getFileStores(); + for (OSFileStore fs : fileStores) { + long free = fs.getUsableSpace(); + long total = fs.getTotalSpace(); + long used = total - free; + + if (total == 0) continue; + + double usage = NumberUtil.mul(NumberUtil.div(used, total, 4), 100); + + MonitorDto.DiskInfo disk = MonitorDto.DiskInfo.builder() + .dirName(fs.getMount()) + .sysTypeName(fs.getType()) + .typeName(fs.getName()) + .total(FileUtil.readableFileSize(total)) + .free(FileUtil.readableFileSize(free)) + .used(FileUtil.readableFileSize(used)) + .usage(usage) + .build(); + list.add(disk); + } + return list; + } +} diff --git a/src/main/java/com/hertz/security/CustomUserDetailsService.java b/src/main/java/com/hertz/security/CustomUserDetailsService.java new file mode 100644 index 0000000..c564bf8 --- /dev/null +++ b/src/main/java/com/hertz/security/CustomUserDetailsService.java @@ -0,0 +1,33 @@ +package com.hertz.security; + +import com.hertz.system.service.UserService; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.stereotype.Service; + +@Service +public class CustomUserDetailsService implements UserDetailsService { + + private final UserService userService; + + public CustomUserDetailsService(UserService userService) { + this.userService = userService; + } + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + var user = userService.findByUsername(username); + if (user == null) { + throw new UsernameNotFoundException("User not found: " + username); + } + // We are using JWT for auth, so this is mostly to satisfy Spring Security's default config + // or if we wanted to support Basic Auth/Form Login alongside JWT. + // For now, we return a minimal UserDetails implementation. + return org.springframework.security.core.userdetails.User + .withUsername(user.getUsername()) + .password(user.getPassword()) + .roles("USER") // Default role, actual authorities are loaded in JwtAuthFilter + .build(); + } +} diff --git a/hertz_springboot/src/main/java/com/hertz/security/JwtAuthFilter.java b/src/main/java/com/hertz/security/JwtAuthFilter.java similarity index 88% rename from hertz_springboot/src/main/java/com/hertz/security/JwtAuthFilter.java rename to src/main/java/com/hertz/security/JwtAuthFilter.java index f7bf564..85a7ccd 100644 --- a/hertz_springboot/src/main/java/com/hertz/security/JwtAuthFilter.java +++ b/src/main/java/com/hertz/security/JwtAuthFilter.java @@ -47,7 +47,11 @@ public class JwtAuthFilter extends OncePerRequestFilter { var auth = new UsernamePasswordAuthenticationToken(username, null, authorities); auth.setDetails(userId); SecurityContextHolder.getContext().setAuthentication(auth); - } catch (Exception ignored) { + // System.out.println("DEBUG: JWT Auth Success for user: " + username); + } catch (Exception e) { + System.err.println("DEBUG: JWT Auth Failed: " + e.getMessage()); + e.printStackTrace(); + logger.error("JWT Authentication failed: " + e.getMessage(), e); SecurityContextHolder.clearContext(); } diff --git a/hertz_springboot/src/main/java/com/hertz/security/JwtService.java b/src/main/java/com/hertz/security/JwtService.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/security/JwtService.java rename to src/main/java/com/hertz/security/JwtService.java diff --git a/hertz_springboot/src/main/java/com/hertz/security/SecurityConfig.java b/src/main/java/com/hertz/security/SecurityConfig.java similarity index 95% rename from hertz_springboot/src/main/java/com/hertz/security/SecurityConfig.java rename to src/main/java/com/hertz/security/SecurityConfig.java index fa24fd0..3fc585d 100644 --- a/hertz_springboot/src/main/java/com/hertz/security/SecurityConfig.java +++ b/src/main/java/com/hertz/security/SecurityConfig.java @@ -1,5 +1,6 @@ package com.hertz.security; +import jakarta.servlet.DispatcherType; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -47,6 +48,7 @@ public class SecurityConfig { .cors(Customizer.withDefaults()) .sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth + .dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll() .requestMatchers("/api/auth/**", "/error", "/uploads/**").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() .anyRequest().authenticated() diff --git a/hertz_springboot/src/main/java/com/hertz/security/SecurityUtils.java b/src/main/java/com/hertz/security/SecurityUtils.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/security/SecurityUtils.java rename to src/main/java/com/hertz/security/SecurityUtils.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/config/MybatisPlusConfig.java b/src/main/java/com/hertz/system/config/MybatisPlusConfig.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/config/MybatisPlusConfig.java rename to src/main/java/com/hertz/system/config/MybatisPlusConfig.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/config/WebMvcConfig.java b/src/main/java/com/hertz/system/config/WebMvcConfig.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/config/WebMvcConfig.java rename to src/main/java/com/hertz/system/config/WebMvcConfig.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/AuthController.java b/src/main/java/com/hertz/system/controller/AuthController.java similarity index 92% rename from hertz_springboot/src/main/java/com/hertz/system/controller/AuthController.java rename to src/main/java/com/hertz/system/controller/AuthController.java index 8e40ee7..b5edab3 100644 --- a/hertz_springboot/src/main/java/com/hertz/system/controller/AuthController.java +++ b/src/main/java/com/hertz/system/controller/AuthController.java @@ -6,6 +6,7 @@ import com.hertz.security.JwtService; import com.hertz.security.SecurityUtils; import com.hertz.system.dto.AuthDtos; import com.hertz.system.mapper.SysRoleMapper; +import com.hertz.system.service.CaptchaService; import com.hertz.system.service.UserService; import jakarta.validation.Valid; import java.util.List; @@ -24,21 +25,25 @@ public class AuthController { private final PasswordEncoder passwordEncoder; private final JwtService jwtService; private final SysRoleMapper roleMapper; + private final CaptchaService captchaService; public AuthController( UserService userService, PasswordEncoder passwordEncoder, JwtService jwtService, - SysRoleMapper roleMapper + SysRoleMapper roleMapper, + CaptchaService captchaService ) { this.userService = userService; this.passwordEncoder = passwordEncoder; this.jwtService = jwtService; this.roleMapper = roleMapper; + this.captchaService = captchaService; } @PostMapping("/register") public ApiResponse register(@Valid @RequestBody AuthDtos.RegisterRequest req) { + captchaService.validateCaptcha(req.uuid(), req.code()); var user = userService.register(req.username(), req.password(), req.nickname()); return ApiResponse.ok(new AuthDtos.MeResponse( user.getId(), @@ -54,6 +59,7 @@ public class AuthController { @PostMapping("/login") public ApiResponse login(@Valid @RequestBody AuthDtos.LoginRequest req) { + captchaService.validateCaptcha(req.uuid(), req.code()); var user = userService.findByUsername(req.username()); if (user == null || user.getStatus() == null || user.getStatus() != 1) { throw new BusinessException(40003, "用户名或密码错误"); diff --git a/src/main/java/com/hertz/system/controller/CaptchaController.java b/src/main/java/com/hertz/system/controller/CaptchaController.java new file mode 100644 index 0000000..1bcec03 --- /dev/null +++ b/src/main/java/com/hertz/system/controller/CaptchaController.java @@ -0,0 +1,25 @@ +package com.hertz.system.controller; + +import com.hertz.common.api.ApiResponse; +import com.hertz.system.dto.AuthDtos; +import com.hertz.system.service.CaptchaService; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/auth/captcha") +public class CaptchaController { + + private final CaptchaService captchaService; + + public CaptchaController(CaptchaService captchaService) { + this.captchaService = captchaService; + } + + @GetMapping + public ApiResponse getCaptcha() { + var res = captchaService.generateCaptcha(); + return ApiResponse.ok(res); + } +} diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/FileController.java b/src/main/java/com/hertz/system/controller/FileController.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/controller/FileController.java rename to src/main/java/com/hertz/system/controller/FileController.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/MenuController.java b/src/main/java/com/hertz/system/controller/MenuController.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/controller/MenuController.java rename to src/main/java/com/hertz/system/controller/MenuController.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/RoleController.java b/src/main/java/com/hertz/system/controller/RoleController.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/controller/RoleController.java rename to src/main/java/com/hertz/system/controller/RoleController.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/controller/UserController.java b/src/main/java/com/hertz/system/controller/UserController.java similarity index 95% rename from hertz_springboot/src/main/java/com/hertz/system/controller/UserController.java rename to src/main/java/com/hertz/system/controller/UserController.java index 4b23be6..b9bce12 100644 --- a/hertz_springboot/src/main/java/com/hertz/system/controller/UserController.java +++ b/src/main/java/com/hertz/system/controller/UserController.java @@ -135,6 +135,13 @@ public class UserController { return ApiResponse.ok(); } + @DeleteMapping + @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:remove')") + public ApiResponse deleteBatch(@RequestBody List ids) { + userService.deleteUsers(ids); + return ApiResponse.ok(); + } + @GetMapping("/{id}/roles") @PreAuthorize("hasRole('ADMIN') or hasAuthority('system:user:view')") public ApiResponse> roleIds(@PathVariable("id") long userId) { diff --git a/hertz_springboot/src/main/java/com/hertz/system/dto/AuthDtos.java b/src/main/java/com/hertz/system/dto/AuthDtos.java similarity index 76% rename from hertz_springboot/src/main/java/com/hertz/system/dto/AuthDtos.java rename to src/main/java/com/hertz/system/dto/AuthDtos.java index 93ece57..1798568 100644 --- a/hertz_springboot/src/main/java/com/hertz/system/dto/AuthDtos.java +++ b/src/main/java/com/hertz/system/dto/AuthDtos.java @@ -6,14 +6,18 @@ import java.util.List; public class AuthDtos { public record LoginRequest( @NotBlank String username, - @NotBlank String password + @NotBlank String password, + String uuid, + String code ) { } public record RegisterRequest( @NotBlank String username, @NotBlank String password, - String nickname + String nickname, + String uuid, + String code ) { } @@ -51,5 +55,11 @@ public class AuthDtos { @NotBlank(message = "新密码不能为空") String newPassword ) { } + + public record CaptchaResponse( + @com.fasterxml.jackson.annotation.JsonProperty("uuid") String uuid, + @com.fasterxml.jackson.annotation.JsonProperty("img") String img + ) { + } } diff --git a/hertz_springboot/src/main/java/com/hertz/system/dto/MenuDto.java b/src/main/java/com/hertz/system/dto/MenuDto.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/dto/MenuDto.java rename to src/main/java/com/hertz/system/dto/MenuDto.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysMenu.java b/src/main/java/com/hertz/system/entity/SysMenu.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/entity/SysMenu.java rename to src/main/java/com/hertz/system/entity/SysMenu.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysRole.java b/src/main/java/com/hertz/system/entity/SysRole.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/entity/SysRole.java rename to src/main/java/com/hertz/system/entity/SysRole.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysRoleMenu.java b/src/main/java/com/hertz/system/entity/SysRoleMenu.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/entity/SysRoleMenu.java rename to src/main/java/com/hertz/system/entity/SysRoleMenu.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysUser.java b/src/main/java/com/hertz/system/entity/SysUser.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/entity/SysUser.java rename to src/main/java/com/hertz/system/entity/SysUser.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/entity/SysUserRole.java b/src/main/java/com/hertz/system/entity/SysUserRole.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/entity/SysUserRole.java rename to src/main/java/com/hertz/system/entity/SysUserRole.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysMenuMapper.java b/src/main/java/com/hertz/system/mapper/SysMenuMapper.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/mapper/SysMenuMapper.java rename to src/main/java/com/hertz/system/mapper/SysMenuMapper.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMapper.java b/src/main/java/com/hertz/system/mapper/SysRoleMapper.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMapper.java rename to src/main/java/com/hertz/system/mapper/SysRoleMapper.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMenuMapper.java b/src/main/java/com/hertz/system/mapper/SysRoleMenuMapper.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/mapper/SysRoleMenuMapper.java rename to src/main/java/com/hertz/system/mapper/SysRoleMenuMapper.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserMapper.java b/src/main/java/com/hertz/system/mapper/SysUserMapper.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserMapper.java rename to src/main/java/com/hertz/system/mapper/SysUserMapper.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserRoleMapper.java b/src/main/java/com/hertz/system/mapper/SysUserRoleMapper.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/mapper/SysUserRoleMapper.java rename to src/main/java/com/hertz/system/mapper/SysUserRoleMapper.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/AuthzService.java b/src/main/java/com/hertz/system/service/AuthzService.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/service/AuthzService.java rename to src/main/java/com/hertz/system/service/AuthzService.java diff --git a/src/main/java/com/hertz/system/service/CaptchaService.java b/src/main/java/com/hertz/system/service/CaptchaService.java new file mode 100644 index 0000000..b1c8aa9 --- /dev/null +++ b/src/main/java/com/hertz/system/service/CaptchaService.java @@ -0,0 +1,8 @@ +package com.hertz.system.service; + +import com.hertz.system.dto.AuthDtos; + +public interface CaptchaService { + AuthDtos.CaptchaResponse generateCaptcha(); + void validateCaptcha(String uuid, String code); +} diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/MenuService.java b/src/main/java/com/hertz/system/service/MenuService.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/service/MenuService.java rename to src/main/java/com/hertz/system/service/MenuService.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/RoleService.java b/src/main/java/com/hertz/system/service/RoleService.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/service/RoleService.java rename to src/main/java/com/hertz/system/service/RoleService.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/UserService.java b/src/main/java/com/hertz/system/service/UserService.java similarity index 95% rename from hertz_springboot/src/main/java/com/hertz/system/service/UserService.java rename to src/main/java/com/hertz/system/service/UserService.java index 0cc4e6f..11a6145 100644 --- a/hertz_springboot/src/main/java/com/hertz/system/service/UserService.java +++ b/src/main/java/com/hertz/system/service/UserService.java @@ -22,6 +22,8 @@ public interface UserService { void deleteUser(Long id); + void deleteUsers(List ids); + void updateUserRoles(long userId, List roleIds); List getUserRoleIds(long userId); diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/AuthzServiceImpl.java b/src/main/java/com/hertz/system/service/impl/AuthzServiceImpl.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/service/impl/AuthzServiceImpl.java rename to src/main/java/com/hertz/system/service/impl/AuthzServiceImpl.java diff --git a/src/main/java/com/hertz/system/service/impl/CaptchaServiceImpl.java b/src/main/java/com/hertz/system/service/impl/CaptchaServiceImpl.java new file mode 100644 index 0000000..f073c81 --- /dev/null +++ b/src/main/java/com/hertz/system/service/impl/CaptchaServiceImpl.java @@ -0,0 +1,54 @@ +package com.hertz.system.service.impl; + +import cn.hutool.captcha.CaptchaUtil; +import cn.hutool.captcha.LineCaptcha; +import cn.hutool.core.lang.UUID; +import com.hertz.common.exception.BusinessException; +import com.hertz.system.dto.AuthDtos; +import com.hertz.system.service.CaptchaService; +import java.util.concurrent.TimeUnit; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +@Service +public class CaptchaServiceImpl implements CaptchaService { + + private final StringRedisTemplate redisTemplate; + + public CaptchaServiceImpl(StringRedisTemplate redisTemplate) { + this.redisTemplate = redisTemplate; + } + + @Override + public AuthDtos.CaptchaResponse generateCaptcha() { + // Width: 120, Height: 40, Code Count: 4, Interference Line Count: 10 + LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(120, 40, 4, 10); + String uuid = UUID.fastUUID().toString(true); + String code = lineCaptcha.getCode(); + + // Store in Redis with 5 minutes expiration + redisTemplate.opsForValue().set("captcha:" + uuid, code, 5, TimeUnit.MINUTES); + + return new AuthDtos.CaptchaResponse(uuid, lineCaptcha.getImageBase64Data()); + } + + @Override + public void validateCaptcha(String uuid, String code) { + if (uuid == null || uuid.isBlank() || code == null || code.isBlank()) { + throw new BusinessException(40001, "验证码不能为空"); + } + String key = "captcha:" + uuid; + String cachedCode = redisTemplate.opsForValue().get(key); + + if (cachedCode == null) { + throw new BusinessException(40002, "验证码已过期"); + } + + if (!code.equalsIgnoreCase(cachedCode)) { + throw new BusinessException(40003, "验证码错误"); + } + + // Validate once, then delete to prevent replay + redisTemplate.delete(key); + } +} diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/MenuServiceImpl.java b/src/main/java/com/hertz/system/service/impl/MenuServiceImpl.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/service/impl/MenuServiceImpl.java rename to src/main/java/com/hertz/system/service/impl/MenuServiceImpl.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/RoleServiceImpl.java b/src/main/java/com/hertz/system/service/impl/RoleServiceImpl.java similarity index 100% rename from hertz_springboot/src/main/java/com/hertz/system/service/impl/RoleServiceImpl.java rename to src/main/java/com/hertz/system/service/impl/RoleServiceImpl.java diff --git a/hertz_springboot/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java b/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java similarity index 96% rename from hertz_springboot/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java rename to src/main/java/com/hertz/system/service/impl/UserServiceImpl.java index 73dd436..45c77ae 100644 --- a/hertz_springboot/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java +++ b/src/main/java/com/hertz/system/service/impl/UserServiceImpl.java @@ -145,6 +145,16 @@ public class UserServiceImpl implements UserService { userRoleMapper.delete(new LambdaQueryWrapper().eq(SysUserRole::getUserId, id)); } + @Override + @Transactional + public void deleteUsers(List ids) { + if (ids == null || ids.isEmpty()) { + return; + } + userMapper.deleteByIds(ids); + userRoleMapper.delete(new LambdaQueryWrapper().in(SysUserRole::getUserId, ids)); + } + @Override @Transactional public void updateUserRoles(long userId, List roleIds) { diff --git a/hertz_springboot/src/main/resources/application.yml b/src/main/resources/application.yml similarity index 64% rename from hertz_springboot/src/main/resources/application.yml rename to src/main/resources/application.yml index c4009ba..06ce376 100644 --- a/hertz_springboot/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -11,11 +11,25 @@ spring: multipart: max-file-size: 2MB max-request-size: 10MB + ai: + ollama: + base-url: http://localhost:11434 + chat: + # 如果需要快速响应,建议切换为标准模型,如: llama3.1, qwen2.5:7b, deepseek-llm:7b + model: deepseek-llm:7b + options: + temperature: 0.7 datasource: url: jdbc:mysql://localhost:3306/hertz_springboot?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver + data: + redis: + host: localhost + port: 6379 + password: + database: 0 mybatis-plus: configuration: @@ -28,3 +42,9 @@ app: upload: root-path: d:\LocalFile\hertz_springboot\uploads avatar-path: avatar/ + +management: + endpoints: + web: + exposure: + include: health,info,metrics diff --git a/src/main/resources/schema/ai_schema.sql b/src/main/resources/schema/ai_schema.sql new file mode 100644 index 0000000..bc03fef --- /dev/null +++ b/src/main/resources/schema/ai_schema.sql @@ -0,0 +1,26 @@ + +-- ---------------------------- +-- Table structure for conversations +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `ai_conversations` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `user_id` bigint NOT NULL COMMENT '用户ID', + `title` varchar(255) NOT NULL COMMENT '对话标题', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + `updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_user_id` (`user_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话记录表'; + +-- ---------------------------- +-- Table structure for messages +-- ---------------------------- +CREATE TABLE IF NOT EXISTS `ai_messages` ( + `id` bigint NOT NULL AUTO_INCREMENT, + `conversation_id` bigint NOT NULL COMMENT '所属对话ID', + `role` enum('user','assistant') NOT NULL COMMENT '消息角色', + `content` text NOT NULL COMMENT '消息内容', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, + PRIMARY KEY (`id`), + KEY `idx_conversation_id` (`conversation_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='对话消息表'; diff --git a/src/main/resources/schema/monitor_schema.sql b/src/main/resources/schema/monitor_schema.sql new file mode 100644 index 0000000..3e1807f --- /dev/null +++ b/src/main/resources/schema/monitor_schema.sql @@ -0,0 +1,14 @@ +-- ---------------------------- +-- Table structure for sys_monitor_log +-- ---------------------------- +DROP TABLE IF EXISTS `sys_monitor_log`; +CREATE TABLE `sys_monitor_log` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', + `cpu_usage` double NOT NULL COMMENT 'CPU使用率(%)', + `memory_usage` double NOT NULL COMMENT '内存使用率(%)', + `memory_total` bigint NOT NULL COMMENT '总内存(字节)', + `memory_used` bigint NOT NULL COMMENT '已用内存(字节)', + `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录时间', + PRIMARY KEY (`id`), + KEY `idx_created_at` (`created_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统监控日志表'; diff --git a/hertz_springboot/src/main/resources/schema.sql b/src/main/resources/schema/schema.sql similarity index 99% rename from hertz_springboot/src/main/resources/schema.sql rename to src/main/resources/schema/schema.sql index ef388e8..ea0531e 100644 --- a/hertz_springboot/src/main/resources/schema.sql +++ b/src/main/resources/schema/schema.sql @@ -104,4 +104,4 @@ INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (1, 2), (1, 3), (1, 4), - (1, 5); + (1, 5); \ No newline at end of file diff --git a/hertz_springboot/src/test/java/com/hertz/HertzApplicationTests.java b/src/test/java/com/hertz/HertzApplicationTests.java similarity index 100% rename from hertz_springboot/src/test/java/com/hertz/HertzApplicationTests.java rename to src/test/java/com/hertz/HertzApplicationTests.java diff --git a/hertz_springboot_ui/.env.development b/ui/.env.dev similarity index 100% rename from hertz_springboot_ui/.env.development rename to ui/.env.dev diff --git a/hertz_springboot_ui/index.html b/ui/index.html similarity index 82% rename from hertz_springboot_ui/index.html rename to ui/index.html index eb8bfcc..c9e0db4 100644 --- a/hertz_springboot_ui/index.html +++ b/ui/index.html @@ -2,7 +2,7 @@ - + Hertz Admin diff --git a/hertz_springboot_ui/jsconfig.json b/ui/jsconfig.json similarity index 100% rename from hertz_springboot_ui/jsconfig.json rename to ui/jsconfig.json diff --git a/hertz_springboot_ui/package-lock.json b/ui/package-lock.json similarity index 99% rename from hertz_springboot_ui/package-lock.json rename to ui/package-lock.json index 66c359f..51cd060 100644 --- a/hertz_springboot_ui/package-lock.json +++ b/ui/package-lock.json @@ -11,6 +11,7 @@ "@element-plus/icons-vue": "^2.3.2", "axios": "^1.13.2", "element-plus": "^2.13.1", + "marked": "^17.0.1", "pinia": "^3.0.4", "vue": "^3.5.24", "vue-router": "^4.6.4" @@ -1656,6 +1657,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", + "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", diff --git a/hertz_springboot_ui/package.json b/ui/package.json similarity index 94% rename from hertz_springboot_ui/package.json rename to ui/package.json index 65f9064..d21df5b 100644 --- a/hertz_springboot_ui/package.json +++ b/ui/package.json @@ -12,6 +12,7 @@ "@element-plus/icons-vue": "^2.3.2", "axios": "^1.13.2", "element-plus": "^2.13.1", + "marked": "^17.0.1", "pinia": "^3.0.4", "vue": "^3.5.24", "vue-router": "^4.6.4" diff --git a/hertz_springboot_ui/public/index.png b/ui/public/favicon.png similarity index 100% rename from hertz_springboot_ui/public/index.png rename to ui/public/favicon.png diff --git a/hertz_springboot_ui/public/logo.png b/ui/public/logo.png similarity index 100% rename from hertz_springboot_ui/public/logo.png rename to ui/public/logo.png diff --git a/hertz_springboot_ui/src/App.vue b/ui/src/App.vue similarity index 100% rename from hertz_springboot_ui/src/App.vue rename to ui/src/App.vue diff --git a/hertz_springboot_ui/src/api/auth.js b/ui/src/api/auth.js similarity index 82% rename from hertz_springboot_ui/src/api/auth.js rename to ui/src/api/auth.js index 1f5583f..a69ec1c 100644 --- a/hertz_springboot_ui/src/api/auth.js +++ b/ui/src/api/auth.js @@ -1,5 +1,10 @@ import { http } from './http' +export async function getCaptcha() { + const { data } = await http.get('/api/auth/captcha') + return data.data +} + export async function login(req) { const { data } = await http.post('/api/auth/login', req) return data.data diff --git a/ui/src/api/chat.js b/ui/src/api/chat.js new file mode 100644 index 0000000..7936b89 --- /dev/null +++ b/ui/src/api/chat.js @@ -0,0 +1,39 @@ +import { http as request } from './http' + +export const chatApi = { + // Create Conversation + async createConversation(title) { + const { data } = await request.post('api/ai/conversations', { title }) + return data.data + }, + // Get List + async getConversations() { + const { data } = await request.get('api/ai/conversations') + return data.data + }, + // Delete + async deleteConversation(id) { + const { data } = await request.delete(`api/ai/conversations/${id}`) + return data.data + }, + // Update Title + async updateConversation(id, title) { + const { data } = await request.put(`api/ai/conversations/${id}`, { title }) + return data.data + }, + // Search + async searchConversations(query) { + const { data } = await request.get('api/ai/conversations/search', { params: { query } }) + return data.data + }, + // Save Message + async saveMessage(conversationId, role, content) { + const { data } = await request.post(`api/ai/conversations/${conversationId}/messages`, { role, content }) + return data.data + }, + // Get Messages + async getMessages(conversationId) { + const { data } = await request.get(`api/ai/conversations/${conversationId}/messages`) + return data.data + } +} diff --git a/hertz_springboot_ui/src/api/common.js b/ui/src/api/common.js similarity index 100% rename from hertz_springboot_ui/src/api/common.js rename to ui/src/api/common.js diff --git a/hertz_springboot_ui/src/api/http.js b/ui/src/api/http.js similarity index 73% rename from hertz_springboot_ui/src/api/http.js rename to ui/src/api/http.js index 57b7e44..040bcb2 100644 --- a/hertz_springboot_ui/src/api/http.js +++ b/ui/src/api/http.js @@ -26,12 +26,14 @@ http.interceptors.response.use( import('../router') .then(({ default: router }) => { const current = router.currentRoute.value - if (current.path !== '/403') { - router.replace({ path: '/403', query: { from: current.fullPath } }) + const path = current.path + const target = path.startsWith('/admin') ? '/admin/403' : '/portal/403' + if (path !== target) { + router.replace({ path: target, query: { from: current.fullPath } }) } }) .catch(() => { - if (window.location.pathname !== '/403') window.location.replace('/403') + if (window.location.pathname !== '/portal/403') window.location.replace('/portal/403') }) } return Promise.reject(err) diff --git a/ui/src/api/monitor.js b/ui/src/api/monitor.js new file mode 100644 index 0000000..bd2b3ba --- /dev/null +++ b/ui/src/api/monitor.js @@ -0,0 +1,6 @@ +import { http } from './http' + +export async function getServerInfo() { + const { data } = await http.get('/api/monitor/server') + return data +} diff --git a/hertz_springboot_ui/src/api/system.js b/ui/src/api/system.js similarity index 95% rename from hertz_springboot_ui/src/api/system.js rename to ui/src/api/system.js index b0c6b62..7110516 100644 --- a/hertz_springboot_ui/src/api/system.js +++ b/ui/src/api/system.js @@ -39,6 +39,10 @@ export async function deleteUser(id) { await http.delete(`/api/system/users/${id}`) } +export async function deleteUsers(ids) { + await http.delete('/api/system/users', { data: ids }) +} + export async function fetchRoles() { const { data } = await http.get('/api/system/roles') return data.data diff --git a/ui/src/assets/img/default_avatar.png b/ui/src/assets/img/default_avatar.png new file mode 100644 index 0000000..187c7f1 Binary files /dev/null and b/ui/src/assets/img/default_avatar.png differ diff --git a/hertz_springboot_ui/src/assets/img/profile_bg.jpg b/ui/src/assets/img/profile_bg.jpg similarity index 100% rename from hertz_springboot_ui/src/assets/img/profile_bg.jpg rename to ui/src/assets/img/profile_bg.jpg diff --git a/hertz_springboot_ui/src/assets/style.css b/ui/src/assets/style.css similarity index 76% rename from hertz_springboot_ui/src/assets/style.css rename to ui/src/assets/style.css index 0ff6052..a9e0c33 100644 --- a/hertz_springboot_ui/src/assets/style.css +++ b/ui/src/assets/style.css @@ -1,10 +1,10 @@ :root { /* iOS Color Palette */ - --ios-primary: #007AFF; - --ios-primary-light: #47a0ff; + --ios-primary: #3542ec; + --ios-primary-light: #0011FF; --ios-success: #34C759; --ios-warning: #FF9500; - --ios-danger: #FF3B30; + --ios-danger: #FF4040; --ios-gray: #8E8E93; --ios-bg: #F2F4F8; --ios-surface: rgba(255, 255, 255, 0.7); @@ -56,7 +56,7 @@ body { .el-card__header { border-bottom: 1px solid var(--ios-border) !important; - padding: 16px 24px !important; + padding: 10px 24px !important; font-weight: 600; font-size: 16px; } @@ -68,7 +68,19 @@ body { transition: all 0.2s ease !important; } -.el-button:not(.is-circle):not(.is-text) { +.el-button.is-link:hover { + color: var(--ios-primary-light); +} + + +.el-button--primary { + --el-button-hover-bg-color: var(--ios-primary-light); + --el-button-hover-border-color: var(--ios-primary-light); + --el-button-active-bg-color: var(--ios-primary-light); + --el-button-active-border-color: var(--ios-primary-light); +} + +.el-button:not(.is-circle):not(.is-text):not(.is-link) { height: 36px !important; padding: 0 20px !important; } @@ -111,6 +123,14 @@ body { background-color: rgba(0, 122, 255, 0.03) !important; } +.el-checkbox__inner { + width: 15px; + height: 15px; + border-radius: 4px; +} + + + /* Dialog Styling */ .el-dialog { border-radius: 20px !important; @@ -240,3 +260,48 @@ html, body { scrollbar-width: none; -ms-overflow-style: none; } + +/* MessageBox Styling */ +.el-message-box { + border-radius: var(--ios-radius) !important; + border: none !important; + box-shadow: var(--ios-shadow-hover) !important; + background-color: rgba(255, 255, 255, 0.95) !important; + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + padding: 0 !important; + width: 400px !important; + max-width: 90vw; +} + +.el-message-box__header { + padding: 20px 24px 10px !important; +} + +.el-message-box__title { + font-weight: 600; + font-size: 18px !important; +} + +.el-message-box__headerbtn { + top: 20px !important; + right: 20px !important; +} + +.el-message-box__content { + padding: 10px 24px 20px !important; + font-size: 15px !important; + color: #1d1d1f; +} + +.el-message-box__btns { + padding: 0 24px 24px !important; + display: flex; + justify-content: flex-end; + gap: 12px; +} + +.el-message-box__btns .el-button { + margin-left: 0 !important; + min-width: 80px; +} diff --git a/ui/src/components/ContentPage.vue b/ui/src/components/ContentPage.vue new file mode 100644 index 0000000..d649248 --- /dev/null +++ b/ui/src/components/ContentPage.vue @@ -0,0 +1,66 @@ + + + diff --git a/hertz_springboot_ui/src/components/Error.vue b/ui/src/components/Error.vue similarity index 93% rename from hertz_springboot_ui/src/components/Error.vue rename to ui/src/components/Error.vue index bd393bb..f481060 100644 --- a/hertz_springboot_ui/src/components/Error.vue +++ b/ui/src/components/Error.vue @@ -3,18 +3,18 @@
{{ codeText }}
-
+
{{ displayTitle }}
{{ displaySubTitle }}
-
+
@@ -97,11 +97,9 @@ function switchAccount() { display: flex; justify-content: center; align-items: center; - height: 100vh; - padding: 16px; - background: radial-gradient(circle at top left, #e3f2fd, transparent 45%), - radial-gradient(circle at bottom right, #f3e5f5, transparent 45%), - #f2f4f8; + min-height: 100%; + padding: 32px 16px; + flex: 1; } .error-card { diff --git a/hertz_springboot_ui/src/layouts/AdminLayout.vue b/ui/src/layouts/AdminLayout.vue similarity index 97% rename from hertz_springboot_ui/src/layouts/AdminLayout.vue rename to ui/src/layouts/AdminLayout.vue index 5d51c43..51d233e 100644 --- a/hertz_springboot_ui/src/layouts/AdminLayout.vue +++ b/ui/src/layouts/AdminLayout.vue @@ -1,7 +1,7 @@ + diff --git a/hertz_springboot_ui/src/views/admin/system/Role.vue b/ui/src/views/admin/system/Role.vue similarity index 88% rename from hertz_springboot_ui/src/views/admin/system/Role.vue rename to ui/src/views/admin/system/Role.vue index 7e759f1..36913cf 100644 --- a/hertz_springboot_ui/src/views/admin/system/Role.vue +++ b/ui/src/views/admin/system/Role.vue @@ -1,15 +1,17 @@