ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Spring Boot 3 + Vue 3 实现活动嘉宾展示与在线预约报名

Spring Boot 3 + Vue 3 实现活动嘉宾展示与在线预约报名 先来聊一个很常见的需求场景。任何一场线下嘉宾秀、发布会、校园晚会或公司年会除了内容本身之外总是绕不开“观众如何了解嘉宾、如何报名、如何查看流程”这类问题。标题里提到的 KISS N TELL 嘉宾秀KDC 26.8.8如果给它做一个官网最核心的功能不是花哨的动画而是活动信息、嘉宾展示、场次安排和在线预约记录。这篇文章就从零开始带大家落地一个“活动嘉宾展示 在线预约报名”的示例系统。我会把 KDC 26.8.8 当作活动项目的版本号或场次编号来贯穿整个数据设计这样后续如果要支持多期活动也能直接复用同一套系统结构。本文适合有一定 Java 或 Vue 基础的读者也适合想学习前后端分离项目如何组织代码的同学。学习完以后你能掌握一套最小可运行的活动官网开发流程包括数据库设计、后端接口、前端页面联调以及部署和上线前需要留意的合规与安全问题。需要说明的是本文中的嘉宾名称、时间、地点均为演示数据不指向任何真实活动。1. 从一个活动官网看技术本质很多新手同学第一次接到“活动展示系统”需求时会本能地想到先做用户注册、权限管理、支付系统。其实对于一个单场活动而言这样设计往往过于复杂。一场嘉宾秀的核心信息通常只有几条活动名称、时间、地点、介绍。有哪些嘉宾每个嘉宾长什么样、负责什么内容。当晚或当天的流程安排。观众如何报名或预约。运营人员如何在后台维护嘉宾和数据。如果把这个需求拆开你会发现它本质上是“内容展示 一条低并发预约接口 一个简单的管理界面”并不需要一开始就引入微服务。用 Spring Boot 3 Vue 3 MySQL 这样一套主流技术栈已经足够应付绝大多数类似活动官网场景。使用前后端分离架构还有一个好处即使以后要做独立的运营后台、公众号 H5 页面、小程序后端接口依然可以复用。活动数据的改动不涉及前端页面重新发版这种灵活性在做活动运营时非常关键。为了避免文章过长这篇实战只完成前端观众侧页面和后端预约接口但数据库和项目结构会预留后台管理能力你可以很容易在此基础上扩展。2. 需求分析与系统模块2.1 观众侧功能观众打开活动页面后最希望在首页看到的是以下几个信息功能模块说明活动 Banner 与时间地点让用户快速确认活动是否值得参加嘉宾介绍列表展示嘉宾头像、昵称、角色、简介活动流程时间线展示每个节目、环节的时间段在线预约报名填写昵称、手机号、人数等完成预约预约成功反馈提示用户预约成功避免重复提交2.2 运营后台功能预留后台不是本文核心但在表设计时需要提前考虑。后台通常包含这些能力维护活动场次例如kdc-26.8.8作为活动编号。维护嘉宾信息新增、排序、隐藏下架。导出预约名单按预约时间或手机号筛选。设置活动状态包括草稿、报名中、已结束。前后端联调阶段只要接口路径和返回结构保持稳定后台页面可以后续再补不需要推翻重新设计。2.3 系统角色边界复杂系统的权限要分管理员、运营人员、普通用户但这个项目场景相对简单。我更推荐一开始就用活动编码区分数据kdc-26.8.8 是一期活动 kdc-26.9.9 是另一期活动每次活动都以独立编码存储某一期活动的数据被误删或修改不会影响其他场次。这是运营类系统最常见的隔离手段而不是直接去建多套数据库。3. 环境准备与项目结构3.1 开发环境说明下面是我使用到的基础环境建议保持一致或接近环境版本建议说明JDK17Spring Boot 3 要求 JDK 17 及以上Maven3.8用于后端依赖与启动MySQL8.0使用 InnoDB 和 utf8mb4Node.js18Vite 5 需要较新版本npm9安装前端依赖使用IDEIDEA / VS Code后端与前端分开打开版本建议只是大多数人使用的稳定方案。如果你本机安装的是更新的版本通常也不影响。文中涉及版本的地方请根据实际环境灵活调整。3.2 工程目录规划我建议把后端和前端放在同一个根目录下这样便于 Git 管理kiss-n-tell-show/ ├── backend/ │ ├── pom.xml │ └── src/main/java/com/example/kisstell/ │ └── src/main/resources/ └── frontend/ ├── package.json ├── vite.config.js └── src/后端只负责提供接口和数据库交互前端只负责渲染页面。两个人协作时一个人可以专注 Vue 页面另一个人专注 Java 接口互相不被代码阻塞。4. 数据库设计4.1 数据表拆分思路对于一个活动嘉宾展示系统我设计了四张表kdc_activity活动主表存放活动名称、时间、地点、状态。kdc_guest嘉宾表通过activity_code关联到具体活动。kdc_schedule场次安排表记录每个时间段的活动内容。kdc_booking预约报名表记录观众提交的报名信息。这里并没有把活动作为“机构”去设计因为单场活动就是一个独立实体。如果以后同一个品牌下有多期活动只需要插入多条activity_code不同的记录。4.2 初始化 SQL实际项目中建议使用数据库迁移工具管理表结构这里为了让新手一眼看懂直接提供一份可执行的初始化 SQL。CREATE DATABASE IF NOT EXISTS kdc_show DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE kdc_show; CREATE TABLE IF NOT EXISTS kdc_activity ( id bigint NOT NULL AUTO_INCREMENT, activity_code varchar(32) NOT NULL COMMENT 活动编号如 kdc-26.8.8, title varchar(128) NOT NULL COMMENT 活动标题, sub_title varchar(256) DEFAULT NULL COMMENT 副标题, start_time datetime DEFAULT NULL COMMENT 开始时间, end_time datetime DEFAULT NULL COMMENT 结束时间, address varchar(255) DEFAULT NULL COMMENT 活动地点, cover_url varchar(512) DEFAULT NULL COMMENT 封面图, status tinyint NOT NULL DEFAULT 0 COMMENT 0草稿 1报名中 2已结束, description text COMMENT 活动详细介绍, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_activity_code (activity_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT活动表; CREATE TABLE IF NOT EXISTS kdc_guest ( id bigint NOT NULL AUTO_INCREMENT, activity_code varchar(32) NOT NULL, name varchar(64) NOT NULL COMMENT 嘉宾昵称或姓名, role varchar(64) DEFAULT NULL COMMENT 身份角色, intro text COMMENT 简介, avatar_url varchar(512) DEFAULT NULL COMMENT 头像图片地址, sort_no int DEFAULT 0 COMMENT 排序, hidden tinyint DEFAULT 0 COMMENT 1隐藏, PRIMARY KEY (id), KEY idx_activity_code (activity_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT嘉宾表; CREATE TABLE IF NOT EXISTS kdc_schedule ( id bigint NOT NULL AUTO_INCREMENT, activity_code varchar(32) NOT NULL, start_time datetime DEFAULT NULL, end_time datetime DEFAULT NULL, item_name varchar(128) NOT NULL COMMENT 环节名称, detail varchar(512) DEFAULT NULL COMMENT 环节说明, sort_no int DEFAULT 0, PRIMARY KEY (id), KEY idx_activity_code (activity_code) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT活动流程表; CREATE TABLE IF NOT EXISTS kdc_booking ( id bigint NOT NULL AUTO_INCREMENT, activity_code varchar(32) NOT NULL, guest_name varchar(64) NOT NULL COMMENT 预约人昵称, phone varchar(32) NOT NULL COMMENT 手机号, ticket_count int DEFAULT 1 COMMENT 预约人数, remark varchar(256) DEFAULT NULL, status tinyint DEFAULT 0 COMMENT 0待确认 1已确认 2已取消, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_activity_phone (activity_code, phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约表; INSERT INTO kdc_activity (activity_code, title, sub_title, start_time, end_time, address, status, description) VALUES (kdc-26.8.8, KISS N TELL 嘉宾秀, KISSTELL DEMO SHOW, 2026-08-08 19:30:00, 2026-08-08 21:30:00, 上海·示例剧场, 1, 这是一个用于技术演示的示例活动。); INSERT INTO kdc_guest (activity_code, name, role, intro, avatar_url, sort_no) VALUES (kdc-26.8.8, 阿伦, 主持人, 负责开场串讲与嘉宾互动。, , 1), (kdc-26.8.8, 小鹿, 神秘嘉宾, 擅长音乐现场即兴表演。, , 2), (kdc-26.8.8, 米娅, 嘉宾, 负责分享个人创作故事。, , 3); INSERT INTO kdc_schedule (activity_code, start_time, end_time, item_name, detail, sort_no) VALUES (kdc-26.8.8, 2026-08-08 19:30:00, 2026-08-08 19:40:00, 开场介绍, 主持人与观众见面, 1), (kdc-26.8.8, 2026-08-08 19:40:00, 2026-08-08 20:20:00, 主题分享, 嘉宾分享故事与作品, 2), (kdc-26.8.8, 2026-08-08 20:20:00, 2026-08-08 21:00:00, 现场互动, 观众提问与即兴表演, 3), (kdc-26.8.8, 2026-08-08 21:00:00, 2026-08-08 21:30:00, 自由交流, 活动结束与合影, 4);这里给预约表建了activity_code phone的唯一索引避免同一个手机号对同一场活动无限提交。它不算复杂的秒杀拦截但能挡住最明显的“重复点击”问题。4.3 为什么用活动编号而不是只靠 id数据库里主键id只是一个物理唯一标识运营人员关心的是“哪一场活动”。如果后端接口只暴露自增 id操作人员很容易填错。将activity_code作为业务主键之外的关键查询条件也会更安全。例如OptionalActivity findByActivityCode(String activityCode);前端页面可以直接把链接设计成/activity/kdc-26.8.8比/activity?id1更直观也更方便做缓存和统计。5. 后端接口实现5.1 创建 Spring Boot 工程后端使用 Spring Boot 3项目坐标可以命名为com.example:kisstell-backend。在backend/pom.xml中加入核心依赖?xml version1.0 encodingUTF-8? project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd modelVersion4.0.0/modelVersion parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent groupIdcom.example/groupId artifactIdkisstell-backend/artifactId version1.0.0/version namekisstell-backend/name descriptionKISS N TELL 嘉宾秀示例后端/description properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies build plugins plugin groupIdorg.springframework.boot/groupId artifactIdspring-boot-maven-plugin/artifactId /plugin /plugins /build /project如果你不使用 Lombok可以手动为实体类生成 getter、setter 和构造方法。我习惯用 Lombok 精简代码但项目中有多条数据一定要避免“toString 递归调用”所以设计实体关联时不要直接使用Data做双向关联。本文的表之间不建立 JPA 对象级强关联全部通过activity_code查询这能减少很多新手常见问题。5.2 配置文件在src/main/resources/application.properties中写入spring.application.namekisstell-backend server.port8080 spring.datasource.urljdbc:mysql://localhost:3306/kdc_show?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue spring.datasource.usernameroot spring.datasource.password你的数据库密码 spring.jpa.hibernate.ddl-autonone spring.jpa.show-sqltrue spring.jpa.open-in-viewfalse spring.jackson.default-property-inclusionnon_null这里ddl-autonone是因为我们已经手动执行了初始化 SQL。如果改成updateHibernate 会自动帮你修改表结构但生产环境发生过太多“自动加字段导致锁表”的事故不建议在非学习项目里使用。5.3 统一结果封装为了让前后端接口风格统一定义一个简单的ApiResultpackage com.example.kisstell.common; public class ApiResultT { private int code; private String message; private T data; public ApiResult() { } public ApiResult(int code, String message, T data) { this.code code; this.message message; this.data data; } public static T ApiResultT ok(T data) { return new ApiResult(0, success, data); } public static T ApiResultT fail(String message) { return new ApiResult(-1, message, null); } public int getCode() { return code; } public void setCode(int code) { this.code code; } public String getMessage() { return message; } public void setMessage(String message) { this.message message; } public T getData() { return data; } public void setData(T data) { this.data data; } }业务异常可以单独定义为了减少篇幅这里只在 Service 层直接记录错误并返回结果。更严谨的做法是定义全局异常处理器把校验异常统一翻译成ApiResult.fail。5.4 实体类实体类对应数据库中的四张表。这里给出Activity的完整代码其余实体写法类似package com.example.kisstell.entity; import jakarta.persistence.*; import java.time.LocalDateTime; Entity Table(name kdc_activity) public class Activity { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name activity_code, nullable false, length 32) private String activityCode; Column(name title, nullable false, length 128) private String title; Column(name sub_title, length 256) private String subTitle; Column(name start_time) private LocalDateTime startTime; Column(name end_time) private LocalDateTime endTime; Column(name address, length 255) private String address; Column(name cover_url, length 512) private String coverUrl; Column(name status) private Integer status; Column(name description, columnDefinition text) private String description; Column(name create_time) private LocalDateTime createTime; // getter 和 setter 省略实际代码请补全 }用 JPA 时要注意字段名统一使用驼峰命名数据库列名使用下划线。这不算强制规范但团队协作时如果能保持一致能减少大量低级错误。5.5 Repository 数据访问层定义ActivityRepository和GuestRepository、BookingRepositorypackage com.example.kisstell.repository; import com.example.kisstell.entity.Activity; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface ActivityRepository extends JpaRepositoryActivity, Long { OptionalActivity findByActivityCode(String activityCode); }package com.example.kisstell.repository; import com.example.kisstell.entity.Guest; import org.springframework.data.jpa.repository.JpaRepository; import java.util.List; public interface GuestRepository extends JpaRepositoryGuest, Long { ListGuest findByActivityCodeAndHiddenOrderBySortNoAsc(String activityCode, Integer hidden); }Spring Data JPA 的方法名解析看起来像“魔法”其实遵循一套固定规则findBy字段、And、OrderBy字段Asc。调试接口时如果发现查询条件不对第一步是检查实体字段名是否拼写正确。5.6 Service 业务层我把逻辑写在 Service 中避免 Controller 里堆大段代码。ActivityQueryService负责查询活动、嘉宾和流程package com.example.kisstell.service; import com.example.kisstell.entity.Activity; import com.example.kisstell.entity.Guest; import com.example.kisstell.repository.ActivityRepository; import com.example.kisstell.repository.GuestRepository; import com.example.kisstell.repository.ScheduleRepository; import org.springframework.stereotype.Service; import java.util.List; import java.util.Optional; Service public class ActivityQueryService { private final ActivityRepository activityRepository; private final GuestRepository guestRepository; private final ScheduleRepository scheduleRepository; public ActivityQueryService(ActivityRepository activityRepository, GuestRepository guestRepository, ScheduleRepository scheduleRepository) { this.activityRepository activityRepository; this.guestRepository guestRepository; this.scheduleRepository scheduleRepository; } public Activity getActivityByCode(String activityCode) { return activityRepository.findByActivityCode(activityCode) .orElseThrow(() - new RuntimeException(活动不存在或已下线)); } public ListGuest listGuests(String activityCode) { return guestRepository.findByActivityCodeAndHiddenOrderBySortNoAsc(activityCode, 0); } }这里直接抛出RuntimeException只是一个教学简化。真实项目中应自定义资源不存在异常并由全局异常处理器转换成 HTTP 404。预约相关的逻辑稍微复杂一点需要先检查活动是否存在再做重复手机号拦截最后保存预约记录。同时返回给前端的手机号要做脱敏处理。这里提供一个简化接口package com.example.kisstell.service; import com.example.kisstell.dto.BookingCreateRequest; import com.example.kisstell.entity.Booking; import com.example.kisstell.repository.BookingRepository; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.stereotype.Service; import java.time.LocalDateTime; Service public class BookingService { private final BookingRepository bookingRepository; public BookingService(BookingRepository bookingRepository) { this.bookingRepository bookingRepository; } public Booking create(BookingCreateRequest request) { Booking booking new Booking(); booking.setActivityCode(request.getActivityCode()); booking.setGuestName(request.getGuestName()); booking.setPhone(request.getPhone()); booking.setTicketCount(request.getTicketCount() null ? 1 : request.getTicketCount()); booking.setRemark(request.getRemark()); booking.setStatus(0); booking.setCreateTime(LocalDateTime.now()); try { return bookingRepository.save(booking); } catch (DataIntegrityViolationException e) { throw new RuntimeException(该手机号已预约过本场活动); } } }5.7 Controller 对外接口PublicController提供三个接口package com.example.kisstell.controller; import com.example.kisstell.common.ApiResult; import com.example.kisstell.dto.BookingCreateRequest; import com.example.kisstell.entity.Activity; import com.example.kisstell.entity.Booking; import com.example.kisstell.entity.Guest; import com.example.kisstell.service.ActivityQueryService; import com.example.kisstell.service.BookingService; import jakarta.validation.Valid; import org.springframework.web.bind.annotation.*; import java.util.List; RestController RequestMapping(/api) public class PublicController { private final ActivityQueryService activityQueryService; private final BookingService bookingService; public PublicController(ActivityQueryService activityQueryService, BookingService bookingService) { this.activityQueryService activityQueryService; this.bookingService bookingService; } GetMapping(/activities/{activityCode}) public ApiResultActivity activity(PathVariable String activityCode) { return ApiResult.ok(activityQueryService.getActivityByCode(activityCode)); } GetMapping(/activities/{activityCode}/guests) public ApiResultListGuest guests(PathVariable String activityCode) { return ApiResult.ok(activityQueryService.listGuests(activityCode)); } PostMapping(/bookings) public ApiResultBooking book(Valid RequestBody BookingCreateRequest request) { return ApiResult.ok(bookingService.create(request)); } }如果前端和后端属于不同域名还需要配置跨域。开发阶段最简单的方法是使用代理但如果你选择直接通过 IP 访问后端则需要添加跨域配置package com.example.kisstell.config; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(http://localhost:5173) .allowedMethods(GET, POST, OPTIONS) .allowedHeaders(*); } }注意生产环境不要用星号允许所有来源更不要把后台管理接口和公开接口放在同一个安全级别。5.8 参数校验BookingCreateRequest可以对手机号格式做前置校验而不是等数据入库后再报错package com.example.kisstell.dto; import jakarta.validation.constraints.*; public class BookingCreateRequest { NotBlank(message 活动编号不能为空) private String activityCode; NotBlank(message 请填写称呼) private String guestName; NotBlank(message 手机号不能为空) Pattern(regexp ^1[3-9]\\d{9}$, message 手机号格式不正确) private String phone; Min(value 1, message 至少预约1人) Max(value 5, message 单次最多预约5人) private Integer ticketCount; Size(max 200, message 备注不能超过200字) private String remark; // getter 和 setter 省略 }这种校验逻辑在正式项目中建议一直保留在 Service 层或独立领域校验里而不完全依赖前端不可信输入。5.9 后端接口总览启动后可访问的接口如下方法路径作用GET/api/activities/kdc-26.8.8获取活动基本信息GET/api/activities/kdc-26.8.8/guests获取嘉宾列表GET/api/activities/kdc-26.8.8/schedules获取活动流程可继续补充POST/api/bookings提交预约在写正式项目时建议再增加查询预约和取消预约的接口。取消预约并不是直接删除数据库而是把状态改成已取消便于后续统计实际到场率。6. 前端页面实现前端使用 Vue 3 Vite 编写。如果你已经做好了后端接口前端的工作就是把后端数据渲染成用户能看懂的页面。6.1 Vite 项目初始化在frontend目录下创建package.json{ name: kiss-n-tell-show-frontend, private: true, version: 1.0.0, scripts: { dev: vite, build: vite build, preview: vite preview }, dependencies: { axios: ^1.6.0, vue: ^3.4.0, vue-router: ^4.3.0 }, devDependencies: { vitejs/plugin-vue: ^5.0.0, vite: ^5.0.0 } }执行npm install如果网络下载较慢可以使用 npm 镜像但不要随意安装来历不明的第三方包。6.2 Vite 代理配置开发环境直接请求后端会碰到跨域问题最简单的办法是在vite.config.js中配置代理。import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { host: localhost, port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })这样前端请求/api/activities/kdc-26.8.8时Vite 会把请求转发到http://localhost:8080/api/activities/kdc-26.8.8。6.3 首页活动展示首页是整个系统的门面。这里用一个组合式 API 实现数据加载代码结构如下template div v-ifloading活动加载中.../div div v-else-iferror{{ error }}/div div v-else classactivity-page h1{{ activity.title }}/h1 p classsubtitle{{ activity.subTitle }}/p p时间{{ formatTime(activity.startTime) }}/p p地点{{ activity.address }}/p h2嘉宾阵容/h2 div classguest-list div v-forguest in guests :keyguest.id classguest-card h3{{ guest.name }}/h3 span{{ guest.role }}/span p{{ guest.intro }}/p /div /div h2活动流程/h2 ul li v-foritem in schedules :keyitem.id {{ item.startTime }} - {{ item.itemName }} /li /ul form submit.preventsubmitBooking input v-modelform.guestName placeholder怎么称呼你 required / input v-modelform.phone placeholder手机号 required / input v-model.numberform.ticketCount typenumber min1 max5 / textarea v-modelform.remark placeholder备注/textarea button typesubmit :disabledsubmitting提交预约/button /form /div /template对应的脚本部分script setup import { ref, onMounted } from vue import axios from axios const activityCode kdc-26.8.8 const loading ref(true) const error ref() const activity ref({}) const guests ref([]) const schedules ref([]) const submitting ref(false) const form ref({ guestName: , phone: , ticketCount: 1, remark: }) async function loadData() { try { const activityResp await axios.get(/api/activities/${activityCode}) activity.value activityResp.data.data const guestsResp await axios.get(/api/activities/${activityCode}/guests) guests.value guestsResp.data.data loading.value false } catch (e) { error.value 活动信息加载失败 loading.value false } } async function submitBooking() { submitting.value true try { await axios.post(/api/bookings, { activityCode, ...form.value }) alert(预约成功) } catch (e) { alert(e.response?.data?.message || 预约失败) } finally { submitting.value false } } function formatTime(timeStr) { if (!timeStr) return return timeStr.replace(T, ).substring(0, 16) } onMounted(() { loadData() }) /script这里的activityCode相当于整个页面的业务主键。以后要做第二期活动只需要新增一个活动页面或把activityCode替换为kdc-27.1.1不需要对系统做大型修改。前端还应该增加一点基本样式但这不是核心逻辑可以按自己的产品风格来设计。需要注意的是不要把手机号等预约数据直接输入到 JavaScript 日志中避免隐私泄露。7. 运行与验证7.1 启动 MySQL先确保本机 MySQL 已启动然后执行mysql -uroot -p init.sql执行成功后再启动后端cd backend mvn spring-boot:run如果看到类似下面的日志说明后端启动成功Tomcat started on port 8080 Started KisstellApplication in 2.1 seconds7.2 启动前端打开一个终端cd frontend npm install npm run dev浏览器访问http://localhost:5173应该能看到活动标题、嘉宾列表和预约表单。7.3 联调测试预约接口先用 curl 做一个接口验证curl -X POST http://localhost:8080/api/bookings \ -H Content-Type: application/json \ -d {activityCode:kdc-26.8.8,guestName:测试用户,phone:13800138000,ticketCount:1,remark:测试预约}预期返回{ code: 0, message: success, data: { id: 1, activityCode: kdc-26.8.8, guestName: 测试用户, phone: 13800138000, ticketCount: 1, status: 0, createTime: 2026-07-01T12:00:00 } }再执行一次同样请求会触发唯一索引返回错误提示{ code: -1, message: 该手机号已预约过本场活动, data: null }8. 常见问题与排查思路我把后台和前端联调时最容易遇到的问题整理成下表问题现象常见原因解决思路启动类报错“Unable to find class”JDK 版本过低使用 JDK 17Maven 依赖下载慢网络受限使用国内 Maven 镜像MySQL 连接失败密码错误或库不存在检查 application.properties访问接口报 404Controller 包扫描路径不对将启动类放在根包下前端访问接口跨域缺少 CORS 配置或代理失效推荐使用 Vite proxy 方案同一手机号重复提交数据库唯一索引没有生效检查表结构是否执行成功日期显示 T 字符串后端 LocalDateTime 默认格式化前端做格式化处理页面有数据但嘉宾展示为空嘉宾表 activity_code 不一致确认前后端使用同一个活动编号npm run dev 报 Node 版本不支持Node 版本低于 18升级 Node.js排查时按“网络层 → 数据层 → 代码层”的顺序来。先看浏览器 Network 里的请求状态码如果请求是 500只看前端控制台是不够的必须去后端日志中找异常堆栈。9. 工程化建议与生产环境注意事项9.1 配置分层与隔离开发和生产的数据库连接串、密码、日志级别不应该完全一样。建议通过spring.profiles.activedev/prod来区分# application-prod.properties spring.datasource.password${DB_PASSWORD}密码不要硬编码在项目中而是从环境变量或配置中心读取。对小型项目而言最节省成本的方式是服务器环境变量。9.2 接口安全公开接口最容易忽略以下安全问题预约接口没有限流导致恶意刷票。管理后台没有认证任何人可访问。收集手机号但页面没有隐私说明。返回数据未脱敏后台查看预约名单直接显示完整手机号。日志包含手机号存在泄露风险。正式上线时至少要给管理接口增加登录认证给公开接口增加简单频控。如果使用了 Spring Security不要把静态资源和后端 API 放在同一套全放行规则中。9.3 数据备份和回滚意识本次项目虽然简单但是属于生产活动时也会储存用户真实提交的报名数据。上线前要约定好数据库每天备份一次。活动开始后不要随意删除预约记录。如果修改嘉宾资料要记录更新时间。在删除或导出手机号前评估是否有必要保留。我见过一些运营同学为了“清掉测试数据”直接执行全表 DELETE结果误删正式报名的案例。正确做法是先 SELECT 确认数据量再在备份库中验证删除 SQL最后按活动编码删除。9.4 活动合规提醒如果这个活动是真实线下活动尤其是售票或营业性演出运营方需要自行了解当地活动审批和内容安全管理要求。技术系统只负责信息展示和预约并不能代替活动本身的报批义务。从技术侧来看系统若面向公众收集手机号必须避免过度收集建议在表单中明确说明用途并在页面上提供隐私政策入口或活动主办方联系方式。对于公开活动页面中国大陆服务器部署的网站仍需按平台要求完成 ICP 备案这里不详细展开但正式上线前必须确认。9.5 如何优雅地扩展成多期活动这套系统最大的
返回列表