commit 9c6c3e091fac3245743f1336954899a400897c5b Author: zyh <50652658+zyh530@users.noreply.github.com> Date: Fri Jul 18 17:58:07 2025 +0800 first commit diff --git a/.claude/commands/ask.md b/.claude/commands/ask.md new file mode 100644 index 0000000..38bbb23 --- /dev/null +++ b/.claude/commands/ask.md @@ -0,0 +1,34 @@ +## Usage +`@ask.md ` + +## Context +- Technical question or architecture challenge: $ARGUMENTS +- Relevant system documentation and design artifacts will be referenced using @ file syntax. +- Current system constraints, scale requirements, and business context will be considered. + +## Your Role +You are a Senior Systems Architect providing expert consultation and architectural guidance. You focus on high-level design, strategic decisions, and architectural patterns rather than implementation details. You orchestrate four specialized architectural advisors: +1. **Systems Designer** – evaluates system boundaries, interfaces, and component interactions. +2. **Technology Strategist** – recommends technology stacks, frameworks, and architectural patterns. +3. **Scalability Consultant** – assesses performance, reliability, and growth considerations. +4. **Risk Analyst** – identifies potential issues, trade-offs, and mitigation strategies. + +## Process +1. **Problem Understanding**: Analyze the technical question and gather architectural context. +2. **Expert Consultation**: + - Systems Designer: Define system boundaries, data flows, and component relationships + - Technology Strategist: Evaluate technology choices, patterns, and industry best practices + - Scalability Consultant: Assess non-functional requirements and scalability implications + - Risk Analyst: Identify architectural risks, dependencies, and decision trade-offs +3. **Architecture Synthesis**: Combine insights to provide comprehensive architectural guidance. +4. **Strategic Validation**: Ensure recommendations align with business goals and technical constraints. + +## Output Format +1. **Architecture Analysis** – comprehensive breakdown of the technical challenge and context. +2. **Design Recommendations** – high-level architectural solutions with rationale and alternatives. +3. **Technology Guidance** – strategic technology choices with pros/cons analysis. +4. **Implementation Strategy** – phased approach and architectural decision framework. +5. **Next Actions** – strategic next steps, proof-of-concepts, and architectural validation points. + +## Note +This command focuses on architectural consultation and strategic guidance. For implementation details and code generation, use @code.md instead. \ No newline at end of file diff --git a/.claude/commands/code.md b/.claude/commands/code.md new file mode 100644 index 0000000..54da163 --- /dev/null +++ b/.claude/commands/code.md @@ -0,0 +1,31 @@ +## Usage +`@code.md ` + +## Context +- Feature/functionality to implement: $ARGUMENTS +- Existing codebase structure and patterns will be referenced using @ file syntax. +- Project requirements, constraints, and coding standards will be considered. + +## Your Role +You are the Development Coordinator directing four coding specialists: +1. **Architect Agent** – designs high-level implementation approach and structure. +2. **Implementation Engineer** – writes clean, efficient, and maintainable code. +3. **Integration Specialist** – ensures seamless integration with existing codebase. +4. **Code Reviewer** – validates implementation quality and adherence to standards. + +## Process +1. **Requirements Analysis**: Break down feature requirements and identify technical constraints. +2. **Implementation Strategy**: + - Architect Agent: Design API contracts, data models, and component structure + - Implementation Engineer: Write core functionality with proper error handling + - Integration Specialist: Ensure compatibility with existing systems and dependencies + - Code Reviewer: Validate code quality, security, and performance considerations +3. **Progressive Development**: Build incrementally with validation at each step. +4. **Quality Validation**: Ensure code meets standards for maintainability and extensibility. + +## Output Format +1. **Implementation Plan** – technical approach with component breakdown and dependencies. +2. **Code Implementation** – complete, working code with comprehensive comments. +3. **Integration Guide** – steps to integrate with existing codebase and systems. +4. **Testing Strategy** – unit tests and validation approach for the implementation. +5. **Next Actions** – deployment steps, documentation needs, and future enhancements. \ No newline at end of file diff --git a/.claude/commands/debug.md b/.claude/commands/debug.md new file mode 100644 index 0000000..f3d2083 --- /dev/null +++ b/.claude/commands/debug.md @@ -0,0 +1,31 @@ +## Usage +`@debug.md ` + +## Context +- Error description: $ARGUMENTS +- Relevant code files will be referenced using @ file syntax as needed. +- Error logs and stack traces will be analyzed in context. + +## Your Role +You are the Debug Coordinator orchestrating four specialist debugging agents: +1. **Error Analyzer** – identifies root cause and error patterns. +2. **Code Inspector** – examines relevant code sections and logic flow. +3. **Environment Checker** – validates configuration, dependencies, and environment. +4. **Fix Strategist** – proposes solution approaches and implementation steps. + +## Process +1. **Initial Assessment**: Analyze the error description and gather context clues. +2. **Agent Delegation**: + - Error Analyzer: Classify error type, severity, and potential impact scope + - Code Inspector: Trace execution path and identify problematic code sections + - Environment Checker: Verify configurations, versions, and external dependencies + - Fix Strategist: Design solution approach with risk assessment +3. **Synthesis**: Combine insights to form comprehensive debugging strategy. +4. **Validation**: Ensure proposed fix addresses root cause, not just symptoms. + +## Output Format +1. **Debug Transcript** – reasoning process and findings from each agent. +2. **Root Cause Analysis** – clear explanation of what went wrong and why. +3. **Solution Implementation** – step-by-step fix with code changes in Markdown. +4. **Verification Plan** – testing strategy to confirm fix and prevent regression. +5. **Next Actions** – follow-up items for monitoring and prevention. \ No newline at end of file diff --git a/.claude/commands/deploy-check.md b/.claude/commands/deploy-check.md new file mode 100644 index 0000000..09395b8 --- /dev/null +++ b/.claude/commands/deploy-check.md @@ -0,0 +1,31 @@ +## Usage +`@deploy-check.md ` + +## Context +- Deployment target/environment: $ARGUMENTS +- Application code, configurations, and infrastructure will be referenced using @ file syntax. +- Production requirements and compliance standards will be validated. + +## Your Role +You are the Deployment Readiness Coordinator managing four deployment specialists: +1. **Quality Assurance Agent** – validates code quality and test coverage. +2. **Security Auditor** – ensures security compliance and vulnerability mitigation. +3. **Operations Engineer** – verifies infrastructure readiness and configuration. +4. **Risk Assessor** – evaluates deployment risks and rollback strategies. + +## Process +1. **Readiness Assessment**: Systematically evaluate all deployment prerequisites. +2. **Multi-layer Validation**: + - Quality Assurance Agent: Verify test coverage, code quality, and functionality + - Security Auditor: Scan for vulnerabilities and validate security configurations + - Operations Engineer: Check infrastructure, monitoring, and operational readiness + - Risk Assessor: Evaluate deployment risks and prepare contingency plans +3. **Go/No-Go Decision**: Synthesize findings into clear deployment recommendation. +4. **Deployment Strategy**: Provide step-by-step deployment plan with safeguards. + +## Output Format +1. **Readiness Report** – comprehensive assessment with pass/fail criteria. +2. **Risk Analysis** – identified risks with mitigation strategies. +3. **Deployment Plan** – step-by-step execution guide with rollback procedures. +4. **Monitoring Strategy** – post-deployment validation and health checks. +5. **Next Actions** – immediate post-deployment tasks and long-term improvements. \ No newline at end of file diff --git a/.claude/commands/optimize.md b/.claude/commands/optimize.md new file mode 100644 index 0000000..ae8cc94 --- /dev/null +++ b/.claude/commands/optimize.md @@ -0,0 +1,31 @@ +## Usage +`@optimize.md ` + +## Context +- Performance target/bottleneck: $ARGUMENTS +- Relevant code and profiling data will be referenced using @ file syntax. +- Current performance metrics and constraints will be analyzed. + +## Your Role +You are the Performance Optimization Coordinator leading four optimization experts: +1. **Profiler Analyst** – identifies bottlenecks through systematic measurement. +2. **Algorithm Engineer** – optimizes computational complexity and data structures. +3. **Resource Manager** – optimizes memory, I/O, and system resource usage. +4. **Scalability Architect** – ensures solutions work under increased load. + +## Process +1. **Performance Baseline**: Establish current metrics and identify critical paths. +2. **Optimization Analysis**: + - Profiler Analyst: Measure execution time, memory usage, and resource consumption + - Algorithm Engineer: Analyze time/space complexity and algorithmic improvements + - Resource Manager: Optimize caching, batching, and resource allocation + - Scalability Architect: Design for horizontal scaling and concurrent processing +3. **Solution Design**: Create optimization strategy with measurable targets. +4. **Impact Validation**: Verify improvements don't compromise functionality or maintainability. + +## Output Format +1. **Performance Analysis** – current bottlenecks with quantified impact. +2. **Optimization Strategy** – systematic approach with technical implementation. +3. **Implementation Plan** – code changes with performance impact estimates. +4. **Measurement Framework** – benchmarking and monitoring setup. +5. **Next Actions** – continuous optimization and monitoring requirements. \ No newline at end of file diff --git a/.claude/commands/refactor.md b/.claude/commands/refactor.md new file mode 100644 index 0000000..f2f103f --- /dev/null +++ b/.claude/commands/refactor.md @@ -0,0 +1,31 @@ +## Usage +`@refactor.md ` + +## Context +- Refactoring scope/target: $ARGUMENTS +- Legacy code and design constraints will be referenced using @ file syntax. +- Existing test coverage and dependencies will be preserved. + +## Your Role +You are the Refactoring Coordinator orchestrating four refactoring specialists: +1. **Structure Analyst** – evaluates current architecture and identifies improvement opportunities. +2. **Code Surgeon** – performs precise code transformations while preserving functionality. +3. **Design Pattern Expert** – applies appropriate patterns for better maintainability. +4. **Quality Validator** – ensures refactoring improves code quality without breaking changes. + +## Process +1. **Current State Analysis**: Map existing code structure, dependencies, and technical debt. +2. **Refactoring Strategy**: + - Structure Analyst: Identify coupling issues, complexity hotspots, and architectural smells + - Code Surgeon: Plan safe transformation steps with rollback strategies + - Design Pattern Expert: Recommend patterns that improve extensibility and testability + - Quality Validator: Establish quality gates and regression prevention measures +3. **Incremental Transformation**: Design step-by-step refactoring with validation points. +4. **Quality Assurance**: Verify improvements in maintainability, readability, and testability. + +## Output Format +1. **Refactoring Assessment** – current issues and improvement opportunities. +2. **Transformation Plan** – step-by-step refactoring strategy with risk mitigation. +3. **Implementation Guide** – concrete code changes with before/after examples. +4. **Validation Strategy** – testing approach to ensure functionality preservation. +5. **Next Actions** – monitoring plan and future refactoring opportunities. \ No newline at end of file diff --git a/.claude/commands/review.md b/.claude/commands/review.md new file mode 100644 index 0000000..f2e3d38 --- /dev/null +++ b/.claude/commands/review.md @@ -0,0 +1,31 @@ +## Usage +`@review.md ` + +## Context +- Code scope for review: $ARGUMENTS +- Target files will be referenced using @ file syntax. +- Project coding standards and conventions will be considered. + +## Your Role +You are the Code Review Coordinator directing four review specialists: +1. **Quality Auditor** – examines code quality, readability, and maintainability. +2. **Security Analyst** – identifies vulnerabilities and security best practices. +3. **Performance Reviewer** – evaluates efficiency and optimization opportunities. +4. **Architecture Assessor** – validates design patterns and structural decisions. + +## Process +1. **Code Examination**: Systematically analyze target code sections and dependencies. +2. **Multi-dimensional Review**: + - Quality Auditor: Assess naming, structure, complexity, and documentation + - Security Analyst: Scan for injection risks, auth issues, and data exposure + - Performance Reviewer: Identify bottlenecks, memory leaks, and optimization points + - Architecture Assessor: Evaluate SOLID principles, patterns, and scalability +3. **Synthesis**: Consolidate findings into prioritized actionable feedback. +4. **Validation**: Ensure recommendations are practical and aligned with project goals. + +## Output Format +1. **Review Summary** – high-level assessment with priority classification. +2. **Detailed Findings** – specific issues with code examples and explanations. +3. **Improvement Recommendations** – concrete refactoring suggestions with code samples. +4. **Action Plan** – prioritized tasks with effort estimates and impact assessment. +5. **Next Actions** – follow-up reviews and monitoring requirements. \ No newline at end of file diff --git a/.claude/commands/scaffold.md b/.claude/commands/scaffold.md new file mode 100644 index 0000000..f1d854a --- /dev/null +++ b/.claude/commands/scaffold.md @@ -0,0 +1,49 @@ +## Usage + +``` +@scaffold.md +``` + +## Context + +- New system or product: $ARGUMENTS +- Describe the product vision, core business goals, key features, and known constraints. +- Include information such as team expertise, preferred tech stack (if any), user scale, compliance needs, etc. + +## Your Role + +You are a Principal Software Architect responsible for **bootstrapping scalable, maintainable system architectures**. You will work collaboratively with four architectural strategists: + +1. **Product-Oriented Architect** – translates business goals into architectural capabilities and services. +2. **System Blueprint Designer** – defines core modules, domains, and architectural style. +3. **Platform Engineer** – proposes deployment, observability, and DevOps setup. +4. **Tech Debt Forecaster** – identifies potential maintainability, security, and extensibility concerns. + +## Process + +1. **Project Decomposition**: + - Extract functional modules and business domains from project vision. + - Identify critical paths, data boundaries, and system drivers. +2. **Core Architecture Drafting**: + - Choose architectural style (e.g., monolith, microservices, serverless). + - Identify key components: APIs, databases, queues, storage, identity, observability. +3. **Tech Stack Planning**: + - Evaluate language, framework, and infra stack for each tier (frontend, backend, data, ops). + - Consider team skillset, speed of development, and maintainability. +4. **Deployment Baseline**: + - Propose local dev setup, CI/CD pipelines, runtime environments (Docker, K8s, etc.) +5. **Risk Scoping**: + - Highlight areas needing POC, benchmarks, or architectural spikes. + +## Output Format + +1. **Architecture Overview** – component diagram in text + architectural style rationale. +2. **Domain Breakdown** – main business modules and their responsibilities. +3. **Tech Stack Proposal** – tech stack per layer with justification and fallback. +4. **Infra Blueprint** – environments, dev workflow, CI/CD, logging/monitoring plan. +5. **Architecture Milestones** – phased delivery, POC goals, validation tasks. + +## Note + +Use this command when **bootstrapping new projects or rearchitecting existing systems**. For deep architectural critique of an existing system, prefer `@ask.md`. For implementation details and code generation, use `@code.md`. + diff --git a/.claude/commands/test.md b/.claude/commands/test.md new file mode 100644 index 0000000..378965f --- /dev/null +++ b/.claude/commands/test.md @@ -0,0 +1,31 @@ +## Usage +`@test.md ` + +## Context +- Target component/feature: $ARGUMENTS +- Existing test files and frameworks will be referenced using @ file syntax. +- Current test coverage and gaps will be assessed. + +## Your Role +You are the Test Strategy Coordinator managing four testing specialists: +1. **Test Architect** – designs comprehensive testing strategy and structure. +2. **Unit Test Specialist** – creates focused unit tests for individual components. +3. **Integration Test Engineer** – designs system interaction and API tests. +4. **Quality Validator** – ensures test coverage, maintainability, and reliability. + +## Process +1. **Test Analysis**: Examine existing code structure and identify testable units. +2. **Strategy Formation**: + - Test Architect: Design test pyramid strategy (unit/integration/e2e ratios) + - Unit Test Specialist: Create isolated tests with proper mocking + - Integration Test Engineer: Design API contracts and data flow tests + - Quality Validator: Ensure test quality, performance, and maintainability +3. **Implementation Planning**: Prioritize tests by risk and coverage impact. +4. **Validation Framework**: Establish success criteria and coverage metrics. + +## Output Format +1. **Test Strategy Overview** – comprehensive testing approach and rationale. +2. **Test Implementation** – concrete test code with clear documentation. +3. **Coverage Analysis** – gap identification and priority recommendations. +4. **Execution Plan** – test running strategy and CI/CD integration. +5. **Next Actions** – test maintenance and expansion roadmap. \ No newline at end of file diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 0000000..589f1b6 --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,10 @@ +{ + "permissions": { + "allow": [ + "Bash(mkdir:*)", + "Bash(rmdir:*)", + "Bash(rm:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..a0558ad --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..63e9001 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..a8fc129 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,12 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..f035e50 --- /dev/null +++ b/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.2.0 + + + + com.yundage + chat + 0.0.1-SNAPSHOT + chat + Chat project for Spring Boot with MyBatis-Flex + + + 17 + 1.8.6 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + com.mysql + mysql-connector-j + runtime + + + + + com.mybatis-flex + mybatis-flex-spring-boot-starter + ${mybatis-flex.version} + + + + + com.mybatis-flex + mybatis-flex-processor + ${mybatis-flex.version} + provided + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.boot + spring-boot-starter-security + + + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + runtime + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-mail + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/ChatApplication.java b/src/main/java/com/yundage/chat/ChatApplication.java new file mode 100644 index 0000000..1808be5 --- /dev/null +++ b/src/main/java/com/yundage/chat/ChatApplication.java @@ -0,0 +1,14 @@ +package com.yundage.chat; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan("com.yundage.chat.mapper") +public class ChatApplication { + + public static void main(String[] args) { + SpringApplication.run(ChatApplication.class, args); + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/config/JwtAuthenticationFilter.java b/src/main/java/com/yundage/chat/config/JwtAuthenticationFilter.java new file mode 100644 index 0000000..fff3393 --- /dev/null +++ b/src/main/java/com/yundage/chat/config/JwtAuthenticationFilter.java @@ -0,0 +1,60 @@ +package com.yundage.chat.config; + +import com.yundage.chat.util.JwtUtil; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@Component +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + @Autowired + private UserDetailsService userDetailsService; + + @Autowired + private JwtUtil jwtUtil; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, + FilterChain chain) throws ServletException, IOException { + + final String requestTokenHeader = request.getHeader("Authorization"); + + String username = null; + String jwtToken = null; + + if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { + jwtToken = requestTokenHeader.substring(7); + try { + username = jwtUtil.extractUsername(jwtToken); + } catch (IllegalArgumentException e) { + logger.error("Unable to get JWT Token"); + } catch (Exception e) { + logger.error("JWT Token has expired"); + } + } + + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + if (jwtUtil.validateToken(jwtToken, userDetails)) { + UsernamePasswordAuthenticationToken authToken = + new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authToken); + } + } + chain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/config/SecurityConfig.java b/src/main/java/com/yundage/chat/config/SecurityConfig.java new file mode 100644 index 0000000..48dd283 --- /dev/null +++ b/src/main/java/com/yundage/chat/config/SecurityConfig.java @@ -0,0 +1,67 @@ +package com.yundage.chat.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.AuthenticationProvider; +import org.springframework.security.authentication.dao.DaoAuthenticationProvider; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; +import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@Configuration +@EnableWebSecurity +@EnableMethodSecurity +public class SecurityConfig { + + @Autowired + private JwtAuthenticationFilter jwtAuthenticationFilter; + + @Autowired + private UserDetailsService userDetailsService; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public AuthenticationProvider authenticationProvider() { + DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); + authProvider.setUserDetailsService(userDetailsService); + authProvider.setPasswordEncoder(passwordEncoder()); + return authProvider; + } + + @Bean + public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception { + return config.getAuthenticationManager(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(authz -> authz + .requestMatchers("/api/auth/**").permitAll() + .requestMatchers("/api/public/**").permitAll() + .requestMatchers("/health").permitAll() + .anyRequest().authenticated() + ) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + ) + .authenticationProvider(authenticationProvider()) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/controller/AuthController.java b/src/main/java/com/yundage/chat/controller/AuthController.java new file mode 100644 index 0000000..4003e88 --- /dev/null +++ b/src/main/java/com/yundage/chat/controller/AuthController.java @@ -0,0 +1,71 @@ +package com.yundage.chat.controller; + +import com.yundage.chat.dto.*; +import com.yundage.chat.service.UserService; +import jakarta.validation.Valid; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/auth") +public class AuthController { + + @Autowired + private UserService userService; + + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody RegisterRequest request) { + try { + AuthResponse response = userService.register(request); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @PostMapping("/login") + public ResponseEntity login(@Valid @RequestBody LoginRequest request) { + try { + AuthResponse response = userService.login(request); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @PostMapping("/forgot-password") + public ResponseEntity forgotPassword(@Valid @RequestBody PasswordResetRequest request) { + try { + userService.requestPasswordReset(request); + Map response = new HashMap<>(); + response.put("message", "密码重置邮件已发送"); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } + + @PostMapping("/reset-password") + public ResponseEntity resetPassword(@Valid @RequestBody ResetPasswordRequest request) { + try { + userService.resetPassword(request); + Map response = new HashMap<>(); + response.put("message", "密码重置成功"); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map error = new HashMap<>(); + error.put("error", e.getMessage()); + return ResponseEntity.badRequest().body(error); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/controller/UserController.java b/src/main/java/com/yundage/chat/controller/UserController.java new file mode 100644 index 0000000..1d57638 --- /dev/null +++ b/src/main/java/com/yundage/chat/controller/UserController.java @@ -0,0 +1,48 @@ +package com.yundage.chat.controller; + +import com.yundage.chat.entity.User; +import com.yundage.chat.mapper.UserMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.List; + +@RestController +@RequestMapping("/api/users") +public class UserController { + + @Autowired + private UserMapper userMapper; + + @GetMapping + public List getAllUsers() { + return userMapper.selectAll(); + } + + @GetMapping("/{id}") + public User getUserById(@PathVariable Long id) { + return userMapper.selectOneById(id); + } + + @PostMapping + public User createUser(@RequestBody User user) { + user.setCreateTime(LocalDateTime.now()); + user.setUpdateTime(LocalDateTime.now()); + userMapper.insert(user); + return user; + } + + @PutMapping("/{id}") + public User updateUser(@PathVariable Long id, @RequestBody User user) { + user.setId(id); + user.setUpdateTime(LocalDateTime.now()); + userMapper.update(user); + return user; + } + + @DeleteMapping("/{id}") + public void deleteUser(@PathVariable Long id) { + userMapper.deleteById(id); + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/dto/AuthResponse.java b/src/main/java/com/yundage/chat/dto/AuthResponse.java new file mode 100644 index 0000000..0a37a50 --- /dev/null +++ b/src/main/java/com/yundage/chat/dto/AuthResponse.java @@ -0,0 +1,68 @@ +package com.yundage.chat.dto; + +public class AuthResponse { + + private String token; + private String type = "Bearer"; + private Long id; + private String username; + private String email; + private String phone; + + public AuthResponse(String token, Long id, String username, String email, String phone) { + this.token = token; + this.id = id; + this.username = username; + this.email = email; + this.phone = phone; + } + + // Getters and Setters + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/dto/LoginRequest.java b/src/main/java/com/yundage/chat/dto/LoginRequest.java new file mode 100644 index 0000000..a11b1ea --- /dev/null +++ b/src/main/java/com/yundage/chat/dto/LoginRequest.java @@ -0,0 +1,29 @@ +package com.yundage.chat.dto; + +import jakarta.validation.constraints.NotBlank; + +public class LoginRequest { + + @NotBlank(message = "用户名/邮箱不能为空") + private String username; + + @NotBlank(message = "密码不能为空") + private String password; + + // Getters and Setters + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/dto/PasswordResetRequest.java b/src/main/java/com/yundage/chat/dto/PasswordResetRequest.java new file mode 100644 index 0000000..06ec793 --- /dev/null +++ b/src/main/java/com/yundage/chat/dto/PasswordResetRequest.java @@ -0,0 +1,20 @@ +package com.yundage.chat.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public class PasswordResetRequest { + + @NotBlank(message = "邮箱不能为空") + @Email(message = "邮箱格式不正确") + private String email; + + // Getters and Setters + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/dto/RegisterRequest.java b/src/main/java/com/yundage/chat/dto/RegisterRequest.java new file mode 100644 index 0000000..6dfcb46 --- /dev/null +++ b/src/main/java/com/yundage/chat/dto/RegisterRequest.java @@ -0,0 +1,54 @@ +package com.yundage.chat.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class RegisterRequest { + + @NotBlank(message = "用户名不能为空") + @Size(min = 2, max = 50, message = "用户名长度必须在2-50个字符之间") + private String username; + + @Email(message = "邮箱格式不正确") + private String email; + + private String phone; + + @NotBlank(message = "密码不能为空") + @Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间") + private String password; + + // Getters and Setters + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/dto/ResetPasswordRequest.java b/src/main/java/com/yundage/chat/dto/ResetPasswordRequest.java new file mode 100644 index 0000000..e2fc3be --- /dev/null +++ b/src/main/java/com/yundage/chat/dto/ResetPasswordRequest.java @@ -0,0 +1,31 @@ +package com.yundage.chat.dto; + +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public class ResetPasswordRequest { + + @NotBlank(message = "重置令牌不能为空") + private String token; + + @NotBlank(message = "新密码不能为空") + @Size(min = 6, max = 100, message = "密码长度必须在6-100个字符之间") + private String newPassword; + + // Getters and Setters + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getNewPassword() { + return newPassword; + } + + public void setNewPassword(String newPassword) { + this.newPassword = newPassword; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/entity/PasswordResetToken.java b/src/main/java/com/yundage/chat/entity/PasswordResetToken.java new file mode 100644 index 0000000..fe89c03 --- /dev/null +++ b/src/main/java/com/yundage/chat/entity/PasswordResetToken.java @@ -0,0 +1,89 @@ +package com.yundage.chat.entity; + +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; +import com.mybatisflex.annotation.Table; + +import java.time.LocalDateTime; + +@Table("password_reset_tokens") +public class PasswordResetToken { + + @Id(keyType = KeyType.Auto) + private Long id; + + private Long userId; + private String token; + private LocalDateTime expiresAt; + private Boolean used; + private LocalDateTime createdAt; + + public PasswordResetToken() { + this.used = false; + } + + public PasswordResetToken(Long userId, String token, LocalDateTime expiresAt) { + this.userId = userId; + this.token = token; + this.expiresAt = expiresAt; + this.used = false; + this.createdAt = LocalDateTime.now(); + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public LocalDateTime getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(LocalDateTime expiresAt) { + this.expiresAt = expiresAt; + } + + public Boolean getUsed() { + return used; + } + + public void setUsed(Boolean used) { + this.used = used; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public boolean isExpired() { + return LocalDateTime.now().isAfter(expiresAt); + } + + public boolean isValid() { + return !used && !isExpired(); + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/entity/User.java b/src/main/java/com/yundage/chat/entity/User.java new file mode 100644 index 0000000..eb96674 --- /dev/null +++ b/src/main/java/com/yundage/chat/entity/User.java @@ -0,0 +1,171 @@ +package com.yundage.chat.entity; + +import com.mybatisflex.annotation.Id; +import com.mybatisflex.annotation.KeyType; +import com.mybatisflex.annotation.Table; +import com.yundage.chat.enums.UserType; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.time.LocalDateTime; +import java.util.Collection; +import java.util.Collections; + +@Table("users") +public class User implements UserDetails { + + @Id(keyType = KeyType.Auto) + private Long id; + + private String username; + private String passwordHash; + private String phone; + private String email; + private String avatarUrl; + private UserType userType; + private Integer membershipLevelId; + private Integer status; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private LocalDateTime lastLoginAt; + + public User() { + this.userType = UserType.PERSONAL; + this.membershipLevelId = 1; + this.status = 1; + } + + // UserDetails implementation + @Override + public Collection getAuthorities() { + return Collections.singletonList(new SimpleGrantedAuthority("ROLE_" + userType.name())); + } + + @Override + public String getPassword() { + return passwordHash; + } + + @Override + public String getUsername() { + return email != null ? email : phone; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return status == 1; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return status == 1; + } + + // Getters and Setters + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDisplayName() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPasswordHash() { + return passwordHash; + } + + public void setPasswordHash(String passwordHash) { + this.passwordHash = passwordHash; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public UserType getUserType() { + return userType; + } + + public void setUserType(UserType userType) { + this.userType = userType; + } + + public Integer getMembershipLevelId() { + return membershipLevelId; + } + + public void setMembershipLevelId(Integer membershipLevelId) { + this.membershipLevelId = membershipLevelId; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } + + public LocalDateTime getLastLoginAt() { + return lastLoginAt; + } + + public void setLastLoginAt(LocalDateTime lastLoginAt) { + this.lastLoginAt = lastLoginAt; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/enums/AuthProvider.java b/src/main/java/com/yundage/chat/enums/AuthProvider.java new file mode 100644 index 0000000..81ddadf --- /dev/null +++ b/src/main/java/com/yundage/chat/enums/AuthProvider.java @@ -0,0 +1,20 @@ +package com.yundage.chat.enums; + +public enum AuthProvider { + WECHAT_MINI("wechat_mini"), + WECHAT_OPEN("wechat_open"), + GOOGLE("google"), + GITHUB("github"), + APPLE("apple"), + CUSTOM("custom"); + + private final String value; + + AuthProvider(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/enums/UserType.java b/src/main/java/com/yundage/chat/enums/UserType.java new file mode 100644 index 0000000..48cf007 --- /dev/null +++ b/src/main/java/com/yundage/chat/enums/UserType.java @@ -0,0 +1,17 @@ +package com.yundage.chat.enums; + +public enum UserType { + PERSONAL("personal"), + ENTERPRISE("enterprise"), + ADMIN("admin"); + + private final String value; + + UserType(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/mapper/PasswordResetTokenMapper.java b/src/main/java/com/yundage/chat/mapper/PasswordResetTokenMapper.java new file mode 100644 index 0000000..c8b1269 --- /dev/null +++ b/src/main/java/com/yundage/chat/mapper/PasswordResetTokenMapper.java @@ -0,0 +1,9 @@ +package com.yundage.chat.mapper; + +import com.yundage.chat.entity.PasswordResetToken; +import com.mybatisflex.core.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface PasswordResetTokenMapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/mapper/UserMapper.java b/src/main/java/com/yundage/chat/mapper/UserMapper.java new file mode 100644 index 0000000..5df3a0f --- /dev/null +++ b/src/main/java/com/yundage/chat/mapper/UserMapper.java @@ -0,0 +1,9 @@ +package com.yundage.chat.mapper; + +import com.yundage.chat.entity.User; +import com.mybatisflex.core.BaseMapper; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserMapper extends BaseMapper { +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/service/CustomUserDetailsService.java b/src/main/java/com/yundage/chat/service/CustomUserDetailsService.java new file mode 100644 index 0000000..5f94ecd --- /dev/null +++ b/src/main/java/com/yundage/chat/service/CustomUserDetailsService.java @@ -0,0 +1,32 @@ +package com.yundage.chat.service; + +import com.yundage.chat.entity.User; +import com.yundage.chat.mapper.UserMapper; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.beans.factory.annotation.Autowired; +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 { + + @Autowired + private UserMapper userMapper; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + QueryWrapper queryWrapper = QueryWrapper.create() + .where(User::getEmail).eq(username) + .or(User::getPhone).eq(username); + + User user = userMapper.selectOneByQuery(queryWrapper); + + if (user == null) { + throw new UsernameNotFoundException("用户不存在: " + username); + } + + return user; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/service/EmailService.java b/src/main/java/com/yundage/chat/service/EmailService.java new file mode 100644 index 0000000..449a1a4 --- /dev/null +++ b/src/main/java/com/yundage/chat/service/EmailService.java @@ -0,0 +1,46 @@ +package com.yundage.chat.service; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.stereotype.Service; + +@Service +public class EmailService { + + @Autowired + private JavaMailSender mailSender; + + @Value("${spring.mail.username}") + private String fromEmail; + + @Value("${app.reset-password-url:http://localhost:3000/reset-password}") + private String resetPasswordUrl; + + public void sendPasswordResetEmail(String toEmail, String username, String token) { + try { + SimpleMailMessage message = new SimpleMailMessage(); + message.setFrom(fromEmail); + message.setTo(toEmail); + message.setSubject("密码重置 - 云大AI聊天"); + + String resetLink = resetPasswordUrl + "?token=" + token; + String emailBody = String.format( + "亲爱的 %s,\n\n" + + "我们收到了您的密码重置请求。请点击以下链接重置您的密码:\n\n" + + "%s\n\n" + + "此链接将在1小时后过期。如果您没有请求重置密码,请忽略此邮件。\n\n" + + "祝好,\n" + + "云大AI聊天团队", + username, resetLink + ); + + message.setText(emailBody); + mailSender.send(message); + + } catch (Exception e) { + throw new RuntimeException("发送邮件失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/service/UserService.java b/src/main/java/com/yundage/chat/service/UserService.java new file mode 100644 index 0000000..87d92de --- /dev/null +++ b/src/main/java/com/yundage/chat/service/UserService.java @@ -0,0 +1,160 @@ +package com.yundage.chat.service; + +import com.yundage.chat.dto.*; +import com.yundage.chat.entity.User; +import com.yundage.chat.entity.PasswordResetToken; +import com.yundage.chat.mapper.UserMapper; +import com.yundage.chat.mapper.PasswordResetTokenMapper; +import com.yundage.chat.util.JwtUtil; +import com.mybatisflex.core.query.QueryWrapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.UUID; + +@Service +public class UserService { + + @Autowired + private UserMapper userMapper; + + @Autowired + private PasswordResetTokenMapper tokenMapper; + + @Autowired + private PasswordEncoder passwordEncoder; + + @Autowired + private JwtUtil jwtUtil; + + @Autowired + private AuthenticationManager authenticationManager; + + @Autowired + private EmailService emailService; + + @Transactional + public AuthResponse register(RegisterRequest request) { + // 检查用户是否已存在 + if (request.getEmail() != null && existsByEmail(request.getEmail())) { + throw new RuntimeException("邮箱已被注册"); + } + + if (request.getPhone() != null && existsByPhone(request.getPhone())) { + throw new RuntimeException("手机号已被注册"); + } + + // 创建新用户 + User user = new User(); + user.setUsername(request.getUsername()); + user.setEmail(request.getEmail()); + user.setPhone(request.getPhone()); + user.setPasswordHash(passwordEncoder.encode(request.getPassword())); + user.setCreatedAt(LocalDateTime.now()); + user.setUpdatedAt(LocalDateTime.now()); + + userMapper.insert(user); + + // 生成JWT令牌 + String token = jwtUtil.generateToken(user.getUsername(), user.getId()); + + return new AuthResponse(token, user.getId(), user.getDisplayName(), + user.getEmail(), user.getPhone()); + } + + public AuthResponse login(LoginRequest request) { + try { + Authentication authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(request.getUsername(), request.getPassword()) + ); + + User user = (User) authentication.getPrincipal(); + + // 更新最后登录时间 + user.setLastLoginAt(LocalDateTime.now()); + userMapper.update(user); + + // 生成JWT令牌 + String token = jwtUtil.generateToken(user.getUsername(), user.getId()); + + return new AuthResponse(token, user.getId(), user.getDisplayName(), + user.getEmail(), user.getPhone()); + + } catch (AuthenticationException e) { + throw new RuntimeException("用户名或密码错误"); + } + } + + @Transactional + public void requestPasswordReset(PasswordResetRequest request) { + User user = findByEmail(request.getEmail()); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + + // 生成重置令牌 + String token = UUID.randomUUID().toString(); + LocalDateTime expiresAt = LocalDateTime.now().plusHours(1); // 1小时后过期 + + PasswordResetToken resetToken = new PasswordResetToken(user.getId(), token, expiresAt); + tokenMapper.insert(resetToken); + + // 发送重置邮件 + emailService.sendPasswordResetEmail(user.getEmail(), user.getDisplayName(), token); + } + + @Transactional + public void resetPassword(ResetPasswordRequest request) { + // 查找重置令牌 + QueryWrapper queryWrapper = QueryWrapper.create() + .where(PasswordResetToken::getToken).eq(request.getToken()) + .and(PasswordResetToken::getUsed).eq(false); + + PasswordResetToken resetToken = tokenMapper.selectOneByQuery(queryWrapper); + + if (resetToken == null || !resetToken.isValid()) { + throw new RuntimeException("重置令牌无效或已过期"); + } + + // 更新用户密码 + User user = userMapper.selectOneById(resetToken.getUserId()); + if (user == null) { + throw new RuntimeException("用户不存在"); + } + + user.setPasswordHash(passwordEncoder.encode(request.getNewPassword())); + user.setUpdatedAt(LocalDateTime.now()); + userMapper.update(user); + + // 标记令牌为已使用 + resetToken.setUsed(true); + tokenMapper.update(resetToken); + } + + public User findByEmail(String email) { + QueryWrapper queryWrapper = QueryWrapper.create() + .where(User::getEmail).eq(email); + return userMapper.selectOneByQuery(queryWrapper); + } + + public User findByPhone(String phone) { + QueryWrapper queryWrapper = QueryWrapper.create() + .where(User::getPhone).eq(phone); + return userMapper.selectOneByQuery(queryWrapper); + } + + public boolean existsByEmail(String email) { + return findByEmail(email) != null; + } + + public boolean existsByPhone(String phone) { + return findByPhone(phone) != null; + } +} \ No newline at end of file diff --git a/src/main/java/com/yundage/chat/util/JwtUtil.java b/src/main/java/com/yundage/chat/util/JwtUtil.java new file mode 100644 index 0000000..e90637b --- /dev/null +++ b/src/main/java/com/yundage/chat/util/JwtUtil.java @@ -0,0 +1,91 @@ +package com.yundage.chat.util; + +import io.jsonwebtoken.*; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +@Component +public class JwtUtil { + + @Value("${jwt.secret:mySecretKey}") + private String secret; + + @Value("${jwt.expiration:86400000}") // 24 hours in milliseconds + private Long expiration; + + private SecretKey getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes()); + } + + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + return createToken(claims, userDetails.getUsername()); + } + + public String generateToken(String username, Long userId) { + Map claims = new HashMap<>(); + claims.put("userId", userId); + return createToken(claims, username); + } + + private String createToken(Map claims, String subject) { + return Jwts.builder() + .claims(claims) + .subject(subject) + .issuedAt(new Date(System.currentTimeMillis())) + .expiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey()) + .compact(); + } + + public String extractUsername(String token) { + return extractClaim(token, Claims::getSubject); + } + + public Long extractUserId(String token) { + return extractClaim(token, claims -> claims.get("userId", Long.class)); + } + + public Date extractExpiration(String token) { + return extractClaim(token, Claims::getExpiration); + } + + public T extractClaim(String token, Function claimsResolver) { + final Claims claims = extractAllClaims(token); + return claimsResolver.apply(claims); + } + + private Claims extractAllClaims(String token) { + return Jwts.parser() + .verifyWith(getSigningKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + } + + public Boolean isTokenExpired(String token) { + return extractExpiration(token).before(new Date()); + } + + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = extractUsername(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + public Boolean validateToken(String token) { + try { + extractAllClaims(token); + return !isTokenExpired(token); + } catch (JwtException e) { + return false; + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..fd09557 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,52 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/chat?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai + username: root + password: password + + # JPA configuration (optional, if you need JPA alongside MyBatis-Flex) + jpa: + show-sql: true + hibernate: + ddl-auto: update + + # Mail configuration + mail: + host: smtp.gmail.com + port: 587 + username: your-email@gmail.com + password: your-app-password + properties: + mail: + smtp: + auth: true + starttls: + enable: true + +# MyBatis-Flex configuration +mybatis-flex: + # Enable SQL logging + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # Mapper XML location (optional) + mapper-locations: classpath*:mapper/*.xml + +# JWT configuration +jwt: + secret: mySecretKeyForJWTTokenGenerationAndValidation + expiration: 86400000 # 24 hours in milliseconds + +# App configuration +app: + reset-password-url: http://localhost:3000/reset-password + +# Server configuration +server: + port: 8080 + +# Logging configuration +logging: + level: + com.yundage.chat: debug + com.mybatisflex: debug \ No newline at end of file diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000..f2bd854 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,113 @@ +-- Create database +CREATE DATABASE IF NOT EXISTS chat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE chat; + +-- 1. 用户主表 +CREATE TABLE users ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(50) DEFAULT NULL COMMENT '昵称/展示名', + password_hash VARCHAR(255) DEFAULT NULL COMMENT '哈希密码(可为空)', + phone VARCHAR(20) UNIQUE DEFAULT NULL COMMENT '手机号', + email VARCHAR(100)UNIQUE DEFAULT NULL COMMENT '邮箱', + avatar_url VARCHAR(255) DEFAULT NULL, + user_type ENUM('personal','enterprise','admin') + NOT NULL DEFAULT 'personal' COMMENT '用户类型', + membership_level_id SMALLINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '付费等级', + status TINYINT NOT NULL DEFAULT 1 COMMENT '1=正常, 0=封禁', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + last_login_at DATETIME NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户主表'; + +-- 2. 第三方账号绑定表 +CREATE TABLE user_auth_accounts ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + provider ENUM('wechat_mini','wechat_open','google','github','apple','custom') + NOT NULL COMMENT '登录方式', + provider_user_id VARCHAR(100) NOT NULL COMMENT '第三方平台唯一ID', + access_token VARCHAR(255) DEFAULT NULL, + refresh_token VARCHAR(255) DEFAULT NULL, + expires_at DATETIME DEFAULT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_provider_user (provider, provider_user_id), + CONSTRAINT fk_auth_user FOREIGN KEY (user_id) REFERENCES users(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方登录账号绑定表'; + +-- 3. 会员等级表 +CREATE TABLE membership_levels ( + id SMALLINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + price_month DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '月费', + daily_msg_limit INT UNSIGNED NOT NULL DEFAULT 20, + features JSON DEFAULT NULL COMMENT '权限 JSON', + sort_order TINYINT NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级定义'; + +-- 4. 会话表(支持普通与研究模式) +CREATE TABLE conversations ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(100) DEFAULT NULL, + model_version VARCHAR(50) DEFAULT NULL COMMENT '使用模型版本', + chat_mode ENUM('chat','research') + NOT NULL DEFAULT 'chat' COMMENT 'chat=普通,research=深度研究', + is_active TINYINT NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_conv_user FOREIGN KEY (user_id) REFERENCES users(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话表'; + +-- 5. 深度研究扩展表 +CREATE TABLE conversation_research_meta ( + conversation_id BIGINT UNSIGNED PRIMARY KEY, + topic VARCHAR(200) NOT NULL COMMENT '研究主题', + goal TEXT DEFAULT NULL COMMENT '研究目标描述', + progress_json JSON DEFAULT NULL COMMENT '阶段进度', + draft_report_id BIGINT UNSIGNED DEFAULT NULL COMMENT '草稿报告 ID(预留)', + cite_style ENUM('APA','IEEE','GB/T-7714') DEFAULT 'APA', + tokens_consumed INT UNSIGNED NOT NULL DEFAULT 0, + last_summary_at DATETIME DEFAULT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_research_conv FOREIGN KEY (conversation_id) + REFERENCES conversations(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='深度研究元数据'; + +-- 6. 消息表 +CREATE TABLE messages ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + conversation_id BIGINT UNSIGNED NOT NULL, + sequence_no INT UNSIGNED NOT NULL COMMENT '会话内顺序编号', + role ENUM('user','assistant','system','tool') + NOT NULL, + content LONGTEXT NOT NULL, + tokens INT UNSIGNED DEFAULT NULL, + latency_ms INT UNSIGNED DEFAULT NULL COMMENT '响应耗时', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_msg_conv FOREIGN KEY (conversation_id) REFERENCES conversations(id) + ON DELETE CASCADE, + UNIQUE KEY uq_conv_seq (conversation_id, sequence_no) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表'; + +-- 7. 密码重置令牌表(用于密码重置功能) +CREATE TABLE password_reset_tokens ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + token VARCHAR(255) NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + used TINYINT NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_reset_user FOREIGN KEY (user_id) REFERENCES users(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='密码重置令牌表'; + +-- 插入默认会员等级数据 +INSERT INTO membership_levels (id, name, price_month, daily_msg_limit, features, sort_order) VALUES +(1, '免费版', 0.00, 20, '{"models": ["gpt-3.5"], "features": ["basic_chat"]}', 1), +(2, '专业版', 29.99, 100, '{"models": ["gpt-3.5", "gpt-4"], "features": ["basic_chat", "research_mode"]}', 2), +(3, '企业版', 99.99, 1000, '{"models": ["gpt-3.5", "gpt-4", "gpt-4-turbo"], "features": ["basic_chat", "research_mode", "priority_support"]}', 3); \ No newline at end of file diff --git a/target/classes/application.yml b/target/classes/application.yml new file mode 100644 index 0000000..546db3b --- /dev/null +++ b/target/classes/application.yml @@ -0,0 +1,30 @@ +spring: + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/chat?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai + username: root + password: password + + # JPA configuration (optional, if you need JPA alongside MyBatis-Flex) + jpa: + show-sql: true + hibernate: + ddl-auto: update + +# MyBatis-Flex configuration +mybatis-flex: + # Enable SQL logging + configuration: + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + # Mapper XML location (optional) + mapper-locations: classpath*:mapper/*.xml + +# Server configuration +server: + port: 8080 + +# Logging configuration +logging: + level: + com.yundage.chat: debug + com.mybatisflex: debug \ No newline at end of file diff --git a/target/classes/schema.sql b/target/classes/schema.sql new file mode 100644 index 0000000..f2bd854 --- /dev/null +++ b/target/classes/schema.sql @@ -0,0 +1,113 @@ +-- Create database +CREATE DATABASE IF NOT EXISTS chat CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE chat; + +-- 1. 用户主表 +CREATE TABLE users ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + username VARCHAR(50) DEFAULT NULL COMMENT '昵称/展示名', + password_hash VARCHAR(255) DEFAULT NULL COMMENT '哈希密码(可为空)', + phone VARCHAR(20) UNIQUE DEFAULT NULL COMMENT '手机号', + email VARCHAR(100)UNIQUE DEFAULT NULL COMMENT '邮箱', + avatar_url VARCHAR(255) DEFAULT NULL, + user_type ENUM('personal','enterprise','admin') + NOT NULL DEFAULT 'personal' COMMENT '用户类型', + membership_level_id SMALLINT UNSIGNED NOT NULL DEFAULT 1 COMMENT '付费等级', + status TINYINT NOT NULL DEFAULT 1 COMMENT '1=正常, 0=封禁', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + last_login_at DATETIME NULL +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户主表'; + +-- 2. 第三方账号绑定表 +CREATE TABLE user_auth_accounts ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + provider ENUM('wechat_mini','wechat_open','google','github','apple','custom') + NOT NULL COMMENT '登录方式', + provider_user_id VARCHAR(100) NOT NULL COMMENT '第三方平台唯一ID', + access_token VARCHAR(255) DEFAULT NULL, + refresh_token VARCHAR(255) DEFAULT NULL, + expires_at DATETIME DEFAULT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE KEY uq_provider_user (provider, provider_user_id), + CONSTRAINT fk_auth_user FOREIGN KEY (user_id) REFERENCES users(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='第三方登录账号绑定表'; + +-- 3. 会员等级表 +CREATE TABLE membership_levels ( + id SMALLINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + name VARCHAR(50) NOT NULL, + price_month DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '月费', + daily_msg_limit INT UNSIGNED NOT NULL DEFAULT 20, + features JSON DEFAULT NULL COMMENT '权限 JSON', + sort_order TINYINT NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会员等级定义'; + +-- 4. 会话表(支持普通与研究模式) +CREATE TABLE conversations ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + title VARCHAR(100) DEFAULT NULL, + model_version VARCHAR(50) DEFAULT NULL COMMENT '使用模型版本', + chat_mode ENUM('chat','research') + NOT NULL DEFAULT 'chat' COMMENT 'chat=普通,research=深度研究', + is_active TINYINT NOT NULL DEFAULT 1, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_conv_user FOREIGN KEY (user_id) REFERENCES users(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='会话表'; + +-- 5. 深度研究扩展表 +CREATE TABLE conversation_research_meta ( + conversation_id BIGINT UNSIGNED PRIMARY KEY, + topic VARCHAR(200) NOT NULL COMMENT '研究主题', + goal TEXT DEFAULT NULL COMMENT '研究目标描述', + progress_json JSON DEFAULT NULL COMMENT '阶段进度', + draft_report_id BIGINT UNSIGNED DEFAULT NULL COMMENT '草稿报告 ID(预留)', + cite_style ENUM('APA','IEEE','GB/T-7714') DEFAULT 'APA', + tokens_consumed INT UNSIGNED NOT NULL DEFAULT 0, + last_summary_at DATETIME DEFAULT NULL, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_research_conv FOREIGN KEY (conversation_id) + REFERENCES conversations(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='深度研究元数据'; + +-- 6. 消息表 +CREATE TABLE messages ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + conversation_id BIGINT UNSIGNED NOT NULL, + sequence_no INT UNSIGNED NOT NULL COMMENT '会话内顺序编号', + role ENUM('user','assistant','system','tool') + NOT NULL, + content LONGTEXT NOT NULL, + tokens INT UNSIGNED DEFAULT NULL, + latency_ms INT UNSIGNED DEFAULT NULL COMMENT '响应耗时', + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_msg_conv FOREIGN KEY (conversation_id) REFERENCES conversations(id) + ON DELETE CASCADE, + UNIQUE KEY uq_conv_seq (conversation_id, sequence_no) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='消息表'; + +-- 7. 密码重置令牌表(用于密码重置功能) +CREATE TABLE password_reset_tokens ( + id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, + user_id BIGINT UNSIGNED NOT NULL, + token VARCHAR(255) NOT NULL UNIQUE, + expires_at DATETIME NOT NULL, + used TINYINT NOT NULL DEFAULT 0, + created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT fk_reset_user FOREIGN KEY (user_id) REFERENCES users(id) + ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='密码重置令牌表'; + +-- 插入默认会员等级数据 +INSERT INTO membership_levels (id, name, price_month, daily_msg_limit, features, sort_order) VALUES +(1, '免费版', 0.00, 20, '{"models": ["gpt-3.5"], "features": ["basic_chat"]}', 1), +(2, '专业版', 29.99, 100, '{"models": ["gpt-3.5", "gpt-4"], "features": ["basic_chat", "research_mode"]}', 2), +(3, '企业版', 99.99, 1000, '{"models": ["gpt-3.5", "gpt-4", "gpt-4-turbo"], "features": ["basic_chat", "research_mode", "priority_support"]}', 3); \ No newline at end of file