ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

活动分组与评分系统开发实战:从Excel到Spring Boot高并发方案

活动分组与评分系统开发实战:从Excel到Spring Boot高并发方案 相信不少开发者在面对“大型活动现场”、“批量人员面试”、“多人分组评审”这类需求时第一反应都是这不就是一张 Excel 表能搞定的事吗但当你真正拿到业务方的需求比如“按组别批量抽签排序”、“几十个评委同步打分”、“现场大屏实时展示进度”、“进度数据需要汇总导出”就会发现纯手工表格根本忙不过来。最近时装周、模特大面试这类活动又进入密集筹备期很多技术朋友也在后台问如果让我用代码实现一套“模特大面试”的分组、签到、评分、汇总系统到底该怎么做为了避免误导这里特别说明我并不是时装周内部技术人员也不是在讲某个真实活动的后台源码。本文只是结合“模特大面试”这种大型线下多人评审场景抽象出一套通用的活动分组与评分管理系统开发思路。无论你接到的需求是模特面试、演员海选、校园招聘还是比赛评审这套流程都成立。1. 场景分析与系统设计1.1 这类活动到底需要什么系统先说业务场景。一场大规模的面试或选拔活动通常具备以下特点候选人数量大动辄几百人甚至上千人。需要分成多个组别每组有独立的面试顺序。现场需要签到签到结果直接影响是否参评。多位评委需要在候选人展示后进行打分。主办方希望实时看到进度结束之后能快速导出统计结果。这种场景如果用人工管理最大的问题是“同步成本过高”。几十个评委的纸质评分表要二次录入几百人的签到表要反复核对现场调度只能靠对讲机吼。稍微出一点错后续的数据汇总就全乱套。因此一个活动面试管理系统的核心价值就是把下面几条链路数字化候选人资料统一维护Excel 批量导入。分组与出场顺序按规则自动生成。现场签到状态实时反馈。评委打分支持多端操作。结果按组别实时汇总。数据可导出方便后续复试通知。1.2 核心业务流程在写代码之前先梳理完整流程。这一步特别重要很多新手喜欢直接建表写接口结果做到一半发现状态字段根本不够用。一套模特大面试信息系统最核心的状态流转是下面这样候选人导入系统初始状态为“待分组”。管理员执行分组和排序候选人状态变为“待面试”。面试当天候选人在入口处签到状态变为“已签到”。候选人在对应组别接受评委打分状态变为“已评分”。全组结束后可查看评分明细最终生成汇总结果。如果把状态做成字段推荐使用数字或者字典码。不要直接在业务表里存中文名称否则后期统计和接口判断都会非常痛苦。0 - 待分组 1 - 待面试 2 - 已签到 3 - 已评分 4 - 已淘汰可选 5 - 已通过可选1.3 技术选型与工程拆分这种系统并不需要特别高深的技术架构常规的单体应用加前端页面就足够支撑几百人的场次。推荐一套非常稳妥的组合后端Spring Boot 2.x / 3.x持久层MyBatis-Plus 或 Spring Data JPA数据库MySQL 5.7 或 8.0前端Vue 3 Element Plus导入导出Easy Excel 或 Apache POI现场大屏前端定时轮询接口即可版本说明上述技术栈的版本需要根据你的项目实际情况调整。比如你的项目是 JDK 8那 Spring Boot 3.x 就无法直接使用需要退回到 Spring Boot 2.x。本文示例以常见环境为例重点演示业务设计思路而不是死磕版本号。2. 数据库表结构设计2.1 候选人表候选人表负责保存所有基础资料。在模特面试场景中通常会包含姓名、性别、身高、体重、三围、联系方式、照片链接等字段。但要注意不是所有字段都必须进入主表过于个性化的资料可以拆成扩展表。CREATE TABLE candidate ( id bigint(20) NOT NULL AUTO_INCREMENT, candidate_no varchar(32) NOT NULL COMMENT 候选人编号如 M001, name varchar(64) NOT NULL COMMENT 姓名, gender tinyint(4) DEFAULT NULL COMMENT 性别1男 2女, height decimal(5,2) DEFAULT NULL COMMENT 身高 cm, weight decimal(5,2) DEFAULT NULL COMMENT 体重 kg, phone varchar(20) DEFAULT NULL COMMENT 联系电话, photo_url varchar(255) DEFAULT NULL COMMENT 照片地址, group_id bigint(20) DEFAULT NULL COMMENT 组别ID, order_no int(11) DEFAULT NULL COMMENT 组内出场顺序, status tinyint(4) NOT NULL DEFAULT 0 COMMENT 状态 0待分组 1待面试 2已签到 3已评分, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_group_status (group_id, status), KEY idx_candidate_no (candidate_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT候选人表;这里加了两个索引idx_group_status用于组别和状态联合查询idx_candidate_no用于按编号精确查找。候选人编号建议用业务编号而不是数据库自增 ID因为数据库自增 ID 容易暴露系统数据量而且现场人员报号时M001这种编号比1024好读得多。2.2 分组表分组表负责维护一共有多少组以及每个组的名称、所属场次、评委关联信息。CREATE TABLE interview_group ( id bigint(20) NOT NULL AUTO_INCREMENT, group_name varchar(64) NOT NULL COMMENT 组名称如 A组、B组, session_id bigint(20) DEFAULT NULL COMMENT 场次ID, interview_room varchar(64) DEFAULT NULL COMMENT 面试房间, start_time datetime DEFAULT NULL COMMENT 计划开始时间, end_time datetime DEFAULT NULL COMMENT 计划结束时间, status tinyint(4) DEFAULT 0 COMMENT 0未开始 1进行中 2已结束, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT面试组别表;分组表不要和候选人表合并成冗余字段。虽然候选人表里已经有group_id但分组自身信息需要独立的表维护否则后续要改组名、调整房间号就会非常麻烦。2.3 评委评分表评分表是整张表结构里最重要的部分。设计上要注意一个核心点一个候选人会被多个评委打分所以评分记录表不能只存一个总分而应该保存“谁在什么维度给哪位候选人打了多少分”。先看维度设计。不同活动的评分维度不一样。模特面试可能会分成“舞台表现”、“镜头感”、“形体比例”、“综合印象”技术面试则会分成“基础知识”、“项目经验”、“沟通表达”。更通用的做法是建一张评分维度配置表然后把维度配置与候选人分数的关系做成明细表。CREATE TABLE score_detail ( id bigint(20) NOT NULL AUTO_INCREMENT, candidate_id bigint(20) NOT NULL COMMENT 候选人ID, judge_id bigint(20) NOT NULL COMMENT 评委ID用户表, group_id bigint(20) DEFAULT NULL COMMENT 组别ID, dimension_id bigint(20) DEFAULT NULL COMMENT 评分维度ID, dimension_name varchar(64) DEFAULT NULL COMMENT 评分维度名称冗余方便导出, score decimal(5,2) DEFAULT NULL COMMENT 具体得分, comment varchar(500) DEFAULT NULL COMMENT 评语, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_candidate (candidate_id), KEY idx_judge (judge_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT评分明细表;为什么要把dimension_name冗余保存因为如果后续维度名称发生变化历史评分数据的展示就会不一致。对这种低频修改的业务字段适当冗余能大幅减少统计时的多表关联。2.4 评分配置实体如果你的系统采用固定维度也可以直接建一张配置表。CREATE TABLE score_dimension ( id bigint(20) NOT NULL AUTO_INCREMENT, dimension_name varchar(64) NOT NULL, full_score decimal(5,2) NOT NULL DEFAULT 10.00 COMMENT 满分, sort_no int(11) DEFAULT 0, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; INSERT INTO score_dimension (dimension_name, full_score, sort_no) VALUES (形体比例, 10.00, 1), (舞台表现, 10.00, 2), (镜头感, 10.00, 3), (综合印象, 10.00, 4);这套设计的扩展性在于如果业务方临时决定增加一项“才艺附加分”只需要在配置表里加一条数据不需要改动代码结构。3. 后端核心功能实现3.1 批量导入候选人现场录入几百个候选人不现实所以导入功能几乎必做。Easy Excel 是目前比较推荐的方案API 简单内存占用也小于原生 POI。// 文件路径src/main/java/com/example/interview/controller/CandidateController.java PostMapping(/candidate/import) public RString importCandidates(RequestParam(file) MultipartFile file) { // 读取 Excel 数据注意使用监听器模式避免大文件 OOM EasyExcel.read(file.getInputStream()) .head(CandidateExcelDTO.class) .registerReadListener(new CandidateImportListener(candidateService)) .sheet() .doRead(); return R.ok(导入成功); }对应的 DTO 和数据监听器需要自己实现。在导入逻辑里最关键的是三点。第一需要做重复校验通常以手机号或身份证号作为唯一标识。第二候选人编号必须自动生成不能用 Excel 里的自填数据否则容易编号冲突。第三导入完成后最好返回“成功多少条、失败多少条、失败原因”的统计信息。public class CandidateExcelDTO { ExcelProperty(value 姓名, index 0) private String name; ExcelProperty(value 性别, index 1) private String gender; ExcelProperty(value 身高(cm), index 2) private BigDecimal height; ExcelProperty(value 体重(kg), index 3) private BigDecimal weight; ExcelProperty(value 联系电话, index 4) private String phone; ExcelProperty(value 备注, index 5) private String remark; }需要特别提醒Excel 导入的性别列业务方常常填“男/女”或“M/F”入库前必须统一转换成数字字典。否则后面前端展示、逻辑判断都会是灾难。3.2 自动分组与组内随机排序手动往 Excel 里拖拽分区很容易出错尤其是候选人数量超过 200 人时。这块用代码处理是最高效的。分组规则通常有两种按人数平均分成 N 组或按固定组容量自动创建组别。下面这段示例演示的是“按组容量平均分配”// 文件路径src/main/java/com/example/interview/service/impl/CandidateServiceImpl.java public void autoGroup(Long sessionId, int groupSize) { // 1. 查询该场次下所有待分组候选人 ListCandidate candidateList candidateMapper.selectList( new LambdaQueryWrapperCandidate() .eq(Candidate::getSessionId, sessionId) .eq(Candidate::getStatus, 0) .orderByAsc(Candidate::getId)); if (candidateList.isEmpty()) { return; } // 2. 计算组数 int totalCount candidateList.size(); int groupCount (int) Math.ceil((double) totalCount / groupSize); // 3. 将候选人随机打散 Collections.shuffle(candidateList); // 4. 如果该场次还没有分组记录则先创建组 ListInterviewGroup groupList createGroupIfNotExist(sessionId, groupCount); // 5. 循环放置候选人 for (int i 0; i candidateList.size(); i) { Candidate candidate candidateList.get(i); int groupIndex i / groupSize; if (groupIndex groupList.size()) { groupIndex groupList.size() - 1; } int orderNo i % groupSize 1; candidate.setGroupId(groupList.get(groupIndex).getId()); candidate.setOrderNo(orderNo); candidate.setStatus(1); // 待面试 candidateMapper.updateById(candidate); } }这里面有几个容易踩坑的细节。第一Collections.shuffle做的是伪随机如果业务方对随机性有很高要求比如需要防止连续好几个组都是同一机构的人建议用更复杂的洗牌算法或导入时先按机构交错排布。第二平均分配场景下最后一组人数可能不足groupSize前端展示时要允许组别实际人数不同。第三重新分组时必须先清空旧的group_id和order_no否则会残留历史数据。3.3 现场签到签到这个动作看起来非常简单但放在高并发现场就有讲究了。模特大面试这类活动通常上午 9 点集中签到短时间内可能几十个人同时排队操作。如果签到时业务逻辑比较重比如还要执行“发送短信”、“刷新大屏缓存”接口响应就会变慢。推荐的做法是签到接口只做状态更新其他动作通过异步线程或消息队列处理。// 文件路径src/main/java/com/example/interview/controller/CheckinController.java PostMapping(/checkin) public RString checkin(RequestBody CheckinRequest request) { // 必须做幂等处理如果已签到直接返回签到成功但不重复更新 Candidate candidate candidateService.getByCandidateNo(request.getCandidateNo()); if (candidate null) { return R.fail(候选人不存在); } if (candidate.getStatus() 2) { return R.fail(该候选人已签到请勿重复操作); } candidateService.checkin(candidate.getId()); return R.ok(签到成功); }幂等是签到接口最容易被忽略的问题。现场网络波动时前端可能会出现超时重试。如果后端不判断状态候选人就会被重复签到两次后续统计就出现“签到数大于总人数”的尴尬结果。所以进入候选人编号时建议同时展示候选人姓名和组别信息。这样操作人员扫码或输入编号后能快速确认是不是本人避免签错。3.4 评委打分与自动汇总评委打分有两种模式模式一候选人挨个进入房间评委在各自设备上提交分数。模式二一组候选人同时展示评委逐一打分。不管哪种模式后端保存的核心实体都是“评委-候选人-维度-分数”四元组。打分接口要注意两个问题。第一是防止评委重复提交。可以用judge_id candidate_id作为唯一约束。第二是允许修改但必须保留修改记录。对严肃的评选活动来说事后追溯是常见需求。先创建防重复索引ALTER TABLE score_detail ADD UNIQUE KEY uk_judge_candidate (judge_id, candidate_id);然后编写提交评分接口// 文件路径src/main/java/com/example/interview/controller/ScoreController.java PostMapping(/score/submit) Transactional(rollbackFor Exception.class) public RString submitScore(RequestBody ScoreSubmitRequest request) { // 校验评委对该候选人是否已提交过 Long existCount scoreDetailMapper.selectCount( new LambdaQueryWrapperScoreDetail() .eq(ScoreDetail::getJudgeId, request.getJudgeId()) .eq(ScoreDetail::getCandidateId, request.getCandidateId())); if (existCount 0) { // 业务上可以直接更新也可以拒绝按需求而定 scoreDetailMapper.delete( new LambdaQueryWrapperScoreDetail() .eq(ScoreDetail::getJudgeId, request.getJudgeId()) .eq(ScoreDetail::getCandidateId, request.getCandidateId())); } // 批量保存多个维度分数 for (ScoreDimensionDTO dimension : request.getDimensions()) { ScoreDetail detail new ScoreDetail(); detail.setCandidateId(request.getCandidateId()); detail.setJudgeId(request.getJudgeId()); detail.setGroupId(request.getGroupId()); detail.setDimensionId(dimension.getDimensionId()); detail.setDimensionName(dimension.getDimensionName()); detail.setScore(dimension.getScore()); scoreDetailMapper.insert(detail); } // 更新候选人状态 candidateService.updateStatus(request.getCandidateId(), 3); return R.ok(提交成功); }这里做了先删后插的处理看起来稍显笨重但胜在逻辑简单稳定。实际项目中更推荐使用INSERT ... ON DUPLICATE KEY UPDATE语法或者 MyBatis-Plus 的saveOrUpdate。不过那样需要额外处理维度维度名称变化的问题。3.5 成绩汇总与导出所有评委提交完成后系统需要计算最终结果。常见的汇总规则有去掉最高分和最低分取平均分。所有评委取平均分。按权重计算加权平均分。汇总时按candidate_id分组查询即可不需要写太复杂的 SQL。下面这段代码会把所有评委对某个候选人的分数累加并计算平均值。public ListCandidateScoreVO calculateFinalScore(Long groupId) { // 查询所有已完成评分的候选人 ListCandidate candidates candidateMapper.selectList( new LambdaQueryWrapperCandidate() .eq(Candidate::getGroupId, groupId) .eq(Candidate::getStatus, 3)); ListCandidateScoreVO resultList new ArrayList(); for (Candidate candidate : candidates) { ListScoreDetail scoreDetails scoreDetailMapper.selectList( new LambdaQueryWrapperScoreDetail() .eq(ScoreDetail::getCandidateId, candidate.getId())); // 按维度维度统计平均分 MapString, DoubleSummaryStatistics statisticsMap scoreDetails.stream() .collect(Collectors.groupingBy( ScoreDetail::getDimensionName, Collectors.summarizingDouble(ScoreDetail::getScore))); // 构造结果对象 CandidateScoreVO vo new CandidateScoreVO(); vo.setCandidate(candidate); for (Map.EntryString, DoubleSummaryStatistics entry : statisticsMap.entrySet()) { vo.getDimensionAvgMap().put(entry.getKey(), entry.getValue().getAverage()); } resultList.add(vo); } // 按平均分降序排序 resultList.sort((o1, o2) - Double.compare(o2.getTotalAvg(), o1.getTotalAvg())); return resultList; }这里需要注意一个问题平均值计算结果的精度。建议四舍五入保留两位小数并且统计时明确“平均分字段”的含义。很多业务方会要求“去掉最高分最低分后取平均”这个逻辑可以在 SQL 或 Java 中实现但无论如何都要提前和业务方确认否则开发完再改算法会牵一发动全身。4. 前端页面核心逻辑4.1 候选人列表与分组展示前端页面最核心的是候选人列表和组别切换。候选人数量多时不要一次性加载全部表格数据后端要做分页查询。面试官最常见的操作是选择一个组然后按顺序浏览候选人。这个时候列表页默认排序就是group_id升序加order_no升序。候选人状态应该用不同标签显示比如待签到是灰色已签到是绿色已评分是蓝色。4.2 评分页面评分页面是大面试系统中最需要流畅度的页面。如果每个维度都做一次网络请求评委评分时会感到明显卡顿。推荐前端暂存分值在点击“提交”时一次性发送全部数据。页面元素大致包含几个区域候选人信息区域展示照片、姓名、编号、身高体重等基础资料。维度评分区域每个维度对应一个数字输入框或滑杆需要显示满分限制。评委评语区可做文本域输入。提交按钮点击后统一提交后端接口。在 Vue 3 Element Plus 中表单校验可以用rules完成。给每个维度的分数添加校验规则保证分数在 0 到满分之间并且必填。template div el-card v-fordim in dimensionList :keydim.id template #header span{{ dim.dimensionName }}/span el-tag满分 {{ dim.fullScore }}/el-tag /template el-input-number v-modelscoreForm[dim.id] :min0 :maxdim.fullScore :precision2 >async function loadProgress() { const res await getGroupProgress(); groupStats.value res.data; } onMounted(() { loadProgress(); timer setInterval(loadProgress, 10000); }); onBeforeUnmount(() { clearInterval(timer); });轮询时间是很有讲究的。太短会造成后端压力大太长则进度刷新不及时。10 秒是比较稳妥的数值。如果现场要求更强实时性可以考虑 WebSocket 推送但服务器端需要额外维护长连接复杂度会高不少。对 200-500 人的单向面试场景轮询已经足够了。5. 典型问题与排查思路5.1 导入 Excel 时出现乱码或数据错位原因大概率是 Excel 模板格式和ExcelProperty映射不一致。比如模板第一列是“姓名”代码却把第二列映射给了name。排查步骤确认模板表头列顺序。确认ExcelProperty中的index从 0 开始。检查 Excel 文件是否为.xlsx格式旧版.xls需要引入对应依赖或另存为新版格式。5.2 候选人重复签到原因通常是没有做幂等校验或者前端的超时重试机制导致请求重复发送。解决思路后端更新前查询当前status已签到则直接返回。在数据库层面增加唯一约束或状态机校验。前端提交后禁用按钮收到响应后再恢复。5.3 大屏数据不刷新原因一般是定时器没有正确销毁或者接口返回数据格式与前端预期不一致。排查步骤打开浏览器控制台确认轮询接口是否正常响应。查看接口返回 JSON 结构确认data字段是否存在。检查组件卸载时是否清除了定时器。5.4 评分提交后总分为空原因大概率是候选人状态没有被更新到“已评分”或者查询汇总接口时过滤条件写了status 3但实际状态还停留在2。解决思路在提交评分的事务内调用状态更新方法保证“评分明细写入”和“候选人状态更新”同时成功或同时失败。问题现象常见原因排查与解决思路Excel 导入乱码模板列顺序不匹配检查 ExcelProperty 的 index 映射签到后人数统计翻倍未做幂等处理增加状态判断与唯一约束评分接口超时维度逐个提交、事务过大前端合并提交后端循环批量插入汇总结果和手算不一致平均分算法或精度问题统一使用两位小数并与业务方核对算法大屏数据不更新定时器未清理或接口报错检查控制台网络请求和组件生命周期6. 工程化与生产环境建议6.1 候选人编号规范候选人编号应当由系统生成不要依赖 Excel 手动填写。建议格式为“场次前缀 组号 三位流水号”例如M-A-001。这样工作人员看到编号就能立即判断候选人属于哪一场哪一组而不需要去数据库里查id。6.2 权限与数据安全这种面试系统会涉及候选人的联系方式、身高体重等个人敏感信息。生产环境中必须注意管理员端和评委端使用独立账号体系。评委只能看到自己所在组的候选人不能全量导出候选人电话。联系方式导出需要单独审批权限。接口必须做登录校验不能只靠前端隐藏按钮来实现防越权。对用户密码使用 BCrypt 等安全哈希算法加密存储。定期备份数据库尤其是在每天活动结束后增加一次全量备份。在业务代码层面后端获取当前登录评委时应从 Session 或 Token 中解析用户 ID而不是信任前端传来的judgeId参数。否则恶意用户可以通过篡改请求参数替别人打分。正确示例如下// 从登录上下文拿评委ID Long judgeId SecurityUtils.getCurrentUserId(); ScoreSubmitRequest safeRequest new ScoreSubmitRequest(); safeRequest.setCandidateId(request.getCandidateId()); safeRequest.setGroupId(request.getGroupId()); safeRequest.setDimensions(request.getDimensions());6.3 事务使用要点任何涉及状态流转的接口都必须添加事务。特别是在“评分提交”和“候选人状态更新”这两个操作之间如果中途抛异常必须要回滚。事务加在 Service 层方法上而不是 Controller 方法上这是很多初学者容易写错的地方。Service public class ScoreServiceImpl { Transactional(rollbackFor Exception.class) public void submitScore(ScoreSubmitRequest request) { // 1. 写评分明细 // 2. 更新候选人状态 // 3. 如果第2步失败第1步自动回滚 } }6.4 日志与可追溯性现场面试系统最怕“说不清”。某个评委修改了分数但后期对不上如果没有日志就说不清楚是谁在什么时间改的。建议在关键节点记录日志候选人导入日志记录文件名称、操作人、导入数量。分组操作日志记录分组规则、操作人、分组后的组别数量。打分日志记录评委 ID、候选人 ID、修改前后的分数值。签到日志记录签到时间、签到操作人。打分修改场景可以建立一张score_audit_log表每次修改都往里面写一条旧值和新值。6.5 性能优化思路一个几百人的现场面试系统真的不需要特别复杂的性能方案但也不是说可以完全忽略。经验上有几点值得提前注意导入 Excel 不要使用同步方式处理超大文件超过 500 行时建议用异步任务避免浏览器请求超时。候选人列表查询一定要分页。几百条数据全量返回对前端渲染是负担而且大面试现场会频繁刷新页面。导出报表不要做成页面同步下载可以使用后台任务生成文件再提供下载链接。数据库连接池参数要调整。现场评委同时在线会达到几十人默认的 HikariCP 连接池最小配置很可能不够用。6.6 上线前检查清单这份清单直接在项目上线前逐项核对[ ] 数据库是否已备份[ ] Excel 模板是否由产品确认过[ ] 候选人导入是否做了重复校验[ ] 签到接口是否确认幂等[ ] 评委打分接口是否有唯一约束[ ] 大屏轮询间隔是否合适[ ] 评委是否只能看到自己组的数据[ ] 联系方式导出接口是否需要权限审批[ ] 日志是否完整记录了关键操作人与操作时间[ ] 是否准备了一份现场应急联系人名单[ ] 是否提前进行了模拟演练包括断网恢复、数据修正场景这些项目不只是技术债问题直接关系到活动现场能不能顺利进行。面试活动的时间窗口非常严格技术系统一旦出现问题几百号人等在门外场面很难控制。7. 常见衍生需求扩展模特大面试这类系统上线之后业务方往往会提出下一轮需求。有些需求非常合理最好在设计表结构时就留出扩展空间。7.1 照片与视频管理如果候选人资料需要包含走秀视频前端会有视频上传需求。上传文件不要直接存数据库 BLOB 字段应该上传到文件服务器或 OSS数据库只保留 URL。上传时注意文件大小限制视频通常要切成不超过 100MB 的分片。7.2 多轮面试模特可能会经历初试、复试、终试三轮环节。每一轮的分组、评委、打分维度可能都不一样。表结构设计时要区分round字段或者在候选人主表之外单独建“候选人轮次表”。如果直接在候选人表上做覆盖更新后期想复盘历史数据几乎不可能。7.3 结果通知面试结束后要给通过的候选人发短信或邮件。建议把所有待通知候选人放到一张通知任务表中由定时任务统一发送发送状态实时更新。不要在主流程里循环发送短信否则会严重阻塞接口响应。CREATE TABLE notify_task ( id bigint(20) NOT NULL AUTO_INCREMENT, candidate_id bigint(20) NOT NULL, notify_type tinyint(4) NOT NULL COMMENT 1短信 2邮件, content varchar(1000) DEFAULT NULL, status tinyint(4) DEFAULT 0 COMMENT 0待发送 1成功 2失败, retry_count int(11) DEFAULT 0, send_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;7.4 临时加人和换组现实使用中经常会出现候选人临时到场但不在名单中或者因为个人原因需要跨组调整。系统必须保留“人工换组”能力。换组操作需要管理员权限同时记录换组日志。换组后候选人组内序号会自动调整到当前组末尾这是比较符合直觉的规则。不要小看这个功能。如果系统不支持灵活的人工干预那现场就只能靠数据库改数据风险非常高。8. 最后的一些建议“模特大面试”也好其他现场评选活动也好信息系统本质上是一个工具核心目标是把活动现场的有序性、公正性和效率提上来。如果你是第一次做这类系统不要急于堆功能先把候选人导入、分组、签到、评分、汇总这几条主链路跑通。主链路稳定比一百个花哨页面更重要。代码实现上本文提供的思路很多地方还属于伪代码级别实际项目使用时要结合自己团队的框架版本稍作调整。比如 Spring Boot 3.x 之后javax包名变成了jakarta拦截器配置也有所变化MyBatis-Plus 的新版本对 Lambda 查询包装器也更严格容易踩泛型推导的坑。这些细节建议你在项目启动前拉通验证一遍别到现场才临时排查。下一步你可以继续深入的方向包括基于 Redis 做现场实时排行榜和性能优化。学习 WebSocket 长连接实现大屏秒级更新。引入消息队列削峰填谷应对集中签到场景。完善接口权限管理用 Spring Security 或 Sa-Token 搭建细粒度权限控制。如果本文对你有帮助可以收藏备用。也欢迎在评论区聊聊你在做活动管理系统时遇到的最头疼的问题大家一起交流排坑。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进