first commit

This commit is contained in:
zyh
2025-07-18 17:58:07 +08:00
commit 9c6c3e091f
41 changed files with 1901 additions and 0 deletions

34
.claude/commands/ask.md Normal file
View File

@@ -0,0 +1,34 @@
## Usage
`@ask.md <TECHNICAL_QUESTION>`
## 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.

31
.claude/commands/code.md Normal file
View File

@@ -0,0 +1,31 @@
## Usage
`@code.md <FEATURE_DESCRIPTION>`
## 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.

31
.claude/commands/debug.md Normal file
View File

@@ -0,0 +1,31 @@
## Usage
`@debug.md <ERROR_DESCRIPTION>`
## 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.

View File

@@ -0,0 +1,31 @@
## Usage
`@deploy-check.md <DEPLOYMENT_TARGET>`
## 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.

View File

@@ -0,0 +1,31 @@
## Usage
`@optimize.md <PERFORMANCE_TARGET>`
## 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.

View File

@@ -0,0 +1,31 @@
## Usage
`@refactor.md <REFACTOR_SCOPE>`
## 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.

View File

@@ -0,0 +1,31 @@
## Usage
`@review.md <CODE_SCOPE>`
## 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.

View File

@@ -0,0 +1,49 @@
## Usage
```
@scaffold.md <PROJECT_DESCRIPTION>
```
## 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`.

31
.claude/commands/test.md Normal file
View File

@@ -0,0 +1,31 @@
## Usage
`@test.md <COMPONENT_OR_FEATURE>`
## 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.

View File

@@ -0,0 +1,10 @@
{
"permissions": {
"allow": [
"Bash(mkdir:*)",
"Bash(rmdir:*)",
"Bash(rm:*)"
],
"deny": []
}
}

8
.idea/.gitignore generated vendored Normal file
View File

@@ -0,0 +1,8 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

18
.idea/compiler.xml generated Normal file
View File

@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="CompilerConfiguration">
<annotationProcessing>
<profile name="Maven default annotation processors profile" enabled="true">
<sourceOutputDir name="target/generated-sources/annotations" />
<sourceTestOutputDir name="target/generated-test-sources/test-annotations" />
<outputRelativeToContentRoot value="true" />
<module name="chat" />
</profile>
</annotationProcessing>
</component>
<component name="JavacSettings">
<option name="ADDITIONAL_OPTIONS_OVERRIDE">
<module name="chat" options="-parameters" />
</option>
</component>
</project>

6
.idea/encodings.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Encoding">
<file url="file://$PROJECT_DIR$/src/main/java" charset="UTF-8" />
</component>
</project>

20
.idea/jarRepositories.xml generated Normal file
View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RemoteRepositoriesConfiguration">
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Central Repository" />
<option name="url" value="https://repo.maven.apache.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="central" />
<option name="name" value="Maven Central repository" />
<option name="url" value="https://repo1.maven.org/maven2" />
</remote-repository>
<remote-repository>
<option name="id" value="jboss.community" />
<option name="name" value="JBoss Community repository" />
<option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
</remote-repository>
</component>
</project>

12
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ExternalStorageConfigurationManager" enabled="true" />
<component name="MavenProjectsManager">
<option name="originalFiles">
<list>
<option value="$PROJECT_DIR$/pom.xml" />
</list>
</option>
</component>
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" project-jdk-name="17" project-jdk-type="JavaSDK" />
</project>

6
.idea/vcs.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

107
pom.xml Normal file
View File

@@ -0,0 +1,107 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>
<groupId>com.yundage</groupId>
<artifactId>chat</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>chat</name>
<description>Chat project for Spring Boot with MyBatis-Flex</description>
<properties>
<java.version>17</java.version>
<mybatis-flex.version>1.8.6</mybatis-flex.version>
</properties>
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis-Flex Spring Boot Starter -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-spring-boot-starter</artifactId>
<version>${mybatis-flex.version}</version>
</dependency>
<!-- MyBatis-Flex Processor for APT -->
<dependency>
<groupId>com.mybatis-flex</groupId>
<artifactId>mybatis-flex-processor</artifactId>
<version>${mybatis-flex.version}</version>
<scope>provided</scope>
</dependency>
<!-- Spring Boot Test Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Mail Starter for password reset -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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<String, String> 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<String, String> 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<String, String> response = new HashMap<>();
response.put("message", "密码重置邮件已发送");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, String> 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<String, String> response = new HashMap<>();
response.put("message", "密码重置成功");
return ResponseEntity.ok(response);
} catch (Exception e) {
Map<String, String> error = new HashMap<>();
error.put("error", e.getMessage());
return ResponseEntity.badRequest().body(error);
}
}
}

View File

@@ -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<User> 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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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();
}
}

View File

@@ -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<? extends GrantedAuthority> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<PasswordResetToken> {
}

View File

@@ -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<User> {
}

View File

@@ -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;
}
}

View File

@@ -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());
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
public String generateToken(String username, Long userId) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
return createToken(claims, username);
}
private String createToken(Map<String, Object> 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> T extractClaim(String token, Function<Claims, T> 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;
}
}
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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

113
target/classes/schema.sql Normal file
View File

@@ -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);