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