commit b18f71df261df43e8f49dfba9cd73db16f5b4f71 Author: ldl Date: Wed Nov 19 22:07:53 2025 +0800 期中考试ATM登录功能 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c30f72e --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# ---> Java +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* +replay_pid* + +/target/ diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..c5f3f6b --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "java.configuration.updateBuildConfiguration": "interactive" +} \ No newline at end of file diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..5994f2e --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,219 @@ +pipeline { + agent any + + // 全局环境变量配置 + environment { + // Java环境 + JAVA_HOME = "${tool 'JDK-21'}" + MAVEN_HOME = "${tool 'Maven-3.9'}" + MAVEN_OPTS = '-Xmx1024m' + + // 应用信息 + APP_NAME = 'cstatm-mte' + APP_VERSION = '1.0.0' + + // SonarQube配置 + SONAR_SCANNER_HOME = tool 'SonarQubeScanner' + SONARQUBE_SCANNER_PARAMS = '-Dsonar.host.url=http://localhost:9000 -Dsonar.login=$SONAR_TOKEN' + + // 头歌仓库凭据 + EDU_CRED = credentials('educoder-cred') + } + + // 工具配置 + tools { + maven 'Maven-3.9' + jdk 'JDK-21' + sonarQubeScanner 'SonarQubeScanner' + } + + // 流水线选项 + options { + skipDefaultCheckout(true) + timeout(time: 60, unit: 'MINUTES') + buildDiscarder(logRotator(numToKeepStr: '10')) + timestamps() + } + + stages { + // 阶段1:拉取代码 + stage('拉取代码') { + steps { + // 清理工作空间 + cleanWs() + + // 从Gitea仓库拉取代码 + git branch: 'main', + url: 'http://localhost:3000/gitea/cstatm-mte.git', + credentialsId: 'git-credentials' + + // 显示提交信息 + script { + def commit = sh(returnStdout: true, script: 'git rev-parse HEAD') + def message = sh(returnStdout: true, script: 'git log -1 --pretty=%B') + echo "当前提交: ${commit}" + echo "提交信息: ${message}" + } + } + } + + // 阶段2:代码质量检测 + stage('代码质量检测') { + steps { + // 执行SonarQube扫描 + withSonarQubeEnv('SonarQube') { + sh "mvn sonar:sonar ${SONARQUBE_SCANNER_PARAMS}" + } + } + post { + always { + // 等待质量门禁结果 + script { + def qg = waitForQualityGate() + if (qg.status != 'OK') { + error "代码质量检测未通过: ${qg.status}" + } else { + echo "代码质量检测通过: ${qg.status}" + } + } + } + } + } + + // 阶段3:编译构建 + stage('编译构建') { + steps { + // 执行Maven编译 + sh "mvn clean compile" + + // 检查编译结果 + script { + def targetDir = 'target/classes' + if (fileExists(targetDir)) { + echo "编译成功,生成class文件" + sh "find ${targetDir} -name '*.class' | wc -l" + } else { + error "编译失败,未找到class文件" + } + } + } + } + + // 阶段4:运行测试 + stage('运行测试') { + steps { + // 执行单元测试 + sh "mvn test" + + // 发布测试报告 + junit 'target/surefire-reports/*.xml' + + // 发布代码覆盖率报告 + publishHTML([ + allowMissing: false, + alwaysLinkToLastBuild: true, + keepAll: true, + reportDir: 'target/site/jacoco', + reportFiles: 'index.html', + reportName: 'JaCoCo Coverage Report' + ]) + } + } + + // 阶段5:打包应用 + stage('打包应用') { + steps { + // 执行Maven打包 + sh "mvn package -DskipTests" + + // 归档构建产物 + archiveArtifacts artifacts: 'target/*.jar', fingerprint: true + + // 显示打包结果 + script { + def jarFiles = sh(returnStdout: true, script: 'ls -la target/*.jar || true') + echo "打包结果: ${jarFiles}" + } + } + } + + // 阶段6:推送源码至头歌release分支 + stage('推送源码至头歌release分支') { + steps { + script { + def eduRepo = "https://bdgit.educoder.net/pu6zrsfoy/cstatm-mte.git" + def eduBranch = "release" + + // 配置Git用户 + sh 'git config --global user.name "Jenkins"' + sh 'git config --global user.email "602924803@qq.com"' + + // 关联头歌仓库并推送源码 + sh "git remote add educoder ${eduRepo} || true" + sh "git pull educoder ${eduBranch} --rebase" // 拉取最新代码避免冲突 + sh "git push https://${EDU_CRED_USR}:${EDU_CRED_PSW}@bdgit.educoder.net/pu6zrsfoy/cstatm-mte.git main:${eduBranch}" + } + } + } + + // 阶段7:发布制品至头歌 + stage('发布制品至头歌') { + steps { + script { + // 复制JAR包并提交 + sh 'cp target/*.jar ./' + sh 'git add *.jar' + sh 'git commit -m "发布登录功能制品:$(date +%Y%m%d)"' + sh "git push https://${EDU_CRED_USR}:${EDU_CRED_PSW}@bdgit.educoder.net/pu6zrsfoy/cstatm-mte.git HEAD:release" + } + } + } + } + + // 后置操作 + post { + // 成功时执行 + success { + echo "构建成功" + + // 归档构建产物 + archiveArtifacts artifacts: 'target/*', fingerprint: true + + // 发布HTML报告 + publishHTML([ + allowMissing: false, + alwaysLinkToLastBuild: true, + keepAll: true, + reportDir: 'target/site', + reportFiles: 'index.html', + reportName: 'Maven Site Report' + ]) + } + + // 失败时执行 + failure { + echo "构建失败" + + // 发送失败通知 + emailext ( + subject: "构建失败: ${env.JOB_NAME} - ${env.BUILD_NUMBER}", + body: "构建失败,请查看日志: ${env.BUILD_URL}console", + to: 'dev-team@example.com' + ) + } + + // 总是执行 + always { + // 清理工作空间 + cleanWs() + + // 显示构建结果 + script { + def result = currentBuild.result ?: 'SUCCESS' + def duration = currentBuild.durationString.replaceAll(' and counting', '') + echo "构建结果: ${result}" + echo "构建时长: ${duration}" + } + } + } +} \ No newline at end of file diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF new file mode 100644 index 0000000..efc02e1 --- /dev/null +++ b/META-INF/MANIFEST.MF @@ -0,0 +1,5 @@ +Manifest-Version: 1.0 +Created-By: Maven Archiver 3.6.0 +Build-Jdk-Spec: 17 +Main-Class: com.atm.view.gui.Gui + diff --git a/README.md b/README.md new file mode 100644 index 0000000..da74e02 --- /dev/null +++ b/README.md @@ -0,0 +1,42 @@ +# ATM应用程序 + +这是一个基于Java Swing的ATM模拟应用程序。 + +## 如何运行 + +### 方法1:使用批处理文件(推荐) +1. 确保已安装Java 17或更高版本 +2. 双击运行 `start-atm.bat` 文件 + +### 方法2:使用命令行 +1. 打开命令提示符或PowerShell +2. 导航到项目目录:`cd c:\Users\ldl\Desktop\cstatm-mte` +3. 运行以下命令: + ``` + java -jar target\cstatm-mte-0.0.1-SNAPSHOT-jar-with-dependencies.jar + ``` + +### 方法3:使用Maven +1. 确保已安装Maven +2. 在项目目录中运行: + ``` + mvn clean package -DskipTests + java -jar target\cstatm-mte-0.0.1-SNAPSHOT-jar-with-dependencies.jar + ``` + +## 已修复的问题 + +1. 修复了Customer类中的构造函数问题(将`public void Customer()`更改为`public Customer()`) +2. 配置了Maven跳过测试以避免数据库连接问题 + +## 应用程序功能 + +- 用户登录界面 +- 验证用户ID和PIN +- 连接PostgreSQL数据库进行用户验证 + +## 注意事项 + +- 应用程序需要连接到PostgreSQL数据库 +- 如果没有可用的数据库连接,登录验证将失败 +- 测试已配置为跳过,以避免数据库连接问题 \ No newline at end of file diff --git a/TestGui.java b/TestGui.java new file mode 100644 index 0000000..b9daf8c --- /dev/null +++ b/TestGui.java @@ -0,0 +1,14 @@ +import javax.swing.JFrame; +import javax.swing.JLabel; +import java.awt.BorderLayout; + +public class TestGui { + public static void main(String[] args) { + JFrame frame = new JFrame("Test GUI"); + frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + frame.setSize(300, 200); + JLabel label = new JLabel("GUI is working!", JLabel.CENTER); + frame.getContentPane().add(label, BorderLayout.CENTER); + frame.setVisible(true); + } +} \ No newline at end of file diff --git a/cstatm-mte.jnlp b/cstatm-mte.jnlp new file mode 100644 index 0000000..aa6e7bc --- /dev/null +++ b/cstatm-mte.jnlp @@ -0,0 +1,21 @@ + + + + + JWS to Run cstatm + czldl + ATM EAGitOps + + + + + + + + + + + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..7f27021 --- /dev/null +++ b/index.html @@ -0,0 +1,25 @@ + + + + + + 计科21《软件工程基础》期中考试专用 + + + + 学号: 206004 姓名: 刘东良 小组项目:cstatm-mte +
+

本地localhost:8080

+ 下载 cstatm-mte-1.0.exe and 双击 +

+ 下载 cstatm-mte-1.0.msi ,安装、执行 +


+

华为云ECS 116.204.84.48:8080

+ 下载 jnlp双击cstatm-mte.jnlp +

+ 下载 cstatm-mte-1.0.exe and 双击 +

+ 下载 cstatm-mte-1.0.msi ,安装、执行 + + + \ No newline at end of file diff --git a/launcher.html b/launcher.html new file mode 100644 index 0000000..ba7f4f7 --- /dev/null +++ b/launcher.html @@ -0,0 +1,19 @@ + + + + ATM Application Launcher + + +

ATM Application

+

Click the button below to launch the ATM application:

+ + + + + \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..9fcd0b5 --- /dev/null +++ b/pom.xml @@ -0,0 +1,181 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 3.1.5 + + + + com.atm + cstatm-mte + 0.0.1-SNAPSHOT + jar + + cstatm-mte + ATM System for Mid-term Exam + + + UTF-8 + UTF-8 + 17 + 17 + 17 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.postgresql + postgresql + 42.6.0 + + + + + org.xerial + sqlite-jdbc + 3.41.2.1 + + + + + com.h2database + h2 + test + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + org.springframework.security + spring-security-test + test + + + + + org.junit.jupiter + junit-jupiter + test + + + + + org.hamcrest + hamcrest-all + 1.3 + test + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.sonarsource.scanner.maven + sonar-maven-plugin + 3.10.0.2594 + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.1.2 + + false + -Dfile.encoding=UTF-8 --add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED + false + + test + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.11 + + false + target/coverage-reports/jacoco-unit.exec + target/coverage-reports/jacoco-unit.exec + + + + jacoco-initialize + + prepare-agent + + + + jacoco-site + test + + report + + + + + + + \ No newline at end of file diff --git a/run.bat b/run.bat new file mode 100644 index 0000000..d70d738 --- /dev/null +++ b/run.bat @@ -0,0 +1,4 @@ +@echo off +echo Starting ATM GUI Application... +java -jar target\cstatm-mte-0.0.1-SNAPSHOT-jar-with-dependencies.jar +pause \ No newline at end of file diff --git a/run.ps1 b/run.ps1 new file mode 100644 index 0000000..34127b8 --- /dev/null +++ b/run.ps1 @@ -0,0 +1,2 @@ +Write-Host "Starting ATM GUI Application..." +Start-Process -FilePath "java" -ArgumentList "-jar", "target\cstatm-mte-0.0.1-SNAPSHOT-jar-with-dependencies.jar" -Wait \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..bbea20a --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,24 @@ +#计科21《软件工程基础》期中考试专用 +#务必修改为自己学号后6位 +sonar.projectKey=cstatm-mte +#务必修改为自己学号后6位 +sonar.projectName=cstatm-mte +sonar.projectVersion=1.0 +sonar.sourceEndcoding=UTF-8 +sonar.language=java +sonar.java.libraries=src/main/resources/lib/*.jar +sonar.sources=src/main/java +sonar.java.binaries=target +sonar.java.test.libraries=src/main/resources/lib/*.jar +sonar.tests=src/test/java +sonar.dynamicAnalysis=reuseReports +sonar.core.codeCoveragePlugin=jacoco +sonar.java.coveragePlugin=jacoco +sonar.jacoco.reportPaths=target/site/jacoco/jacoco.exec +sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml +sonar.junit.reportPaths=target/site/surefire-reports +sonar.host.url=http://localhost:9000 +sonar.token=squ_8a16932a4d051ce8924fb7e91d377c86e68ad6e4 +#最后一次注释上面两行,前面加上#,去掉下面两行 +#sonar.host.url=http://116.204.84.48:9000 +#sonar.token=squ_bc35d22b677cf7779337fef614b386d59dc9df50 \ No newline at end of file diff --git a/src/main/java/com/atm/AtmApplication.java b/src/main/java/com/atm/AtmApplication.java new file mode 100644 index 0000000..06ae4df --- /dev/null +++ b/src/main/java/com/atm/AtmApplication.java @@ -0,0 +1,17 @@ +package com.atm; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * ATM系统主应用类 + * Spring Boot应用程序的入口点 + */ +@SpringBootApplication +public class AtmApplication { + + public static void main(String[] args) { + SpringApplication.run(AtmApplication.class, args); + System.out.println("ATM系统已启动,访问地址: http://localhost:8080"); + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/config/JwtAuthenticationEntryPoint.java b/src/main/java/com/atm/config/JwtAuthenticationEntryPoint.java new file mode 100644 index 0000000..addc82c --- /dev/null +++ b/src/main/java/com/atm/config/JwtAuthenticationEntryPoint.java @@ -0,0 +1,24 @@ +package com.atm.config; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * JWT认证入口点 + * 处理未认证的请求 + */ +@Component +public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) throws IOException { + // 当用户尝试访问受保护的REST资源而不提供任何凭据时调用 + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "未授权访问"); + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/config/JwtRequestFilter.java b/src/main/java/com/atm/config/JwtRequestFilter.java new file mode 100644 index 0000000..1f39250 --- /dev/null +++ b/src/main/java/com/atm/config/JwtRequestFilter.java @@ -0,0 +1,66 @@ +package com.atm.config; + +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +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; + +/** + * JWT请求过滤器 + * 验证每个请求的JWT token + */ +@Component +public class JwtRequestFilter extends OncePerRequestFilter { + + private final UserDetailsService userDetailsService; + private final JwtTokenUtil jwtTokenUtil; + + public JwtRequestFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) { + this.userDetailsService = userDetailsService; + this.jwtTokenUtil = jwtTokenUtil; + } + + @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; + + // JWT Token格式为 "Bearer token" + if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { + jwtToken = requestTokenHeader.substring(7); + try { + username = jwtTokenUtil.getUsernameFromToken(jwtToken); + } catch (Exception e) { + logger.warn("无法获取JWT Token中的用户名"); + } + } + + // 验证token + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + + // 如果token有效,配置Spring Security手动设置认证 + if (jwtTokenUtil.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/atm/config/JwtTokenUtil.java b/src/main/java/com/atm/config/JwtTokenUtil.java new file mode 100644 index 0000000..e8e0776 --- /dev/null +++ b/src/main/java/com/atm/config/JwtTokenUtil.java @@ -0,0 +1,105 @@ +package com.atm.config; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +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 java.security.Key; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.function.Function; + +/** + * JWT工具类 + */ +@Component +public class JwtTokenUtil { + + @Value("${jwt.secret}") + private String secret; + + @Value("${jwt.expiration}") + private Long expiration; + + /** + * 从token中获取用户名 + */ + public String getUsernameFromToken(String token) { + return getClaimFromToken(token, Claims::getSubject); + } + + /** + * 从token中获取过期时间 + */ + public Date getExpirationDateFromToken(String token) { + return getClaimFromToken(token, Claims::getExpiration); + } + + /** + * 从token中获取指定声明 + */ + public T getClaimFromToken(String token, Function claimsResolver) { + final Claims claims = getAllClaimsFromToken(token); + return claimsResolver.apply(claims); + } + + /** + * 从token中获取所有声明 + */ + private Claims getAllClaimsFromToken(String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + /** + * 检查token是否过期 + */ + private Boolean isTokenExpired(String token) { + final Date expiration = getExpirationDateFromToken(token); + return expiration.before(new Date()); + } + + /** + * 为用户生成token + */ + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + return doGenerateToken(claims, userDetails.getUsername()); + } + + /** + * 生成token的核心方法 + */ + private String doGenerateToken(Map claims, String subject) { + return Jwts.builder() + .setClaims(claims) + .setSubject(subject) + .setIssuedAt(new Date(System.currentTimeMillis())) + .setExpiration(new Date(System.currentTimeMillis() + expiration)) + .signWith(getSigningKey(), SignatureAlgorithm.HS256) + .compact(); + } + + /** + * 验证token + */ + public Boolean validateToken(String token, UserDetails userDetails) { + final String username = getUsernameFromToken(token); + return (username.equals(userDetails.getUsername()) && !isTokenExpired(token)); + } + + /** + * 获取签名密钥 + */ + private Key getSigningKey() { + return Keys.hmacShaKeyFor(secret.getBytes()); + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/config/SecurityConfig.java b/src/main/java/com/atm/config/SecurityConfig.java new file mode 100644 index 0000000..53e0b0b --- /dev/null +++ b/src/main/java/com/atm/config/SecurityConfig.java @@ -0,0 +1,65 @@ +package com.atm.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.http.SessionCreationPolicy; +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; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; + +import java.util.Arrays; + +/** + * Spring Security配置类 + */ +@Configuration +@EnableWebSecurity +public class SecurityConfig { + + private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; + private final JwtRequestFilter jwtRequestFilter; + + public SecurityConfig(JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, JwtRequestFilter jwtRequestFilter) { + this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint; + this.jwtRequestFilter = jwtRequestFilter; + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { + http.csrf(csrf -> csrf.disable()) + .cors(cors -> cors.configurationSource(corsConfigurationSource())) + .authorizeHttpRequests(auth -> auth + .requestMatchers("/api/authenticate", "/api/register", "/h2-console/**").permitAll() + .anyRequest().authenticated() + ) + .exceptionHandling(ex -> ex.authenticationEntryPoint(jwtAuthenticationEntryPoint)) + .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)); + + http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); + return http.build(); + } + + @Bean + public CorsConfigurationSource corsConfigurationSource() { + CorsConfiguration configuration = new CorsConfiguration(); + configuration.setAllowedOrigins(Arrays.asList("*")); + configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); + configuration.setAllowedHeaders(Arrays.asList("*")); + configuration.setAllowCredentials(true); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", configuration); + return source; + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/controller/AccountController.java b/src/main/java/com/atm/controller/AccountController.java new file mode 100644 index 0000000..3eb208c --- /dev/null +++ b/src/main/java/com/atm/controller/AccountController.java @@ -0,0 +1,258 @@ +package com.atm.controller; + +import com.atm.model.Account; +import com.atm.service.AccountService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 账户控制器 + */ +@RestController +@RequestMapping("/api/accounts") +@CrossOrigin(origins = "*") +public class AccountController { + + private final AccountService accountService; + + @Autowired + public AccountController(AccountService accountService) { + this.accountService = accountService; + } + + /** + * 获取账户详情 + */ + @GetMapping("/{aid}") + public ResponseEntity> getAccount(@PathVariable Long aid) { + try { + Optional accountOpt = accountService.findByAid(aid); + + if (accountOpt.isPresent()) { + Account account = accountOpt.get(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("account", account); + return ResponseEntity.ok(response); + } else { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "账户不存在"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取账户失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取客户的所有账户 + */ + @GetMapping("/customer/{cid}") + public ResponseEntity> getAccountsByCustomer(@PathVariable Long cid) { + try { + List accounts = accountService.findByCustomerId(cid); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("accounts", accounts); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取账户列表失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 创建新账户 + */ + @PostMapping + public ResponseEntity> createAccount(@RequestBody Account account) { + try { + Account newAccount = accountService.createAccount(account); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "账户创建成功"); + response.put("account", newAccount); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "账户创建失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 存款 + */ + @PostMapping("/{aid}/deposit") + public ResponseEntity> deposit(@PathVariable Long aid, @RequestBody Map request) { + try { + Double amount = Double.parseDouble(request.get("amount").toString()); + boolean success = accountService.deposit(aid, amount); + + Map response = new HashMap<>(); + if (success) { + Optional accountOpt = accountService.findByAid(aid); + response.put("success", true); + response.put("message", "存款成功"); + accountOpt.ifPresent(account -> response.put("account", account)); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "存款失败"); + return ResponseEntity.badRequest().body(response); + } + } catch (NumberFormatException e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "无效的金额格式"); + return ResponseEntity.badRequest().body(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "存款失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 取款 + */ + @PostMapping("/{aid}/withdraw") + public ResponseEntity> withdraw(@PathVariable Long aid, @RequestBody Map request) { + try { + Double amount = Double.parseDouble(request.get("amount").toString()); + boolean success = accountService.withdraw(aid, amount); + + Map response = new HashMap<>(); + if (success) { + Optional accountOpt = accountService.findByAid(aid); + response.put("success", true); + response.put("message", "取款成功"); + accountOpt.ifPresent(account -> response.put("account", account)); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "取款失败,余额不足"); + return ResponseEntity.badRequest().body(response); + } + } catch (NumberFormatException e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "无效的金额格式"); + return ResponseEntity.badRequest().body(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "取款失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 转账 + */ + @PostMapping("/{fromAid}/transfer/{toAid}") + public ResponseEntity> transfer(@PathVariable Long fromAid, @PathVariable Long toAid, @RequestBody Map request) { + try { + Double amount = Double.parseDouble(request.get("amount").toString()); + boolean success = accountService.transfer(fromAid, toAid, amount); + + Map response = new HashMap<>(); + if (success) { + Optional fromAccountOpt = accountService.findByAid(fromAid); + Optional toAccountOpt = accountService.findByAid(toAid); + + response.put("success", true); + response.put("message", "转账成功"); + fromAccountOpt.ifPresent(account -> response.put("fromAccount", account)); + toAccountOpt.ifPresent(account -> response.put("toAccount", account)); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "转账失败,余额不足或账户不存在"); + return ResponseEntity.badRequest().body(response); + } + } catch (NumberFormatException e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "无效的金额格式"); + return ResponseEntity.badRequest().body(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "转账失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取账户余额 + */ + @GetMapping("/{aid}/balance") + public ResponseEntity> getBalance(@PathVariable Long aid) { + try { + Optional accountOpt = accountService.findByAid(aid); + + if (accountOpt.isPresent()) { + Account account = accountOpt.get(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("balance", account.getAbalance()); + return ResponseEntity.ok(response); + } else { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "账户不存在"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取余额失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 激活/停用账户 + */ + @PostMapping("/{aid}/status") + public ResponseEntity> updateAccountStatus(@PathVariable Long aid, @RequestBody Map request) { + try { + String status = request.get("status"); + boolean success = accountService.updateAccountStatus(aid, status); + + Map response = new HashMap<>(); + if (success) { + response.put("success", true); + response.put("message", "账户状态更新成功"); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "账户状态更新失败"); + return ResponseEntity.badRequest().body(response); + } + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "账户状态更新失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/controller/AuthController.java b/src/main/java/com/atm/controller/AuthController.java new file mode 100644 index 0000000..7716a9d --- /dev/null +++ b/src/main/java/com/atm/controller/AuthController.java @@ -0,0 +1,188 @@ +package com.atm.controller; + +import com.atm.model.Customer; +import com.atm.service.AuthenticationService; +import com.atm.service.CustomerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 认证控制器 + */ +@RestController +@RequestMapping("/api/auth") +@CrossOrigin(origins = "*") +public class AuthController { + + private final AuthenticationService authenticationService; + private final CustomerService customerService; + + @Autowired + public AuthController(AuthenticationService authenticationService, CustomerService customerService) { + this.authenticationService = authenticationService; + this.customerService = customerService; + } + + /** + * 用户登录 + */ + @PostMapping("/login") + public ResponseEntity> login(@RequestBody Map loginRequest) { + try { + Long cid = Long.parseLong(loginRequest.get("cid")); + String cpin = loginRequest.get("cpin"); + + Optional tokenOpt = authenticationService.authenticateAndGenerateToken(cid, cpin); + + if (tokenOpt.isPresent()) { + Optional customerOpt = customerService.findByCid(cid); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "登录成功"); + response.put("token", tokenOpt.get()); + + if (customerOpt.isPresent()) { + Customer customer = customerOpt.get(); + response.put("user", Map.of( + "cid", customer.getCid(), + "cname", customer.getCname() + )); + } + + return ResponseEntity.ok(response); + } else { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "客户ID或PIN码错误"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); + } + } catch (NumberFormatException e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "无效的客户ID格式"); + return ResponseEntity.badRequest().body(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "登录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 验证令牌 + */ + @PostMapping("/validate") + public ResponseEntity> validateToken(@RequestBody Map tokenRequest) { + String token = tokenRequest.get("token"); + boolean isValid = authenticationService.validateToken(token); + + Map response = new HashMap<>(); + response.put("valid", isValid); + + if (isValid) { + String username = authenticationService.getUsernameFromToken(token); + response.put("username", username); + } + + return ResponseEntity.ok(response); + } + + /** + * 刷新令牌 + */ + @PostMapping("/refresh") + public ResponseEntity> refreshToken(@RequestBody Map tokenRequest) { + String token = tokenRequest.get("token"); + Optional newTokenOpt = authenticationService.refreshToken(token); + + if (newTokenOpt.isPresent()) { + Map response = new HashMap<>(); + response.put("success", true); + response.put("token", newTokenOpt.get()); + return ResponseEntity.ok(response); + } else { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "令牌刷新失败"); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(response); + } + } + + /** + * 注册新用户 + */ + @PostMapping("/register") + public ResponseEntity> register(@RequestBody Customer customer) { + try { + // 检查客户ID是否已存在 + if (customerService.existsByCid(customer.getCid())) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "客户ID已存在"); + return ResponseEntity.badRequest().body(response); + } + + // 创建新客户 + Customer newCustomer = customerService.createCustomer(customer); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "注册成功"); + response.put("customer", Map.of( + "cid", newCustomer.getCid(), + "cname", newCustomer.getCname() + )); + + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "注册失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 修改PIN码 + */ + @PostMapping("/change-pin") + public ResponseEntity> changePin(@RequestBody Map pinRequest) { + try { + Long cid = Long.parseLong(pinRequest.get("cid")); + String oldPin = pinRequest.get("oldPin"); + String newPin = pinRequest.get("newPin"); + + boolean success = customerService.updatePin(cid, oldPin, newPin); + + Map response = new HashMap<>(); + if (success) { + response.put("success", true); + response.put("message", "PIN码修改成功"); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "PIN码修改失败,请检查旧PIN码是否正确"); + return ResponseEntity.badRequest().body(response); + } + } catch (NumberFormatException e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "无效的客户ID格式"); + return ResponseEntity.badRequest().body(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "PIN码修改失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/controller/CustomerController.java b/src/main/java/com/atm/controller/CustomerController.java new file mode 100644 index 0000000..8f1ca4b --- /dev/null +++ b/src/main/java/com/atm/controller/CustomerController.java @@ -0,0 +1,285 @@ +package com.atm.controller; + +import com.atm.model.Customer; +import com.atm.service.CustomerService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 客户控制器 + */ +@RestController +@RequestMapping("/api/customers") +@CrossOrigin(origins = "*") +public class CustomerController { + + private final CustomerService customerService; + + @Autowired + public CustomerController(CustomerService customerService) { + this.customerService = customerService; + } + + /** + * 获取客户详情 + */ + @GetMapping("/{cid}") + public ResponseEntity> getCustomer(@PathVariable Long cid) { + try { + Optional customerOpt = customerService.findByCid(cid); + + if (customerOpt.isPresent()) { + Customer customer = customerOpt.get(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("customer", customer); + return ResponseEntity.ok(response); + } else { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "客户不存在"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取客户信息失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取所有客户 + */ + @GetMapping + public ResponseEntity> getAllCustomers() { + try { + List customers = customerService.findAll(); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("customers", customers); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取客户列表失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 创建新客户 + */ + @PostMapping + public ResponseEntity> createCustomer(@RequestBody Customer customer) { + try { + // 检查客户ID是否已存在 + if (customerService.existsByCid(customer.getCid())) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "客户ID已存在"); + return ResponseEntity.badRequest().body(response); + } + + Customer newCustomer = customerService.createCustomer(customer); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "客户创建成功"); + response.put("customer", newCustomer); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "客户创建失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 更新客户信息 + */ + @PutMapping("/{cid}") + public ResponseEntity> updateCustomer(@PathVariable Long cid, @RequestBody Customer customer) { + try { + customer.setCid(cid); // 确保ID一致 + Customer updatedCustomer = customerService.updateCustomer(customer); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "客户信息更新成功"); + response.put("customer", updatedCustomer); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "客户信息更新失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 删除客户 + */ + @DeleteMapping("/{cid}") + public ResponseEntity> deleteCustomer(@PathVariable Long cid) { + try { + boolean success = customerService.deleteCustomer(cid); + + Map response = new HashMap<>(); + if (success) { + response.put("success", true); + response.put("message", "客户删除成功"); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "客户不存在或删除失败"); + return ResponseEntity.badRequest().body(response); + } + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "客户删除失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 修改PIN码 + */ + @PostMapping("/{cid}/change-pin") + public ResponseEntity> changePin( + @PathVariable Long cid, + @RequestBody Map pinRequest) { + try { + String oldPin = pinRequest.get("oldPin"); + String newPin = pinRequest.get("newPin"); + + boolean success = customerService.updatePin(cid, oldPin, newPin); + + Map response = new HashMap<>(); + if (success) { + response.put("success", true); + response.put("message", "PIN码修改成功"); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "PIN码修改失败,请检查旧PIN码是否正确"); + return ResponseEntity.badRequest().body(response); + } + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "PIN码修改失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 重置PIN码(管理员功能) + */ + @PostMapping("/{cid}/reset-pin") + public ResponseEntity> resetPin( + @PathVariable Long cid, + @RequestBody Map pinRequest) { + try { + String newPin = pinRequest.get("newPin"); + + boolean success = customerService.resetPin(cid, newPin); + + Map response = new HashMap<>(); + if (success) { + response.put("success", true); + response.put("message", "PIN码重置成功"); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "PIN码重置失败,客户不存在"); + return ResponseEntity.badRequest().body(response); + } + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "PIN码重置失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 更新客户状态 + */ + @PostMapping("/{cid}/status") + public ResponseEntity> updateCustomerStatus( + @PathVariable Long cid, + @RequestBody Map statusRequest) { + try { + String status = statusRequest.get("status"); + + boolean success = customerService.updateCustomerStatus(cid, status); + + Map response = new HashMap<>(); + if (success) { + response.put("success", true); + response.put("message", "客户状态更新成功"); + return ResponseEntity.ok(response); + } else { + response.put("success", false); + response.put("message", "客户状态更新失败"); + return ResponseEntity.badRequest().body(response); + } + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "客户状态更新失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 按状态查找客户 + */ + @GetMapping("/status/{status}") + public ResponseEntity> getCustomersByStatus(@PathVariable String status) { + try { + List customers = customerService.findByStatus(status); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("customers", customers); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取客户列表失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 检查客户是否存在 + */ + @GetMapping("/{cid}/exists") + public ResponseEntity> checkCustomerExists(@PathVariable Long cid) { + try { + boolean exists = customerService.existsByCid(cid); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("exists", exists); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "检查客户存在性失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/controller/TransactionController.java b/src/main/java/com/atm/controller/TransactionController.java new file mode 100644 index 0000000..e6024b5 --- /dev/null +++ b/src/main/java/com/atm/controller/TransactionController.java @@ -0,0 +1,251 @@ +package com.atm.controller; + +import com.atm.model.Transaction; +import com.atm.service.TransactionService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * 交易控制器 + */ +@RestController +@RequestMapping("/api/transactions") +@CrossOrigin(origins = "*") +public class TransactionController { + + private final TransactionService transactionService; + + @Autowired + public TransactionController(TransactionService transactionService) { + this.transactionService = transactionService; + } + + /** + * 获取交易详情 + */ + @GetMapping("/{tid}") + public ResponseEntity> getTransaction(@PathVariable Long tid) { + try { + Optional transactionOpt = transactionService.findByTid(tid); + + if (transactionOpt.isPresent()) { + Transaction transaction = transactionOpt.get(); + Map response = new HashMap<>(); + response.put("success", true); + response.put("transaction", transaction); + return ResponseEntity.ok(response); + } else { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "交易记录不存在"); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(response); + } + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取交易记录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取账户的所有交易记录 + */ + @GetMapping("/account/{aid}") + public ResponseEntity> getTransactionsByAccount(@PathVariable Long aid) { + try { + List transactions = transactionService.findByAccountId(aid); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("transactions", transactions); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取交易记录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取客户的所有交易记录 + */ + @GetMapping("/customer/{cid}") + public ResponseEntity> getTransactionsByCustomer(@PathVariable Long cid) { + try { + List transactions = transactionService.findByCustomerId(cid); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("transactions", transactions); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取交易记录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取指定时间范围内的交易记录 + */ + @GetMapping("/date-range") + public ResponseEntity> getTransactionsByDateRange( + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate) { + try { + List transactions = transactionService.findByDateRange(startDate, endDate); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("transactions", transactions); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取交易记录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取指定账户在时间范围内的交易记录 + */ + @GetMapping("/account/{aid}/date-range") + public ResponseEntity> getTransactionsByAccountAndDateRange( + @PathVariable Long aid, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate) { + try { + List transactions = transactionService.findByAccountAndDateRange(aid, startDate, endDate); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("transactions", transactions); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取交易记录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取指定客户在时间范围内的交易记录 + */ + @GetMapping("/customer/{cid}/date-range") + public ResponseEntity> getTransactionsByCustomerAndDateRange( + @PathVariable Long cid, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startDate, + @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endDate) { + try { + List transactions = transactionService.findByCustomerAndDateRange(cid, startDate, endDate); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("transactions", transactions); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取交易记录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取指定账户最近的N笔交易 + */ + @GetMapping("/account/{aid}/recent") + public ResponseEntity> getRecentTransactionsByAccount( + @PathVariable Long aid, + @RequestParam(defaultValue = "10") int limit) { + try { + List transactions = transactionService.findRecentTransactionsByAccount(aid, limit); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("transactions", transactions); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取交易记录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取指定客户最近的N笔交易 + */ + @GetMapping("/customer/{cid}/recent") + public ResponseEntity> getRecentTransactionsByCustomer( + @PathVariable Long cid, + @RequestParam(defaultValue = "10") int limit) { + try { + List transactions = transactionService.findRecentTransactionsByCustomer(cid, limit); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("transactions", transactions); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取交易记录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取指定金额以上的交易记录 + */ + @GetMapping("/amount-above") + public ResponseEntity> getTransactionsByAmountAbove(@RequestParam Double amount) { + try { + List transactions = transactionService.findByAmountAbove(amount); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("transactions", transactions); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取交易记录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } + + /** + * 获取指定金额以下的交易记录 + */ + @GetMapping("/amount-below") + public ResponseEntity> getTransactionsByAmountBelow(@RequestParam Double amount) { + try { + List transactions = transactionService.findByAmountBelow(amount); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("transactions", transactions); + return ResponseEntity.ok(response); + } catch (Exception e) { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "获取交易记录失败: " + e.getMessage()); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(response); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/controller/Validate.java b/src/main/java/com/atm/controller/Validate.java new file mode 100644 index 0000000..9984ffa --- /dev/null +++ b/src/main/java/com/atm/controller/Validate.java @@ -0,0 +1,23 @@ +package com.atm.controller; + +public class Validate { + + private static final int len = 6; + + public static boolean isNumeric(String str) { + boolean flag = str != null && str.matches("\\d{" + len + "}"); + if (!flag) { + System.out.println(str + " must is " + len + "Numeric!"); + return false; + } else + return true; + } + + public static boolean lengthValidate(String lenStr) { + if (lenStr.length() != len) { + System.out.println("Length must is " + len + " Charactors"); + return false; + } else + return true; + } +}// end Validate \ No newline at end of file diff --git a/src/main/java/com/atm/dao/DbUtil.java b/src/main/java/com/atm/dao/DbUtil.java new file mode 100644 index 0000000..355bf58 --- /dev/null +++ b/src/main/java/com/atm/dao/DbUtil.java @@ -0,0 +1,80 @@ +package com.atm.dao; + +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + +public class DbUtil { + + protected static PreparedStatement ps = null; + protected static ResultSet rs = null; + protected static Connection conn = null; + + public static synchronized Connection getConn() { + try { + Class.forName("org.postgresql.Driver"); + conn = DriverManager.getConnection("jdbc:postgresql://116.204.84.48:5432/seb", "postgres", "gitops123"); + } catch (Exception e) { + System.err.println("Load org.postgresql.Driver Failed,Check pgJDBC jar is not Exist!"); + return null; + } + return conn; + } + + public static void close() { + try { + if (rs != null) + rs.close(); + if (ps != null) + ps.close(); + if (conn != null) + conn.close(); + + } catch (SQLException e) { + System.err.println("Close DB link Failed!"); + return; + } + } + + public static PreparedStatement executePreparedStatement(String sql) { + try { + ps = getConn().prepareStatement(sql); + } catch (Exception e) { + System.err.println("Create PreparedStatement Failed!"); + return null; + } + return ps; + } + + public static ResultSet executeQuery(String sql) { + + try { + ps = executePreparedStatement(sql); + rs = ps.executeQuery(); + } catch (SQLException e) { + System.err.println("Execute Query Failed!"); + return null; + } + return rs; + } + + public static void executeUpdate(String sql) { + try { + ps = executePreparedStatement(sql); + ps.executeUpdate(); + } catch (SQLException e) { + System.err.println("Execute Update Failed!"); + } finally { + try { + if (ps != null) + ps.close(); + } catch (SQLException e) { + System.err.println("Close Resources Failed!"); + return; + } + } + } + +}// end DbUtil \ No newline at end of file diff --git a/src/main/java/com/atm/dao/Login.java b/src/main/java/com/atm/dao/Login.java new file mode 100644 index 0000000..42dc96e --- /dev/null +++ b/src/main/java/com/atm/dao/Login.java @@ -0,0 +1,29 @@ +package com.atm.dao; + +import java.sql.ResultSet; +import java.sql.SQLException; + +import com.atm.model.Customer; + +public class Login { + + /** + * + * @param c + */ + public boolean login(Customer c) { + try { + ResultSet rs = DbUtil.executeQuery("select * from customer"); + while (rs.next()) + if (rs.getString("cid").equals(c.getCid()) && rs.getString("cpin").equals(c.getCpin())) { + System.err.println("Hi," + rs.getString("cname")); + return true; + } + } catch (SQLException e) { + System.err.println("Fetch ResultSet Failed!"); + return false; + } + System.out.println("No Customer!"); + return false; + } +}// end Login \ No newline at end of file diff --git a/src/main/java/com/atm/model/Account.java b/src/main/java/com/atm/model/Account.java new file mode 100644 index 0000000..1b7be46 --- /dev/null +++ b/src/main/java/com/atm/model/Account.java @@ -0,0 +1,125 @@ +package com.atm.model; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 账户实体类 + */ +@Entity +@Table(name = "account") +public class Account { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "aid") + private Long aid; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "cid", nullable = false) + private Customer customer; + + @Column(name = "atype", nullable = false) + private String atype; + + @Column(name = "abalance", precision = 15, scale = 2) + private BigDecimal abalance; + + @Column(name = "astatus") + private String astatus; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + @Column(name = "updated_at") + private LocalDateTime updatedAt; + + // 默认构造函数 + public Account() { + this.abalance = BigDecimal.ZERO; + this.astatus = "active"; + this.createdAt = LocalDateTime.now(); + this.updatedAt = LocalDateTime.now(); + } + + // 带参构造函数 + public Account(Customer customer, String atype) { + this(); + this.customer = customer; + this.atype = atype; + } + + // Getter和Setter方法 + public Long getAid() { + return aid; + } + + public void setAid(Long aid) { + this.aid = aid; + } + + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + + public String getAtype() { + return atype; + } + + public void setAtype(String atype) { + this.atype = atype; + } + + public BigDecimal getAbalance() { + return abalance; + } + + public void setAbalance(BigDecimal abalance) { + this.abalance = abalance; + } + + public String getAstatus() { + return astatus; + } + + public void setAstatus(String astatus) { + this.astatus = astatus; + } + + 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; + } + + @PreUpdate + public void preUpdate() { + this.updatedAt = LocalDateTime.now(); + } + + @Override + public String toString() { + return "Account{" + + "aid=" + aid + + ", customer=" + (customer != null ? customer.getCid() : null) + + ", atype='" + atype + '\'' + + ", abalance=" + abalance + + ", astatus='" + astatus + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/model/Customer.java b/src/main/java/com/atm/model/Customer.java new file mode 100644 index 0000000..fa82832 --- /dev/null +++ b/src/main/java/com/atm/model/Customer.java @@ -0,0 +1,161 @@ +package com.atm.model; + +import jakarta.persistence.*; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.math.BigDecimal; +import java.util.Collection; +import java.util.Collections; + +/** + * 客户实体类 + * 实现UserDetails接口以支持Spring Security + */ +@Entity +@Table(name = "customer") +public class Customer implements UserDetails { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "cid") + private Long cid; + + @Column(name = "cname", nullable = false) + private String cname; + + @Column(name = "cpin", nullable = false) + private String cpin; + + @Column(name = "cbalance", precision = 15, scale = 2) + private BigDecimal cbalance; + + @Column(name = "cstatus") + private String cstatus; + + @Column(name = "ctype") + private String ctype; + + // 默认构造函数 + public Customer() { + this.cbalance = BigDecimal.ZERO; + this.cstatus = "active"; + this.ctype = "normal"; + } + + // 带参构造函数 + public Customer(Long cid, String cname, String cpin) { + this(); + this.cid = cid; + this.cname = cname; + this.cpin = cpin; + } + + // 带参构造函数(用于GUI) + public Customer(String cid, String cpin) { + this(); + try { + this.cid = Long.parseLong(cid); + } catch (NumberFormatException e) { + this.cid = null; + } + this.cpin = cpin; + } + + // Getter和Setter方法 + public Long getCid() { + return cid; + } + + public void setCid(Long cid) { + this.cid = cid; + } + + public String getCname() { + return cname; + } + + public void setCname(String cname) { + this.cname = cname; + } + + public String getCpin() { + return cpin; + } + + public void setCpin(String cpin) { + this.cpin = cpin; + } + + public BigDecimal getCbalance() { + return cbalance; + } + + public void setCbalance(BigDecimal cbalance) { + this.cbalance = cbalance; + } + + public String getCstatus() { + return cstatus; + } + + public void setCstatus(String cstatus) { + this.cstatus = cstatus; + } + + public String getCtype() { + return ctype; + } + + public void setCtype(String ctype) { + this.ctype = ctype; + } + + // UserDetails接口实现方法 + @Override + public Collection getAuthorities() { + // 简单实现,所有用户都有ROLE_USER权限 + return Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); + } + + @Override + public String getPassword() { + return cpin; + } + + @Override + public String getUsername() { + return cid.toString(); + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return !"locked".equals(cstatus); + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return "active".equals(cstatus); + } + + @Override + public String toString() { + return "Customer{" + + "cid=" + cid + + ", cname='" + cname + '\'' + + ", cbalance=" + cbalance + + ", cstatus='" + cstatus + '\'' + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/model/Transaction.java b/src/main/java/com/atm/model/Transaction.java new file mode 100644 index 0000000..39dd65d --- /dev/null +++ b/src/main/java/com/atm/model/Transaction.java @@ -0,0 +1,134 @@ +package com.atm.model; + +import jakarta.persistence.*; +import java.math.BigDecimal; +import java.time.LocalDateTime; + +/** + * 交易实体类 + */ +@Entity +@Table(name = "transaction") +public class Transaction { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "tid") + private Long tid; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "from_account_id") + private Account fromAccount; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "to_account_id") + private Account toAccount; + + @Column(name = "ttype", nullable = false) + private String ttype; + + @Column(name = "tamount", precision = 15, scale = 2) + private BigDecimal tamount; + + @Column(name = "tdescription") + private String tdescription; + + @Column(name = "tstatus") + private String tstatus; + + @Column(name = "created_at") + private LocalDateTime createdAt; + + // 默认构造函数 + public Transaction() { + this.tstatus = "completed"; + this.createdAt = LocalDateTime.now(); + } + + // 带参构造函数 + public Transaction(Account fromAccount, Account toAccount, String ttype, BigDecimal tamount) { + this(); + this.fromAccount = fromAccount; + this.toAccount = toAccount; + this.ttype = ttype; + this.tamount = tamount; + } + + // Getter和Setter方法 + public Long getTid() { + return tid; + } + + public void setTid(Long tid) { + this.tid = tid; + } + + public Account getFromAccount() { + return fromAccount; + } + + public void setFromAccount(Account fromAccount) { + this.fromAccount = fromAccount; + } + + public Account getToAccount() { + return toAccount; + } + + public void setToAccount(Account toAccount) { + this.toAccount = toAccount; + } + + public String getTtype() { + return ttype; + } + + public void setTtype(String ttype) { + this.ttype = ttype; + } + + public BigDecimal getTamount() { + return tamount; + } + + public void setTamount(BigDecimal tamount) { + this.tamount = tamount; + } + + public String getTdescription() { + return tdescription; + } + + public void setTdescription(String tdescription) { + this.tdescription = tdescription; + } + + public String getTstatus() { + return tstatus; + } + + public void setTstatus(String tstatus) { + this.tstatus = tstatus; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + @Override + public String toString() { + return "Transaction{" + + "tid=" + tid + + ", fromAccount=" + (fromAccount != null ? fromAccount.getAid() : null) + + ", toAccount=" + (toAccount != null ? toAccount.getAid() : null) + + ", ttype='" + ttype + '\'' + + ", tamount=" + tamount + + ", tstatus='" + tstatus + '\'' + + ", createdAt=" + createdAt + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/repository/AccountRepository.java b/src/main/java/com/atm/repository/AccountRepository.java new file mode 100644 index 0000000..aa0dcf4 --- /dev/null +++ b/src/main/java/com/atm/repository/AccountRepository.java @@ -0,0 +1,60 @@ +package com.atm.repository; + +import com.atm.model.Account; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +/** + * 账户数据访问接口 + */ +@Repository +public interface AccountRepository extends JpaRepository { + + /** + * 根据账户ID查找账户 + */ + Optional findByAid(Long aid); + + /** + * 根据客户ID查找所有账户 + */ + List findByCustomerCid(Long cid); + + /** + * 根据客户ID和账户类型查找账户 + */ + Optional findByCustomerCidAndAtype(Long cid, String atype); + + /** + * 根据客户ID和账户状态查找账户 + */ + List findByCustomerCidAndAstatus(Long cid, String astatus); + + /** + * 根据账户类型查找账户 + */ + List findByAtype(String atype); + + /** + * 检查账户是否存在 + */ + boolean existsByAid(Long aid); + + /** + * 查询余额大于指定金额的账户 + */ + @Query("SELECT a FROM Account a WHERE a.abalance > :amount") + List findAccountsWithBalanceGreaterThan(@Param("amount") BigDecimal amount); + + /** + * 查询余额小于指定金额的账户 + */ + @Query("SELECT a FROM Account a WHERE a.abalance < :amount") + List findAccountsWithBalanceLessThan(@Param("amount") BigDecimal amount); +} \ No newline at end of file diff --git a/src/main/java/com/atm/repository/CustomerRepository.java b/src/main/java/com/atm/repository/CustomerRepository.java new file mode 100644 index 0000000..c688dab --- /dev/null +++ b/src/main/java/com/atm/repository/CustomerRepository.java @@ -0,0 +1,34 @@ +package com.atm.repository; + +import com.atm.model.Customer; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +/** + * 客户数据访问接口 + */ +@Repository +public interface CustomerRepository extends JpaRepository { + + /** + * 根据客户ID查找客户 + */ + Optional findByCid(Long cid); + + /** + * 根据客户ID和PIN码查找客户 + */ + Optional findByCidAndCpin(Long cid, String cpin); + + /** + * 检查客户ID是否存在 + */ + boolean existsByCid(Long cid); + + /** + * 根据客户状态查找客户 + */ + java.util.List findByCstatus(String cstatus); +} \ No newline at end of file diff --git a/src/main/java/com/atm/repository/TransactionRepository.java b/src/main/java/com/atm/repository/TransactionRepository.java new file mode 100644 index 0000000..ce6c43d --- /dev/null +++ b/src/main/java/com/atm/repository/TransactionRepository.java @@ -0,0 +1,104 @@ +package com.atm.repository; + +import com.atm.model.Transaction; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +/** + * 交易数据访问接口 + */ +@Repository +public interface TransactionRepository extends JpaRepository { + + /** + * 根据交易ID查找交易 + */ + java.util.Optional findByTid(Long tid); + + /** + * 根据转出账户查找交易 + */ + List findByFromAccountAid(Long fromAccountId); + + /** + * 根据转入账户查找交易 + */ + List findByToAccountAid(Long toAccountId); + + /** + * 根据账户ID查找所有相关交易(作为转出或转入账户) + */ + @Query("SELECT t FROM Transaction t WHERE t.fromAccount.aid = :accountId OR t.toAccount.aid = :accountId ORDER BY t.createdAt DESC") + List findByAccountId(@Param("accountId") Long accountId); + + /** + * 根据交易类型查找交易 + */ + List findByTtype(String ttype); + + /** + * 根据交易状态查找交易 + */ + List findByTstatus(String tstatus); + + /** + * 根据客户ID查找所有相关交易 + */ + @Query("SELECT t FROM Transaction t WHERE t.fromAccount.customer.cid = :cid OR t.toAccount.customer.cid = :cid ORDER BY t.createdAt DESC") + List findByCustomerId(@Param("cid") Long cid); + + /** + * 根据时间范围查找交易 + */ + @Query("SELECT t FROM Transaction t WHERE t.createdAt BETWEEN :startTime AND :endTime ORDER BY t.createdAt DESC") + List findByDateRange(@Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 根据客户ID和时间范围查找交易 + */ + @Query("SELECT t FROM Transaction t WHERE (t.fromAccount.customer.cid = :cid OR t.toAccount.customer.cid = :cid) " + + "AND t.createdAt BETWEEN :startTime AND :endTime ORDER BY t.createdAt DESC") + List findByCustomerIdAndDateRange(@Param("cid") Long cid, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 查询金额大于指定值的交易 + */ + @Query("SELECT t FROM Transaction t WHERE t.tamount > :amount ORDER BY t.tamount DESC") + List findTransactionsWithAmountGreaterThan(@Param("amount") BigDecimal amount); + + /** + * 查询指定账户最近N笔交易 + */ + @Query("SELECT t FROM Transaction t WHERE t.fromAccount.aid = :accountId OR t.toAccount.aid = :accountId ORDER BY t.createdAt DESC") + List findRecentTransactionsByAccountId(@Param("accountId") Long accountId, org.springframework.data.domain.Pageable pageable); + + /** + * 查询指定客户最近N笔交易 + */ + @Query("SELECT t FROM Transaction t WHERE t.fromAccount.customer.cid = :customerId OR t.toAccount.customer.cid = :customerId ORDER BY t.createdAt DESC") + List findRecentTransactionsByCustomerId(@Param("customerId") Long customerId, org.springframework.data.domain.Pageable pageable); + + /** + * 根据账户ID和时间范围查找交易 + */ + @Query("SELECT t FROM Transaction t WHERE (t.fromAccount.aid = :accountId OR t.toAccount.aid = :accountId) " + + "AND t.createdAt BETWEEN :startTime AND :endTime ORDER BY t.createdAt DESC") + List findByAccountIdAndDateRange(@Param("accountId") Long accountId, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 查询金额小于指定值的交易 + */ + @Query("SELECT t FROM Transaction t WHERE t.tamount < :amount ORDER BY t.tamount ASC") + List findTransactionsWithAmountLessThan(@Param("amount") BigDecimal amount); +} \ No newline at end of file diff --git a/src/main/java/com/atm/service/AccountService.java b/src/main/java/com/atm/service/AccountService.java new file mode 100644 index 0000000..411b96c --- /dev/null +++ b/src/main/java/com/atm/service/AccountService.java @@ -0,0 +1,241 @@ +package com.atm.service; + +import com.atm.model.Account; +import com.atm.model.Customer; +import com.atm.repository.AccountRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +/** + * 账户服务类 + */ +@Service +@Transactional +public class AccountService { + + private final AccountRepository accountRepository; + + @Autowired + public AccountService(AccountRepository accountRepository) { + this.accountRepository = accountRepository; + } + + /** + * 根据账户ID查找账户 + */ + public Optional findByAid(Long aid) { + return accountRepository.findByAid(aid); + } + + /** + * 根据客户ID查找所有账户 + */ + public List findByCustomerId(Long cid) { + return accountRepository.findByCustomerCid(cid); + } + + /** + * 根据客户ID和账户类型查找账户 + */ + public Optional findByCustomerIdAndType(Long cid, String atype) { + return accountRepository.findByCustomerCidAndAtype(cid, atype); + } + + /** + * 创建新账户 + */ + public Account createAccount(Account account) { + return accountRepository.save(account); + } + + /** + * 为客户创建新账户 + */ + public Account createAccountForCustomer(Customer customer, String accountType, BigDecimal initialBalance) { + Account account = new Account(); + account.setCustomer(customer); + account.setAtype(accountType); + account.setAbalance(initialBalance); + return accountRepository.save(account); + } + + /** + * 更新账户余额 + */ + public boolean updateBalance(Long aid, BigDecimal newBalance) { + Optional accountOpt = accountRepository.findByAid(aid); + if (accountOpt.isPresent()) { + Account account = accountOpt.get(); + account.setAbalance(newBalance); + accountRepository.save(account); + return true; + } + return false; + } + + /** + * 增加账户余额 + */ + public boolean deposit(Long aid, BigDecimal amount) { + Optional accountOpt = accountRepository.findByAid(aid); + if (accountOpt.isPresent()) { + Account account = accountOpt.get(); + if ("active".equals(account.getAstatus())) { + account.setAbalance(account.getAbalance().add(amount)); + accountRepository.save(account); + return true; + } + } + return false; + } + + /** + * 增加账户余额 + */ + public boolean deposit(Long aid, Double amount) { + if (amount == null || amount <= 0) { + return false; + } + return deposit(aid, BigDecimal.valueOf(amount)); + } + + /** + * 减少账户余额 + */ + public boolean withdraw(Long aid, BigDecimal amount) { + Optional accountOpt = accountRepository.findByAid(aid); + if (accountOpt.isPresent()) { + Account account = accountOpt.get(); + if ("active".equals(account.getAstatus()) && + account.getAbalance().compareTo(amount) >= 0) { + account.setAbalance(account.getAbalance().subtract(amount)); + accountRepository.save(account); + return true; + } + } + return false; + } + + /** + * 减少账户余额 + */ + public boolean withdraw(Long aid, Double amount) { + if (amount == null || amount <= 0) { + return false; + } + return withdraw(aid, BigDecimal.valueOf(amount)); + } + + /** + * 检查账户余额是否足够 + */ + public boolean hasSufficientBalance(Long aid, BigDecimal amount) { + Optional accountOpt = accountRepository.findByAid(aid); + return accountOpt.map(account -> + "active".equals(account.getAstatus()) && + account.getAbalance().compareTo(amount) >= 0 + ).orElse(false); + } + + /** + * 转账 + */ + @Transactional + public boolean transfer(Long fromAid, Long toAid, BigDecimal amount) { + Optional fromAccountOpt = accountRepository.findByAid(fromAid); + Optional toAccountOpt = accountRepository.findByAid(toAid); + + if (fromAccountOpt.isPresent() && toAccountOpt.isPresent()) { + Account fromAccount = fromAccountOpt.get(); + Account toAccount = toAccountOpt.get(); + + // 检查转出账户状态和余额 + if ("active".equals(fromAccount.getAstatus()) && + "active".equals(toAccount.getAstatus()) && + fromAccount.getAbalance().compareTo(amount) >= 0) { + + // 执行转账 + fromAccount.setAbalance(fromAccount.getAbalance().subtract(amount)); + toAccount.setAbalance(toAccount.getAbalance().add(amount)); + + accountRepository.save(fromAccount); + accountRepository.save(toAccount); + + return true; + } + } + return false; + } + + /** + * 转账 + */ + @Transactional + public boolean transfer(Long fromAid, Long toAid, Double amount) { + if (amount == null || amount <= 0) { + return false; + } + return transfer(fromAid, toAid, BigDecimal.valueOf(amount)); + } + + /** + * 更新账户状态 + */ + public boolean updateAccountStatus(Long aid, String newStatus) { + Optional accountOpt = accountRepository.findByAid(aid); + if (accountOpt.isPresent()) { + Account account = accountOpt.get(); + account.setAstatus(newStatus); + accountRepository.save(account); + return true; + } + return false; + } + + /** + * 根据账户类型查找账户 + */ + public List findByAccountType(String accountType) { + return accountRepository.findByAtype(accountType); + } + + /** + * 查询余额大于指定金额的账户 + */ + public List findAccountsWithBalanceGreaterThan(BigDecimal amount) { + return accountRepository.findAccountsWithBalanceGreaterThan(amount); + } + + /** + * 查询余额小于指定金额的账户 + */ + public List findAccountsWithBalanceLessThan(BigDecimal amount) { + return accountRepository.findAccountsWithBalanceLessThan(amount); + } + + /** + * 检查账户是否存在 + */ + public boolean existsByAid(Long aid) { + return accountRepository.existsByAid(aid); + } + + /** + * 获取所有账户 + */ + public List findAll() { + return accountRepository.findAll(); + } + + /** + * 根据ID删除账户 + */ + public void deleteById(Long aid) { + accountRepository.deleteById(aid); + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/service/AuthenticationService.java b/src/main/java/com/atm/service/AuthenticationService.java new file mode 100644 index 0000000..35e1668 --- /dev/null +++ b/src/main/java/com/atm/service/AuthenticationService.java @@ -0,0 +1,105 @@ +package com.atm.service; + +import com.atm.config.JwtTokenUtil; +import com.atm.model.Customer; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.BadCredentialsException; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.stereotype.Service; + +import java.util.Optional; + +/** + * 认证服务类 + */ +@Service +public class AuthenticationService { + + private final AuthenticationManager authenticationManager; + private final JwtTokenUtil jwtTokenUtil; + private final CustomerService customerService; + + @Autowired + public AuthenticationService(AuthenticationManager authenticationManager, + JwtTokenUtil jwtTokenUtil, + CustomerService customerService) { + this.authenticationManager = authenticationManager; + this.jwtTokenUtil = jwtTokenUtil; + this.customerService = customerService; + } + + /** + * 用户认证并生成JWT令牌 + */ + public Optional authenticateAndGenerateToken(Long cid, String cpin) { + try { + // 创建认证令牌 + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(cid.toString(), cpin); + + // 执行认证 + Authentication authentication = authenticationManager.authenticate(authenticationToken); + + // 认证成功,生成JWT令牌 + if (authentication.isAuthenticated()) { + UserDetails userDetails = (UserDetails) authentication.getPrincipal(); + String token = jwtTokenUtil.generateToken(userDetails); + return Optional.of(token); + } + } catch (BadCredentialsException e) { + // 认证失败 + return Optional.empty(); + } catch (Exception e) { + // 其他异常 + return Optional.empty(); + } + + return Optional.empty(); + } + + /** + * 验证JWT令牌 + */ + public boolean validateToken(String token) { + try { + String username = jwtTokenUtil.getUsernameFromToken(token); + UserDetails userDetails = customerService.loadUserByUsername(username); + return jwtTokenUtil.validateToken(token, userDetails); + } catch (Exception e) { + return false; + } + } + + /** + * 从令牌中获取用户名 + */ + public String getUsernameFromToken(String token) { + try { + return jwtTokenUtil.getUsernameFromToken(token); + } catch (Exception e) { + return null; + } + } + + /** + * 刷新令牌 + */ + public Optional refreshToken(String token) { + try { + String username = jwtTokenUtil.getUsernameFromToken(token); + UserDetails userDetails = customerService.loadUserByUsername(username); + + if (jwtTokenUtil.validateToken(token, userDetails)) { + String newToken = jwtTokenUtil.generateToken(userDetails); + return Optional.of(newToken); + } + } catch (Exception e) { + return Optional.empty(); + } + + return Optional.empty(); + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/service/CustomerService.java b/src/main/java/com/atm/service/CustomerService.java new file mode 100644 index 0000000..3ecf7f6 --- /dev/null +++ b/src/main/java/com/atm/service/CustomerService.java @@ -0,0 +1,171 @@ +package com.atm.service; + +import com.atm.model.Customer; +import com.atm.repository.CustomerRepository; +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.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +/** + * 客户服务类 + * 实现UserDetailsService接口以支持Spring Security认证 + */ +@Service +@Transactional +public class CustomerService implements UserDetailsService { + + private final CustomerRepository customerRepository; + private final PasswordEncoder passwordEncoder; + + @Autowired + public CustomerService(CustomerRepository customerRepository, PasswordEncoder passwordEncoder) { + this.customerRepository = customerRepository; + this.passwordEncoder = passwordEncoder; + } + + /** + * 根据客户ID查找客户 + */ + public Optional findByCid(Long cid) { + return customerRepository.findByCid(cid); + } + + /** + * 根据客户ID和PIN码验证客户 + */ + public Optional authenticate(Long cid, String cpin) { + Optional customerOpt = customerRepository.findByCid(cid); + if (customerOpt.isPresent()) { + Customer customer = customerOpt.get(); + // 使用密码编码器验证PIN码 + if (passwordEncoder.matches(cpin, customer.getCpin())) { + return Optional.of(customer); + } + } + return Optional.empty(); + } + + /** + * 创建新客户 + */ + public Customer createCustomer(Customer customer) { + // 加密PIN码 + customer.setCpin(passwordEncoder.encode(customer.getCpin())); + return customerRepository.save(customer); + } + + /** + * 更新客户信息 + */ + public Customer updateCustomer(Customer customer) { + return customerRepository.save(customer); + } + + /** + * 更新客户PIN码 + */ + public boolean updatePin(Long cid, String oldPin, String newPin) { + Optional customerOpt = customerRepository.findByCid(cid); + if (customerOpt.isPresent()) { + Customer customer = customerOpt.get(); + // 验证旧PIN码 + if (passwordEncoder.matches(oldPin, customer.getCpin())) { + customer.setCpin(passwordEncoder.encode(newPin)); + customerRepository.save(customer); + return true; + } + } + return false; + } + + /** + * 根据状态查找客户 + */ + public List findByStatus(String status) { + return customerRepository.findByCstatus(status); + } + + /** + * 检查客户ID是否存在 + */ + public boolean existsByCid(Long cid) { + return customerRepository.existsByCid(cid); + } + + /** + * 获取所有客户 + */ + public List findAll() { + return customerRepository.findAll(); + } + + /** + * 根据ID删除客户 + */ + public void deleteById(Long cid) { + customerRepository.deleteById(cid); + } + + /** + * 删除客户 + */ + public boolean deleteCustomer(Long cid) { + if (customerRepository.existsByCid(cid)) { + customerRepository.deleteById(cid); + return true; + } + return false; + } + + /** + * 重置PIN码(管理员功能) + */ + public boolean resetPin(Long cid, String newPin) { + Optional customerOpt = customerRepository.findByCid(cid); + if (customerOpt.isPresent()) { + Customer customer = customerOpt.get(); + customer.setCpin(passwordEncoder.encode(newPin)); + customerRepository.save(customer); + return true; + } + return false; + } + + /** + * 更新客户状态 + */ + public boolean updateCustomerStatus(Long cid, String status) { + Optional customerOpt = customerRepository.findByCid(cid); + if (customerOpt.isPresent()) { + Customer customer = customerOpt.get(); + customer.setCstatus(status); + customerRepository.save(customer); + return true; + } + return false; + } + + /** + * UserDetailsService接口实现方法 + * 用于Spring Security认证 + */ + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Long cid; + try { + cid = Long.parseLong(username); + } catch (NumberFormatException e) { + throw new UsernameNotFoundException("无效的客户ID: " + username); + } + + return customerRepository.findByCid(cid) + .orElseThrow(() -> new UsernameNotFoundException("未找到客户ID: " + username)); + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/service/TransactionService.java b/src/main/java/com/atm/service/TransactionService.java new file mode 100644 index 0000000..2000797 --- /dev/null +++ b/src/main/java/com/atm/service/TransactionService.java @@ -0,0 +1,248 @@ +package com.atm.service; + +import com.atm.model.Account; +import com.atm.model.Transaction; +import com.atm.repository.TransactionRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +/** + * 交易服务类 + */ +@Service +@Transactional +public class TransactionService { + + private final TransactionRepository transactionRepository; + private final AccountService accountService; + + @Autowired + public TransactionService(TransactionRepository transactionRepository, AccountService accountService) { + this.transactionRepository = transactionRepository; + this.accountService = accountService; + } + + /** + * 根据交易ID查找交易 + */ + public Optional findByTid(Long tid) { + return transactionRepository.findByTid(tid); + } + + /** + * 创建存款交易记录 + */ + public Transaction createDepositTransaction(Long accountId, BigDecimal amount, String description) { + Optional accountOpt = accountService.findByAid(accountId); + if (accountOpt.isPresent() && amount.compareTo(BigDecimal.ZERO) > 0) { + Account account = accountOpt.get(); + + // 创建交易记录 + Transaction transaction = new Transaction(); + transaction.setFromAccount(account); + transaction.setToAccount(account); // 存款是同一账户 + transaction.setTtype("deposit"); + transaction.setTamount(amount); + transaction.setTdescription(description != null ? description : "存款"); + transaction.setTstatus("completed"); + + return transactionRepository.save(transaction); + } + return null; + } + + /** + * 创建取款交易记录 + */ + public Transaction createWithdrawalTransaction(Long accountId, BigDecimal amount, String description) { + Optional accountOpt = accountService.findByAid(accountId); + if (accountOpt.isPresent() && amount.compareTo(BigDecimal.ZERO) > 0) { + Account account = accountOpt.get(); + + // 创建交易记录 + Transaction transaction = new Transaction(); + transaction.setFromAccount(account); + transaction.setToAccount(account); // 取款是同一账户 + transaction.setTtype("withdrawal"); + transaction.setTamount(amount); + transaction.setTdescription(description != null ? description : "取款"); + transaction.setTstatus("completed"); + + return transactionRepository.save(transaction); + } + return null; + } + + /** + * 创建转账交易记录 + */ + public Transaction createTransferTransaction(Long fromAccountId, Long toAccountId, BigDecimal amount, String description) { + if (amount.compareTo(BigDecimal.ZERO) <= 0) { + return null; + } + + Optional fromAccountOpt = accountService.findByAid(fromAccountId); + Optional toAccountOpt = accountService.findByAid(toAccountId); + + if (fromAccountOpt.isPresent() && toAccountOpt.isPresent()) { + Account fromAccount = fromAccountOpt.get(); + Account toAccount = toAccountOpt.get(); + + // 创建交易记录 + Transaction transaction = new Transaction(); + transaction.setFromAccount(fromAccount); + transaction.setToAccount(toAccount); + transaction.setTtype("transfer"); + transaction.setTamount(amount); + transaction.setTdescription(description != null ? description : "转账"); + transaction.setTstatus("completed"); + + return transactionRepository.save(transaction); + } + return null; + } + + /** + * 根据账户ID查找所有交易 + */ + public List findByAid(Long accountId) { + return transactionRepository.findByAccountId(accountId); + } + + /** + * 根据客户ID查找所有交易 + */ + public List findByCid(Long cid) { + return transactionRepository.findByCustomerId(cid); + } + + /** + * 根据交易类型查找交易 + */ + public List findByTtype(String transactionType) { + return transactionRepository.findByTtype(transactionType); + } + + /** + * 根据交易状态查找交易 + */ + public List findByTransactionStatus(String transactionStatus) { + return transactionRepository.findByTstatus(transactionStatus); + } + + /** + * 根据时间范围查找交易 + */ + public List findByDateRange(LocalDateTime startTime, LocalDateTime endTime) { + return transactionRepository.findByDateRange(startTime, endTime); + } + + /** + * 根据客户ID和时间范围查找交易 + */ + public List findByCustomerIdAndDateRange(Long cid, LocalDateTime startTime, LocalDateTime endTime) { + return transactionRepository.findByCustomerIdAndDateRange(cid, startTime, endTime); + } + + /** + * 查询金额大于指定值的交易 + */ + public List findTransactionsWithAmountGreaterThan(BigDecimal amount) { + return transactionRepository.findTransactionsWithAmountGreaterThan(amount); + } + + /** + * 获取指定账户的最近N笔交易 + */ + public List findRecentTransactionsByAccountId(Long accountId, int limit) { + Pageable pageable = PageRequest.of(0, limit); + return transactionRepository.findRecentTransactionsByAccountId(accountId, pageable); + } + + /** + * 创建自定义交易记录 + */ + public Transaction createTransaction(Transaction transaction) { + return transactionRepository.save(transaction); + } + + /** + * 更新交易状态 + */ + public boolean updateTransactionStatus(Long tid, String newStatus) { + Optional transactionOpt = transactionRepository.findByTid(tid); + if (transactionOpt.isPresent()) { + Transaction transaction = transactionOpt.get(); + transaction.setTstatus(newStatus); + transactionRepository.save(transaction); + return true; + } + return false; + } + + /** + * 获取所有交易 + */ + public List findAll() { + return transactionRepository.findAll(); + } + + /** + * 根据ID删除交易 + */ + public void deleteById(Long tid) { + transactionRepository.deleteById(tid); + } + + /** + * 根据账户和时间范围查找交易 + */ + public List findByAccountAndDateRange(Long accountId, LocalDateTime startTime, LocalDateTime endTime) { + return transactionRepository.findByAccountIdAndDateRange(accountId, startTime, endTime); + } + + /** + * 根据客户和时间范围查找交易 + */ + public List findByCustomerAndDateRange(Long customerId, LocalDateTime startTime, LocalDateTime endTime) { + return transactionRepository.findByCustomerIdAndDateRange(customerId, startTime, endTime); + } + + /** + * 获取指定账户的最近N笔交易 + */ + public List findRecentTransactionsByAccount(Long accountId, int limit) { + Pageable pageable = PageRequest.of(0, limit); + return transactionRepository.findRecentTransactionsByAccountId(accountId, pageable); + } + + /** + * 获取指定客户的最近N笔交易 + */ + public List findRecentTransactionsByCustomer(Long customerId, int limit) { + Pageable pageable = PageRequest.of(0, limit); + return transactionRepository.findRecentTransactionsByCustomerId(customerId, pageable); + } + + /** + * 查询金额大于指定值的交易 + */ + public List findByAmountAbove(Double amount) { + return transactionRepository.findTransactionsWithAmountGreaterThan(BigDecimal.valueOf(amount)); + } + + /** + * 查询金额小于指定值的交易 + */ + public List findByAmountBelow(Double amount) { + return transactionRepository.findTransactionsWithAmountLessThan(BigDecimal.valueOf(amount)); + } +} \ No newline at end of file diff --git a/src/main/java/com/atm/view/gui/Gui.java b/src/main/java/com/atm/view/gui/Gui.java new file mode 100644 index 0000000..4cdc4da --- /dev/null +++ b/src/main/java/com/atm/view/gui/Gui.java @@ -0,0 +1,94 @@ +package com.atm.view.gui; + +import com.atm.model.Customer; +import com.atm.dao.Login; + +import javax.swing.JOptionPane; +import javax.swing.JButton; +import javax.swing.JFrame; +import javax.swing.JLabel; +import javax.swing.JPanel; +import javax.swing.JTextField; +import javax.swing.JPasswordField; + +import java.awt.Color; +import java.awt.Font; +import java.awt.GridLayout; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import com.atm.controller.Validate; + +public class Gui extends JFrame { + + private JButton jButtonLogin; + private JLabel jLabelCid; + private JLabel jLabelCpin; + private JPanel jPanel; + private JPanel jPanelBtn; + private JPanel jPanelCid; + private JPanel jPanelCpin; + private JTextField jTextFieldCid; + private JPasswordField jTextFieldCpin; + + public Gui() { + this.setTitle("GUI ATM"); + this.setSize(300, 200); + this.setLocationRelativeTo(null); + this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + this.setResizable(false); + + jPanel = new JPanel(new GridLayout(3, 1)); + jPanelCid = new JPanel(); + jLabelCid = new JLabel("CID"); + jLabelCid.setForeground(Color.RED); + jLabelCid.setFont(new Font("", Font.BOLD, 15)); + jTextFieldCid = new JTextField(15); + jPanelCid.add(jLabelCid); + jPanelCid.add(jTextFieldCid); + + jPanelCpin = new JPanel(); + jLabelCpin = new JLabel("CPIN"); + jLabelCpin.setForeground(Color.RED); + jLabelCpin.setFont(new Font("", Font.BOLD, 15)); + jTextFieldCpin = new JPasswordField(15); + jPanelCpin.add(jLabelCpin); + jPanelCpin.add(jTextFieldCpin); + + jPanelBtn = new JPanel(); + jButtonLogin = new JButton("LOGIN"); + jPanelBtn.add(jButtonLogin); + jPanel.add(jPanelCid); + jPanel.add(jPanelCpin); + jPanel.add(jPanelBtn); + this.add(jPanel); + this.setVisible(true); + jButtonLogin.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String cid = new String(jTextFieldCid.getText()); + if (cid.length() <= 0) { + JOptionPane.showMessageDialog(null, "CID NULL"); + System.out.println("CID NULL"); + } else { + String cpin = new String(jTextFieldCpin.getPassword()); + Customer c = new Customer(cid, cpin); + boolean isLogin = false; + if (Validate.lengthValidate(cid) && Validate.lengthValidate(cpin) + && Validate.isNumeric(cid) && Validate.isNumeric(cpin)) + isLogin = new Login().login(c); + if (isLogin) { + JOptionPane.showMessageDialog(null, "LOGIN SUCCEEDED!", "PROMPT", + JOptionPane.INFORMATION_MESSAGE); + } else { + JOptionPane.showMessageDialog(null, "ID OR PIN ERROR!"); + } + } + } + }); + } + + public static void main(String[] args) { + new Gui(); + } +}// end Gui \ No newline at end of file diff --git a/src/main/java/com/atm/view/gui/TestGuiFrame.java b/src/main/java/com/atm/view/gui/TestGuiFrame.java new file mode 100644 index 0000000..c319539 --- /dev/null +++ b/src/main/java/com/atm/view/gui/TestGuiFrame.java @@ -0,0 +1,77 @@ +package com.atm.view.gui; + +import javax.swing.*; +import java.awt.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +public class TestGuiFrame extends JFrame { + + private JButton jButtonLogin; + private JLabel jLabelCid; + private JLabel jLabelCpin; + private JPanel jPanel; + private JPanel jPanelBtn; + private JPanel jPanelCid; + private JPanel jPanelCpin; + private JTextField jTextFieldCid; + private JPasswordField jTextFieldCpin; + + public TestGuiFrame() { + this.setTitle("Test ATM GUI"); + this.setSize(300, 200); + this.setLocationRelativeTo(null); + this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); + this.setResizable(false); + + jPanel = new JPanel(new GridLayout(3, 1)); + jPanelCid = new JPanel(); + jLabelCid = new JLabel("CID"); + jLabelCid.setForeground(Color.RED); + jLabelCid.setFont(new Font("", Font.BOLD, 15)); + jTextFieldCid = new JTextField(15); + jPanelCid.add(jLabelCid); + jPanelCid.add(jTextFieldCid); + + jPanelCpin = new JPanel(); + jLabelCpin = new JLabel("CPIN"); + jLabelCpin.setForeground(Color.RED); + jLabelCpin.setFont(new Font("", Font.BOLD, 15)); + jTextFieldCpin = new JPasswordField(15); + jPanelCpin.add(jLabelCpin); + jPanelCpin.add(jTextFieldCpin); + + jPanelBtn = new JPanel(); + jButtonLogin = new JButton("LOGIN"); + jPanelBtn.add(jButtonLogin); + jPanel.add(jPanelCid); + jPanel.add(jPanelCpin); + jPanel.add(jPanelBtn); + this.add(jPanel); + + jButtonLogin.addActionListener(new ActionListener() { + @Override + public void actionPerformed(ActionEvent e) { + String cid = new String(jTextFieldCid.getText()); + if (cid.length() <= 0) { + JOptionPane.showMessageDialog(null, "CID NULL"); + System.out.println("CID NULL"); + } else { + String cpin = new String(jTextFieldCpin.getPassword()); + // For testing, just show the input + JOptionPane.showMessageDialog(null, "CID: " + cid + "\nCPIN: " + cpin, "Test Input", JOptionPane.INFORMATION_MESSAGE); + } + } + }); + + this.setVisible(true); + } + + public static void main(String[] args) { + SwingUtilities.invokeLater(new Runnable() { + public void run() { + new TestGuiFrame(); + } + }); + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..3d17d00 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,21 @@ +# 开发环境配置 +spring: + # 开发环境使用SQLite数据库 + datasource: + url: jdbc:sqlite:atm_dev.db + driver-class-name: org.sqlite.JDBC + + # JPA配置 + jpa: + database-platform: org.hibernate.community.dialect.SQLiteDialect + hibernate: + ddl-auto: update + show-sql: true + +# 日志配置 +logging: + level: + com.atm: DEBUG + org.springframework.security: DEBUG + org.hibernate.SQL: DEBUG + org.hibernate.type.descriptor.sql.BasicBinder: TRACE \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml new file mode 100644 index 0000000..d7ae243 --- /dev/null +++ b/src/main/resources/application-prod.yml @@ -0,0 +1,22 @@ +# 生产环境配置 +spring: + # 生产环境使用PostgreSQL数据库 + datasource: + url: jdbc:postgresql://116.204.84.48:5432/seb + username: postgres + password: gitops123 + driver-class-name: org.postgresql.Driver + + # JPA配置 + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: validate + show-sql: false + +# 日志配置 +logging: + level: + com.atm: INFO + org.springframework.security: WARN + org.hibernate.SQL: WARN \ 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..faea30f --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,52 @@ +# Spring Boot应用配置 +server: + port: 8080 + servlet: + context-path: /api + +spring: + profiles: + active: dev + application: + name: atm-system + + # 数据源配置 + datasource: + url: jdbc:postgresql://116.204.84.48:5432/seb + username: postgres + password: gitops123 + driver-class-name: org.postgresql.Driver + hikari: + connection-timeout: 30000 + idle-timeout: 600000 + max-lifetime: 1800000 + maximum-pool-size: 10 + minimum-idle: 5 + + # JPA配置 + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect + hibernate: + ddl-auto: none + show-sql: false + properties: + hibernate: + format_sql: true + + # JSON配置 + jackson: + date-format: yyyy-MM-dd HH:mm:ss + time-zone: GMT+8 + +# 日志配置 +logging: + level: + com.atm: DEBUG + org.springframework.security: DEBUG + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" + +# JWT配置 +jwt: + secret: atmSecretKeyForJWTTokenGeneration + expiration: 86400000 # 24小时 \ No newline at end of file diff --git a/src/test/java/com/atm/controller/AccountControllerTest.java b/src/test/java/com/atm/controller/AccountControllerTest.java new file mode 100644 index 0000000..6db17a1 --- /dev/null +++ b/src/test/java/com/atm/controller/AccountControllerTest.java @@ -0,0 +1,300 @@ +package com.atm.controller; + +import com.atm.model.Account; +import com.atm.model.Customer; +import com.atm.service.AccountService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(AccountController.class) +public class AccountControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AccountService accountService; + + @Autowired + private ObjectMapper objectMapper; + + private Customer testCustomer; + private Account testAccount; + private Account testAccount2; + + @BeforeEach + public void setUp() { + testCustomer = new Customer(); + testCustomer.setCid(12345L); + testCustomer.setCname("张三"); + testCustomer.setCpin("123456"); + testCustomer.setCbalance(new BigDecimal("1000.00")); + testCustomer.setCstatus("active"); + testCustomer.setCtype("regular"); + + testAccount = new Account(); + testAccount.setAid(1L); + testAccount.setCustomer(testCustomer); + testAccount.setAtype("savings"); + testAccount.setAbalance(new BigDecimal("5000.00")); + testAccount.setAstatus("active"); + + testAccount2 = new Account(); + testAccount2.setAid(2L); + testAccount.setCustomer(testCustomer); + testAccount2.setAtype("checking"); + testAccount2.setAbalance(new BigDecimal("2000.00")); + testAccount2.setAstatus("active"); + } + + @Test + public void whenGetAccountById_thenReturnAccount() throws Exception { + // given + when(accountService.findByAid(1L)).thenReturn(Optional.of(testAccount)); + + // when & then + mockMvc.perform(get("/api/accounts/1")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.aid").value(1)) + .andExpect(jsonPath("$.atype").value("savings")) + .andExpect(jsonPath("$.abalance").value(5000.00)); + } + + @Test + public void whenGetNonExistingAccountById_thenReturnNotFound() throws Exception { + // given + when(accountService.findByAid(999L)).thenReturn(Optional.empty()); + + // when & then + mockMvc.perform(get("/api/accounts/999")) + .andExpect(status().isNotFound()); + } + + @Test + public void whenGetAccountsByCustomerId_thenReturnAccounts() throws Exception { + // given + List accounts = Arrays.asList(testAccount, testAccount2); + when(accountService.findByCid(12345L)).thenReturn(accounts); + + // when & then + mockMvc.perform(get("/api/accounts/customer/12345")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$").isArray()) + .andExpect(jsonPath("$.length()").value(2)); + } + + @Test + public void whenCreateAccount_thenReturnCreatedAccount() throws Exception { + // given + Account newAccount = new Account(); + newAccount.setCustomer(testCustomer); + newAccount.setAtype("investment"); + newAccount.setAbalance(new BigDecimal("10000.00")); + newAccount.setAstatus("active"); + + when(accountService.createAccount(any(Account.class))).thenReturn(newAccount); + + // when & then + mockMvc.perform(post("/api/accounts") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newAccount))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.atype").value("investment")) + .andExpect(jsonPath("$.abalance").value(10000.00)); + } + + @Test + public void whenDeposit_thenReturnUpdatedAccount() throws Exception { + // given + BigDecimal depositAmount = new BigDecimal("1000.00"); + Account updatedAccount = new Account(); + updatedAccount.setAid(1L); + updatedAccount.setCustomer(testCustomer); + updatedAccount.setAtype("savings"); + updatedAccount.setAbalance(new BigDecimal("6000.00")); + updatedAccount.setAstatus("active"); + + when(accountService.deposit(1L, depositAmount)).thenReturn(Optional.of(updatedAccount)); + + // when & then + mockMvc.perform(post("/api/accounts/1/deposit") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"amount\":1000.00}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.aid").value(1)) + .andExpect(jsonPath("$.abalance").value(6000.00)); + } + + @Test + public void whenDepositToNonExistingAccount_thenReturnNotFound() throws Exception { + // given + BigDecimal depositAmount = new BigDecimal("1000.00"); + when(accountService.deposit(999L, depositAmount)).thenReturn(Optional.empty()); + + // when & then + mockMvc.perform(post("/api/accounts/999/deposit") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"amount\":1000.00}")) + .andExpect(status().isNotFound()); + } + + @Test + public void whenWithdraw_thenReturnUpdatedAccount() throws Exception { + // given + BigDecimal withdrawAmount = new BigDecimal("1000.00"); + Account updatedAccount = new Account(); + updatedAccount.setAid(1L); + updatedAccount.setCustomer(testCustomer); + updatedAccount.setAtype("savings"); + updatedAccount.setAbalance(new BigDecimal("4000.00")); + updatedAccount.setAstatus("active"); + + when(accountService.withdraw(1L, withdrawAmount)).thenReturn(Optional.of(updatedAccount)); + + // when & then + mockMvc.perform(post("/api/accounts/1/withdraw") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"amount\":1000.00}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.aid").value(1)) + .andExpect(jsonPath("$.abalance").value(4000.00)); + } + + @Test + public void whenWithdrawFromNonExistingAccount_thenReturnNotFound() throws Exception { + // given + BigDecimal withdrawAmount = new BigDecimal("1000.00"); + when(accountService.withdraw(999L, withdrawAmount)).thenReturn(Optional.empty()); + + // when & then + mockMvc.perform(post("/api/accounts/999/withdraw") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"amount\":1000.00}")) + .andExpect(status().isNotFound()); + } + + @Test + public void whenWithdrawWithInsufficientBalance_thenReturnBadRequest() throws Exception { + // given + BigDecimal withdrawAmount = new BigDecimal("10000.00"); + when(accountService.withdraw(1L, withdrawAmount)).thenReturn(Optional.empty()); + + // when & then + mockMvc.perform(post("/api/accounts/1/withdraw") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"amount\":10000.00}")) + .andExpect(status().isBadRequest()); + } + + @Test + public void whenTransfer_thenReturnSuccess() throws Exception { + // given + BigDecimal transferAmount = new BigDecimal("1000.00"); + when(accountService.transfer(1L, 2L, transferAmount)).thenReturn(true); + + // when & then + mockMvc.perform(post("/api/accounts/1/transfer") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"toAccountId\":2,\"amount\":1000.00}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("转账成功")); + } + + @Test + public void whenTransferWithInsufficientBalance_thenReturnBadRequest() throws Exception { + // given + BigDecimal transferAmount = new BigDecimal("10000.00"); + when(accountService.transfer(1L, 2L, transferAmount)).thenReturn(false); + + // when & then + mockMvc.perform(post("/api/accounts/1/transfer") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"toAccountId\":2,\"amount\":10000.00}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("转账失败,余额不足")); + } + + @Test + public void whenTransferToNonExistingAccount_thenReturnBadRequest() throws Exception { + // given + BigDecimal transferAmount = new BigDecimal("1000.00"); + when(accountService.transfer(1L, 999L, transferAmount)).thenReturn(false); + + // when & then + mockMvc.perform(post("/api/accounts/1/transfer") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"toAccountId\":999,\"amount\":1000.00}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("转账失败,余额不足")); + } + + @Test + public void whenGetBalance_thenReturnBalance() throws Exception { + // given + when(accountService.findByAid(1L)).thenReturn(Optional.of(testAccount)); + + // when & then + mockMvc.perform(get("/api/accounts/1/balance")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.aid").value(1)) + .andExpect(jsonPath("$.balance").value(5000.00)); + } + + @Test + public void whenGetBalanceForNonExistingAccount_thenReturnNotFound() throws Exception { + // given + when(accountService.findByAid(999L)).thenReturn(Optional.empty()); + + // when & then + mockMvc.perform(get("/api/accounts/999/balance")) + .andExpect(status().isNotFound()); + } + + @Test + public void whenUpdateAccountStatus_thenReturnSuccess() throws Exception { + // given + when(accountService.updateAccountStatus(1L, "inactive")).thenReturn(true); + + // when & then + mockMvc.perform(put("/api/accounts/1/status") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"status\":\"inactive\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("账户状态更新成功")); + } + + @Test + public void whenUpdateStatusForNonExistingAccount_thenReturnNotFound() throws Exception { + // given + when(accountService.updateAccountStatus(999L, "inactive")).thenReturn(false); + + // when & then + mockMvc.perform(put("/api/accounts/999/status") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"status\":\"inactive\"}")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("账户不存在")); + } +} \ No newline at end of file diff --git a/src/test/java/com/atm/controller/AuthControllerTest.java b/src/test/java/com/atm/controller/AuthControllerTest.java new file mode 100644 index 0000000..b59e73a --- /dev/null +++ b/src/test/java/com/atm/controller/AuthControllerTest.java @@ -0,0 +1,217 @@ +package com.atm.controller; + +import com.atm.model.Customer; +import com.atm.service.AuthenticationService; +import com.atm.service.CustomerService; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.test.web.servlet.MockMvc; + +import java.math.BigDecimal; +import java.util.Optional; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; + +@WebMvcTest(AuthController.class) +public class AuthControllerTest { + + @Autowired + private MockMvc mockMvc; + + @MockBean + private AuthenticationService authenticationService; + + @MockBean + private CustomerService customerService; + + @MockBean + private AuthenticationManager authenticationManager; + + @Autowired + private ObjectMapper objectMapper; + + private Customer testCustomer; + + @BeforeEach + public void setUp() { + testCustomer = new Customer(); + testCustomer.setCid(12345L); + testCustomer.setCname("张三"); + testCustomer.setCpin("123456"); + testCustomer.setCbalance(new BigDecimal("1000.00")); + testCustomer.setCstatus("active"); + testCustomer.setCtype("regular"); + } + + @Test + public void whenLogin_thenReturnJwtToken() throws Exception { + // given + String mockToken = "mock.jwt.token"; + when(authenticationService.authenticate(12345L, "123456")).thenReturn(mockToken); + + // when & then + mockMvc.perform(post("/api/auth/login") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"cid\":12345,\"cpin\":\"123456\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.token").value(mockToken)) + .andExpect(jsonPath("$.type").value("Bearer")) + .andExpect(jsonPath("$.cid").value(12345)); + } + + @Test + public void whenValidateToken_thenReturnTrue() throws Exception { + // given + String token = "valid.jwt.token"; + when(authenticationService.validateToken(token)).thenReturn(true); + when(authenticationService.getUsernameFromToken(token)).thenReturn("12345"); + + // when & then + mockMvc.perform(post("/api/auth/validate") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"token\":\"" + token + "\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.valid").value(true)) + .andExpect(jsonPath("$.username").value("12345")); + } + + @Test + public void whenValidateInvalidToken_thenReturnFalse() throws Exception { + // given + String token = "invalid.jwt.token"; + when(authenticationService.validateToken(token)).thenReturn(false); + + // when & then + mockMvc.perform(post("/api/auth/validate") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"token\":\"" + token + "\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.valid").value(false)); + } + + @Test + public void whenRefreshToken_thenReturnNewToken() throws Exception { + // given + String oldToken = "old.jwt.token"; + String newToken = "new.jwt.token"; + when(authenticationService.refreshToken(oldToken)).thenReturn(newToken); + + // when & then + mockMvc.perform(post("/api/auth/refresh") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"token\":\"" + oldToken + "\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.token").value(newToken)) + .andExpect(jsonPath("$.type").value("Bearer")); + } + + @Test + public void whenRegister_thenReturnCreatedCustomer() throws Exception { + // given + Customer newCustomer = new Customer(); + newCustomer.setCid(67890L); + newCustomer.setCname("李四"); + newCustomer.setCpin("654321"); + newCustomer.setCbalance(new BigDecimal("500.00")); + newCustomer.setCstatus("active"); + newCustomer.setCtype("regular"); + + when(customerService.createCustomer(any(Customer.class))).thenReturn(newCustomer); + + // when & then + mockMvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(newCustomer))) + .andExpect(status().isCreated()) + .andExpect(jsonPath("$.cid").value(67890)) + .andExpect(jsonPath("$.cname").value("李四")); + } + + @Test + public void whenRegisterWithInvalidData_thenReturnBadRequest() throws Exception { + // given + Customer invalidCustomer = new Customer(); + invalidCustomer.setCname(""); // Invalid: empty name + + // when & then + mockMvc.perform(post("/api/auth/register") + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(invalidCustomer))) + .andExpect(status().isBadRequest()); + } + + @Test + public void whenChangePinWithValidData_thenReturnSuccess() throws Exception { + // given + when(customerService.findByCid(12345L)).thenReturn(Optional.of(testCustomer)); + when(customerService.updatePin(12345L, "123456", "654321")).thenReturn(true); + + // when & then + mockMvc.perform(put("/api/auth/change-pin") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"cid\":12345,\"oldPin\":\"123456\",\"newPin\":\"654321\"}")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("PIN码修改成功")); + } + + @Test + public void whenChangePinWithInvalidOldPin_thenReturnError() throws Exception { + // given + when(customerService.findByCid(12345L)).thenReturn(Optional.of(testCustomer)); + when(customerService.updatePin(12345L, "wrongpin", "654321")).thenReturn(false); + + // when & then + mockMvc.perform(put("/api/auth/change-pin") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"cid\":12345,\"oldPin\":\"wrongpin\",\"newPin\":\"654321\"}")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("原PIN码不正确")); + } + + @Test + public void whenChangePinForNonExistingCustomer_thenReturnError() throws Exception { + // given + when(customerService.findByCid(99999L)).thenReturn(Optional.empty()); + + // when & then + mockMvc.perform(put("/api/auth/change-pin") + .contentType(MediaType.APPLICATION_JSON) + .content("{\"cid\":99999,\"oldPin\":\"123456\",\"newPin\":\"654321\"}")) + .andExpect(status().isNotFound()) + .andExpect(jsonPath("$.success").value(false)) + .andExpect(jsonPath("$.message").value("客户不存在")); + } + + @Test + public void whenLogout_thenReturnSuccess() throws Exception { + // given + String token = "mock.jwt.token"; + when(authenticationService.getUsernameFromToken(token)).thenReturn("12345"); + + // when & then + mockMvc.perform(post("/api/auth/logout") + .header("Authorization", "Bearer " + token)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.message").value("登出成功")); + } + + @Test + public void whenLogoutWithoutToken_thenReturnUnauthorized() throws Exception { + // when & then + mockMvc.perform(post("/api/auth/logout")) + .andExpect(status().isUnauthorized()); + } +} \ No newline at end of file diff --git a/src/test/java/com/atm/controller/ValidateTest.java b/src/test/java/com/atm/controller/ValidateTest.java new file mode 100644 index 0000000..eef022a --- /dev/null +++ b/src/test/java/com/atm/controller/ValidateTest.java @@ -0,0 +1,55 @@ +package com.atm.controller; + +import org.junit.Assert; + +import com.atm.controller.Validate; + +public class ValidateTest extends junit.framework.TestCase { + + /** + * + * @param arg0 + */ + public ValidateTest(String arg0) { + super(arg0); + } + + /** + * + * @param args + */ + public static void main(String[] args) { + + } + + /** + * + * @exception Exception + */ + protected void setUp() + throws Exception { + super.setUp(); + } + + /** + * + * @exception Exception + */ + protected void tearDown() + throws Exception { + super.tearDown(); + } + + public final void testIsNumeric() { + Assert.assertTrue(Validate.isNumeric("123456")); + } + public final void testIsNumeric_Failed() { + Assert.assertFalse(Validate.isNumeric("12345d")); + } + public final void testLengthValidate() { + Assert.assertTrue(Validate.lengthValidate("123456")); + } + public final void testLengthValidate_Failed() { + Assert.assertFalse(Validate.lengthValidate("1234567")); + } +}// end ValidateTest \ No newline at end of file diff --git a/src/test/java/com/atm/dao/LoginIntegratedTest.java b/src/test/java/com/atm/dao/LoginIntegratedTest.java new file mode 100644 index 0000000..1c07a60 --- /dev/null +++ b/src/test/java/com/atm/dao/LoginIntegratedTest.java @@ -0,0 +1,16 @@ +package com.atm.dao; + +import org.junit.runners.Suite; +import org.junit.runner.RunWith; +import junit.framework.TestSuite; +import junit.framework.Test; + +@RunWith(Suite.class) +@Suite.SuiteClasses({ com.atm.dao.LoginTest.class, com.atm.controller.ValidateTest.class }) +public class LoginIntegratedTest { + + //public static Test suit() { + // TestSuite suite = new TestSuite(); + // return suite; + //} +}// end LoginITest \ No newline at end of file diff --git a/src/test/java/com/atm/dao/LoginTest.java b/src/test/java/com/atm/dao/LoginTest.java new file mode 100644 index 0000000..77eb3d4 --- /dev/null +++ b/src/test/java/com/atm/dao/LoginTest.java @@ -0,0 +1,49 @@ +package com.atm.dao; + +import org.junit.Assert; + +import com.atm.dao.Login; +import com.atm.model.Customer; + +public class LoginTest extends junit.framework.TestCase { + + /** + * + * @param arg0 + */ + public LoginTest(String arg0) { + super(arg0); + } + + /** + * + * @param args + */ + public static void main(String[] args) { + + } + + /** + * + * @exception Exception + */ + protected void setUp() + throws Exception { + super.setUp(); + } + + /** + * + * @exception Exception + */ + protected void tearDown() + throws Exception { + super.tearDown(); + } + public final void testLogin() { + Assert.assertTrue(new Login().login(new Customer("123456", "123456"))); + } + public final void testLogin_failed() { + Assert.assertFalse(new Login().login(new Customer("123457", "123458"))); + } +}// end LoginTest \ No newline at end of file diff --git a/src/test/java/com/atm/integration/ATMSystemIntegrationTest.java b/src/test/java/com/atm/integration/ATMSystemIntegrationTest.java new file mode 100644 index 0000000..732aa9d --- /dev/null +++ b/src/test/java/com/atm/integration/ATMSystemIntegrationTest.java @@ -0,0 +1,235 @@ +package com.atm.integration; + +import com.atm.model.Account; +import com.atm.model.Customer; +import com.atm.model.Transaction; +import com.atm.repository.AccountRepository; +import com.atm.repository.CustomerRepository; +import com.atm.repository.TransactionRepository; +import com.atm.service.AccountService; +import com.atm.service.CustomerService; +import com.atm.service.TransactionService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.TestPropertySource; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@SpringBootTest +@TestPropertySource(locations = "classpath:application-test.properties") +@Transactional +public class ATMSystemIntegrationTest { + + @Autowired + private CustomerRepository customerRepository; + + @Autowired + private AccountRepository accountRepository; + + @Autowired + private TransactionRepository transactionRepository; + + @Autowired + private CustomerService customerService; + + @Autowired + private AccountService accountService; + + @Autowired + private TransactionService transactionService; + + private Customer testCustomer; + private Account testAccount1; + private Account testAccount2; + + @BeforeEach + public void setUp() { + // 创建测试客户 + testCustomer = new Customer(); + testCustomer.setCid(System.currentTimeMillis()); // 使用时间戳确保唯一性 + testCustomer.setCname("测试用户"); + testCustomer.setCpin("123456"); + testCustomer.setCbalance(new BigDecimal("10000.00")); + testCustomer.setCstatus("active"); + testCustomer.setCtype("regular"); + + testCustomer = customerRepository.save(testCustomer); + + // 创建测试账户1 + testAccount1 = new Account(); + testAccount1.setCustomer(testCustomer); + testAccount1.setAtype("savings"); + testAccount1.setAbalance(new BigDecimal("5000.00")); + testAccount1.setAstatus("active"); + + testAccount1 = accountRepository.save(testAccount1); + + // 创建测试账户2 + testAccount2 = new Account(); + testAccount2.setCustomer(testCustomer); + testAccount2.setAtype("checking"); + testAccount2.setAbalance(new BigDecimal("3000.00")); + testAccount2.setAstatus("active"); + + testAccount2 = accountRepository.save(testAccount2); + } + + @Test + public void testCustomerAccountTransactionFlow() { + // 1. 验证客户创建成功 + Optional foundCustomer = customerService.findByCid(testCustomer.getCid()); + assertTrue(foundCustomer.isPresent()); + assertEquals("测试用户", foundCustomer.get().getCname()); + + // 2. 验证账户创建成功 + List accounts = accountService.findByCid(testCustomer.getCid()); + assertEquals(2, accounts.size()); + + // 3. 执行存款操作 + BigDecimal depositAmount = new BigDecimal("1000.00"); + Optional updatedAccount1 = accountService.deposit(testAccount1.getAid(), depositAmount); + assertTrue(updatedAccount1.isPresent()); + assertEquals(new BigDecimal("6000.00"), updatedAccount1.get().getAbalance()); + + // 4. 验证存款交易记录 + Transaction depositTransaction = transactionService.createDepositTransaction( + testAccount1.getAid(), depositAmount, "测试存款"); + assertNotNull(depositTransaction); + assertEquals("deposit", depositTransaction.getTtype()); + assertEquals(depositAmount, depositTransaction.getTamount()); + + // 5. 执行取款操作 + BigDecimal withdrawAmount = new BigDecimal("500.00"); + Optional updatedAccount2 = accountService.withdraw(testAccount2.getAid(), withdrawAmount); + assertTrue(updatedAccount2.isPresent()); + assertEquals(new BigDecimal("2500.00"), updatedAccount2.get().getAbalance()); + + // 6. 验证取款交易记录 + Transaction withdrawTransaction = transactionService.createWithdrawalTransaction( + testAccount2.getAid(), withdrawAmount, "测试取款"); + assertNotNull(withdrawTransaction); + assertEquals("withdrawal", withdrawTransaction.getTtype()); + assertEquals(withdrawAmount, withdrawTransaction.getTamount()); + + // 7. 执行转账操作 + BigDecimal transferAmount = new BigDecimal("1000.00"); + boolean transferResult = accountService.transfer(testAccount1.getAid(), testAccount2.getAid(), transferAmount); + assertTrue(transferResult); + + // 验证转账后账户余额 + Optional fromAccount = accountService.findByAid(testAccount1.getAid()); + Optional toAccount = accountService.findByAid(testAccount2.getAid()); + assertTrue(fromAccount.isPresent()); + assertTrue(toAccount.isPresent()); + assertEquals(new BigDecimal("5000.00"), fromAccount.get().getAbalance()); + assertEquals(new BigDecimal("3500.00"), toAccount.get().getAbalance()); + + // 8. 验证转账交易记录 + Transaction transferTransaction = transactionService.createTransferTransaction( + testAccount1.getAid(), testAccount2.getAid(), transferAmount, "测试转账"); + assertNotNull(transferTransaction); + assertEquals("transfer", transferTransaction.getTtype()); + assertEquals(transferAmount, transferTransaction.getTamount()); + + // 9. 查询客户的所有交易记录 + List customerTransactions = transactionService.findByCid(testCustomer.getCid()); + assertEquals(3, customerTransactions.size()); + + // 10. 查询账户的所有交易记录 + List account1Transactions = transactionService.findByAid(testAccount1.getAid()); + List account2Transactions = transactionService.findByAid(testAccount2.getAid()); + assertEquals(2, account1Transactions.size()); // 存款 + 转账 + assertEquals(2, account2Transactions.size()); // 取款 + 转账 + } + + @Test + public void testAccountStatusUpdate() { + // 更新账户状态为inactive + boolean updateResult = accountService.updateAccountStatus(testAccount1.getAid(), "inactive"); + assertTrue(updateResult); + + // 验证状态更新 + Optional updatedAccount = accountService.findByAid(testAccount1.getAid()); + assertTrue(updatedAccount.isPresent()); + assertEquals("inactive", updatedAccount.get().getAstatus()); + + // 验证无法对inactive账户进行操作 + BigDecimal depositAmount = new BigDecimal("100.00"); + Optional depositResult = accountService.deposit(testAccount1.getAid(), depositAmount); + assertFalse(depositResult.isPresent()); + } + + @Test + public void testCustomerPinUpdate() { + // 更新客户PIN码 + boolean updateResult = customerService.updatePin(testCustomer.getCid(), "123456", "654321"); + assertTrue(updateResult); + + // 验证PIN码更新 + Optional updatedCustomer = customerService.findByCid(testCustomer.getCid()); + assertTrue(updatedCustomer.isPresent()); + // 注意:由于PIN码是加密存储的,我们无法直接比较,但可以通过认证来验证 + // 在实际应用中,应该使用专门的认证方法来验证 + } + + @Test + public void testTransactionHistoryQuery() { + // 创建一些交易记录 + transactionService.createDepositTransaction(testAccount1.getAid(), new BigDecimal("100.00"), "测试存款1"); + transactionService.createDepositTransaction(testAccount1.getAid(), new BigDecimal("200.00"), "测试存款2"); + transactionService.createWithdrawalTransaction(testAccount2.getAid(), new BigDecimal("50.00"), "测试取款1"); + + // 查询账户1的交易记录 + List account1Transactions = transactionService.findByAid(testAccount1.getAid()); + assertEquals(2, account1Transactions.size()); + + // 查询账户2的交易记录 + List account2Transactions = transactionService.findByAid(testAccount2.getAid()); + assertEquals(1, account2Transactions.size()); + + // 查询客户的交易记录 + List customerTransactions = transactionService.findByCid(testCustomer.getCid()); + assertEquals(3, customerTransactions.size()); + + // 按交易类型查询 + List depositTransactions = transactionService.findByTtype("deposit"); + assertEquals(2, depositTransactions.size()); + + List withdrawalTransactions = transactionService.findByTtype("withdrawal"); + assertEquals(1, withdrawalTransactions.size()); + } + + @Test + public void testAccountBalanceQueries() { + // 查询余额大于指定金额的账户 + List highBalanceAccounts = accountService.findByAbalanceGreaterThan(new BigDecimal("4000.00")); + assertEquals(1, highBalanceAccounts.size()); + assertEquals(testAccount1.getAid(), highBalanceAccounts.get(0).getAid()); + + // 查询余额小于指定金额的账户 + List lowBalanceAccounts = accountService.findByAbalanceLessThan(new BigDecimal("4000.00")); + assertEquals(1, lowBalanceAccounts.size()); + assertEquals(testAccount2.getAid(), lowBalanceAccounts.get(0).getAid()); + } + + @Test + public void testTransactionDateRangeQuery() { + // 创建一些交易记录 + transactionService.createDepositTransaction(testAccount1.getAid(), new BigDecimal("100.00"), "测试存款1"); + transactionService.createWithdrawalTransaction(testAccount2.getAid(), new BigDecimal("50.00"), "测试取款1"); + + // 查询日期范围内的交易记录 + LocalDateTime startDate = LocalDateTime.now().minusDays(1); + LocalDateTime endDate = LocalDateTime.now().plusDays(1); + List dateRangeTransactions = transactionService.findByDateRange(startDate, endDate); + assertEquals(2, dateRangeTransactions.size()); + } +} \ No newline at end of file diff --git a/src/test/java/com/atm/repository/AccountRepositoryTest.java b/src/test/java/com/atm/repository/AccountRepositoryTest.java new file mode 100644 index 0000000..a2e1a48 --- /dev/null +++ b/src/test/java/com/atm/repository/AccountRepositoryTest.java @@ -0,0 +1,209 @@ +package com.atm.repository; + +import com.atm.model.Account; +import com.atm.model.Customer; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@ActiveProfiles("test") +public class AccountRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private AccountRepository accountRepository; + + private Customer createTestCustomer(Long cid, String name) { + Customer customer = new Customer(); + customer.setCid(cid); + customer.setCname(name); + customer.setCpin("123456"); + customer.setCbalance(new BigDecimal("1000.00")); + customer.setCstatus("active"); + customer.setCtype("regular"); + entityManager.persist(customer); + return customer; + } + + private Account createTestAccount(Long aid, Customer customer, String type, BigDecimal balance) { + Account account = new Account(); + // 不再设置aid,让数据库自动生成 + account.setCustomer(customer); + account.setAtype(type); + account.setAbalance(balance); + account.setAstatus("active"); + entityManager.persist(account); + entityManager.flush(); // 确保账户被持久化并生成ID + return account; + } + + @Test + public void whenFindByAid_thenReturnAccount() { + // given + Customer customer = createTestCustomer(12345L, "张三"); + Account account = createTestAccount(null, customer, "checking", new BigDecimal("1000.00")); + entityManager.flush(); + entityManager.clear(); // 清除持久化上下文,确保从数据库重新加载 + + // when + Optional found = accountRepository.findByAid(account.getAid()); + + // then + assertTrue(found.isPresent()); + assertEquals(account.getAid(), found.get().getAid()); + assertEquals("checking", found.get().getAtype()); + assertEquals(new BigDecimal("1000.00"), found.get().getAbalance()); + } + + @Test + public void whenFindByCustomer_thenReturnAccounts() { + // given + Customer customer1 = createTestCustomer(12345L, "张三"); + Customer customer2 = createTestCustomer(67890L, "李四"); + + Account account1 = createTestAccount(null, customer1, "checking", new BigDecimal("1000.00")); + Account account2 = createTestAccount(null, customer1, "savings", new BigDecimal("2000.00")); + Account account3 = createTestAccount(null, customer2, "checking", new BigDecimal("1500.00")); + entityManager.flush(); + + // when + List customer1Accounts = accountRepository.findByCustomerCid(customer1.getCid()); + List customer2Accounts = accountRepository.findByCustomerCid(customer2.getCid()); + + // then + assertEquals(2, customer1Accounts.size()); + assertEquals(1, customer2Accounts.size()); + } + + @Test + public void whenFindByCid_thenReturnAccounts() { + // given + Customer customer1 = createTestCustomer(12345L, "张三"); + Customer customer2 = createTestCustomer(67890L, "李四"); + + createTestAccount(1001L, customer1, "checking", new BigDecimal("1000.00")); + createTestAccount(1002L, customer1, "savings", new BigDecimal("2000.00")); + createTestAccount(1003L, customer2, "checking", new BigDecimal("1500.00")); + entityManager.flush(); + + // when + List customer1Accounts = accountRepository.findByCustomerCid(12345L); + List customer2Accounts = accountRepository.findByCustomerCid(67890L); + + // then + assertEquals(2, customer1Accounts.size()); + assertEquals(1, customer2Accounts.size()); + } + + @Test + public void whenFindByAtype_thenReturnAccounts() { + // given + Customer customer1 = createTestCustomer(12345L, "张三"); + Customer customer2 = createTestCustomer(67890L, "李四"); + + createTestAccount(1001L, customer1, "checking", new BigDecimal("1000.00")); + createTestAccount(1002L, customer1, "savings", new BigDecimal("2000.00")); + createTestAccount(1003L, customer2, "checking", new BigDecimal("1500.00")); + entityManager.flush(); + + // when + List checkingAccounts = accountRepository.findByAtype("checking"); + List savingsAccounts = accountRepository.findByAtype("savings"); + + // then + assertEquals(2, checkingAccounts.size()); + assertEquals(1, savingsAccounts.size()); + } + + @Test + public void whenFindByAstatus_thenReturnAccounts() { + // given + Customer customer1 = createTestCustomer(12345L, "张三"); + Customer customer2 = createTestCustomer(67890L, "李四"); + + createTestAccount(1001L, customer1, "checking", new BigDecimal("1000.00")); + Account inactiveAccount = createTestAccount(1002L, customer1, "savings", new BigDecimal("2000.00")); + inactiveAccount.setAstatus("inactive"); + entityManager.persist(inactiveAccount); + entityManager.flush(); + + // when + List activeAccounts = accountRepository.findByCustomerCidAndAstatus(12345L, "active"); + List inactiveAccounts = accountRepository.findByCustomerCidAndAstatus(12345L, "inactive"); + + // then + assertEquals(1, activeAccounts.size()); + assertEquals(1, inactiveAccounts.size()); + } + + @Test + public void whenExistsByAid_thenReturnTrue() { + // given + Customer customer = createTestCustomer(12345L, "张三"); + createTestAccount(1001L, customer, "checking", new BigDecimal("1000.00")); + entityManager.flush(); + + // when + boolean exists = accountRepository.existsByAid(1001L); + + // then + assertTrue(exists); + } + + @Test + public void whenNotExistsByAid_thenReturnFalse() { + // when + boolean exists = accountRepository.existsByAid(9999L); + + // then + assertFalse(exists); + } + + @Test + public void whenFindByAbalanceGreaterThan_thenReturnAccounts() { + // given + Customer customer1 = createTestCustomer(12345L, "张三"); + Customer customer2 = createTestCustomer(67890L, "李四"); + + Account account1 = createTestAccount(null, customer1, "checking", new BigDecimal("1000.00")); + Account account2 = createTestAccount(null, customer1, "savings", new BigDecimal("2000.00")); + Account account3 = createTestAccount(null, customer2, "checking", new BigDecimal("1500.00")); + entityManager.flush(); + + // when + List highBalanceAccounts = accountRepository.findAccountsWithBalanceGreaterThan(new BigDecimal("1500.00")); + + // then + assertEquals(1, highBalanceAccounts.size()); + assertEquals(account2.getAid(), highBalanceAccounts.get(0).getAid()); + } + + @Test + public void whenFindByAbalanceLessThan_thenReturnAccounts() { + // given + Customer customer = createTestCustomer(12345L, "张三"); + + Account account1 = createTestAccount(null, customer, "checking", new BigDecimal("500.00")); + Account account2 = createTestAccount(null, customer, "savings", new BigDecimal("1500.00")); + Account account3 = createTestAccount(null, customer, "investment", new BigDecimal("2000.00")); + entityManager.flush(); + + // when + List accounts = accountRepository.findAccountsWithBalanceLessThan(new BigDecimal("1000.00")); + + // then + assertEquals(1, accounts.size()); + assertTrue(accounts.stream().anyMatch(a -> a.getAid().equals(account1.getAid()))); + } +} \ No newline at end of file diff --git a/src/test/java/com/atm/repository/CustomerRepositoryTest.java b/src/test/java/com/atm/repository/CustomerRepositoryTest.java new file mode 100644 index 0000000..a935821 --- /dev/null +++ b/src/test/java/com/atm/repository/CustomerRepositoryTest.java @@ -0,0 +1,134 @@ +package com.atm.repository; + +import com.atm.model.Customer; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager; +import org.springframework.test.context.ActiveProfiles; + +import java.math.BigDecimal; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; + +@DataJpaTest +@ActiveProfiles("test") +public class CustomerRepositoryTest { + + @Autowired + private TestEntityManager entityManager; + + @Autowired + private CustomerRepository customerRepository; + + @Test + public void whenFindByCid_thenReturnCustomer() { + // given + Customer customer = new Customer(); + customer.setCid(12345L); + customer.setCname("张三"); + customer.setCpin("123456"); + customer.setCbalance(new BigDecimal("1000.00")); + customer.setCstatus("active"); + customer.setCtype("regular"); + + entityManager.persist(customer); + entityManager.flush(); + + // when + Optional found = customerRepository.findByCid(12345L); + + // then + assertTrue(found.isPresent()); + assertEquals(12345L, found.get().getCid()); + assertEquals("张三", found.get().getCname()); + } + + @Test + public void whenFindByCidAndCpin_thenReturnCustomer() { + // given + Customer customer = new Customer(); + customer.setCid(12345L); + customer.setCname("张三"); + customer.setCpin("123456"); + customer.setCbalance(new BigDecimal("1000.00")); + customer.setCstatus("active"); + customer.setCtype("regular"); + + entityManager.persist(customer); + entityManager.flush(); + + // when + Optional found = customerRepository.findByCidAndCpin(12345L, "123456"); + + // then + assertTrue(found.isPresent()); + assertEquals(12345L, found.get().getCid()); + assertEquals("123456", found.get().getCpin()); + } + + @Test + public void whenExistsByCid_thenReturnTrue() { + // given + Customer customer = new Customer(); + customer.setCid(12345L); + customer.setCname("张三"); + customer.setCpin("123456"); + customer.setCbalance(new BigDecimal("1000.00")); + customer.setCstatus("active"); + customer.setCtype("regular"); + + entityManager.persist(customer); + entityManager.flush(); + + // when + boolean exists = customerRepository.existsByCid(12345L); + + // then + assertTrue(exists); + } + + @Test + public void whenNotExistsByCid_thenReturnFalse() { + // when + boolean exists = customerRepository.existsByCid(99999L); + + // then + assertFalse(exists); + } + + @Test + public void whenFindByCstatus_thenReturnCustomers() { + // given + Customer customer1 = new Customer(); + customer1.setCid(12345L); + customer1.setCname("张三"); + customer1.setCpin("123456"); + customer1.setCbalance(new BigDecimal("1000.00")); + customer1.setCstatus("active"); + customer1.setCtype("regular"); + + Customer customer2 = new Customer(); + customer2.setCid(67890L); + customer2.setCname("李四"); + customer2.setCpin("654321"); + customer2.setCbalance(new BigDecimal("2000.00")); + customer2.setCstatus("inactive"); + customer2.setCtype("premium"); + + entityManager.persist(customer1); + entityManager.persist(customer2); + entityManager.flush(); + + // when + var activeCustomers = customerRepository.findByCstatus("active"); + var inactiveCustomers = customerRepository.findByCstatus("inactive"); + + // then + assertEquals(1, activeCustomers.size()); + assertEquals(1, inactiveCustomers.size()); + assertEquals("张三", activeCustomers.get(0).getCname()); + assertEquals("李四", inactiveCustomers.get(0).getCname()); + } +} \ No newline at end of file diff --git a/src/test/java/com/atm/service/AccountServiceTest.java b/src/test/java/com/atm/service/AccountServiceTest.java new file mode 100644 index 0000000..5c53ad8 --- /dev/null +++ b/src/test/java/com/atm/service/AccountServiceTest.java @@ -0,0 +1,348 @@ +package com.atm.service; + +import com.atm.model.Account; +import com.atm.model.Customer; +import com.atm.repository.AccountRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class AccountServiceTest { + + @Mock + private AccountRepository accountRepository; + + @InjectMocks + private AccountService accountService; + + private Customer testCustomer; + private Account testAccount; + + @BeforeEach + public void setUp() { + testCustomer = new Customer(); + testCustomer.setCid(12345L); + testCustomer.setCname("张三"); + testCustomer.setCpin("123456"); + testCustomer.setCbalance(new BigDecimal("1000.00")); + testCustomer.setCstatus("active"); + testCustomer.setCtype("regular"); + + testAccount = new Account(); + testAccount.setAid(1L); + testAccount.setCustomer(testCustomer); + testAccount.setAtype("savings"); + testAccount.setAbalance(new BigDecimal("5000.00")); + testAccount.setAstatus("active"); + } + + @Test + public void whenFindByAid_thenReturnAccount() { + // given + when(accountRepository.findByAid(1L)).thenReturn(Optional.of(testAccount)); + + // when + Optional result = accountService.findByAid(1L); + + // then + assertTrue(result.isPresent()); + assertEquals(1L, result.get().getAid()); + assertEquals("savings", result.get().getAtype()); + } + + @Test + public void whenFindByCustomer_thenReturnAccounts() { + // given + when(accountRepository.findByCustomerCid(testCustomer.getCid())).thenReturn(Arrays.asList(testAccount)); + + // when + List result = accountService.findByCustomerId(testCustomer.getCid()); + + // then + assertEquals(1, result.size()); + assertEquals(testAccount.getAid(), result.get(0).getAid()); + } + + @Test + public void whenFindByCid_thenReturnAccounts() { + // given + when(accountRepository.findByCustomerCid(12345L)).thenReturn(Arrays.asList(testAccount)); + + // when + List result = accountService.findByCustomerId(12345L); + + // then + assertEquals(1, result.size()); + assertEquals(testAccount.getAid(), result.get(0).getAid()); + } + + @Test + public void whenFindByAtype_thenReturnAccounts() { + // given + when(accountRepository.findByAtype("savings")).thenReturn(Arrays.asList(testAccount)); + + // when + List result = accountService.findByAtype("savings"); + + // then + assertEquals(1, result.size()); + assertEquals(testAccount.getAid(), result.get(0).getAid()); + } + + @Test + public void whenFindByAstatus_thenReturnAccounts() { + // given + when(accountRepository.findByCustomerCidAndAstatus(testCustomer.getCid(), "active")).thenReturn(Arrays.asList(testAccount)); + + // when + List result = accountService.findByCustomerIdAndStatus(testCustomer.getCid(), "active"); + + // then + assertEquals(1, result.size()); + assertEquals(testAccount.getAid(), result.get(0).getAid()); + } + + @Test + public void whenCreateAccount_thenReturnSavedAccount() { + // given + Account newAccount = new Account(); + newAccount.setCustomer(testCustomer); + newAccount.setAtype("checking"); + newAccount.setAbalance(new BigDecimal("1000.00")); + newAccount.setAstatus("active"); + + when(accountRepository.save(any(Account.class))).thenReturn(newAccount); + + // when + Account result = accountService.createAccount(newAccount); + + // then + assertNotNull(result); + assertEquals("checking", result.getAtype()); + verify(accountRepository, times(1)).save(any(Account.class)); + } + + @Test + public void whenUpdateAccount_thenReturnUpdatedAccount() { + // given + Account updatedAccount = new Account(); + updatedAccount.setAid(1L); + updatedAccount.setCustomer(testCustomer); + updatedAccount.setAtype("savings"); + updatedAccount.setAbalance(new BigDecimal("6000.00")); + updatedAccount.setAstatus("active"); + + when(accountRepository.save(any(Account.class))).thenReturn(updatedAccount); + + // when + Account result = accountService.updateAccount(updatedAccount); + + // then + assertNotNull(result); + assertEquals(new BigDecimal("6000.00"), result.getAbalance()); + verify(accountRepository, times(1)).save(any(Account.class)); + } + + @Test + public void whenDeleteAccount_thenReturnTrue() { + // given + when(accountRepository.existsByAid(1L)).thenReturn(true); + doNothing().when(accountRepository).deleteById(1L); + + // when + boolean result = accountService.deleteAccount(1L); + + // then + assertTrue(result); + verify(accountRepository, times(1)).deleteById(1L); + } + + @Test + public void whenDeleteNonExistingAccount_thenReturnFalse() { + // given + when(accountRepository.existsByAid(999L)).thenReturn(false); + + // when + boolean result = accountService.deleteAccount(999L); + + // then + assertFalse(result); + verify(accountRepository, never()).deleteById(any()); + } + + @Test + public void whenDeposit_thenReturnUpdatedAccount() { + // given + BigDecimal depositAmount = new BigDecimal("500.00"); + BigDecimal expectedBalance = new BigDecimal("5500.00"); + + when(accountRepository.findByAid(1L)).thenReturn(Optional.of(testAccount)); + when(accountRepository.save(any(Account.class))).thenReturn(testAccount); + + // when + boolean result = accountService.deposit(1L, depositAmount); + + // then + assertTrue(result); + verify(accountRepository, times(1)).save(any(Account.class)); + } + + @Test + public void whenDepositToNonExistingAccount_thenReturnFalse() { + // given + when(accountRepository.findByAid(999L)).thenReturn(Optional.empty()); + + // when + boolean result = accountService.deposit(999L, new BigDecimal("500.00")); + + // then + assertFalse(result); + verify(accountRepository, never()).save(any(Account.class)); + } + + @Test + public void whenWithdrawWithSufficientBalance_thenReturnTrue() { + // given + BigDecimal withdrawAmount = new BigDecimal("1000.00"); + BigDecimal expectedBalance = new BigDecimal("4000.00"); + + when(accountRepository.findByAid(1L)).thenReturn(Optional.of(testAccount)); + when(accountRepository.save(any(Account.class))).thenReturn(testAccount); + + // when + boolean result = accountService.withdraw(1L, withdrawAmount); + + // then + assertTrue(result); + verify(accountRepository, times(1)).save(any(Account.class)); + } + + @Test + public void whenWithdrawWithInsufficientBalance_thenReturnFalse() { + // given + BigDecimal withdrawAmount = new BigDecimal("10000.00"); + + when(accountRepository.findByAid(1L)).thenReturn(Optional.of(testAccount)); + + // when + boolean result = accountService.withdraw(1L, withdrawAmount); + + // then + assertFalse(result); + verify(accountRepository, never()).save(any(Account.class)); + } + + @Test + public void whenTransferWithSufficientBalance_thenReturnTrue() { + // given + Account toAccount = new Account(); + toAccount.setAid(2L); + toAccount.setCustomer(testCustomer); + toAccount.setAtype("checking"); + toAccount.setAbalance(new BigDecimal("2000.00")); + toAccount.setAstatus("active"); + + BigDecimal transferAmount = new BigDecimal("1000.00"); + + when(accountRepository.findByAid(1L)).thenReturn(Optional.of(testAccount)); + when(accountRepository.findByAid(2L)).thenReturn(Optional.of(toAccount)); + when(accountRepository.save(any(Account.class))).thenReturn(testAccount); + + // when + boolean result = accountService.transfer(1L, 2L, transferAmount); + + // then + assertTrue(result); + verify(accountRepository, times(2)).save(any(Account.class)); + } + + @Test + public void whenTransferWithInsufficientBalance_thenReturnFalse() { + // given + Account toAccount = new Account(); + toAccount.setAid(2L); + toAccount.setCustomer(testCustomer); + toAccount.setAtype("checking"); + toAccount.setAbalance(new BigDecimal("2000.00")); + toAccount.setAstatus("active"); + + BigDecimal transferAmount = new BigDecimal("10000.00"); + + when(accountRepository.findByAid(1L)).thenReturn(Optional.of(testAccount)); + when(accountRepository.findByAid(2L)).thenReturn(Optional.of(toAccount)); + + // when + boolean result = accountService.transfer(1L, 2L, transferAmount); + + // then + assertFalse(result); + verify(accountRepository, never()).save(any(Account.class)); + } + + @Test + public void whenUpdateAccountStatus_thenReturnTrue() { + // given + when(accountRepository.findByAid(1L)).thenReturn(Optional.of(testAccount)); + when(accountRepository.save(any(Account.class))).thenReturn(testAccount); + + // when + boolean result = accountService.updateAccountStatus(1L, "inactive"); + + // then + assertTrue(result); + verify(accountRepository, times(1)).save(any(Account.class)); + } + + @Test + public void whenExistsByAid_thenReturnTrue() { + // given + when(accountRepository.existsByAid(1L)).thenReturn(true); + + // when + boolean result = accountService.existsByAid(1L); + + // then + assertTrue(result); + } + + @Test + public void whenFindByAbalanceGreaterThan_thenReturnAccounts() { + // given + when(accountRepository.findAccountsWithBalanceGreaterThan(new BigDecimal("3000.00"))) + .thenReturn(Arrays.asList(testAccount)); + + // when + List result = accountService.findAccountsWithBalanceGreaterThan(new BigDecimal("3000.00")); + + // then + assertEquals(1, result.size()); + assertEquals(testAccount.getAid(), result.get(0).getAid()); + } + + @Test + public void whenFindByAbalanceLessThan_thenReturnAccounts() { + // given + when(accountRepository.findAccountsWithBalanceLessThan(new BigDecimal("6000.00"))) + .thenReturn(Arrays.asList(testAccount)); + + // when + List result = accountService.findAccountsWithBalanceLessThan(new BigDecimal("6000.00")); + + // then + assertEquals(1, result.size()); + assertEquals(testAccount.getAid(), result.get(0).getAid()); + } +} \ No newline at end of file diff --git a/src/test/java/com/atm/service/CustomerServiceTest.java b/src/test/java/com/atm/service/CustomerServiceTest.java new file mode 100644 index 0000000..44d2bd0 --- /dev/null +++ b/src/test/java/com/atm/service/CustomerServiceTest.java @@ -0,0 +1,257 @@ +package com.atm.service; + +import com.atm.model.Customer; +import com.atm.repository.CustomerRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; + +import java.math.BigDecimal; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class CustomerServiceTest { + + @Mock + private CustomerRepository customerRepository; + + @InjectMocks + private CustomerService customerService; + + private Customer testCustomer; + + @BeforeEach + public void setUp() { + testCustomer = new Customer(); + testCustomer.setCid(12345L); + testCustomer.setCname("张三"); + testCustomer.setCpin("123456"); + testCustomer.setCbalance(new BigDecimal("1000.00")); + testCustomer.setCstatus("active"); + testCustomer.setCtype("regular"); + } + + @Test + public void whenLoadUserByUsername_thenReturnUserDetails() { + // given + when(customerRepository.findByCid(12345L)).thenReturn(Optional.of(testCustomer)); + + // when + UserDetails userDetails = customerService.loadUserByUsername("12345"); + + // then + assertNotNull(userDetails); + assertEquals("12345", userDetails.getUsername()); + assertTrue(userDetails.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_USER"))); + } + + @Test + public void whenLoadUserByUsernameNotFound_thenThrowException() { + // given + when(customerRepository.findByCid(99999L)).thenReturn(Optional.empty()); + + // when & then + assertThrows(UsernameNotFoundException.class, () -> { + customerService.loadUserByUsername("99999"); + }); + } + + @Test + public void whenFindByCid_thenReturnCustomer() { + // given + when(customerRepository.findByCid(12345L)).thenReturn(Optional.of(testCustomer)); + + // when + Optional result = customerService.findByCid(12345L); + + // then + assertTrue(result.isPresent()); + assertEquals(12345L, result.get().getCid()); + assertEquals("张三", result.get().getCname()); + } + + @Test + public void whenFindByCidAndCpin_thenReturnCustomer() { + // given + when(customerRepository.findByCidAndCpin(12345L, "123456")).thenReturn(Optional.of(testCustomer)); + + // when + Optional result = customerService.findByCidAndCpin(12345L, "123456"); + + // then + assertTrue(result.isPresent()); + assertEquals(12345L, result.get().getCid()); + assertEquals("123456", result.get().getCpin()); + } + + @Test + public void whenCreateCustomer_thenReturnSavedCustomer() { + // given + Customer newCustomer = new Customer(); + newCustomer.setCid(67890L); + newCustomer.setCname("李四"); + newCustomer.setCpin("654321"); + newCustomer.setCbalance(new BigDecimal("2000.00")); + newCustomer.setCstatus("active"); + newCustomer.setCtype("premium"); + + when(customerRepository.save(any(Customer.class))).thenReturn(newCustomer); + + // when + Customer result = customerService.createCustomer(newCustomer); + + // then + assertNotNull(result); + assertEquals(67890L, result.getCid()); + assertEquals("李四", result.getCname()); + verify(customerRepository, times(1)).save(any(Customer.class)); + } + + @Test + public void whenUpdateCustomer_thenReturnUpdatedCustomer() { + // given + Customer updatedCustomer = new Customer(); + updatedCustomer.setCid(12345L); + updatedCustomer.setCname("张三更新"); + updatedCustomer.setCpin("123456"); + updatedCustomer.setCbalance(new BigDecimal("1500.00")); + updatedCustomer.setCstatus("active"); + updatedCustomer.setCtype("premium"); + + when(customerRepository.save(any(Customer.class))).thenReturn(updatedCustomer); + + // when + Customer result = customerService.updateCustomer(updatedCustomer); + + // then + assertNotNull(result); + assertEquals("张三更新", result.getCname()); + assertEquals(new BigDecimal("1500.00"), result.getCbalance()); + verify(customerRepository, times(1)).save(any(Customer.class)); + } + + @Test + public void whenDeleteCustomer_thenReturnTrue() { + // given + when(customerRepository.existsByCid(12345L)).thenReturn(true); + doNothing().when(customerRepository).deleteById(12345L); + + // when + boolean result = customerService.deleteCustomer(12345L); + + // then + assertTrue(result); + verify(customerRepository, times(1)).deleteById(12345L); + } + + @Test + public void whenDeleteNonExistingCustomer_thenReturnFalse() { + // given + when(customerRepository.existsByCid(99999L)).thenReturn(false); + + // when + boolean result = customerService.deleteCustomer(99999L); + + // then + assertFalse(result); + verify(customerRepository, never()).deleteById(any()); + } + + @Test + public void whenUpdatePinWithCorrectOldPin_thenReturnTrue() { + // given + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + String encodedPin = passwordEncoder.encode("123456"); + testCustomer.setCpin(encodedPin); + + when(customerRepository.findByCid(12345L)).thenReturn(Optional.of(testCustomer)); + when(customerRepository.save(any(Customer.class))).thenReturn(testCustomer); + + // when + boolean result = customerService.updatePin(12345L, "123456", "654321"); + + // then + assertTrue(result); + verify(customerRepository, times(1)).save(any(Customer.class)); + } + + @Test + public void whenUpdatePinWithIncorrectOldPin_thenReturnFalse() { + // given + BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + String encodedPin = passwordEncoder.encode("123456"); + testCustomer.setCpin(encodedPin); + + when(customerRepository.findByCid(12345L)).thenReturn(Optional.of(testCustomer)); + + // when + boolean result = customerService.updatePin(12345L, "wrongpin", "654321"); + + // then + assertFalse(result); + verify(customerRepository, never()).save(any(Customer.class)); + } + + @Test + public void whenResetPin_thenReturnTrue() { + // given + when(customerRepository.findByCid(12345L)).thenReturn(Optional.of(testCustomer)); + when(customerRepository.save(any(Customer.class))).thenReturn(testCustomer); + + // when + boolean result = customerService.resetPin(12345L, "newpin123"); + + // then + assertTrue(result); + verify(customerRepository, times(1)).save(any(Customer.class)); + } + + @Test + public void whenUpdateCustomerStatus_thenReturnTrue() { + // given + when(customerRepository.findByCid(12345L)).thenReturn(Optional.of(testCustomer)); + when(customerRepository.save(any(Customer.class))).thenReturn(testCustomer); + + // when + boolean result = customerService.updateCustomerStatus(12345L, "inactive"); + + // then + assertTrue(result); + verify(customerRepository, times(1)).save(any(Customer.class)); + } + + @Test + public void whenExistsByCid_thenReturnTrue() { + // given + when(customerRepository.existsByCid(12345L)).thenReturn(true); + + // when + boolean result = customerService.existsByCid(12345L); + + // then + assertTrue(result); + } + + @Test + public void whenFindByStatus_thenReturnCustomers() { + // given + when(customerRepository.findByCstatus("active")).thenReturn(java.util.Arrays.asList(testCustomer)); + + // when + var result = customerService.findByStatus("active"); + + // then + assertEquals(1, result.size()); + assertEquals(testCustomer.getCid(), result.get(0).getCid()); + } +} \ No newline at end of file diff --git a/src/test/java/com/atm/service/TransactionServiceTest.java b/src/test/java/com/atm/service/TransactionServiceTest.java new file mode 100644 index 0000000..faea80b --- /dev/null +++ b/src/test/java/com/atm/service/TransactionServiceTest.java @@ -0,0 +1,324 @@ +package com.atm.service; + +import com.atm.model.Account; +import com.atm.model.Customer; +import com.atm.model.Transaction; +import com.atm.repository.TransactionRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +public class TransactionServiceTest { + + @Mock + private TransactionRepository transactionRepository; + + @Mock + private AccountService accountService; + + @InjectMocks + private TransactionService transactionService; + + private Customer testCustomer; + private Account testAccount; + private Account testAccount2; + private Transaction testTransaction; + + @BeforeEach + public void setUp() { + testCustomer = new Customer(); + testCustomer.setCid(12345L); + testCustomer.setCname("张三"); + testCustomer.setCpin("123456"); + testCustomer.setCbalance(new BigDecimal("1000.00")); + testCustomer.setCstatus("active"); + testCustomer.setCtype("regular"); + + testAccount = new Account(); + testAccount.setAid(1L); + testAccount.setCustomer(testCustomer); + testAccount.setAtype("savings"); + testAccount.setAbalance(new BigDecimal("5000.00")); + testAccount.setAstatus("active"); + + testAccount2 = new Account(); + testAccount2.setAid(2L); + testAccount2.setCustomer(testCustomer); + testAccount2.setAtype("checking"); + testAccount2.setAbalance(new BigDecimal("2000.00")); + testAccount2.setAstatus("active"); + + testTransaction = new Transaction(); + testTransaction.setTid(1001L); + testTransaction.setFromAccount(testAccount); + testTransaction.setToAccount(testAccount2); + testTransaction.setTtype("transfer"); + testTransaction.setTamount(new BigDecimal("500.00")); + testTransaction.setTdescription("Test transfer"); + testTransaction.setTstatus("completed"); + testTransaction.setCreatedAt(LocalDateTime.now()); + } + + @Test + public void whenFindByTid_thenReturnTransaction() { + // given + when(transactionRepository.findByTid(1001L)).thenReturn(Optional.of(testTransaction)); + + // when + Optional result = transactionService.findByTid(1001L); + + // then + assertTrue(result.isPresent()); + assertEquals(1001L, result.get().getTid()); + assertEquals("transfer", result.get().getTtype()); + } + + @Test + public void whenFindByFromAccount_thenReturnTransactions() { + // given + when(transactionRepository.findByFromAccount(testAccount)).thenReturn(Arrays.asList(testTransaction)); + + // when + List result = transactionService.findByFromAccount(testAccount); + + // then + assertEquals(1, result.size()); + assertEquals(testTransaction.getTid(), result.get(0).getTid()); + } + + @Test + public void whenFindByToAccount_thenReturnTransactions() { + // given + when(transactionRepository.findByToAccount(testAccount2)).thenReturn(Arrays.asList(testTransaction)); + + // when + List result = transactionService.findByToAccount(testAccount2); + + // then + assertEquals(1, result.size()); + assertEquals(testTransaction.getTid(), result.get(0).getTid()); + } + + @Test + public void whenFindByTtype_thenReturnTransactions() { + // given + when(transactionRepository.findByTtype("transfer")).thenReturn(Arrays.asList(testTransaction)); + + // when + List result = transactionService.findByTtype("transfer"); + + // then + assertEquals(1, result.size()); + assertEquals(testTransaction.getTid(), result.get(0).getTid()); + } + + @Test + public void whenFindByTstatus_thenReturnTransactions() { + // given + when(transactionRepository.findByTstatus("completed")).thenReturn(Arrays.asList(testTransaction)); + + // when + List result = transactionService.findByTstatus("completed"); + + // then + assertEquals(1, result.size()); + assertEquals(testTransaction.getTid(), result.get(0).getTid()); + } + + @Test + public void whenCreateDepositTransaction_thenReturnTransaction() { + // given + BigDecimal amount = new BigDecimal("1000.00"); + String description = "Deposit test"; + + when(transactionRepository.save(any(Transaction.class))).thenReturn(testTransaction); + + // when + Transaction result = transactionService.createDepositTransaction(1L, amount, description); + + // then + assertNotNull(result); + assertEquals("deposit", result.getTtype()); + assertEquals(amount, result.getTamount()); + assertEquals(description, result.getTdescription()); + verify(transactionRepository, times(1)).save(any(Transaction.class)); + } + + @Test + public void whenCreateDepositTransactionWithFailedDeposit_thenReturnNull() { + // given + BigDecimal amount = new BigDecimal("1000.00"); + String description = "Deposit test"; + + when(accountService.deposit(1L, amount)).thenReturn(Optional.empty()); + + // when + Transaction result = transactionService.createDepositTransaction(1L, amount, description); + + // then + assertNull(result); + verify(transactionRepository, never()).save(any(Transaction.class)); + } + + @Test + public void whenCreateWithdrawalTransaction_thenReturnTransaction() { + // given + BigDecimal amount = new BigDecimal("500.00"); + String description = "Withdrawal test"; + + when(transactionRepository.save(any(Transaction.class))).thenReturn(testTransaction); + + // when + Transaction result = transactionService.createWithdrawalTransaction(1L, amount, description); + + // then + assertNotNull(result); + assertEquals("withdrawal", result.getTtype()); + assertEquals(amount, result.getTamount()); + assertEquals(description, result.getTdescription()); + verify(transactionRepository, times(1)).save(any(Transaction.class)); + } + + @Test + public void whenCreateWithdrawalTransactionWithFailedWithdrawal_thenReturnNull() { + // given + BigDecimal amount = new BigDecimal("500.00"); + String description = "Withdrawal test"; + + when(accountService.withdraw(1L, amount)).thenReturn(Optional.empty()); + + // when + Transaction result = transactionService.createWithdrawalTransaction(1L, amount, description); + + // then + assertNull(result); + verify(transactionRepository, never()).save(any(Transaction.class)); + } + + @Test + public void whenCreateTransferTransaction_thenReturnTransaction() { + // given + BigDecimal amount = new BigDecimal("500.00"); + String description = "Transfer test"; + + when(transactionRepository.save(any(Transaction.class))).thenReturn(testTransaction); + + // when + Transaction result = transactionService.createTransferTransaction(1L, 2L, amount, description); + + // then + assertNotNull(result); + assertEquals("transfer", result.getTtype()); + assertEquals(amount, result.getTamount()); + assertEquals(description, result.getTdescription()); + verify(transactionRepository, times(1)).save(any(Transaction.class)); + } + + @Test + public void whenCreateTransferTransactionWithFailedTransfer_thenReturnNull() { + // given + Long fromAid = 1L; + Long toAid = 2L; + BigDecimal amount = new BigDecimal("300.00"); + String description = "Transfer test"; + + when(transactionRepository.save(any(Transaction.class))).thenReturn(testTransaction); + + // when + Transaction result = transactionService.createTransferTransaction(fromAid, toAid, amount, description); + + // then + assertNotNull(result); + assertEquals("transfer", result.getTtype()); + assertEquals(amount, result.getTamount()); + assertEquals(description, result.getTdescription()); + verify(transactionRepository, times(1)).save(any(Transaction.class)); + } + + @Test + public void whenFindByAid_thenReturnTransactions() { + // given + when(transactionRepository.findByAccountId(1L)).thenReturn(Arrays.asList(testTransaction)); + + // when + List result = transactionService.findByAid(1L); + + // then + assertEquals(1, result.size()); + assertEquals(testTransaction.getTid(), result.get(0).getTid()); + } + + @Test + public void whenFindByCid_thenReturnTransactions() { + // given + when(transactionRepository.findByCustomerId(12345L)).thenReturn(Arrays.asList(testTransaction)); + + // when + List result = transactionService.findByCid(12345L); + + // then + assertEquals(1, result.size()); + assertEquals(testTransaction.getTid(), result.get(0).getTid()); + } + + @Test + public void whenFindByDateRange_thenReturnTransactions() { + // given + LocalDateTime startDate = LocalDateTime.now().minusDays(7); + LocalDateTime endDate = LocalDateTime.now(); + + when(transactionRepository.findByDateRange(startDate, endDate)) + .thenReturn(Arrays.asList(testTransaction)); + + // when + List result = transactionService.findByDateRange(startDate, endDate); + + // then + assertEquals(1, result.size()); + assertEquals(testTransaction.getTid(), result.get(0).getTid()); + } + + @Test + public void whenFindByAmountGreaterThan_thenReturnTransactions() { + // given + BigDecimal amount = new BigDecimal("400.00"); + + when(transactionRepository.findTransactionsWithAmountGreaterThan(amount)) + .thenReturn(Arrays.asList(testTransaction)); + + // when + List result = transactionService.findByAmountAbove(amount); + + // then + assertEquals(1, result.size()); + assertEquals(testTransaction.getTid(), result.get(0).getTid()); + } + + @Test + public void whenFindRecentTransactionsByAid_thenReturnTransactions() { + // given + when(transactionRepository.findRecentTransactionsByAccountId(1L, org.springframework.data.domain.PageRequest.of(0, 10))) + .thenReturn(Arrays.asList(testTransaction)); + + // when + List result = transactionService.findRecentTransactionsByAccount(1L, 10); + + // then + assertEquals(1, result.size()); + assertEquals(testTransaction.getTid(), result.get(0).getTid()); + } +} \ No newline at end of file diff --git a/src/test/resources/application-test.properties b/src/test/resources/application-test.properties new file mode 100644 index 0000000..628ee71 --- /dev/null +++ b/src/test/resources/application-test.properties @@ -0,0 +1,13 @@ +# 测试配置文件 +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driver-class-name=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password= + +# JPA配置 +spring.jpa.database-platform=org.hibernate.dialect.H2Dialect +spring.jpa.hibernate.ddl-auto=create-drop +spring.jpa.show-sql=true + +# 日志配置 +logging.level.com.atm=DEBUG \ No newline at end of file diff --git a/start-atm.bat b/start-atm.bat new file mode 100644 index 0000000..47f5717 --- /dev/null +++ b/start-atm.bat @@ -0,0 +1,32 @@ +@echo off +echo ======================================== +echo ATM Application Launcher +echo ======================================== +echo. +echo Starting ATM Application... +echo. + +REM Check if Java is installed +java -version >nul 2>&1 +if %errorlevel% neq 0 ( + echo ERROR: Java is not installed or not in PATH + echo Please install Java and try again + pause + exit /b 1 +) + +REM Check if the JAR file exists +if not exist "target\cstatm-mte-0.0.1-SNAPSHOT-jar-with-dependencies.jar" ( + echo ERROR: JAR file not found + echo Please run 'mvn clean package' first + pause + exit /b 1 +) + +REM Run the application +echo Launching GUI... +java -jar target\cstatm-mte-0.0.1-SNAPSHOT-jar-with-dependencies.jar + +echo. +echo Application closed. +pause \ No newline at end of file diff --git a/期中考试指南.md b/期中考试指南.md new file mode 100644 index 0000000..9558b65 --- /dev/null +++ b/期中考试指南.md @@ -0,0 +1,1042 @@ +# 银行ATM系统登录功能开发自动化流程指南手册 + +**工具链:EA+JDK+GIT+GITEA+JENKINS+SONARQUBE+TRAE+MAVEN+GRADLE+SQLITE/PG** + +## 一、手册目标 +以"银行ATM系统"为例,详细说明从**模型设计→代码开发→质量检测→自动发布**的全流程自动化步骤,基于工具链:EA(建模)、TRAE(开发)、Git/Gitea(代码管理)、SonarQube(质量检测)、Jenkins(持续集成),最终将成果同步至头歌仓库`release`分支。 + + +## 二、环境准备清单 +| 工具/环境 | 版本/配置要求 | 安装验证命令 | +|------------------|-------------------------------|-------------------------------| +| JDK | 21 | `java -version` | +| Git | 最新版 | `git --version` | +| Gitea | 本地部署(SQLite数据库) | 访问`http://localhost:3000` | +| Jenkins | 本地部署(8080端口) | 访问`http://localhost:8080` | +| SonarQube | 社区版(9000端口) | 访问`http://localhost:9000` | +| TRAE | 最新版(绑定JDK 21) | 启动TRAE客户端 | +| 头歌仓库 | 地址:`https://bdgit.educoder.net/pu6zrsfoy/cstatm-mte.git`,分支`release` | 账号`602924803@qq.com`,密码`osgis123` | + + +## 三、详细流程步骤 + +### 阶段1:登录功能模型设计(EA) +**目标**:通过UML建模明确登录功能的需求、交互流程和类结构。 + +1. **需求图设计** + - 创建"需求图":银行ATM系统登录功能需求+验收测试 + - 需求层次结构(3个以上层次表达): + ``` + ① 顾客登录验证(最高优先级) + ├─ ①.1 顾客以编号和密码登录,显示成功或失败结果 + │ ├─ 成功:显示顾客姓名和性别(男为先生,女为女士) + │ └─ 失败:显示具体失败原因 + └─ ①.2 账号密码规则验证(最高优先级) + └─ 账号密码均为[6-12]位随机数字(可取6和12位) + + ② 安全控制需求(高优先级) + ├─ ②.1 错误次数限制 + │ └─ 账号或密码错误输入5次,锁定账号1个工作日(测试时可改为1分钟) + └─ ②.2 每日登录次数限制 + └─ 顾客每日限5次登录成功,否则提示拒绝登录和服务等待 + + ③ 系统性能需求(中等优先级) + └─ ③.1 响应时间要求 + └─ 登录验证响应时间不超过3秒 + + ④ 用户体验需求(最低优先级) + ├─ ④.1 界面友好性 + │ └─ 提供清晰的登录指引和错误提示 + └─ ④.2 多语言支持 + └─ 支持中英文切换 + ``` + - 需求关系表示(聚合关系符号): + - ①、②需求采用聚合关系(空心菱形实线)连接 + - 需求间存在依赖关系:①→②→③→④ + - 验收测试状态(必须显示Pass): + ``` + 验收测试1:正常登录流程 ✓ Pass + 验收测试2:错误密码处理 ✓ Pass + 验收测试3:账号锁定机制 ✓ Pass + 验收测试4:每日次数限制 ✓ Pass + 验收测试5:密码格式验证 ✓ Pass + ``` + - 优先级标识: + - ①、②需求:最高优先级(红色标记) + - ③需求:中等优先级(黄色标记) + - ④需求:最低优先级(绿色标记) + +2. **用例图设计** + - 打开EA,新建项目`cstatm-mte.eap`,创建"用例图": + - 参与者(Actor):`用户`、`管理员` + - 用例(Use Case):`登录系统`(包含"输入账号密码""验证身份""登录成功/失败反馈") + - 关系:`用户`和`管理员`均关联`登录系统`用例。 + +3. **活动图/时序图设计** + - 创建"活动图":登录功能控制流程 + ``` + 开始 + ├─ 显示登录界面(参数:界面标题="银行ATM系统登录") + ├─ 用户输入账号密码(参数:账号长度=[6-12],密码长度=[6-12]) + ├─ 验证输入格式(参数:正则表达式="^[0-9]{6,12}$") + │ ├─ 格式错误→显示错误信息(参数:错误代码=1001,提示="账号或密码格式不正确") + │ └─ 格式正确→继续验证 + ├─ 查询用户信息(参数:SQL="SELECT * FROM users WHERE account=?") + │ ├─ 用户不存在→显示错误信息(参数:错误代码=1002,提示="账号不存在") + │ └─ 用户存在→继续验证 + ├─ 验证密码(参数:加密方式=BCrypt) + │ ├─ 密码错误→记录错误次数(参数:maxErrorCount=5) + │ │ ├─ 错误次数<5→显示错误信息(参数:错误代码=1003,提示="账号或密码错误") + │ │ └─ 错误次数=5→锁定账号(参数:锁定时间=1分钟,提示="账号已锁定,请稍后再试") + │ └─ 密码正确→继续验证 + ├─ 检查账号状态(参数:状态字段=account_status) + │ ├─ 账号已锁定→显示错误信息(参数:错误代码=1004,提示="账号已锁定,请联系管理员") + │ └─ 账号正常→继续验证 + ├─ 检查每日登录次数(参数:maxDailyLogins=5) + │ ├─ 次数已达上限→显示错误信息(参数:错误代码=1005,提示="今日登录次数已达上限,请明天再试") + │ └─ 次数未达上限→继续验证 + ├─ 登录成功→显示用户信息(参数:姓名,性别称谓,欢迎信息) + └─ 结束 + ``` + - 创建"时序图":登录功能交互流程(片段表达5条路径) + ```text + 片段1:基本路径(登录成功) + 用户→登录界面:输入账号密码(account="123456", password="654321") + 登录界面→登录服务:验证登录请求(account="123456", password="654321") + 登录服务→数据库:查询用户信息(SQL="SELECT * FROM users WHERE account='123456'") + 数据库→登录服务:返回用户信息(id=1, account="123456", password="$2a$10$...", name="张三", gender="男") + 登录服务→登录界面:返回成功结果(code=200, message="登录成功", data={name:"张三", title:"先生"}) + 登录界面→用户:显示欢迎信息("欢迎您,张三先生") + + 片段2:备选路径1(账号或密码错误) + 用户→登录界面:输入账号密码(account="123456", password="wrong") + 登录界面→登录服务:验证登录请求(account="123456", password="wrong") + 登录服务→数据库:查询用户信息(SQL="SELECT * FROM users WHERE account='123456'") + 数据库→登录服务:返回用户信息(id=1, account="123456", password="$2a$10$...") + 登录服务→登录界面:返回失败结果(code=401, message="账号或密码错误") + 登录界面→用户:显示错误信息("账号或密码错误") + + 片段3:备选路径2(失败5次被锁) + [重复片段2操作5次] + 登录服务→数据库:更新账号状态(SQL="UPDATE users SET account_status='locked', lock_time=NOW() WHERE account='123456'") + 登录服务→登录界面:返回锁定结果(code=403, message="账号已锁定,请1分钟后再试") + 登录界面→用户:显示锁定信息("账号已锁定,请1分钟后再试") + + 片段4:备选路径3(登录5次被拒) + [已成功登录5次] + 用户→登录界面:输入账号密码(account="123456", password="654321") + 登录界面→登录服务:验证登录请求(account="123456", password="654321") + 登录服务→数据库:查询今日登录次数(SQL="SELECT COUNT(*) FROM login_logs WHERE account='123456' AND DATE(create_time)=CURDATE()") + 数据库→登录服务:返回登录次数(count=5) + 登录服务→登录界面:返回拒绝结果(code=403, message="今日登录次数已达上限,请明天再试") + 登录界面→用户:显示拒绝信息("今日登录次数已达上限,请明天再试") + + 片段5:异常路径(网络或数据库故障) + 用户→登录界面:输入账号密码(account="123456", password="654321") + 登录界面→登录服务:验证登录请求(account="123456", password="654321") + 登录服务→数据库:查询用户信息(SQL="SELECT * FROM users WHERE account='123456'") + 数据库→登录服务:返回异常(exception="ConnectionTimeoutException") + 登录服务→登录界面:返回系统错误结果(code=500, message="系统繁忙,请稍后再试") + 登录界面→用户:显示系统错误("系统繁忙,请稍后再试") + ``` + +4. **系统测试设计** + - 创建"系统测试模型图":登录功能测试用例 + ```text + 测试套件:银行ATM系统登录功能测试 + + 测试用例1:基本路径(登录成功) ✓ Pass + ├─ 测试步骤: + │ 1. 输入有效账号(123456)和密码(654321) + │ 2. 点击登录按钮 + │ 3. 验证显示欢迎信息("欢迎您,张三先生") + ├─ 测试数据:account="123456", password="654321" + ├─ 预期结果:code=200, message="登录成功", data={name:"张三", title:"先生"} + └─ 测试状态:Pass + + 测试用例2:备选路径1(账号或密码错误) ✓ Pass + ├─ 测试步骤: + │ 1. 输入有效账号(123456)和错误密码(wrongpass) + │ 2. 点击登录按钮 + │ 3. 验证显示错误信息("账号或密码错误") + ├─ 测试数据:account="123456", password="wrongpass" + ├─ 预期结果:code=401, message="账号或密码错误" + └─ 测试状态:Pass + + 测试用例3:备选路径2(失败5次被锁) ✓ Pass + ├─ 测试步骤: + │ 1. 连续5次输入有效账号(123456)和错误密码(wrongpass) + │ 2. 第6次输入正确密码(654321) + │ 3. 验证显示锁定信息("账号已锁定,请1分钟后再试") + ├─ 测试数据:account="123456", password="wrongpass"[×5], "654321" + ├─ 预期结果:code=403, message="账号已锁定,请1分钟后再试" + └─ 测试状态:Pass + + 测试用例4:备选路径3(登录5次被拒) ✓ Pass + ├─ 测试步骤: + │ 1. 成功登录5次 + │ 2. 第6次尝试登录 + │ 3. 验证显示拒绝信息("今日登录次数已达上限,请明天再试") + ├─ 测试数据:account="123456", password="654321"[×6] + ├─ 预期结果:code=403, message="今日登录次数已达上限,请明天再试" + └─ 测试状态:Pass + + 测试用例5:异常路径(网络或数据库故障) ✓ Pass + ├─ 测试步骤: + │ 1. 断开数据库连接 + │ 2. 输入有效账号(123456)和密码(654321) + │ 3. 点击登录按钮 + │ 4. 验证显示系统错误信息("系统繁忙,请稍后再试") + ├─ 测试数据:account="123456", password="654321", 数据库状态=断开 + ├─ 预期结果:code=500, message="系统繁忙,请稍后再试" + └─ 测试状态:Pass + + 测试覆盖率:100%(所有路径已覆盖) + 测试通过率:100%(5/5 Pass) + ``` + - EA测试管理配置: + - 创建测试计划:登录功能测试计划 + - 关联需求:①顾客登录验证、②安全控制需求 + - 测试执行状态:全部Pass(与EA测试管理一致) + +5. **序列图设计** + - 创建"序列图":`登录流程` + - 参与者:`用户`、`登录页面`、`后端服务`、`数据库` + - 交互流程: + 1. 用户→登录页面:输入账号(`username`)、密码(`password`) + 2. 登录页面→后端服务:发送登录请求(`/api/login`) + 3. 后端服务→数据库:查询用户信息(`SELECT * FROM user WHERE username = ?`) + 4. 数据库→后端服务:返回用户数据(含加密密码) + 5. 后端服务→登录页面:返回登录结果(成功/失败) + 6. 登录页面→用户:显示反馈(跳转主页/错误提示)。 + +6. **低阶时序图设计(行为模型)** + - 创建"低阶时序图":`CustomerLoginSequence` + - 基于类图的协作对象: + - UI类:`LoginFrame(JFrame)`, `JButton`, `JTextField`, `JPasswordField`, `JOptionPane` + - JDBC类:`Class`, `DriverManager`, `Connection`, `PreparedStatement`, `ResultSet` + - 业务类:`LoginController`, `UserService`, `User` + - 对象生命周期: + - `LoginFrame`在登录流程开始时创建,结束时销毁(叉号标记) + - `Connection`在每次数据库操作时创建,使用后立即关闭(叉号标记) + - `PreparedStatement`和`ResultSet`在查询时创建,使用后立即关闭(叉号标记) + - 消息签名(全英文): + - `LoginFrame -> JButton: addActionListener(this)` + - `JButton -> LoginFrame: actionPerformed(ActionEvent e)` + - `LoginFrame -> JTextField: getText(): String username` + - `LoginFrame -> JPasswordField: getPassword(): char[] password` + - `LoginFrame -> LoginController: login(String username, char[] password): LoginResult` + - `LoginController -> UserService: authenticateUser(String username, String password): User user` + - `UserService -> Class: forName("org.postgresql.Driver"): Class` + - `UserService -> DriverManager: getConnection(String url, String user, String pass): Connection conn` + - `UserService -> Connection: prepareStatement(String sql): PreparedStatement stmt` + - `UserService -> PreparedStatement: setString(int index, String value): void` + - `UserService -> PreparedStatement: executeQuery(): ResultSet rs` + - `UserService -> ResultSet: next(): boolean hasNext` + - `UserService -> ResultSet: getString(String column): String value` + - `UserService -> Connection: close(): void` + - `UserService -> PreparedStatement: close(): void` + - `UserService -> ResultSet: close(): void` + - 返回消息(虚线箭头): + - `JTextField --> LoginFrame: "123456"` + - `JPasswordField --> LoginFrame: "******"` + - `LoginController --> LoginFrame: LoginResult(SUCCESS, "Login successful")` + - `UserService --> LoginController: User(id, username, password, role)` + - 片段Fragment表达: + - **alt**分支(账号验证): + ```text + alt [username exists] + UserService -> PreparedStatement: setString(1, username) + UserService -> PreparedStatement: executeQuery() + else [username does not exist] + UserService -> LoginController: return null + LoginController -> LoginFrame: LoginResult(FAILED, "Invalid username") + end + ``` + - **loop**循环(密码尝试次数): + ```text + loop [attempts < 3] + UserService -> LoginController: User user + alt [password matches] + LoginController -> LoginFrame: LoginResult(SUCCESS, "Login successful") + else [password does not match] + LoginController -> LoginFrame: LoginResult(FAILED, "Invalid password") + LoginFrame -> JOptionPane: showMessageDialog("Invalid password. Attempts left: " + (3-attempts)) + end + end + ``` + - **opt**选项(记住登录状态): + ```text + opt [rememberMe is checked] + LoginController -> UserService: createSessionToken(User user): String token + LoginController -> LoginFrame: LoginResult(SUCCESS, "Login successful", token) + end + ``` + - EA时序图设置: + - 字体大小:消息文本12pt,对象名称14pt + - 生命线颜色:UI对象蓝色,业务对象绿色,数据库对象橙色 + - 消息箭头:同步消息实线,返回消息虚线,异步消息开放式箭头 + - 片段区域:alt/loop/opt使用不同背景色区分,确保清晰可读 + +7. **类图设计** + - 从需求、用例模型分析识别所有类: + - **界面类**(包:`org.atm.ldl.view.gui`): + - `LoginFrame`类:用户登录界面 + - `MainPanel`类:登录成功后主界面 + - **控制类**(包:`org.atm.ldl.ctrl`): + - `LoginController`类:处理登录逻辑 + - `MainController`类:处理主界面操作 + - **数据模型类**(包:`org.atm.ldl.model`): + - `User`类:用户数据模型 + - `LoginResult`类:登录结果封装 + - **数据访问类**(包:`org.atm.ldl.dao`): + - `UserDao`类:用户数据访问对象 + - `DatabaseUtil`类:数据库连接工具 + - **服务类**(包:`org.atm.ldl.service`): + - `UserService`类:用户业务服务 + - `PasswordService`类:密码处理服务 + + - **类间关系**: + - `LoginFrame` → `LoginController`:依赖关系 + - `LoginController` → `UserService`:依赖关系 + - `LoginController` → `LoginFrame`:关联关系(1对1) + - `LoginController` → `MainPanel`:关联关系(1对1) + - `UserService` → `UserDao`:依赖关系 + - `UserService` → `PasswordService`:依赖关系 + - `UserDao` → `User`:依赖关系 + - `UserDao` → `DatabaseUtil`:依赖关系 + - `LoginResult` → `User`:关联关系(1对1) + - `MainController` → `MainPanel`:关联关系(1对1) + - `MainController` → `User`:关联关系(1对1) + + - **UML类图设计**(全英文,宏观表达): + ```text + +----------------+ +-----------------+ + | LoginFrame |<>-----| LoginController | + +----------------+ +-----------------+ + | | + | uses | uses + | | + +----------------+ +-----------------+ + | MainPanel | | UserService | + +----------------+ +-----------------+ + | + | uses + | + +----------------+ +-----------------+ + | MainController| | UserDao | + +----------------+ +-----------------+ + | | + | uses | uses + | | + +----------------+ +-----------------+ + | User | | DatabaseUtil | + +----------------+ +-----------------+ + | + | uses + | + +----------------+ + | LoginResult | + +----------------+ + + +----------------+ + | PasswordService| + +----------------+ + ``` + + - **包图设计**(分层架构): + ```text + +------------------------+ + | org.atm.ldl | + +------------------------+ + | +----------------------+ | + | | view.gui | | + | |----------------------| | + | | LoginFrame | | + | | MainPanel | | + | +----------------------+ | + | +----------------------+ | + | | ctrl | | + | |----------------------| | + | | LoginController | | + | | MainController | | + | +----------------------+ | + | +----------------------+ | + | | model | | + | |----------------------| | + | | User | | + | | LoginResult | | + | +----------------------+ | + | +----------------------+ | + | | dao | | + | |----------------------| | + | | UserDao | | + | | DatabaseUtil | | + | +----------------------+ | + | +----------------------+ | + | | service | | + | |----------------------| | + | | UserService | | + | | PasswordService | | + | +----------------------+ | + +------------------------+ + ``` + +8. **提交模型至Gitea** + - 本地初始化仓库并关联Gitea: + ```bash + mkdir cstatm-mte && cd cstatm-mte + git init + git remote add origin http://localhost:3000/gitea/cstatm-mte.git # 替换为你的Gitea仓库地址 + ``` + - 提交EA模型文件: + ```bash + git add cstatm-mte.eap + git commit -m "EA建模:登录功能模型图" + git push -u origin main + ``` + + +### 阶段2:登录功能代码开发(TRAE + Git) +**目标**:基于EA模型,使用TRAE生成并开发登录功能代码,提交至Gitea。 + +1. **TRAE关联Gitea仓库** + - 打开TRAE客户端,选择"克隆仓库",输入Gitea仓库地址`http://localhost:3000/gitea/cstatm-mte.git`,克隆至本地目录(如`D:\cstatm-mte`)。 + +2. **TRAE生成Spring Boot项目** + - 在TRAE中新建"Spring Boot项目",技术栈选择: + - **依赖配置**: + - `Spring Web`:提供RESTful API框架 + - `Spring Data JPA`:简化数据访问层开发 + - `Spring Security`:提供密码加密功能 + - `SQLite JDBC`:轻量级数据库驱动(开发环境) + - `Spring Boot DevTools`:热部署支持 + - **数据库配置**:开发环境用SQLite(`application-dev.yml`配置) + - **项目结构生成**:标准Maven项目结构 + ```text + cstatm-mte/ + ├── src/ + │ ├── main/ + │ │ ├── java/ + │ │ │ └── org/atm/ldl/ + │ │ │ ├── view/gui/ # 界面类 + │ │ │ ├── ctrl/ # 控制类 + │ │ │ ├── model/ # 数据模型类 + │ │ │ ├── dao/ # 数据访问类 + │ │ │ ├── service/ # 服务类 + │ │ │ └── config/ # 配置类 + │ │ └── resources/ + │ │ ├── application.yml # 主配置文件 + │ │ ├── application-dev.yml # 开发环境配置 + │ │ ├── application-prod.yml # 生产环境配置 + │ │ └── static/ # 静态资源 + │ └── test/ + │ └── java/ # 测试代码 + ├── pom.xml # Maven依赖配置 + └── README.md # 项目说明 + ``` + +3. **Maven依赖配置(pom.xml)** + - 基于EA类图生成核心类所需的依赖: + ```xml + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + + org.springframework.boot + spring-boot-starter-security + + + + + org.xerial + sqlite-jdbc + 3.41.2.1 + + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + + + org.springframework.boot + spring-boot-starter-test + test + + + ``` + +4. **基于EA模型生成核心类** + - 输入提示:"基于阶段1的EA类图设计,生成登录功能相关的类,包括界面类、控制类、数据模型类、数据访问类和服务类,确保与模型一致" + - TRAE自动生成代码后,手动补充配置类和启动类: + - `SecurityConfig`:密码加密配置 + - `AtmApplication`:Spring Boot启动类 + +5. **实现登录核心逻辑(与阶段1模型一致)** + - `UserService.authenticateUser()`:对应EA模型中的UserService类方法,查询数据库用户,通过BCrypt加密比对密码 + - `LoginController.login()`:对应EA模型中的LoginController类方法,接收JSON参数(`username`、`password`),调用`authenticateUser()`,返回登录结果 + - `UserDao.findByUsername()`:对应EA模型中的UserDao类方法,实现用户数据查询 + - `PasswordService.encodePassword()`:对应EA模型中的PasswordService类方法,实现密码加密 + +6. **Maven常用命令** + ```bash + # 编译项目 + mvn compile + + # 清理项目 + mvn clean + + # 运行测试 + mvn test + + # 构建项目(打包) + mvn package + + # 安装到本地仓库 + mvn install + + # 跳过测试打包 + mvn package -DskipTests + + # 运行Spring Boot应用 + mvn spring-boot:run + + # 生成项目报告 + mvn site + + # 查看依赖树 + mvn dependency:tree + ``` + +7. **本地测试** + - 启动Spring Boot应用:`mvn spring-boot:run` + - 通过Postman发送POST请求: + ```http + POST http://localhost:8080/api/login + Content-Type: application/json + { "username": "admin", "password": "123456" } + ``` + - 验证返回结果:登录成功返回`{"code":200,"token":"xxx"}`,失败返回`{"code":401,"msg":"账号或密码错误"}`。 + +8. **提交代码至Gitea** + ```bash + git add src/ pom.xml # 添加开发的Java代码和Maven配置 + git commit -m "开发登录功能:完成User实体、登录接口及验证逻辑,与EA模型一致" + git push origin main # 推送到Gitea主分支 + ``` + + +### 阶段3:持续集成与质量检测(Jenkins + SonarQube) +**目标**:通过Jenkins自动拉取代码、构建项目、执行SonarQube检测,确保代码质量。 + +#### 3.1 Jenkins环境配置 + +##### 3.1.1 Jenkins安装与配置 +1. **安装Jenkins** + - 下载Jenkins WAR包或使用Docker镜像 + - 启动Jenkins服务:`java -jar jenkins.war` + - 访问Jenkins控制台:`http://localhost:8080` + - 完成初始化向导,安装推荐插件 + +2. **全局工具配置** + - 进入"管理Jenkins→全局工具配置" + - 配置JDK:选择JDK 21,设置JAVA_HOME + - 配置Maven:选择Maven 3.9,设置MAVEN_HOME + - 配置Git:确保Git命令可用 + +3. **必要插件安装** + - 进入"管理Jenkins→插件管理" + - 安装以下必要插件: + - SonarQube Scanner + - Pipeline + - Git + - Email Extension + - Build Timestamp + - Workspace Cleanup + +##### 3.1.2 SonarQube服务器配置 +1. **配置SonarQube服务器** + - 进入"管理Jenkins→配置系统" + - 找到"SonarQube servers"部分 + - 添加SonarQube服务器: + - Name: SonarQube + - Server URL: http://localhost:9000 + - Server authentication token: 配置SonarQube访问令牌 + +2. **配置SonarQube Scanner** + - 进入"管理Jenkins→全局工具配置" + - 找到"SonarQube Scanner"部分 + - 安装并配置SonarQube Scanner + +#### 3.2 SonarQube质量门禁设置 + +##### 3.2.1 SonarQube服务器配置 +1. **安装SonarQube** + - 下载SonarQube Community Edition + - 启动SonarQube服务 + - 访问SonarQube控制台:`http://localhost:9000` + - 完成初始化设置 + +2. **创建项目** + - 登录SonarQube控制台 + - 创建新项目,设置项目密钥和名称 + - 生成访问令牌,用于Jenkins集成 + +##### 3.2.2 质量门禁规则配置 +1. **创建自定义质量门禁** + - 进入"质量门禁"页面 + - 创建新的质量门禁规则 + - 添加以下条件: + - 阻断性Bug数 = 0 + - 严重Bug数 ≤ 5 + - 代码重复率 < 5% + - 测试覆盖率 > 50% + - 可维护性评级 = A或B + - 可靠性评级 = A或B + - 安全性评级 = A或B + +2. **将质量门禁关联到项目** + - 进入项目设置页面 + - 选择"质量门禁" + - 选择创建的质量门禁规则 + +#### 3.3 Jenkins任务配置 + +##### 3.3.1 创建流水线任务 +1. **新建任务** + - 登录Jenkins控制台 + - 点击"新建任务" + - 输入任务名称: `cstatm-mte-ci` + - 选择"流水线"类型 + - 点击"确定" + +2. **配置任务** + - 在"流水线"部分: + - 选择"Pipeline script from SCM" + - SCM选择"Git" + - 仓库URL: `http://localhost:3000/your-name/cstatm-mte.git` + - 凭据: 选择Git访问凭据 + - 分支: `*/main` + - 脚本路径: `Jenkinsfile` + +##### 3.3.2 配置触发器 +1. **构建触发器设置** + - 在"构建触发器"部分: + - 勾选"Poll SCM" + - 设置调度表达式: `H/5 * * * *` (每5分钟检查一次) + - 勾选"GitHub hook trigger for GITScm polling" (如果使用GitHub) + +2. **Webhook配置** + - 在Gitea仓库设置中: + - 添加Webhook + - URL: `http://localhost:8080/github-webhook/` + - 选择触发事件: 推送事件 + +##### 3.3.3 凭证管理 +1. **Git访问凭据** + - 进入"凭据→全局凭据" + - 添加"Username with password"类型凭据 + - 用户名: Git用户名 + - 密码: Git密码或访问令牌 + - ID: `git-credentials` + +2. **SonarQube访问令牌** + - 进入"凭据→全局凭据" + - 添加"Secret text"类型凭据 + - Secret: SonarQube访问令牌 + - ID: `sonar-token` + +#### 3.4 SonarQube配置详解 + +##### 3.4.1 sonar-project.properties配置 +```properties +# 项目基本信息 +sonar.projectKey=cstatm-mte +sonar.projectName=ATM System +sonar.projectVersion=1.0.0 + +# 编码和语言配置 +sonar.sourceEncoding=UTF-8 +sonar.language=java + +# 源代码路径配置 +sonar.sources=src/main/java +sonar.java.binaries=target/classes +sonar.java.test.binaries=target/test-classes + +# 测试代码路径配置 +sonar.tests=src/test/java +sonar.inclusions=**/*.java + +# 代码覆盖率配置 +sonar.coverage.jacoco.xmlReportPaths=target/site/jacoco/jacoco.xml +sonar.java.coveragePlugin=jacoco + +# 测试报告配置 +sonar.junit.reportsPath=target/surefire-reports + +# SonarQube服务器配置 +sonar.host.url=http://localhost:9000 +sonar.login=your-sonar-token + +# 排除文件配置 +sonar.exclusions=**/generated/**,**/model/**,**/*.sql,**/*.xml,**/*.properties +sonar.test.exclusions=**/generated/**,**/model/** + +# 代码复杂度阈值配置 +sonar.squid.complexity.file.threshold=200 +sonar.squid.complexity.function.threshold=20 +sonar.squid.complexity.class.threshold=100 + +# 重复代码阈值配置 +sonar.cpd.exclusions=**/*Test.java,**/generated/**,**/model/** +sonar.cpd.minimum.tokens=50 + +# 高级配置选项 +sonar.security.hotspots.enabled=true +sonar.java.source=17 +sonar.java.target=17 +sonar.scanner.forceUpdate=true +sonar.scm.revision=${env.GIT_COMMIT} +sonar.scm.branch=${env.GIT_BRANCH} +``` + +##### 3.4.2 质量配置文件设置 +1. **创建质量配置文件** + - 登录SonarQube控制台 + - 进入"质量配置"页面 + - 创建新的质量配置文件,基于"Sonar way"配置 + - 设置配置文件名称为"ATM System Quality Profile" + +2. **配置规则集** + - 添加以下规则集: + - Common Java + - SonarAnalyzer for Java + - Security Hotspots for Java + - 自定义规则阈值: + - 代码复杂度: 函数复杂度不超过10 + - 代码重复率: 不超过5% + - 方法长度: 不超过50行 + - 类长度: 不超过500行 + +#### 3.5 执行流程 + +##### 3.5.1 构建触发 +1. **手动触发** + - 登录Jenkins控制台 + - 进入任务页面 + - 点击"立即构建" + +2. **自动触发** + - 代码提交到Gitea仓库 + - Webhook通知Jenkins + - Jenkins自动启动构建 + +##### 3.5.2 流水线执行 +1. **拉取代码阶段** + - 清理工作空间 + - 从Git仓库拉取最新代码 + - 显示提交信息 + +2. **代码质量检测阶段** + - 执行SonarQube扫描 + - 等待质量门禁结果 + - 如果质量门禁不通过,构建失败 + +3. **编译构建阶段** + - 执行Maven编译 + - 检查编译结果 + +4. **运行测试阶段** + - 执行单元测试 + - 发布测试报告 + - 发布代码覆盖率报告 + +5. **打包应用阶段** + - 执行Maven打包 + - 归档构建产物 + +6. **生成数字签名阶段** + - 生成密钥库 + - 对JAR文件进行签名 + - 验证签名 + +7. **生成安装包阶段** + - 使用jpackage生成安装包 + - 生成Windows安装包 + - 生成MSI安装包 + +8. **创建WAR包阶段** + - 执行Maven WAR打包 + - 归档WAR包 + +9. **部署应用阶段** + - 停止Tomcat服务 + - 备份旧版本 + - 部署新版本 + - 启动Tomcat服务 + - 验证部署 + +10. **发送通知阶段** + - 构建通知内容 + - 发送邮件通知 + +##### 3.5.3 报告生成 +1. **测试报告** + - 访问路径: `${BUILD_URL}testReport/` + - 包含测试用例执行结果 + - 显示失败测试的详细信息 + +2. **代码覆盖率报告** + - 访问路径: `${BUILD_URL}coverage/` + - 显示代码覆盖率百分比 + - 按包和类显示覆盖率详情 + +3. **SonarQube报告** + - 访问路径: `http://localhost:9000/dashboard?id=cstatm-mte` + - 显示代码质量分析结果 + - 包含Bug、漏洞、代码异味等信息 + +##### 3.5.4 部署和通知 +1. **部署流程** + - 自动部署到测试环境 + - 验证应用是否正常运行 + - 记录部署日志 + +2. **通知机制** + - 构建成功: 发送成功通知 + - 构建失败: 发送失败通知 + - 包含构建详情和报告链接 + +#### 3.6 常见问题及解决方案 + +##### 3.6.1 Jenkins问题 +1. **构建失败: 找不到Maven** + - 检查Maven安装路径 + - 确认全局工具配置中的Maven设置 + - 验证MAVEN_HOME环境变量 + +2. **构建失败: Git权限问题** + - 检查Git凭据配置 + - 确认仓库访问权限 + - 验证SSH密钥或用户名密码 + +3. **构建超时** + - 增加构建超时时间 + - 优化构建脚本 + - 检查网络连接 + +##### 3.6.2 SonarQube问题 +1. **分析失败: 连接SonarQube服务器失败** + - 检查SonarQube服务器状态 + - 验证服务器URL配置 + - 确认访问令牌有效性 + +2. **质量门禁不通过** + - 查看详细质量报告 + - 修复代码质量问题 + - 调整质量门禁规则 + +3. **代码覆盖率报告缺失** + - 检查JaCoCo配置 + - 确认测试执行 + - 验证报告路径设置 + +##### 3.6.3 集成问题 +1. **Jenkins与SonarQube集成失败** + - 检查SonarQube插件安装 + - 验证服务器配置 + - 确认凭据设置 + +2. **部署失败** + - 检查目标服务器状态 + - 验证部署脚本 + - 确认权限设置 + +3. **通知发送失败** + - 检查邮件服务器配置 + - 验证收件人地址 + - 确认SMTP设置 + +#### 3.7 最佳实践 + +##### 3.7.1 Jenkins最佳实践 +1. **流水线设计** + - 保持流水线简洁明了 + - 使用共享库减少重复代码 + - 实现错误处理和重试机制 + +2. **资源管理** + - 定期清理旧构建 + - 限制并发构建数量 + - 使用资源锁避免冲突 + +3. **安全性** + - 使用凭据管理敏感信息 + - 限制脚本执行权限 + - 定期更新插件和系统 + +##### 3.7.2 SonarQube最佳实践 +1. **质量配置** + - 根据项目特点定制质量规则 + - 定期审查和更新质量门禁 + - 使用增量分析提高效率 + +2. **代码质量** + - 及时修复代码问题 + - 定期审查代码质量报告 + - 建立代码质量文化 + +3. **团队协作** + - 集成代码审查流程 + - 设置质量责任人 + - 定期分享质量改进经验 + +##### 3.7.3 集成最佳实践 +1. **CI/CD流程** + - 实现快速反馈机制 + - 自动化测试和部署 + - 监控整个流程 + +2. **版本管理** + - 使用语义化版本 + - 维护清晰的变更日志 + - 实现版本回滚机制 + +3. **监控和告警** + - 设置关键指标监控 + - 配置合理的告警规则 + - 建立应急响应流程 + +#### 3.8 总结 +通过Jenkins和SonarQube的集成,我们实现了银行ATM系统的持续集成与质量检测流程。这个流程包括代码拉取、质量检测、编译构建、测试执行、应用打包、数字签名、安装包生成、WAR包创建、应用部署和通知发送等完整环节。 + +这种自动化流程不仅提高了开发效率,还确保了代码质量和系统稳定性。通过质量门禁机制,我们可以在代码合并前发现并解决潜在问题,减少了生产环境中的故障风险。 + +同时,通过详细的报告和通知机制,团队成员可以及时了解构建状态和代码质量情况,促进了团队协作和持续改进。 + + +### 阶段4:自动发布至头歌仓库(Jenkins + 头歌) +**目标**:质检通过后,通过Jenkins自动将源码和制品推送至头歌`release`分支。 + +1. **配置头歌仓库凭据** + - 在Jenkins"凭据→全局凭据"中添加"Username with password": + - 用户名:`602924803@qq.com` + - 密码:`osgis123` + - ID:`educoder-cred` + +2. **扩展Jenkinsfile(添加发布步骤)** + 修改`Jenkinsfile`,在"构建项目"阶段后添加发布步骤: + ```groovy + stage('推送源码至头歌release分支') { + steps { + script { + def eduRepo = "https://bdgit.educoder.net/pu6zrsfoy/cstatm-mte.git" + def eduBranch = "release" + // 配置Git用户 + sh 'git config --global user.name "Jenkins"' + sh 'git config --global user.email "602924803@qq.com"' + // 关联头歌仓库并推送源码 + sh "git remote add educoder ${eduRepo} || true" + sh "git pull educoder ${eduBranch} --rebase" // 拉取最新代码避免冲突 + sh "git push https://${EDU_CRED_USR}:${EDU_CRED_PSW}@bdgit.educoder.net/pu6zrsfoy/cstatm-mte.git main:${eduBranch}" + } + } + } + stage('发布制品至头歌') { + steps { + script { + // 复制JAR包并提交 + sh 'cp target/*.jar ./' + sh 'git add *.jar' + sh 'git commit -m "发布登录功能制品:$(date +%Y%m%d)"' + sh "git push https://${EDU_CRED_USR}:${EDU_CRED_PSW}@bdgit.educoder.net/pu6zrsfoy/cstatm-mte.git HEAD:${eduBranch}" + } + } + } + ``` + (注:`EDU_CRED_USR`和`EDU_CRED_PSW`通过`environment { EDU_CRED = credentials('educoder-cred') }`注入) + +3. **验证发布结果** + - 登录头歌仓库`https://bdgit.educoder.net/pu6zrsfoy/cstatm-mte.git`,切换至`release`分支 + - 确认源码文件(如`User.java`、`LoginController.java`)和制品JAR包已同步。 + + +## 四、常见问题与解决方法 +1. **Gitea推送失败**:检查仓库地址是否正确,本地Git用户配置是否与Gitea账号匹配 +2. **SonarQube检测不通过**:登录SonarQube查看具体问题(如未使用的变量、代码重复),在TRAE中优化代码后重新提交 +3. **Jenkins推送头歌失败**:检查凭据密码是否正确,头歌仓库`release`分支是否存在,手动执行`git pull`解决冲突 + + +## 五、GUI应用开发流程总结 +通过本次期中考试,我们完成了银行ATM系统的GUI应用开发,实现了从模型设计到自动发布的完整CI/CD流程。 + +### 5.1 完整开发流程回顾 + +#### 1. **模型设计阶段(EA建模)** +- **用例分析**:明确用户登录功能需求和场景 +- **序列图设计**:定义登录流程中GUI界面与后端的交互顺序 +- **类图设计**:设计用户实体、服务层和GUI界面组件的关系 +- **GUI组件设计**:规划登录界面布局和组件交互 +- **Gitea提交**:将设计模型文件提交到代码仓库 + +#### 2. **代码开发阶段(TRAE)** +- **项目结构搭建**:使用Maven创建标准目录结构 +- **依赖配置**:添加JavaFX/Swing和PostgreSQL JDBC依赖 +- **核心代码实现**: + - `User`实体类(用户信息模型) + - `DbUtil`数据库工具类(数据库连接管理) + - `UserService`服务类(业务逻辑处理) + - `LoginFrame`/`LoginController`(GUI界面实现) +- **本地测试**:验证GUI界面功能和数据库连接 +- **Gitea提交**:将源代码提交到远程仓库 + +#### 3. **质量检测阶段(Jenkins + SonarQube)** +- **Jenkins配置**:设置JDK、Maven和SonarQube连接 +- **代码风格检查**:执行CheckStyle规则验证 +- **代码质量分析**:SonarQube检测潜在Bug和代码异味 +- **单元测试**:验证核心功能模块 +- **构建打包**:生成可执行JAR文件 + +#### 4. **自动发布阶段(Jenkins + 头歌)** +- **凭据配置**:设置头歌仓库访问凭据 +- **源码推送**:将通过质量检测的代码推送到头歌release分支 +- **制品发布**:上传构建好的GUI应用JAR包 +- **文档生成**:创建应用发布说明文档 +- **验证部署**:确认应用可正常运行 + +### 5.2 关键成果 + +1. **完整的GUI应用**:实现了银行ATM系统的用户登录界面,包含用户交互、数据验证和数据库集成 + +2. **自动化CI/CD流水线**:通过Jenkins实现了代码拉取、质量检测、构建和发布的全自动流程 + +3. **代码质量保障**:利用SonarQube确保代码符合质量标准,减少潜在问题 + +4. **标准化发布流程**:建立了从开发到部署的标准化流程,提高开发效率和代码质量 + +### 5.3 学习要点 + +- **软件工程实践**:体验了从需求分析、设计、开发到部署的完整软件开发生命周期 +- **工具链集成**:学习了EA、Git、Jenkins、SonarQube等工具的协同工作方式 +- **GUI应用开发**:掌握了基于Java的图形用户界面开发技术 +- **持续集成思想**:理解了自动化构建、测试和部署的重要性 +- **代码质量管理**:学会使用工具检测和改进代码质量 + +通过这个完整的开发流程,我们不仅实现了登录功能,更重要的是掌握了现代软件工程的实践方法和工具使用,为后续更复杂的软件项目开发奠定了基础。 \ No newline at end of file