Spring Boot + Vue3 实战:活动管理系统数据建模与前后端开发
最近在开发一个活动管理系统的过程中遇到了一个典型需求如何高效、结构化地存储和展示一场大型活动例如演唱会、庆典的详细信息并实现灵活的前端渲染与后台管理。这类需求的核心在于数据模型的设计。本文将以一个虚构的“六巡首场烟花 One Spark”高雄演唱会活动为例手把手带你从零设计一套完整的活动数据模型并基于 Spring Boot MyBatis-Plus 实现后端 API最后使用 Vue3 构建一个简单的前端管理页面。通过这个实战项目你将掌握复杂业务对象的数据建模、前后端分离开发以及基础的管理功能实现。无论你是正在学习全栈开发的学生还是需要快速为业务搭建活动管理模块的开发者这套从数据库设计到接口联调的完整流程都能直接复用。1. 活动数据模型分析与设计在设计任何系统之前首先要对业务进行抽象。一场大型活动通常包含哪些信息我们可以将其拆解为核心信息、时间地点、票务、阵容、媒体素材等多个维度。1.1 核心实体分析以“六巡首场烟花 One Spark 高雄演唱会”为例我们可以提炼出以下关键实体及其属性活动Activity最核心的实体承载活动的基本信息。基础信息活动唯一标识ID、标题、副标题、描述。状态与分类活动状态如预售、售票中、已结束、活动类型演唱会、音乐节、话剧等。时间信息活动开始时间、活动结束时间。组织方主办方、承办方。场次Session一个活动可能有多场例如巡演的不同城市站。场次是活动的具体发生实例。关联信息所属活动ID。具体信息场次名称、场次日期、具体开始时间、具体结束时间。地点信息场馆名称、场馆地址省、市、区、详细地址、经纬度用于地图展示。票务Ticket与场次强关联定义票价和库存。关联信息所属场次ID。票档信息票档名称如VIP区、看台区、早鸟票、价格、货币单位。库存与限制总库存、已售数量、每人限购数量。销售状态票档状态启用、停售。1.2 数据库表结构设计基于以上分析我们设计三张核心数据库表。这里以 MySQL 为例。表1:activity(活动主表)CREATE TABLE activity ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, title varchar(200) NOT NULL COMMENT 活动标题如六巡首场烟花 One Spark, subtitle varchar(500) DEFAULT NULL COMMENT 活动副标题, description text COMMENT 活动详细描述, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 活动状态0-待发布1-预售2-售票中3-已结束4-已取消, type varchar(50) DEFAULT NULL COMMENT 活动类型concert, festival, drama 等, start_time datetime DEFAULT NULL COMMENT 活动开始时间整体, end_time datetime DEFAULT NULL COMMENT 活动结束时间整体, organizer varchar(200) DEFAULT NULL COMMENT 主办方, contractor varchar(200) DEFAULT NULL COMMENT 承办方, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_status (status), KEY idx_time (start_time, end_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT活动主表;表2:activity_session(活动场次表)CREATE TABLE activity_session ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, activity_id bigint(20) NOT NULL COMMENT 关联活动ID, session_name varchar(200) NOT NULL COMMENT 场次名称如2025-11-22 高雄首场, session_date date NOT NULL COMMENT 场次日期, start_time time NOT NULL COMMENT 具体开始时间, end_time time DEFAULT NULL COMMENT 具体结束时间, venue_name varchar(200) NOT NULL COMMENT 场馆名称, venue_province varchar(50) DEFAULT NULL COMMENT 场馆所在省, venue_city varchar(50) DEFAULT NULL COMMENT 场馆所在市, venue_district varchar(50) DEFAULT NULL COMMENT 场馆所在区, venue_address varchar(500) DEFAULT NULL COMMENT 场馆详细地址, venue_longitude decimal(10,7) DEFAULT NULL COMMENT 场馆经度, venue_latitude decimal(10,7) DEFAULT NULL COMMENT 场馆纬度, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_activity_id (activity_id), KEY idx_session_date (session_date), CONSTRAINT fk_session_activity FOREIGN KEY (activity_id) REFERENCES activity (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT活动场次表;表3:session_ticket(场次票档表)CREATE TABLE session_ticket ( id bigint(20) NOT NULL AUTO_INCREMENT COMMENT 主键ID, session_id bigint(20) NOT NULL COMMENT 关联场次ID, ticket_name varchar(100) NOT NULL COMMENT 票档名称如VIP尊享席, price decimal(10,2) NOT NULL COMMENT 价格, currency varchar(10) NOT NULL DEFAULT CNY COMMENT 货币单位, total_stock int(11) NOT NULL COMMENT 总库存, sold_stock int(11) NOT NULL DEFAULT 0 COMMENT 已售库存, purchase_limit int(11) DEFAULT NULL COMMENT 每人限购数量, status tinyint(4) NOT NULL DEFAULT 1 COMMENT 状态0-停售1-启用, sort_order int(11) DEFAULT 0 COMMENT 排序字段, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_session_id (session_id), KEY idx_status (status), CONSTRAINT fk_ticket_session FOREIGN KEY (session_id) REFERENCES activity_session (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT场次票档表;设计要点说明外键与级联activity_session和session_ticket表通过外键关联到上级表并设置了ON DELETE CASCADE。这意味着删除一个活动其所有场次和票档会被自动删除确保数据一致性但生产环境需谨慎评估是否使用级联删除。索引优化为常用的查询字段如status,activity_id,session_date添加了索引提升查询效率。字段冗余activity_session表中冗余了venue_province、venue_city等字段避免了关联地址表简化查询是空间换时间的常见做法。数据精度价格使用DECIMAL(10,2)经纬度使用DECIMAL(10,7)确保计算和定位的精度。2. 后端开发Spring Boot API 实现接下来我们使用 Spring Boot 快速搭建后端服务提供对上述数据模型的增删改查CRUDAPI。2.1 环境准备与项目初始化JDK: 17 或以上构建工具: Maven 3.6IDE: IntelliJ IDEA 或 Eclipse数据库: MySQL 8.0使用 Spring Initializr 或 IDE 创建 Spring Boot 项目选择以下依赖Spring WebMyBatis FrameworkMySQL DriverLombok (简化代码)生成的pom.xml关键依赖如下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-test/artifactId scopetest/scope /dependency !-- 添加 MyBatis-Plus 简化CRUD -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency /dependencies2.2 数据层实现实体类与 Mapper首先创建与数据库表对应的实体类。这里使用 Lombok 注解减少 getter/setter 代码。文件路径src/main/java/com/example/demo/entity/Activity.javapackage com.example.demo.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.time.LocalDateTime; import java.util.Date; Data TableName(activity) public class Activity { TableId(type IdType.AUTO) private Long id; private String title; private String subtitle; private String description; private Integer status; // 状态枚举值 private String type; private Date startTime; private Date endTime; private String organizer; private String contractor; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }文件路径src/main/java/com/example.demo/entity/ActivitySession.javapackage com.example.demo.entity; import com.baomidou.mybatisplus.annotation.*; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; Data TableName(activity_session) public class ActivitySession { TableId(type IdType.AUTO) private Long id; private Long activityId; private String sessionName; private LocalDate sessionDate; private LocalTime startTime; private LocalTime endTime; private String venueName; private String venueProvince; private String venueCity; private String venueDistrict; private String venueAddress; private BigDecimal venueLongitude; private BigDecimal venueLatitude; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; }SessionTicket实体类结构类似此处省略。接下来创建 Mapper 接口。得益于 MyBatis-Plus我们只需继承BaseMapper即可获得基础的 CRUD 方法。文件路径src/main/java/com/example/demo/mapper/ActivityMapper.javapackage com.example.demo.mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.example.demo.entity.Activity; import org.apache.ibatis.annotations.Mapper; Mapper public interface ActivityMapper extends BaseMapperActivity { // 可以在此定义自定义的复杂SQL查询方法 // 例如ListActivity selectByStatusAndTimeRange(Param(status) Integer status, ...); }ActivitySessionMapper和SessionTicketMapper的创建方式完全相同。2.3 业务层与服务层实现我们创建 Service 接口及其实现类来封装业务逻辑。文件路径src/main/java/com/example/demo/service/ActivityService.javapackage com.example.demo.service; import com.baomidou.mybatisplus.extension.service.IService; import com.example.demo.entity.Activity; import com.example.demo.vo.ActivityDetailVO; public interface ActivityService extends IServiceActivity { /** * 根据活动ID获取活动详情包含其所有场次及票档信息 * param id 活动ID * return 活动详情视图对象 */ ActivityDetailVO getActivityDetailById(Long id); /** * 创建活动包含场次和票档的级联创建 * param activityDetailVO 活动详情数据 * return 是否创建成功 */ boolean createActivityWithSessions(ActivityDetailVO activityDetailVO); }文件路径src/main/java/com/example/demo/service/impl/ActivityServiceImpl.javapackage com.example.demo.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.example.demo.entity.Activity; import com.example.demo.entity.ActivitySession; import com.example.demo.entity.SessionTicket; import com.example.demo.mapper.ActivityMapper; import com.example.demo.mapper.ActivitySessionMapper; import com.example.demo.mapper.SessionTicketMapper; import com.example.demo.service.ActivityService; import com.example.demo.vo.ActivityDetailVO; import com.example.demo.vo.SessionVO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; import java.util.stream.Collectors; Service RequiredArgsConstructor public class ActivityServiceImpl extends ServiceImplActivityMapper, Activity implements ActivityService { private final ActivitySessionMapper sessionMapper; private final SessionTicketMapper ticketMapper; Override public ActivityDetailVO getActivityDetailById(Long id) { // 1. 查询活动主体信息 Activity activity this.getById(id); if (activity null) { return null; } ActivityDetailVO detailVO new ActivityDetailVO(); // 使用 Bean 拷贝工具如 MapStruct 或 Spring BeanUtils将 activity 属性拷贝到 detailVO // 这里为简化手动设置 detailVO.setId(activity.getId()); detailVO.setTitle(activity.getTitle()); // ... 拷贝其他属性 // 2. 查询该活动下的所有场次 ListActivitySession sessions sessionMapper.selectList( new LambdaQueryWrapperActivitySession().eq(ActivitySession::getActivityId, id) ); // 3. 为每个场次查询票档信息并组装成 SessionVO ListSessionVO sessionVOs sessions.stream().map(session - { SessionVO sessionVO new SessionVO(); // 拷贝场次属性... sessionVO.setId(session.getId()); sessionVO.setSessionName(session.getSessionName()); // 查询该场次的票档 ListSessionTicket tickets ticketMapper.selectList( new LambdaQueryWrapperSessionTicket().eq(SessionTicket::getSessionId, session.getId()) ); sessionVO.setTickets(tickets); return sessionVO; }).collect(Collectors.toList()); detailVO.setSessions(sessionVOs); return detailVO; } Override Transactional(rollbackFor Exception.class) // 开启事务确保原子性 public boolean createActivityWithSessions(ActivityDetailVO activityDetailVO) { // 1. 保存活动主体 Activity activity new Activity(); // 将 VO 属性拷贝到 entity ... this.save(activity); // 2. 保存场次和票档 ListSessionVO sessions activityDetailVO.getSessions(); if (sessions ! null !sessions.isEmpty()) { for (SessionVO sessionVO : sessions) { ActivitySession session new ActivitySession(); session.setActivityId(activity.getId()); // 设置外键 // 拷贝其他属性... sessionMapper.insert(session); // 3. 保存该场次的票档 ListSessionTicket tickets sessionVO.getTickets(); if (tickets ! null !tickets.isEmpty()) { for (SessionTicket ticket : tickets) { ticket.setSessionId(session.getId()); // 设置外键 ticketMapper.insert(ticket); } } } } return true; } }关键点Transactional在创建活动的方法上添加事务注解确保活动、场次、票档要么全部保存成功要么全部回滚保证数据一致性。LambdaQueryWrapperMyBatis-Plus 提供的链式查询条件构造器使代码更简洁。VO (View Object)ActivityDetailVO和SessionVO是专门用于接口传输的视图对象它们聚合了多个实体Activity, Session, Ticket的数据避免直接暴露数据库实体结构也更符合前端需求。2.4 控制层RESTful API 设计最后创建 Controller 暴露 HTTP 接口。文件路径src/main/java/com/example/demo/controller/ActivityController.javapackage com.example.demo.controller; import com.example.demo.common.Result; import com.example.demo.service.ActivityService; import com.example.demo.vo.ActivityDetailVO; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; RestController RequestMapping(/api/activity) RequiredArgsConstructor public class ActivityController { private final ActivityService activityService; GetMapping(/{id}) public ResultActivityDetailVO getDetail(PathVariable Long id) { ActivityDetailVO detail activityService.getActivityDetailById(id); if (detail null) { return Result.error(404, 活动不存在); } return Result.success(detail); } PostMapping public ResultBoolean create(RequestBody ActivityDetailVO activityDetailVO) { // 实际项目中需要添加参数校验例如使用 Valid boolean success activityService.createActivityWithSessions(activityDetailVO); return success ? Result.success(true) : Result.error(500, 创建活动失败); } // 其他接口分页查询活动列表、更新活动、删除活动等... }文件路径src/main/java/com/example/demo/common/Result.java(统一响应体)package com.example.demo.common; import lombok.Data; Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }2.5 应用配置文件路径src/main/resources/application.ymlspring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/activity_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: your_username password: your_password mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 控制台打印SQL生产环境关闭 global-config: db-config: logic-delete-field: deleted # 全局逻辑删除字段如果表中有此字段 logic-delete-value: 1 logic-not-delete-value: 0 server: port: 8080启动 Spring Boot 应用后即可通过http://localhost:8080/api/activity/{id}等接口访问服务。3. 前端开发Vue3 管理页面示例后端 API 准备好后我们使用 Vue3 和 Element Plus 构建一个简单的前端管理页面用于创建和查看活动。3.1 环境准备与项目初始化使用 Vite 快速创建 Vue3 项目。npm create vuelatest activity-manager-frontend # 按照提示选择 Vue Router, Pinia, Element Plus 等 cd activity-manager-frontend npm install npm run dev3.2 活动创建页面组件文件路径src/views/ActivityCreate.vuetemplate div classactivity-create h2创建新活动/h2 el-form :modelactivityForm :rulesrules refformRef label-width120px !-- 活动基础信息 -- el-form-item label活动标题 proptitle el-input v-modelactivityForm.title placeholder例如六巡首场烟花 One Spark/el-input /el-form-item el-form-item label活动副标题 propsubtitle el-input v-modelactivityForm.subtitle/el-input /el-form-item el-form-item label活动状态 propstatus el-select v-modelactivityForm.status placeholder请选择 el-option label待发布 :value0/el-option el-option label预售 :value1/el-option el-option label售票中 :value2/el-option /el-select /el-form-item el-form-item label活动时间范围 proptimeRange el-date-picker v-modelactivityForm.timeRange typedaterange range-separator至 start-placeholder开始日期 end-placeholder结束日期 value-formatYYYY-MM-DD / /el-form-item !-- 场次信息支持动态添加 -- div v-for(session, index) in activityForm.sessions :keyindex classsession-block h3场次 {{ index 1 }}/h3 el-form-item :label场次名称 :propsessions[${index}].sessionName :rules{ required: true, message: 请输入场次名称, trigger: blur } el-input v-modelsession.sessionName placeholder例如2025-11-22 高雄首场/el-input /el-form-item el-form-item label场次日期 :propsessions[${index}].sessionDate :rules{ required: true, message: 请选择场次日期, trigger: change } el-date-picker v-modelsession.sessionDate typedate placeholder选择日期 value-formatYYYY-MM-DD/ /el-form-item el-form-item label场馆信息 el-input v-modelsession.venueName placeholder场馆名称 stylewidth: 48%; margin-right: 2%;/el-input el-input v-modelsession.venueCity placeholder所在城市 stylewidth: 48%;/el-input el-input v-modelsession.venueAddress placeholder详细地址 stylemargin-top: 10px;/el-input /el-form-item !-- 票档信息支持动态添加 -- h4票档设置/h4 div v-for(ticket, tIndex) in session.tickets :keytIndex classticket-row el-input v-modelticket.ticketName placeholder票档名 stylewidth: 30%;/el-input el-input-number v-modelticket.price :min0 :precision2 placeholder价格 stylewidth: 20%; margin-left: 10px;/el-input-number el-input-number v-modelticket.totalStock :min1 placeholder库存 stylewidth: 20%; margin-left: 10px;/el-input-number el-button clickremoveTicket(index, tIndex) typedanger sizesmall stylemargin-left: 10px;删除/el-button /div el-button clickaddTicket(index) typeprimary sizesmall stylemargin-top: 10px;添加票档/el-button el-button clickremoveSession(index) typedanger sizesmall stylemargin-top: 10px; margin-left: 10px; v-ifactivityForm.sessions.length 1删除场次/el-button /div el-button clickaddSession typeprimary stylemargin-bottom: 20px;添加场次/el-button el-form-item el-button typeprimary clicksubmitForm立即创建/el-button el-button clickresetForm重置/el-button /el-form-item /el-form /div /template script setup import { ref, reactive } from vue import { ElMessage } from element-plus import axios from axios const formRef ref() const activityForm reactive({ title: , subtitle: , status: 2, timeRange: [], sessions: [ { sessionName: , sessionDate: , venueName: , venueCity: 高雄市, venueAddress: , tickets: [ { ticketName: 普通票, price: 580, totalStock: 1000, currency: TWD }, { ticketName: VIP票, price: 1280, totalStock: 200, currency: TWD } ] } ] }) const rules reactive({ title: [{ required: true, message: 请输入活动标题, trigger: blur }], status: [{ required: true, message: 请选择活动状态, trigger: change }], timeRange: [{ type: array, required: true, message: 请选择活动时间范围, trigger: change }] }) const addSession () { activityForm.sessions.push({ sessionName: , sessionDate: , venueName: , venueCity: 高雄市, venueAddress: , tickets: [{ ticketName: , price: 0, totalStock: 0, currency: TWD }] }) } const removeSession (index) { activityForm.sessions.splice(index, 1) } const addTicket (sessionIndex) { activityForm.sessions[sessionIndex].tickets.push({ ticketName: , price: 0, totalStock: 0, currency: TWD }) } const removeTicket (sessionIndex, ticketIndex) { activityForm.sessions[sessionIndex].tickets.splice(ticketIndex, 1) } const submitForm async () { // 1. 表单验证 await formRef.value.validate((valid) { if (!valid) { ElMessage.error(请检查表单填写是否正确) return } }) // 2. 数据转换将前端表单数据转换为后端需要的 ActivityDetailVO 结构 const payload { title: activityForm.title, subtitle: activityForm.subtitle, status: activityForm.status, startTime: activityForm.timeRange[0], endTime: activityForm.timeRange[1], sessions: activityForm.sessions.map(session ({ sessionName: session.sessionName, sessionDate: session.sessionDate, venueName: session.venueName, venueCity: session.venueCity, venueAddress: session.venueAddress, tickets: session.tickets })) } // 3. 调用后端API try { const response await axios.post(http://localhost:8080/api/activity, payload) if (response.data.code 200) { ElMessage.success(活动创建成功) resetForm() } else { ElMessage.error(创建失败${response.data.message}) } } catch (error) { console.error(API调用失败:, error) ElMessage.error(网络请求失败请检查后端服务) } } const resetForm () { formRef.value.resetFields() // 手动重置 sessions 数组 activityForm.sessions [{ sessionName: , sessionDate: , venueName: , venueCity: 高雄市, venueAddress: , tickets: [{ ticketName: , price: 0, totalStock: 0, currency: TWD }] }] } /script style scoped .session-block { border: 1px solid #e4e7ed; padding: 20px; margin-bottom: 20px; border-radius: 4px; } .ticket-row { display: flex; align-items: center; margin-bottom: 10px; } /style这个组件实现了动态表单可以灵活地添加/删除场次和每个场次下的票档并将数据组装成后端需要的 JSON 格式进行提交。4. 常见问题与排查思路在实际开发和联调中你可能会遇到以下典型问题问题现象可能原因排查步骤与解决方案后端启动失败数据库连接错误1. 数据库服务未启动。2.application.yml中数据库配置错误用户名、密码、URL。3. MySQL 驱动版本与数据库版本不匹配。1. 检查 MySQL 服务状态 (systemctl status mysql或查看服务列表)。2. 仔细核对application.yml中的url,username,password。3. 确认pom.xml中的mysql-connector-j版本与本地 MySQL 版本兼容。前端调用 API 报跨域错误 (CORS)浏览器同源策略阻止了前端localhost:5173请求后端localhost:8080。后端解决在 Spring Boot 的Configuration类中添加全局 CORS 配置。前端解决开发阶段在 Vite 配置中设置代理 (vite.config.js)。插入数据时报外键约束失败1. 插入activity_session时提供的activity_id在activity表中不存在。2. 插入顺序错误先插入了子表记录。1. 检查插入数据中的外键 ID 值是否正确确保父记录已存在。2. 确保业务逻辑的保存顺序先保存 Activity再保存 Session最后保存 Ticket。前端表单提交后后端接收到的日期字段为null前端提交的日期字符串格式与后端RequestBody映射的 Java 对象期望的格式不一致。1. 在前端确保使用value-format统一日期格式如YYYY-MM-DD。2. 在后端实体类的日期字段上使用JsonFormat注解指定格式或配置全局的 Jackson 日期格式化。MyBatis-Plus 查询不到数据1. 实体类属性名与数据库字段名未正确映射下划线转驼峰默认开启。2. 表名或字段名有大小写问题。1. 检查实体类TableName和TableField注解是否正确。2. 在application.yml中配置mybatis-plus.configuration.map-underscore-to-camel-case: true默认已开启。3. 开启 SQL 日志查看实际执行的 SQL 语句。事务Transactional未回滚1. 异常未被抛出到方法外被 catch 住了。2. 异常类型不是RuntimeException或Error。1. 确保异常在方法内抛出或在Transactional注解中指定rollbackFor Exception.class。2. 检查方法是否是public的Spring AOP 代理对非 public 方法无效。5. 最佳实践与工程建议API 设计规范RESTful 风格使用合适的 HTTP 方法GET/POST/PUT/DELETE和资源路径。版本控制在 API 路径中加入版本号如/api/v1/activity为后续迭代留有余地。统一响应体如本文的ResultT类统一返回结构包含code,message,data。分页查询列表接口务必支持分页使用 MyBatis-Plus 的Page对象非常方便。数据验证与安全后端验证在 Controller 的入参上使用Valid注解并结合校验注解如NotBlank,Min或在 Service 层进行业务逻辑校验。防 SQL 注入坚持使用 MyBatis-Plus 的条件构造器或#{}预编译占位符绝对禁止在 SQL 中拼接用户输入。敏感信息脱敏返回给前端的用户手机号、邮箱等需进行脱敏处理。性能与可维护性索引优化根据查询模式如按状态、按时间范围查活动合理设计数据库索引。逻辑删除使用deleted字段标记删除而非物理删除便于数据恢复和审计。MyBatis-Plus 支持全局配置。DTO/VO 分离严格区分数据库实体Entity、数据传输对象DTO和视图对象VO。Entity 对应表结构DTO 用于接收参数VO 用于返回前端。这提高了代码的清晰度和安全性。服务拆分当业务复杂后考虑将活动服务、票务服务、用户服务等拆分为独立的微服务。前端工程化API 封装将所有的axios请求封装在统一的src/api/目录下便于管理和维护。状态管理对于跨组件共享的数据如用户登录状态使用 Pinia 进行集中管理。组件化将场次信息块、票档信息行等可复用的 UI 部分抽离成独立的子组件。通过以上步骤我们完成了一个从数据库设计、后端 API 开发到前端页面实现的完整活动管理模块原型。这个原型具备了核心的 CRUD 功能并考虑了数据一致性和基本的用户体验。你可以在此基础上继续扩展用户管理、订单系统、支付集成、座位图选座等更复杂的功能。