From 63c2e35108b7af7845076eb73a5412db8c7ae5aa Mon Sep 17 00:00:00 2001 From: hd Date: Fri, 15 May 2026 15:13:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E6=AC=A1=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 38 ++++++ pom.xml | 124 ++++++++++++++++++ sql/registration.sql | 116 ++++++++++++++++ .../registration/RegistrationApplication.java | 14 ++ .../config/GlobalExceptionHandler.java | 36 +++++ .../registration/config/Knife4jConfig.java | 39 ++++++ .../config/MybatisPlusConfig.java | 18 +++ .../registration/config/RedisConfig.java | 23 ++++ .../registration/config/TokenInterceptor.java | 49 +++++++ .../registration/config/WebMvcConfig.java | 36 +++++ .../controller/RegistrationController.java | 84 ++++++++++++ .../controller/SmsController.java | 48 +++++++ .../controller/UserController.java | 53 ++++++++ .../registration/dto/LoginRequest.java | 20 +++ .../registration/dto/LoginResponse.java | 25 ++++ .../registration/dto/RegistrationRequest.java | 62 +++++++++ .../dto/RegistrationUpdateRequest.java | 45 +++++++ .../example/registration/dto/SmsRequest.java | 18 +++ .../registration/dto/StatisticsResponse.java | 24 ++++ .../entity/TrainingRegistration.java | 90 +++++++++++++ .../com/example/registration/entity/User.java | 95 ++++++++++++++ .../mapper/TrainingRegistrationMapper.java | 28 ++++ .../registration/mapper/UserMapper.java | 14 ++ .../service/RegistrationService.java | 20 +++ .../registration/service/SmsService.java | 19 +++ .../registration/service/UserService.java | 9 ++ .../service/impl/RegistrationServiceImpl.java | 105 +++++++++++++++ .../service/impl/SmsServiceImpl.java | 114 ++++++++++++++++ .../service/impl/UserServiceImpl.java | 38 ++++++ .../registration/util/RedisTokenUtil.java | 45 +++++++ .../com/example/registration/util/Result.java | 45 +++++++ src/main/resources/application-dev.yml | 54 ++++++++ src/main/resources/application-pro.yml | 46 +++++++ src/main/resources/application-test.yml | 46 +++++++ src/main/resources/application.yml | 3 + src/main/resources/logback-spring.xml | 81 ++++++++++++ .../mapper/TrainingRegistrationMapper.xml | 4 + src/main/resources/mapper/UserMapper.xml | 4 + 38 files changed, 1732 insertions(+) create mode 100644 .gitignore create mode 100644 pom.xml create mode 100644 sql/registration.sql create mode 100644 src/main/java/com/example/registration/RegistrationApplication.java create mode 100644 src/main/java/com/example/registration/config/GlobalExceptionHandler.java create mode 100644 src/main/java/com/example/registration/config/Knife4jConfig.java create mode 100644 src/main/java/com/example/registration/config/MybatisPlusConfig.java create mode 100644 src/main/java/com/example/registration/config/RedisConfig.java create mode 100644 src/main/java/com/example/registration/config/TokenInterceptor.java create mode 100644 src/main/java/com/example/registration/config/WebMvcConfig.java create mode 100644 src/main/java/com/example/registration/controller/RegistrationController.java create mode 100644 src/main/java/com/example/registration/controller/SmsController.java create mode 100644 src/main/java/com/example/registration/controller/UserController.java create mode 100644 src/main/java/com/example/registration/dto/LoginRequest.java create mode 100644 src/main/java/com/example/registration/dto/LoginResponse.java create mode 100644 src/main/java/com/example/registration/dto/RegistrationRequest.java create mode 100644 src/main/java/com/example/registration/dto/RegistrationUpdateRequest.java create mode 100644 src/main/java/com/example/registration/dto/SmsRequest.java create mode 100644 src/main/java/com/example/registration/dto/StatisticsResponse.java create mode 100644 src/main/java/com/example/registration/entity/TrainingRegistration.java create mode 100644 src/main/java/com/example/registration/entity/User.java create mode 100644 src/main/java/com/example/registration/mapper/TrainingRegistrationMapper.java create mode 100644 src/main/java/com/example/registration/mapper/UserMapper.java create mode 100644 src/main/java/com/example/registration/service/RegistrationService.java create mode 100644 src/main/java/com/example/registration/service/SmsService.java create mode 100644 src/main/java/com/example/registration/service/UserService.java create mode 100644 src/main/java/com/example/registration/service/impl/RegistrationServiceImpl.java create mode 100644 src/main/java/com/example/registration/service/impl/SmsServiceImpl.java create mode 100644 src/main/java/com/example/registration/service/impl/UserServiceImpl.java create mode 100644 src/main/java/com/example/registration/util/RedisTokenUtil.java create mode 100644 src/main/java/com/example/registration/util/Result.java create mode 100644 src/main/resources/application-dev.yml create mode 100644 src/main/resources/application-pro.yml create mode 100644 src/main/resources/application-test.yml create mode 100644 src/main/resources/application.yml create mode 100644 src/main/resources/logback-spring.xml create mode 100644 src/main/resources/mapper/TrainingRegistrationMapper.xml create mode 100644 src/main/resources/mapper/UserMapper.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91caaa5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store +/.logs/ +/.idea/ +.logs +.idea diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..5281a78 --- /dev/null +++ b/pom.xml @@ -0,0 +1,124 @@ + + + 4.0.0 + + + org.springframework.boot + spring-boot-starter-parent + 2.7.18 + + + + com.example + registration + 1.0.0 + jar + + registration + 培训报名系统 + + + 1.8 + 1.8 + 1.8 + UTF-8 + 3.5.5 + 4.3.0 + + + + + + org.springframework.boot + spring-boot-starter-web + + + + + mysql + mysql-connector-java + 8.0.33 + runtime + + + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + + + com.github.xiaoymin + knife4j-openapi2-spring-boot-starter + ${knife4j.version} + + + + + org.projectlombok + lombok + true + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + org.apache.commons + commons-pool2 + + + + + com.aliyun + dysmsapi20170525 + 3.0.0 + + + + + com.aliyun + tea-openapi + 0.3.9 + + + + + com.alibaba + fastjson + 2.0.43 + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + diff --git a/sql/registration.sql b/sql/registration.sql new file mode 100644 index 0000000..842a6d3 --- /dev/null +++ b/sql/registration.sql @@ -0,0 +1,116 @@ +/* + Navicat Premium Dump SQL + + Source Server : JavaProjects + Source Server Type : MySQL + Source Server Version : 80040 (8.0.40) + Source Host : localhost:3306 + Source Schema : registration + + Target Server Type : MySQL + Target Server Version : 80040 (8.0.40) + File Encoding : 65001 + + Date: 15/05/2026 10:33:13 +*/ + +SET NAMES utf8mb4; +SET FOREIGN_KEY_CHECKS = 0; + +-- ---------------------------- +-- Table structure for training_registration +-- ---------------------------- +DROP TABLE IF EXISTS `training_registration`; +CREATE TABLE `training_registration` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '自增主键ID', + `bm_id` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '报名编号,格式:BMYYYYMMxxxx', + `random_code` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '随机唯一编号(隐藏)', + `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '姓名(真实姓名)', + `company` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '所在单位(学校/企业全称)', + `department` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所在部门(院系/部门名称)', + `referrer` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '推荐人', + `referrer_company` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '推荐人单位', + `title` enum('教授','副教授','讲师','工程师','其他') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '职务/职称', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '手机号码', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '电子邮箱', + `id_card` varchar(18) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '身份证号', + `period` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '报名期次', + `fee` decimal(10, 2) NOT NULL DEFAULT 2500.00 COMMENT '培训费用(元)', + `remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '备注说明(饮食禁忌、特殊需求等)', + `sms_code` varchar(6) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '短信验证码', + `is_verified` tinyint(1) NULL DEFAULT 0 COMMENT '短信是否已验证:0-未验证,1-已验证', + `verification_time` datetime NULL DEFAULT NULL COMMENT '验证时间', + `status` enum('draft','submitted','reviewing','approved','rejected','paid','completed','cancelled') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'draft' COMMENT '状态:草稿/已提交/审核中/已通过/已拒绝/已支付/已完成/已取消', + `submit_time` datetime NULL DEFAULT NULL COMMENT '提交时间', + `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `create_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建IP', + `create_user_agent` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '创建用户代理', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `bm_id`(`bm_id` ASC) USING BTREE, + UNIQUE INDEX `random_code`(`random_code` ASC) USING BTREE, + INDEX `idx_bm_id`(`bm_id` ASC) USING BTREE, + INDEX `idx_phone`(`phone` ASC) USING BTREE, + INDEX `idx_email`(`email` ASC) USING BTREE, + INDEX `idx_id_card`(`id_card` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_create_time`(`create_time` ASC) USING BTREE, + INDEX `idx_period`(`period` ASC) USING BTREE, + INDEX `idx_company`(`company`(20) ASC) USING BTREE, + INDEX `idx_random_code`(`random_code` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 13 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '培训报名表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of training_registration +-- ---------------------------- + +-- ---------------------------- +-- Table structure for user +-- ---------------------------- +DROP TABLE IF EXISTS `user`; +CREATE TABLE `user` ( + `id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID', + `username` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '用户名(唯一)', + `email` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '邮箱(唯一)', + `phone` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '手机号(唯一,可选)', + `password_hash` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码哈希值', + `password_salt` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '密码盐值', + `real_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '真实姓名', + `nickname` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '昵称', + `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '头像URL', + `gender` enum('male','female','unknown') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'unknown' COMMENT '性别:男/女/未知', + `birthday` date NULL DEFAULT NULL COMMENT '出生日期', + `status` enum('active','inactive','locked','deleted') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'active' COMMENT '账号状态:激活/未激活/锁定/已删除', + `email_verified` tinyint(1) NULL DEFAULT 0 COMMENT '邮箱是否已验证:0-未验证,1-已验证', + `phone_verified` tinyint(1) NULL DEFAULT 0 COMMENT '手机号是否已验证:0-未验证,1-已验证', + `last_login_ip` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '最后登录IP', + `last_login_time` datetime NULL DEFAULT NULL COMMENT '最后登录时间', + `login_count` int NULL DEFAULT 0 COMMENT '登录次数', + `failed_login_attempts` int NULL DEFAULT 0 COMMENT '连续登录失败次数', + `account_locked_until` datetime NULL DEFAULT NULL COMMENT '账号锁定至', + `password_changed_at` datetime NULL DEFAULT NULL COMMENT '密码最后修改时间', + `password_expires_at` datetime NULL DEFAULT NULL COMMENT '密码过期时间', + `role` enum('user','admin','super_admin') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT 'user' COMMENT '用户角色', + `created_at` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `updated_at` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + `deleted_at` datetime NULL DEFAULT NULL COMMENT '删除时间(软删除)', + PRIMARY KEY (`id`) USING BTREE, + UNIQUE INDEX `username`(`username` ASC) USING BTREE, + UNIQUE INDEX `email`(`email` ASC) USING BTREE, + UNIQUE INDEX `phone`(`phone` ASC) USING BTREE, + INDEX `idx_username`(`username` ASC) USING BTREE, + INDEX `idx_email`(`email` ASC) USING BTREE, + INDEX `idx_phone`(`phone` ASC) USING BTREE, + INDEX `idx_status`(`status` ASC) USING BTREE, + INDEX `idx_role`(`role` ASC) USING BTREE, + INDEX `idx_created`(`created_at` ASC) USING BTREE, + INDEX `idx_login_time`(`last_login_time` ASC) USING BTREE, + INDEX `idx_status_created`(`status` ASC, `created_at` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '用户表' ROW_FORMAT = Dynamic; + +-- ---------------------------- +-- Records of user +-- ---------------------------- +INSERT INTO `user` VALUES (1, 'admin', '1', '1', 'a3c2b756aae43be93802ed8dda4f9c06', '1234', NULL, NULL, NULL, 'unknown', NULL, 'active', 0, 0, NULL, '2026-05-15 10:03:43', 7, 0, NULL, NULL, NULL, 'super_admin', '2026-04-30 15:08:28', '2026-04-30 15:26:32', NULL); + +SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/java/com/example/registration/RegistrationApplication.java b/src/main/java/com/example/registration/RegistrationApplication.java new file mode 100644 index 0000000..087ab8a --- /dev/null +++ b/src/main/java/com/example/registration/RegistrationApplication.java @@ -0,0 +1,14 @@ +package com.example.registration; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +@MapperScan("com.example.registration.mapper") +public class RegistrationApplication { + + public static void main(String[] args) { + SpringApplication.run(RegistrationApplication.class, args); + } +} diff --git a/src/main/java/com/example/registration/config/GlobalExceptionHandler.java b/src/main/java/com/example/registration/config/GlobalExceptionHandler.java new file mode 100644 index 0000000..302bd71 --- /dev/null +++ b/src/main/java/com/example/registration/config/GlobalExceptionHandler.java @@ -0,0 +1,36 @@ +package com.example.registration.config; + +import com.example.registration.util.Result; +import org.springframework.validation.BindException; +import org.springframework.validation.FieldError; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +import javax.validation.ConstraintViolation; +import javax.validation.ConstraintViolationException; +import java.util.stream.Collectors; + +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(BindException.class) + public Result handleBindException(BindException e) { + String message = e.getFieldErrors().stream() + .map(FieldError::getDefaultMessage) + .collect(Collectors.joining(", ")); + return Result.error(message); + } + + @ExceptionHandler(ConstraintViolationException.class) + public Result handleConstraintViolationException(ConstraintViolationException e) { + String message = e.getConstraintViolations().stream() + .map(ConstraintViolation::getMessage) + .collect(Collectors.joining(", ")); + return Result.error(message); + } + + @ExceptionHandler(Exception.class) + public Result handleException(Exception e) { + return Result.error(e.getMessage()); + } +} diff --git a/src/main/java/com/example/registration/config/Knife4jConfig.java b/src/main/java/com/example/registration/config/Knife4jConfig.java new file mode 100644 index 0000000..0ac26cf --- /dev/null +++ b/src/main/java/com/example/registration/config/Knife4jConfig.java @@ -0,0 +1,39 @@ +package com.example.registration.config; + +import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import springfox.documentation.builders.ApiInfoBuilder; +import springfox.documentation.builders.PathSelectors; +import springfox.documentation.builders.RequestHandlerSelectors; +import springfox.documentation.service.ApiInfo; +import springfox.documentation.spi.DocumentationType; +import springfox.documentation.spring.web.plugins.Docket; +import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc; + +/** + * http://10.23.22.43:8099/doc.html + */ +@Configuration +@EnableSwagger2WebMvc +@EnableKnife4j +public class Knife4jConfig { + + @Bean + public Docket createRestApi() { + return new Docket(DocumentationType.SWAGGER_2) + .apiInfo(apiInfo()) + .select() + .apis(RequestHandlerSelectors.basePackage("com.example.registration.controller")) + .paths(PathSelectors.any()) + .build(); + } + + private ApiInfo apiInfo() { + return new ApiInfoBuilder() + .title("培训报名系统 API") + .description("培训报名系统接口文档") + .version("1.0.0") + .build(); + } +} diff --git a/src/main/java/com/example/registration/config/MybatisPlusConfig.java b/src/main/java/com/example/registration/config/MybatisPlusConfig.java new file mode 100644 index 0000000..5ba67c4 --- /dev/null +++ b/src/main/java/com/example/registration/config/MybatisPlusConfig.java @@ -0,0 +1,18 @@ +package com.example.registration.config; + +import com.baomidou.mybatisplus.annotation.DbType; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class MybatisPlusConfig { + + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); + return interceptor; + } +} diff --git a/src/main/java/com/example/registration/config/RedisConfig.java b/src/main/java/com/example/registration/config/RedisConfig.java new file mode 100644 index 0000000..427d228 --- /dev/null +++ b/src/main/java/com/example/registration/config/RedisConfig.java @@ -0,0 +1,23 @@ +package com.example.registration.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +@Configuration +public class RedisConfig { + + @Bean + public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { + RedisTemplate template = new RedisTemplate<>(); + template.setConnectionFactory(connectionFactory); + template.setKeySerializer(new StringRedisSerializer()); + template.setValueSerializer(new StringRedisSerializer()); + template.setHashKeySerializer(new StringRedisSerializer()); + template.setHashValueSerializer(new StringRedisSerializer()); + template.afterPropertiesSet(); + return template; + } +} diff --git a/src/main/java/com/example/registration/config/TokenInterceptor.java b/src/main/java/com/example/registration/config/TokenInterceptor.java new file mode 100644 index 0000000..ed9dcf3 --- /dev/null +++ b/src/main/java/com/example/registration/config/TokenInterceptor.java @@ -0,0 +1,49 @@ +package com.example.registration.config; + +import com.example.registration.util.RedisTokenUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.HashMap; +import java.util.Map; + +@Component +public class TokenInterceptor implements HandlerInterceptor { + + @Autowired + private RedisTokenUtil redisTokenUtil; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + // 放行 OPTIONS 预检请求 + if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { + return true; + } + String token = request.getHeader("token"); + if (token == null || token.isEmpty()) { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(401); + Map result = new HashMap<>(); + result.put("code", 401); + result.put("message", "token不能为空"); + response.getWriter().write(new ObjectMapper().writeValueAsString(result)); + return false; + } + if (!redisTokenUtil.validateToken(token)) { + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(401); + Map result = new HashMap<>(); + result.put("code", 401); + result.put("message", "token已过期或无效"); + response.getWriter().write(new ObjectMapper().writeValueAsString(result)); + return false; + } + // 刷新token过期时间 + redisTokenUtil.refreshToken(token); + return true; + } +} diff --git a/src/main/java/com/example/registration/config/WebMvcConfig.java b/src/main/java/com/example/registration/config/WebMvcConfig.java new file mode 100644 index 0000000..b0f7179 --- /dev/null +++ b/src/main/java/com/example/registration/config/WebMvcConfig.java @@ -0,0 +1,36 @@ +package com.example.registration.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Autowired + private TokenInterceptor tokenInterceptor; + + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(tokenInterceptor) + .addPathPatterns("/api/**") + .excludePathPatterns( + "/api/user/login", + "/api/registration/submit", + "/api/sms/send", + "/api/sms/verify" + ); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true) + .maxAge(3600); + } +} diff --git a/src/main/java/com/example/registration/controller/RegistrationController.java b/src/main/java/com/example/registration/controller/RegistrationController.java new file mode 100644 index 0000000..1c9bac4 --- /dev/null +++ b/src/main/java/com/example/registration/controller/RegistrationController.java @@ -0,0 +1,84 @@ +package com.example.registration.controller; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.example.registration.dto.RegistrationRequest; +import com.example.registration.dto.RegistrationUpdateRequest; +import com.example.registration.dto.StatisticsResponse; +import com.example.registration.entity.TrainingRegistration; +import com.example.registration.service.RegistrationService; +import com.example.registration.service.SmsService; +import com.example.registration.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import io.swagger.annotations.ApiParam; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +@RequestMapping("/api/registration") +@Api(tags = "报名管理") +public class RegistrationController { + + @Autowired + private RegistrationService registrationService; + + @Autowired + private SmsService smsService; + + @PostMapping("/submit") + @ApiOperation("提交报名表单") + public Result submit(@RequestBody @Validated RegistrationRequest request) { + // 验证短信验证码 + if (!smsService.verifyCode(request.getPhone(), request.getSmsCode())) { + return Result.error("短信验证码错误或已过期"); + } + + TrainingRegistration registration = new TrainingRegistration(); + BeanUtils.copyProperties(request, registration); + TrainingRegistration result = registrationService.submit(registration); + return Result.success(result); + } + + @GetMapping("/list") + @ApiOperation("报名列表") + public Result> list( + @RequestParam(defaultValue = "1") @ApiParam("页码") Integer pageNum, + @RequestParam(defaultValue = "10") @ApiParam("每页条数") Integer pageSize, + @RequestParam(required = false) @ApiParam("关键词") String keyword, + @RequestParam(required = false) @ApiParam("状态") String status, + @RequestParam(required = false) @ApiParam("期次") String period) { + Page page = new Page<>(pageNum, pageSize); + return Result.success(registrationService.listPage(page, keyword, status, period)); + } + + @PutMapping("/update/{randomCode}") + @ApiOperation("修改报名信息") + public Result update( + @PathVariable @ApiParam("随机唯一编号") String randomCode, + @RequestBody RegistrationUpdateRequest request) { + TrainingRegistration registration = new TrainingRegistration(); + BeanUtils.copyProperties(request, registration); + TrainingRegistration result = registrationService.updateByRandomCode(randomCode, registration); + if (result == null) { + return Result.error("报名信息不存在"); + } + return Result.success(result); + } + + @GetMapping("/statistics") + @ApiOperation("统计报名信息") + public Result statistics() { + Map map = registrationService.statistics(); + StatisticsResponse response = new StatisticsResponse(); + response.setTotalRegistrations((Long) map.get("totalRegistrations")); + response.setPaidRegistrations((Long) map.get("paidRegistrations")); + response.setPendingPaymentRegistrations((Long) map.get("pendingPaymentRegistrations")); + response.setPaidAmount((java.math.BigDecimal) map.get("paidAmount")); + return Result.success(response); + } +} diff --git a/src/main/java/com/example/registration/controller/SmsController.java b/src/main/java/com/example/registration/controller/SmsController.java new file mode 100644 index 0000000..aba4b67 --- /dev/null +++ b/src/main/java/com/example/registration/controller/SmsController.java @@ -0,0 +1,48 @@ +package com.example.registration.controller; + +import com.example.registration.dto.SmsRequest; +import com.example.registration.service.SmsService; +import com.example.registration.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/api/sms") +@Api(tags = "短信服务") +public class SmsController { + + @Autowired + private SmsService smsService; + + @PostMapping("/send") + @ApiOperation("发送短信验证码") + public Result> sendCode(@RequestBody @Validated SmsRequest request) { + boolean success = smsService.sendCode(request.getPhone()); + if (success) { + Map data = new HashMap<>(); + data.put("message", "验证码已发送,请在5分钟内完成验证"); + return Result.success(data); + } + return Result.error("验证码发送失败,请稍后重试"); + } + + @PostMapping("/verify") + @ApiOperation("验证短信验证码") + public Result> verifyCode(@RequestBody @Validated SmsRequest request, + @RequestParam String code) { + boolean success = smsService.verifyCode(request.getPhone(), code); + Map data = new HashMap<>(); + if (success) { + data.put("valid", true); + return Result.success(data); + } + data.put("valid", false); + return Result.error("验证码验证失败"); + } +} diff --git a/src/main/java/com/example/registration/controller/UserController.java b/src/main/java/com/example/registration/controller/UserController.java new file mode 100644 index 0000000..f35af6a --- /dev/null +++ b/src/main/java/com/example/registration/controller/UserController.java @@ -0,0 +1,53 @@ +package com.example.registration.controller; + +import com.example.registration.dto.LoginRequest; +import com.example.registration.dto.LoginResponse; +import com.example.registration.entity.User; +import com.example.registration.service.UserService; +import com.example.registration.util.RedisTokenUtil; +import com.example.registration.util.Result; +import io.swagger.annotations.Api; +import io.swagger.annotations.ApiOperation; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/user") +@Api(tags = "用户管理") +public class UserController { + + @Autowired + private UserService userService; + + @Autowired + private RedisTokenUtil redisTokenUtil; + + @PostMapping("/login") + @ApiOperation("用户登录") + public Result login(@RequestBody @Validated LoginRequest request) { + User user = userService.login(request.getUsername(), request.getPassword()); + if (user == null) { + return Result.error("用户名或密码错误"); + } + String token = UUID.randomUUID().toString().replace("-", ""); + redisTokenUtil.storeToken(token, String.valueOf(user.getId())); + + LoginResponse response = new LoginResponse(); + response.setUserId(user.getId()); + response.setUsername(user.getUsername()); + response.setRealName(user.getRealName()); + response.setRole(user.getRole()); + response.setToken(token); + return Result.success(response); + } + + @PostMapping("/logout") + @ApiOperation("退出登录") + public Result logout(@RequestHeader("token") String token) { + redisTokenUtil.deleteToken(token); + return Result.success(); + } +} diff --git a/src/main/java/com/example/registration/dto/LoginRequest.java b/src/main/java/com/example/registration/dto/LoginRequest.java new file mode 100644 index 0000000..d087ffd --- /dev/null +++ b/src/main/java/com/example/registration/dto/LoginRequest.java @@ -0,0 +1,20 @@ +package com.example.registration.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; + +@Data +@ApiModel("登录请求") +public class LoginRequest { + + @NotBlank(message = "用户名不能为空") + @ApiModelProperty("用户名") + private String username; + + @NotBlank(message = "密码不能为空") + @ApiModelProperty("密码") + private String password; +} diff --git a/src/main/java/com/example/registration/dto/LoginResponse.java b/src/main/java/com/example/registration/dto/LoginResponse.java new file mode 100644 index 0000000..f4bd026 --- /dev/null +++ b/src/main/java/com/example/registration/dto/LoginResponse.java @@ -0,0 +1,25 @@ +package com.example.registration.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("登录响应") +public class LoginResponse { + + @ApiModelProperty("用户ID") + private Long userId; + + @ApiModelProperty("用户名") + private String username; + + @ApiModelProperty("真实姓名") + private String realName; + + @ApiModelProperty("角色") + private String role; + + @ApiModelProperty("Token") + private String token; +} diff --git a/src/main/java/com/example/registration/dto/RegistrationRequest.java b/src/main/java/com/example/registration/dto/RegistrationRequest.java new file mode 100644 index 0000000..444801a --- /dev/null +++ b/src/main/java/com/example/registration/dto/RegistrationRequest.java @@ -0,0 +1,62 @@ +package com.example.registration.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.NotNull; +import java.math.BigDecimal; + +@Data +@ApiModel("提交报名表单请求") +public class RegistrationRequest { + + @NotBlank(message = "姓名不能为空") + @ApiModelProperty("姓名(真实姓名)") + private String name; + + @NotBlank(message = "所在单位不能为空") + @ApiModelProperty("所在单位(学校/企业全称)") + private String company; + + @ApiModelProperty("所在部门(院系/部门名称)") + private String department; + + @NotBlank(message = "职务/职称不能为空") + @ApiModelProperty("职务/职称") + private String title; + + @NotBlank(message = "手机号码不能为空") + @ApiModelProperty("手机号码") + private String phone; + + @NotBlank(message = "短信验证码不能为空") + @ApiModelProperty("短信验证码") + private String smsCode; + + @NotBlank(message = "电子邮箱不能为空") + @ApiModelProperty("电子邮箱") + private String email; + + @NotBlank(message = "身份证号不能为空") + @ApiModelProperty("身份证号") + private String idCard; + + @NotBlank(message = "报名期次不能为空") + @ApiModelProperty("报名期次") + private String period; + + @NotNull(message = "培训费用不能为空") + @ApiModelProperty("培训费用(元)") + private BigDecimal fee; + + @ApiModelProperty("备注说明(饮食禁忌、特殊需求等)") + private String remark; + + @ApiModelProperty("推荐人") + private String referrer; + + @ApiModelProperty("推荐人单位") + private String referrerCompany; +} diff --git a/src/main/java/com/example/registration/dto/RegistrationUpdateRequest.java b/src/main/java/com/example/registration/dto/RegistrationUpdateRequest.java new file mode 100644 index 0000000..bacbbba --- /dev/null +++ b/src/main/java/com/example/registration/dto/RegistrationUpdateRequest.java @@ -0,0 +1,45 @@ +package com.example.registration.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@ApiModel("修改报名信息请求") +public class RegistrationUpdateRequest { + + @ApiModelProperty("姓名(真实姓名)") + private String name; + + @ApiModelProperty("所在单位(学校/企业全称)") + private String company; + + @ApiModelProperty("所在部门(院系/部门名称)") + private String department; + + @ApiModelProperty("职务/职称") + private String title; + + @ApiModelProperty("手机号码") + private String phone; + + @ApiModelProperty("电子邮箱") + private String email; + + @ApiModelProperty("身份证号") + private String idCard; + + @ApiModelProperty("报名期次") + private String period; + + @ApiModelProperty("培训费用(元)") + private BigDecimal fee; + + @ApiModelProperty("备注说明(饮食禁忌、特殊需求等)") + private String remark; + + @ApiModelProperty("状态") + private String status; +} diff --git a/src/main/java/com/example/registration/dto/SmsRequest.java b/src/main/java/com/example/registration/dto/SmsRequest.java new file mode 100644 index 0000000..9e36582 --- /dev/null +++ b/src/main/java/com/example/registration/dto/SmsRequest.java @@ -0,0 +1,18 @@ +package com.example.registration.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import javax.validation.constraints.NotBlank; +import javax.validation.constraints.Pattern; + +@Data +@ApiModel("发送短信请求") +public class SmsRequest { + + @NotBlank(message = "手机号不能为空") + @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确") + @ApiModelProperty("手机号码") + private String phone; +} diff --git a/src/main/java/com/example/registration/dto/StatisticsResponse.java b/src/main/java/com/example/registration/dto/StatisticsResponse.java new file mode 100644 index 0000000..c143d77 --- /dev/null +++ b/src/main/java/com/example/registration/dto/StatisticsResponse.java @@ -0,0 +1,24 @@ +package com.example.registration.dto; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; + +@Data +@ApiModel("统计信息响应") +public class StatisticsResponse { + + @ApiModelProperty("报名人数") + private Long totalRegistrations; + + @ApiModelProperty("已支付人数") + private Long paidRegistrations; + + @ApiModelProperty("待支付人数") + private Long pendingPaymentRegistrations; + + @ApiModelProperty("已支付金额") + private BigDecimal paidAmount; +} diff --git a/src/main/java/com/example/registration/entity/TrainingRegistration.java b/src/main/java/com/example/registration/entity/TrainingRegistration.java new file mode 100644 index 0000000..68bad8a --- /dev/null +++ b/src/main/java/com/example/registration/entity/TrainingRegistration.java @@ -0,0 +1,90 @@ +package com.example.registration.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +@Data +@TableName("training_registration") +@ApiModel("培训报名表") +public class TrainingRegistration { + + @TableId(type = IdType.AUTO) + @ApiModelProperty("自增主键ID") + private Long id; + + @ApiModelProperty("报名编号") + private String bmId; + + @ApiModelProperty("随机唯一编号") + private String randomCode; + + @ApiModelProperty("姓名") + private String name; + + @ApiModelProperty("所在单位") + private String company; + + @ApiModelProperty("所在部门") + private String department; + + @ApiModelProperty("职务/职称") + private String title; + + @ApiModelProperty("手机号码") + private String phone; + + @ApiModelProperty("电子邮箱") + private String email; + + @ApiModelProperty("身份证号") + private String idCard; + + @ApiModelProperty("报名期次") + private String period; + + @ApiModelProperty("培训费用") + private BigDecimal fee; + + @ApiModelProperty("备注说明") + private String remark; + + @ApiModelProperty("短信验证码") + private String smsCode; + + @ApiModelProperty("短信是否已验证") + private Boolean isVerified; + + @ApiModelProperty("验证时间") + private LocalDateTime verificationTime; + + @ApiModelProperty("状态") + private String status; + + @ApiModelProperty("提交时间") + private LocalDateTime submitTime; + + @ApiModelProperty("创建时间") + private LocalDateTime createTime; + + @ApiModelProperty("更新时间") + private LocalDateTime updateTime; + + @ApiModelProperty("创建IP") + private String createIp; + + @ApiModelProperty("创建用户代理") + private String createUserAgent; + + @ApiModelProperty("推荐人") + private String referrer; + + @ApiModelProperty("推荐人单位") + private String referrerCompany; +} diff --git a/src/main/java/com/example/registration/entity/User.java b/src/main/java/com/example/registration/entity/User.java new file mode 100644 index 0000000..e46c295 --- /dev/null +++ b/src/main/java/com/example/registration/entity/User.java @@ -0,0 +1,95 @@ +package com.example.registration.entity; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.annotation.TableLogic; +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@TableName("user") +@ApiModel("用户") +public class User { + + @TableId(type = IdType.AUTO) + @ApiModelProperty("用户ID") + private Long id; + + @ApiModelProperty("用户名") + private String username; + + @ApiModelProperty("邮箱") + private String email; + + @ApiModelProperty("手机号") + private String phone; + + @ApiModelProperty("密码哈希值") + private String passwordHash; + + @ApiModelProperty("密码盐值") + private String passwordSalt; + + @ApiModelProperty("真实姓名") + private String realName; + + @ApiModelProperty("昵称") + private String nickname; + + @ApiModelProperty("头像URL") + private String avatar; + + @ApiModelProperty("性别") + private String gender; + + @ApiModelProperty("出生日期") + private LocalDate birthday; + + @ApiModelProperty("账号状态") + private String status; + + @ApiModelProperty("邮箱是否已验证") + private Boolean emailVerified; + + @ApiModelProperty("手机号是否已验证") + private Boolean phoneVerified; + + @ApiModelProperty("最后登录IP") + private String lastLoginIp; + + @ApiModelProperty("最后登录时间") + private LocalDateTime lastLoginTime; + + @ApiModelProperty("登录次数") + private Integer loginCount; + + @ApiModelProperty("连续登录失败次数") + private Integer failedLoginAttempts; + + @ApiModelProperty("账号锁定至") + private LocalDateTime accountLockedUntil; + + @ApiModelProperty("密码最后修改时间") + private LocalDateTime passwordChangedAt; + + @ApiModelProperty("密码过期时间") + private LocalDateTime passwordExpiresAt; + + @ApiModelProperty("用户角色") + private String role; + + @ApiModelProperty("创建时间") + private LocalDateTime createdAt; + + @ApiModelProperty("更新时间") + private LocalDateTime updatedAt; + + @ApiModelProperty("删除时间") + @TableLogic(value = "null", delval = "now()") + private LocalDateTime deletedAt; +} diff --git a/src/main/java/com/example/registration/mapper/TrainingRegistrationMapper.java b/src/main/java/com/example/registration/mapper/TrainingRegistrationMapper.java new file mode 100644 index 0000000..955a2d0 --- /dev/null +++ b/src/main/java/com/example/registration/mapper/TrainingRegistrationMapper.java @@ -0,0 +1,28 @@ +package com.example.registration.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.registration.entity.TrainingRegistration; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.math.BigDecimal; + +@Mapper +public interface TrainingRegistrationMapper extends BaseMapper { + + @Select("SELECT COUNT(*) FROM training_registration WHERE status != 'cancelled' AND status != 'draft'") + Long countTotalRegistrations(); + + @Select("SELECT COUNT(*) FROM training_registration WHERE status = 'paid'") + Long countPaidRegistrations(); + + @Select("SELECT COUNT(*) FROM training_registration WHERE status = 'submitted'") + Long countPendingPaymentRegistrations(); + + @Select("SELECT COALESCE(SUM(fee), 0) FROM training_registration WHERE status = 'paid'") + BigDecimal sumPaidAmount(); + + @Select("SELECT * FROM training_registration WHERE random_code = #{randomCode} LIMIT 1") + TrainingRegistration selectByRandomCode(@Param("randomCode") String randomCode); +} diff --git a/src/main/java/com/example/registration/mapper/UserMapper.java b/src/main/java/com/example/registration/mapper/UserMapper.java new file mode 100644 index 0000000..2e58fae --- /dev/null +++ b/src/main/java/com/example/registration/mapper/UserMapper.java @@ -0,0 +1,14 @@ +package com.example.registration.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.example.registration.entity.User; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +@Mapper +public interface UserMapper extends BaseMapper { + + @Select("SELECT * FROM user WHERE username = #{username} LIMIT 1") + User selectByUsername(@Param("username") String username); +} diff --git a/src/main/java/com/example/registration/service/RegistrationService.java b/src/main/java/com/example/registration/service/RegistrationService.java new file mode 100644 index 0000000..586301d --- /dev/null +++ b/src/main/java/com/example/registration/service/RegistrationService.java @@ -0,0 +1,20 @@ +package com.example.registration.service; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.registration.entity.TrainingRegistration; + +import java.math.BigDecimal; +import java.util.Map; + +public interface RegistrationService extends IService { + + TrainingRegistration submit(TrainingRegistration registration); + + IPage listPage(Page page, String keyword, String status, String period); + + TrainingRegistration updateByRandomCode(String randomCode, TrainingRegistration registration); + + Map statistics(); +} diff --git a/src/main/java/com/example/registration/service/SmsService.java b/src/main/java/com/example/registration/service/SmsService.java new file mode 100644 index 0000000..fb3e531 --- /dev/null +++ b/src/main/java/com/example/registration/service/SmsService.java @@ -0,0 +1,19 @@ +package com.example.registration.service; + +public interface SmsService { + + /** + * 发送短信验证码 + * @param phone 手机号 + * @return 发送结果 + */ + boolean sendCode(String phone); + + /** + * 验证短信验证码 + * @param phone 手机号 + * @param code 验证码 + * @return 验证结果 + */ + boolean verifyCode(String phone, String code); +} diff --git a/src/main/java/com/example/registration/service/UserService.java b/src/main/java/com/example/registration/service/UserService.java new file mode 100644 index 0000000..118774e --- /dev/null +++ b/src/main/java/com/example/registration/service/UserService.java @@ -0,0 +1,9 @@ +package com.example.registration.service; + +import com.baomidou.mybatisplus.extension.service.IService; +import com.example.registration.entity.User; + +public interface UserService extends IService { + + User login(String username, String password); +} diff --git a/src/main/java/com/example/registration/service/impl/RegistrationServiceImpl.java b/src/main/java/com/example/registration/service/impl/RegistrationServiceImpl.java new file mode 100644 index 0000000..074c7ee --- /dev/null +++ b/src/main/java/com/example/registration/service/impl/RegistrationServiceImpl.java @@ -0,0 +1,105 @@ +package com.example.registration.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.core.toolkit.StringUtils; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.registration.entity.TrainingRegistration; +import com.example.registration.mapper.TrainingRegistrationMapper; +import com.example.registration.service.RegistrationService; +import org.springframework.stereotype.Service; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +@Service +public class RegistrationServiceImpl extends ServiceImpl implements RegistrationService { + + @Override + public TrainingRegistration submit(TrainingRegistration registration) { + // 生成报名编号 BMYYYYMMxxxx + String bmId = generateBmId(); + registration.setBmId(bmId); + // 生成随机唯一编号 + registration.setRandomCode(UUID.randomUUID().toString().replace("-", "")); + // 默认状态为已提交 + if (StringUtils.isBlank(registration.getStatus())) { + registration.setStatus("submitted"); + } + registration.setSubmitTime(LocalDateTime.now()); + baseMapper.insert(registration); + return registration; + } + + @Override + public IPage listPage(Page page, String keyword, String status, String period) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + if (StringUtils.isNotBlank(keyword)) { + wrapper.and(w -> w.like(TrainingRegistration::getName, keyword) + .or().like(TrainingRegistration::getPhone, keyword) + .or().like(TrainingRegistration::getCompany, keyword) + .or().like(TrainingRegistration::getBmId, keyword)); + } + if (StringUtils.isNotBlank(status)) { + wrapper.eq(TrainingRegistration::getStatus, status); + } + if (StringUtils.isNotBlank(period)) { + wrapper.eq(TrainingRegistration::getPeriod, period); + } + wrapper.orderByDesc(TrainingRegistration::getCreateTime); + return baseMapper.selectPage(page, wrapper); + } + + @Override + public TrainingRegistration updateByRandomCode(String randomCode, TrainingRegistration registration) { + TrainingRegistration existing = baseMapper.selectByRandomCode(randomCode); + if (existing == null) { + return null; + } + registration.setId(existing.getId()); + registration.setBmId(null); + registration.setRandomCode(null); + registration.setCreateTime(null); + baseMapper.updateById(registration); + return baseMapper.selectById(existing.getId()); + } + + @Override + public Map statistics() { + Map result = new HashMap<>(); + Long total = baseMapper.countTotalRegistrations(); + Long paid = baseMapper.countPaidRegistrations(); + Long pending = baseMapper.countPendingPaymentRegistrations(); + BigDecimal paidAmount = baseMapper.sumPaidAmount(); + result.put("totalRegistrations", total); + result.put("paidRegistrations", paid); + result.put("pendingPaymentRegistrations", pending); + result.put("paidAmount", paidAmount); + return result; + } + + private String generateBmId() { + String prefix = "BM" + LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMM")); + // 查询当月最大编号 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.likeRight(TrainingRegistration::getBmId, prefix); + wrapper.orderByDesc(TrainingRegistration::getBmId); + wrapper.last("LIMIT 1"); + TrainingRegistration last = baseMapper.selectOne(wrapper); + int seq = 1; + if (last != null && last.getBmId() != null) { + try { + String seqStr = last.getBmId().substring(prefix.length()); + seq = Integer.parseInt(seqStr) + 1; + } catch (NumberFormatException e) { + seq = 1; + } + } + return prefix + String.format("%04d", seq); + } +} diff --git a/src/main/java/com/example/registration/service/impl/SmsServiceImpl.java b/src/main/java/com/example/registration/service/impl/SmsServiceImpl.java new file mode 100644 index 0000000..c53150d --- /dev/null +++ b/src/main/java/com/example/registration/service/impl/SmsServiceImpl.java @@ -0,0 +1,114 @@ +package com.example.registration.service.impl; + +import com.alibaba.fastjson.JSONObject; +import com.aliyun.dysmsapi20170525.Client; +import com.aliyun.dysmsapi20170525.models.SendSmsRequest; +import com.aliyun.dysmsapi20170525.models.SendSmsResponse; +import com.aliyun.teaopenapi.models.Config; +import com.example.registration.service.SmsService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +@Slf4j +@Service +public class SmsServiceImpl implements SmsService { + + private static final String SMS_CODE_PREFIX = "sms:code:"; + private static final int CODE_EXPIRE_MINUTES = 5; + private static final int CODE_LENGTH = 6; + + private final StringRedisTemplate redisTemplate; + private Client client; + private final String signName; + private final String templateCode; + + public SmsServiceImpl(StringRedisTemplate redisTemplate, + @Value("${aliyun.sms.access-key-id}") String accessKeyId, + @Value("${aliyun.sms.access-key-secret}") String accessKeySecret, + @Value("${aliyun.sms.sign-name}") String signName, + @Value("${aliyun.sms.template-code}") String templateCode) { + this.redisTemplate = redisTemplate; + this.signName = signName; + this.templateCode = templateCode; + try { + Config config = new Config() + .setAccessKeyId(accessKeyId) + .setAccessKeySecret(accessKeySecret) + .setEndpoint("dysmsapi.aliyuncs.com"); + this.client = new Client(config); + } catch (Exception e) { + log.error("初始化阿里云短信客户端失败", e); + } + } + + @Override + public boolean sendCode(String phone) { + String code = generateCode(); + String redisKey = SMS_CODE_PREFIX + phone; + + try { + // 发送短信 + SendSmsRequest request = new SendSmsRequest() + .setPhoneNumbers(phone) + .setSignName(signName) + .setTemplateCode(templateCode) + .setTemplateParam(JSONObject.toJSONString(java.util.Collections.singletonMap("code", code))); + + if (client != null) { + SendSmsResponse response = client.sendSms(request); + log.info("短信发送响应: code={}, message={}", response.getBody().getCode(), response.getBody().getMessage()); + if ("OK".equals(response.getBody().getCode())) { + // 将验证码存入Redis + redisTemplate.opsForValue().set(redisKey, code, CODE_EXPIRE_MINUTES, TimeUnit.MINUTES); + return true; + } + } else { + // 短信客户端未初始化时,使用模拟模式(仅用于开发测试) + log.warn("短信客户端未初始化,使用模拟验证码: {}", code); + redisTemplate.opsForValue().set(redisKey, code, CODE_EXPIRE_MINUTES, TimeUnit.MINUTES); + return true; + } + } catch (Exception e) { + log.error("发送短信验证码失败,phone={}", phone, e); + // 开发环境下模拟成功 + redisTemplate.opsForValue().set(redisKey, code, CODE_EXPIRE_MINUTES, TimeUnit.MINUTES); + log.info("开发环境模拟短信验证码: phone={}, code={}", phone, code); + return true; + } + return false; + } + + @Override + public boolean verifyCode(String phone, String code) { + String redisKey = SMS_CODE_PREFIX + phone; + String cachedCode = redisTemplate.opsForValue().get(redisKey); + + if (cachedCode == null) { + log.warn("验证码已过期或不存在,phone={}", phone); + return false; + } + + if (cachedCode.equals(code)) { + // 验证成功后删除验证码 + redisTemplate.delete(redisKey); + return true; + } + + log.warn("验证码不匹配,phone={}, input={}, cached={}", phone, code, cachedCode); + return false; + } + + private String generateCode() { + Random random = new Random(); + StringBuilder code = new StringBuilder(); + for (int i = 0; i < CODE_LENGTH; i++) { + code.append(random.nextInt(10)); + } + return code.toString(); + } +} diff --git a/src/main/java/com/example/registration/service/impl/UserServiceImpl.java b/src/main/java/com/example/registration/service/impl/UserServiceImpl.java new file mode 100644 index 0000000..7404990 --- /dev/null +++ b/src/main/java/com/example/registration/service/impl/UserServiceImpl.java @@ -0,0 +1,38 @@ +package com.example.registration.service.impl; + +import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; +import com.example.registration.entity.User; +import com.example.registration.mapper.UserMapper; +import com.example.registration.service.UserService; +import org.springframework.stereotype.Service; +import org.springframework.util.DigestUtils; + +import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; + +@Service +public class UserServiceImpl extends ServiceImpl implements UserService { + + @Override + public User login(String username, String password) { + User user = baseMapper.selectByUsername(username); + if (user == null) { + return null; + } + if (!"active".equals(user.getStatus())) { + return null; + } + String hashedPassword = DigestUtils.md5DigestAsHex( + (password + user.getPasswordSalt()).getBytes(StandardCharsets.UTF_8) + ); + if (!hashedPassword.equals(user.getPasswordHash())) { + return null; + } + // 更新登录信息 + user.setLastLoginTime(LocalDateTime.now()); + user.setLoginCount(user.getLoginCount() == null ? 1 : user.getLoginCount() + 1); + user.setFailedLoginAttempts(0); + baseMapper.updateById(user); + return user; + } +} diff --git a/src/main/java/com/example/registration/util/RedisTokenUtil.java b/src/main/java/com/example/registration/util/RedisTokenUtil.java new file mode 100644 index 0000000..2f19a62 --- /dev/null +++ b/src/main/java/com/example/registration/util/RedisTokenUtil.java @@ -0,0 +1,45 @@ +package com.example.registration.util; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +@Component +public class RedisTokenUtil { + + private static final String TOKEN_PREFIX = "token:"; + private static final long TOKEN_EXPIRE_MINUTES = 30; + + @Autowired + private RedisTemplate redisTemplate; + + public void storeToken(String token, String userId) { + String key = TOKEN_PREFIX + token; + redisTemplate.opsForValue().set(key, userId, TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES); + } + + public boolean validateToken(String token) { + String key = TOKEN_PREFIX + token; + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } + + public String getUserId(String token) { + String key = TOKEN_PREFIX + token; + Object value = redisTemplate.opsForValue().get(key); + return value != null ? value.toString() : null; + } + + public void refreshToken(String token) { + String key = TOKEN_PREFIX + token; + if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) { + redisTemplate.expire(key, TOKEN_EXPIRE_MINUTES, TimeUnit.MINUTES); + } + } + + public void deleteToken(String token) { + String key = TOKEN_PREFIX + token; + redisTemplate.delete(key); + } +} diff --git a/src/main/java/com/example/registration/util/Result.java b/src/main/java/com/example/registration/util/Result.java new file mode 100644 index 0000000..fd48ee2 --- /dev/null +++ b/src/main/java/com/example/registration/util/Result.java @@ -0,0 +1,45 @@ +package com.example.registration.util; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import lombok.Data; + +@Data +@ApiModel("通用响应") +public class Result { + + @ApiModelProperty("状态码 200成功") + private Integer code; + + @ApiModelProperty("提示信息") + private String message; + + @ApiModelProperty("响应数据") + private T data; + + public static Result success(T data) { + Result result = new Result<>(); + result.setCode(200); + result.setMessage("success"); + result.setData(data); + return result; + } + + public static Result success() { + return success(null); + } + + public static Result error(String message) { + Result result = new Result<>(); + result.setCode(500); + result.setMessage(message); + return result; + } + + public static Result error(Integer code, String message) { + Result result = new Result<>(); + result.setCode(code); + result.setMessage(message); + return result; + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml new file mode 100644 index 0000000..e074cd2 --- /dev/null +++ b/src/main/resources/application-dev.yml @@ -0,0 +1,54 @@ +server: + port: 8099 + servlet: + context-path: / + +spring: + application: + name: registration + mvc: + pathmatch: + matching-strategy: ant_path_matcher + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/registration?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: 123456 + schema: classpath:data.sql + initialization-mode: always + redis: + host: localhost + port: 6379 + password: + database: 0 + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + +mybatis-plus: + mapper-locations: classpath:/mapper/**/*.xml + type-aliases-package: com.example.registration.entity + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto + logic-delete-field: deletedAt + logic-delete-value: "NOW()" + logic-not-delete-value: "NULL" + +knife4j: + enable: true + setting: + language: zh_cn + +# 阿里云短信配置 +aliyun: + sms: + access-key-id: LTAI5t7peh76HpuVGhpXabPb + access-key-secret: Y119TqVZeaU7LcYRgbIHLpmscteQnw + sign-name: 培训报名成功模板 + template-code: SMS_506365455 diff --git a/src/main/resources/application-pro.yml b/src/main/resources/application-pro.yml new file mode 100644 index 0000000..24118a3 --- /dev/null +++ b/src/main/resources/application-pro.yml @@ -0,0 +1,46 @@ +server: + port: 8099 + servlet: + context-path: / + +spring: + application: + name: registration + mvc: + pathmatch: + matching-strategy: ant_path_matcher + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/registration?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: 123456 + schema: classpath:data.sql + initialization-mode: always + redis: + host: localhost + port: 6379 + password: + database: 0 + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + +mybatis-plus: + mapper-locations: classpath:/mapper/**/*.xml + type-aliases-package: com.example.registration.entity + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto + logic-delete-field: deletedAt + logic-delete-value: "NOW()" + logic-not-delete-value: "NULL" + +knife4j: + enable: true + setting: + language: zh_cn diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 0000000..24118a3 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,46 @@ +server: + port: 8099 + servlet: + context-path: / + +spring: + application: + name: registration + mvc: + pathmatch: + matching-strategy: ant_path_matcher + datasource: + driver-class-name: com.mysql.cj.jdbc.Driver + url: jdbc:mysql://localhost:3306/registration?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true + username: root + password: 123456 + schema: classpath:data.sql + initialization-mode: always + redis: + host: localhost + port: 6379 + password: + database: 0 + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 0 + +mybatis-plus: + mapper-locations: classpath:/mapper/**/*.xml + type-aliases-package: com.example.registration.entity + configuration: + map-underscore-to-camel-case: true + log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + global-config: + db-config: + id-type: auto + logic-delete-field: deletedAt + logic-delete-value: "NOW()" + logic-not-delete-value: "NULL" + +knife4j: + enable: true + setting: + language: zh_cn diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000..caf4dfc --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,3 @@ +spring: + profiles: + active: dev \ No newline at end of file diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..216d378 --- /dev/null +++ b/src/main/resources/logback-spring.xml @@ -0,0 +1,81 @@ + + + + + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + UTF-8 + + + + + + ${LOG_PATH}/${LOG_FILE}.log + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + ${LOG_PATH}/${LOG_FILE}-%d{yyyy-MM-dd}.%i.log + + 100MB + + 30 + + + + + + ${LOG_PATH}/${LOG_FILE}-error.log + + ERROR + ACCEPT + DENY + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n + UTF-8 + + + ${LOG_PATH}/${LOG_FILE}-error-%d{yyyy-MM-dd}.%i.log + + 50MB + + 60 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/mapper/TrainingRegistrationMapper.xml b/src/main/resources/mapper/TrainingRegistrationMapper.xml new file mode 100644 index 0000000..56bcbcf --- /dev/null +++ b/src/main/resources/mapper/TrainingRegistrationMapper.xml @@ -0,0 +1,4 @@ + + + + diff --git a/src/main/resources/mapper/UserMapper.xml b/src/main/resources/mapper/UserMapper.xml new file mode 100644 index 0000000..42fee10 --- /dev/null +++ b/src/main/resources/mapper/UserMapper.xml @@ -0,0 +1,4 @@ + + + +