拓冰建站拓冰建站
首页 / 资讯中心 / 正文

预约设置模块实战:Excel导入、日历展示与单日设置的数据一致性设计

1. 先想清楚一件事Excel 导入、日历展示、单日设置为什么必须共用同一套数据1.1 三个入口本质上是“日期 × 号源 × 状态”这个三元组的三种操作姿态我做过的几个带预约功能的系统从体检预约到门诊排班再到健身房私教课时最后都落到了同一个核心问题上某一天到底能不能约、能约多少人、这一天是正常开放还是停诊休息。说白了预约设置模块管理的就是“日期、号源、状态”这三个纬度。Excel 批量导入、日历展示、单日设置这三个功能看起来是三个独立页面实际是对同一份数据的三种编辑视角。Excel 适合批量录入未来一个月的排班日历适合让人一眼看出哪几天满了、哪几天闲着单日设置适合临时把某个日期的号源改一下。如果这三者各自建表、各自维护很快就会出现同一个日期在 Excel 里显示开放、在日历里显示停诊的诡异情况。所以做这个模块之前先得把数据模型统一起来。1.2 模块边界这个模块不该管什么有些团队容易把预约设置模块做成一个“大杂烩”。预约设置只负责“配置每一天的接待能力”至于真正产生一条预约记录、扣减号源、发送通知那是预约下单模块的事。我把这条边界卡得很死设置模块只管日期、总号源、已用号源、状态、更新时间。已用号源是下单模块写入的设置模块只能读取不能修改。这样拆的好处是职责清晰后期无论是做批量导入还是做日历展示都不用担心连带影响下单逻辑。1.3 一条必须提前确认的业务规则覆盖策略开工前一定要跟业务方确认清楚Excel 导入时如果遇到系统里已经存在的日期是覆盖原设置还是跳过我见过最典型的返工案例就是这里。产品经理一开始说“直接覆盖就行”结果运营导了一份只填了部分日期的表格把已经手动调好的日期全部重置了。比较稳妥的做法是导入页面提供“覆盖”和“跳过”两个选项默认选跳过。如果不是明确要重新铺一遍排班表就应该让已存在的日期保留原值。这个规则同时影响表结构和导入逻辑所以放在需求阶段就要敲定。2. 表结构设计一张“每日一行”的主表撑起三种操作模式2.1 为什么以日期为唯一锚点预约设置的最小单位就是一个自然日。不管日历展示还是单日设置操作对象都是某一天。所以主表直接用日期作为业务唯一键一张表就够。下面是我常用的建表语句CREATE TABLE appointment_setting ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, setting_date DATE NOT NULL COMMENT 预约日期, week_day TINYINT NOT NULL COMMENT 星期几: 1-7, total_slots INT NOT NULL DEFAULT 0 COMMENT 总号源数, used_slots INT NOT NULL DEFAULT 0 COMMENT 已用号源数, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态: 1开放 0停诊, is_holiday TINYINT NOT NULL DEFAULT 0 COMMENT 是否节假日: 1是 0否, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_setting_date (setting_date) ) ENGINE InnoDB COMMENT 预约设置表;setting_date加了唯一索引从数据库层面防止同一天出现两条设置记录。导入逻辑里就算没有提前查重真正入库时也会被唯一索引拦一道。2.2 为什么必须同时记录 total_slots 和 used_slotstotal_slots是配置的号源上限used_slots是已经约出去的数量。有人会问已用号源查预约订单表不就知道了为什么要冗余一个字段存着因为在批量导入时系统要立刻判断“新导入的总号源数是不是小于已经预约的数量”。如果已用号源每次都要实时聚合订单表一次导入几百行数据时每行都聚合一次性能会非常难看。冗余字段可以接受一定的更新延迟换取读取和校验的速度。这里有个要注意的规则导入或单日修改时如果total_slots被改成比used_slots还小的值系统必须报错不能静默通过。否则会出现“设置显示还有 5 个号实际库里已经约了 8 个”的超卖状态。2.3 乐观锁两个管理员同时改同一天怎么办排班工作一般由值班人员操作但真出现两个人同时改同一天的情况我也碰到过。A 把号源改成 100B 同时把状态改成停诊后提交的人会把先提交的人覆盖掉。解决方式不复杂用版本号实现乐观锁。更新语句加上WHERE id ? AND version ?影响行数为 0 就说明数据已经被别人改过这时返回“该日期已被其他人修改请刷新后重试”。虽然会把操作逼回重试但至少不会出现静默覆盖。2.4 是否需要单独的导入日志表建议加一张操作日志表记录每次导入的文件名、操作人、导入条数、成功条数、失败条数、失败原因摘要。这不只是为了溯源更重要的是给“覆盖”模式兜底。万一运营导入错了能根据日志快速定位是哪批数据出了问题甚至用日志里的原始文件做回滚。日志表结构很简单字段就是主键、操作类型、文件 MD5、业务日期范围、成功数、失败数、错误详情 JSON、操作人、操作时间。别在这个表上做过度设计够用就行。3. Excel 批量导入从模板设计到逐行校验的完整链路3.1 模板设计让 Excel 老手也能十分钟填完做过几次导入功能之后我最大的体会是模板设计决定了导入功能一半的成败。模板字段太多填的人会烦字段太少又没法满足业务。对于预约设置我最终保留了这几列列名必填说明预约日期是格式 yyyy-MM-dd例如 2025-04-01总号源是大于 0 的正整数状态是1开放0停诊备注否最长 200 字模板文件放在下载中心做成接口导入页面上放一个“下载模板”按钮。模板里的日期列我会特意写两行示例数据方便用户照葫芦画瓢。这里有一个小技巧示例日期的年份一定要用当年的真实日期不要写 2020 年这种旧数据否则用户复制下来很容易忘了改年份。3.2 文件解析日期格式、空行、合并单元格这些坑一次说清讲几个我在解析 Excel 时踩过的坑每一个都真实地把导入任务打断过。第一个是日期格式。用户手填日期可能会出现2025/4/1、2025.4.1、4月1日、甚至“2025年4月1日”这几种写法。还有一种更隐蔽的Excel 里的日期列本质是序列号比如 45888直接读出来就是数字。所以解析时不能只做字符串拆分要先把单元格读成日期对象再统一格式化成yyyy-MM-dd。我用 Apache POI 时会判断单元格类型数字型就按 Excel 日期序列号转字符串型就按多种格式尝试解析解析不了的直接记成该行错误。第二个是空行。用户导入前经常在 Excel 里把格式先涂了一大片导致末尾出现几百行“看起来空但实际上有格式”的行。遍历时如果用getLastRowNum()获取总行数会把后面这些空白格式行也算进去。正确做法是读取每行后先检查所有关键列是否为空全空就直接跳过不给用户报一堆无用错误。第三个是合并单元格。模板里我不会做纵向合并但用户自己填表时可能会为了表头美观把备注列合并一下这会直接导致行数据错位。解析前先判断第一个单元格是否是合并单元格区域的一部分如果是用区域左上角的值代替。这个逻辑在 POI 里用isMergedRegion判断即可。3.3 逐行校验改掉“一键导入全部报错”的粗暴方式很多导入功能做得很糙遇到一个错误就整批回滚然后返回一句“第 3 行格式错误”。运营人员打开 Excel 数半天改完再传一次又发现第 8 行报错。来回几次就崩溃了。我的做法是所有的行先全部解析然后逐行校验把每一行的错误累积起来一次性返回给前端。接口返回结构大致是public class ImportResponse { private int totalCount; // 总行数 private int successCount; // 成功行数 private int failCount; // 失败行数 private ListImportError errors; // 错误明细 } public class ImportError { private int rowIndex; // Excel 行号从 2 开始跳过表头 private String column; // 出错列名 private String message; // 具体原因 }前端拿到这个列表后直接在结果表格里渲染让用户一眼看到“第 5 行日期格式错误第 7 行总号源不能为空”。这样改一轮就能全部改完。校验规则一般包括日期不能为空、日期不能早于今天、日期不能超出后台设置的可预约天数范围、总号源必须大于 0 且小于某个上限值一般限制 999防止用户手抖填了 999999。3.4 入库策略新增、覆盖、跳过三分支解析和校验通过之后进入真正的数据入库阶段。这一步要先区分三种情况该日期在表中不存在直接 INSERT。该日期存在导入模式是“跳过”这条记录标为已跳过不更新数据库。该日期存在导入模式是“覆盖”执行 UPDATE更新total_slots和status同时校验新号源数不能小于used_slots。我会先把所有待处理的日期用一条SELECT ... WHERE setting_date IN (...)查出来放进内存 Map而不是每行都查一次库。批量导入功能如果每行一次查询500 行数据就会产生 500 次数据库往返慢得让人怀疑人生。3.5 大文件导入的性能处理几百行的 Excel 用普通逐行插入问题不大但如果遇到几千行的排班表就得做批处理。我一般用 MyBatis 的批量插入每 200 条提交一次。另外解析 Excel 时尽量用流式读取不要一次性把整个工作簿都 load 进内存。有一个更务实的建议导入接口做成同步的还是异步的取决于业务方的心态。如果文件经常在 500 行以内同步接口加个 loading 就够用户体验反而直观。如果经常几千行甚至上万行就把文件先上传到临时目录返回一个任务 ID后台异步处理前端轮询进度。预约设置这种业务绝大多数情况都到不了异步的复杂度别过度设计。4. 日历展示后端数据如何变成一眼可读的日期状态4.1 日历视图需要的数据结构日历展示的重点是“一屏看完一个月的号源情况”。后端接口如果设计成一次返回这个月每一天的完整实体前端还要自己筛选计算那就太被动了。我习惯让后端直接返回前端需要的视图数据{ year: 2025, month: 4, days: [ { date: 2025-04-01, weekDay: 2, totalSlots: 80, usedSlots: 45, status: 1, isHoliday: 0, displayType: partial } ] }displayType是后端计算好的状态标记取值一般有四类open正常开放、full已约满、rest停诊、partial部分约满。这个字段的意义在于前端拿到后直接映射到颜色无需关心业务判断逻辑。哪天规则改了只改后端一处就行不用联动改前端。4.2 前端渲染直接用日历组件还是自己画一个网格市面上日历组件很多比如 FullCalendar 功能强大但偏重一般的管理系统用不上那么重的交互。我是推荐自己画一个简单的月历网格的逻辑并不复杂算出当月第一天是星期几然后按 7 列排日期格子每个格子填充日期数据。好处是完全可控样式好调也不会有组件升级带来的兼容问题。如果非要用现成组件也建议只用它生成日历骨架数据渲染还是自己控制。特别注意一点日历的“今天”和“当前选中日期”要高亮区分很多用户会把这两个概念混在一起选中某个日期后找不到今天在哪。4.3 “周跨月”和“时区”两个隐蔽问题日历表格经常会在首尾展示上一个月和下一个月的一两天这本来是显示惯例。但接口如果只返回当月的数据就会导致跨界日期没有数据显示成一片空白。处理方式有两种一种是前端把跨界的日期发到后端查另一种是后端接口允许传“起始日期”和“结束日期”前端把当月网格实际展示的 42 个格子全部传过去。我推荐第二种一次性返回网格内全部日期的数据避免多次请求。时区问题更隐蔽。日期这个字段前端传的可能是2025-04-01T00:00:0008:00后端如果用 UTC 存储再转换回来在多时区部署时很容易变成2025-03-31。预约设置这类业务日期和时间无关我强烈建议后端接口接收和返回都使用yyyy-MM-dd字符串数据库字段用 DATE 类型全程不经过java.util.Date的时区转换。这条经验能帮你少掉一堆头发。5. 单日设置小功能里的并发、校验与幂等5.1 单日设置的交互清单日历上点任意一天弹出一个右侧抽屉或者底部弹层这个就是单日设置。表单元素不多大概是这样的日期展示不可编辑明确告诉用户你改的是哪一天总号源数字输入框状态切换开放 / 停诊备注输入框保存、取消按钮这里有个细节容易被忽略日期选择器默认会允许用户选到已经过去的日期。单日设置一定要在保存时做后端校验禁止修改今天之前的设置。有些业务甚至不允许修改今天的设置因为今天的预约已经开始了改号源没有意义只允许往前调“停诊”。这种规则最好在前端也同步禁掉别让用户点到提交按钮才报错。5.2 保存逻辑新建、更新、锁定状态一次说清单日保存的逻辑分两种情况。如果那天还没有设置记录需要 INSERT 一条新记录如果已有记录走 UPDATE。用数据库的唯一索引作为兜底先尝试 UPDATE影响行数为 0 时再 INSERT。如果并发下 INSERT 撞了唯一索引捕获 DuplicateKeyException 后重新走 UPDATE。这套“先更后插 唯一索引兜底”的逻辑能保证幂等。UPDATE 的 SQL 要注意带上版本号UPDATE appointment_setting SET total_slots #{totalSlots}, status #{status}, version version 1 WHERE setting_date #{settingDate} AND version #{version}更新时还要校验一次total_slots不能小于used_slots。这个校验必须放到 SQL 条件里而不是只在 Java 里查一次否则并发下单场景下还是可能有缝隙。5.3 并发场景两个管理员同时改同一天怎么处理前面提到的乐观锁在单日设置里的表现尤为关键。我曾经遇到过一个真实案例两个客服同时接到不同用户的电话一个说要增加明天的号源另一个说要停诊两人同时打开日历点了同一天。后保存的人把先保存的人覆盖了结果第二天系统不仅没停诊反而多了 50 个号。加了乐观锁之后第二个保存的人会看到一个明确提示“该设置已被他人修改当前页面显示的是旧数据请刷新后重试。”虽然体验上多了个步骤但比起数据错乱带来的投诉这个提示完全可以接受。5.4 单日设置和批量导入的联动提醒单日设置保存成功后如果当天刚好也在某次批量导入的文件里用户可能没有感知直到下次刷新日历才看到数据被改。我建议在单日设置页面上加一行“最近 7 天是否有批量导入记录”的提示如果有弹一个确认框问用户“该日期在最近一次导入中已配置是否仍要修改”这个细节虽然不起眼但能省掉很多“谁改了我的数据”的扯皮。6. 实测踩坑记录与上线后的稳定性补丁6.1 日期序列号把我坑了一次第一次做这套模块的 Excel 导入功能时我直接把 POI 读到的单元格值toString()拿来用了。测试时填的日期都是手动输入的字符串格式没问题。结果用户拿到模板后在 Excel 里用日期控件选了日期单元格实际存的是 45000 这样的序列号导入直接报“日期格式错误”。后来我在解析层加了一个统一的日期转换方法先判断单元格类型数字型按 Excel 日期序列号换算字符串型按yyyy-MM-dd、yyyy/MM/dd、yyyy.MM.dd多格式解析。这个函数写好之后后续所有导入模块都复用了也算吃一堑长一智。6.2 空行和“看不见的字符”用户从别处复制数据粘到模板里经常带回一些看不见的异常字符。最常见的是日期字符串前后带空格还有那种不间断空格\u00A0。解析时只 trim 是不够的我一般会统一把字符串里的常见空格字符替换掉然后再做格式判断。另外模板里的“状态”列用户可能填“是/否”“开放/停诊”“1/0”好几种写法。后来我干脆做成下拉选项限制输入Excel 里用数据验证限制取值范围能挡掉大部分脏数据。这里要提醒一下光在 Excel 里做数据验证是不够的后端必须重新校验一遍因为用户完全可能绕过下拉框直接粘贴。6.3 批量导入的“覆水难收”前面提到覆盖策略这里再补充一个我后来加的保险措施。在导入预览页面除了显示成功和失败数量我还会明确显示“将覆盖 N 条已存在数据”并把将被覆盖的旧值和新值都列出来让用户确认。这个步骤不能省尤其是配置了几十个日期的排班表一旦覆盖错了想恢复特别麻烦。为了给这个操作留后路我还在导入逻辑里做了一步自动备份导入前把受影响日期的旧数据查出来连同导入文件一起存到操作日志表的 JSON 字段里。真出问题时可以让技术人员从日志里恢复。这个功能做起来很简单但关键时候真的能救场。6.4 性能优化批量导入慢了怎么处理之前有个业务方导入了明年全年的排班计划Excel 里 4000 多行。第一版导入逻辑是逐行查、逐行插全程跑了两分多钟用户差点以为系统卡死了。优化后改成先用一条 IN 查询把已存在日期全部查出放入 Map新增行和更新行分开批量提交每 200 条提交一次事务进度条按批次推进优化后 4000 行数据大概 8 秒跑完。对预约设置这种低频操作来说完全够用。如果以后数据量再涨再考虑异步队列现在真没必要把系统搞复杂。6.5 权限控制谁有资格导入和修改预约设置的权限要细分至少分两种角色查看者只能看日历不能操作排班管理员可以单日设置和导入。如果公司组织架构再复杂一点还得分科室隔离A 科室的人不能改 B 科室的日期。权限控制做在前端容易做在后端更关键。导入接口、单日设置接口都必须从登录上下文里取角色和科室信息不能只靠前端把科室 ID 传过来否则接口被直接调用就能越权。最后说几句实在话预约设置模块表面上是个标准的增删改查但真正上线后出问题的往往不是 CRUD 本身而是数据一致性、并发覆盖和批量导入的异常处理这些“边缘地带”。我个人的建议是先用二八原则把主流程跑通也就是 Excel 导入 日历展示 单日设置三个主入口再把校验规则一条条补严。这套模块做完之后再维护起来业务方用得顺手你也少熬夜。如果你们系统里正好要排期做这个功能把上面提到的几个坑提前规避掉上线的平稳度会提升很多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门