新冠肺炎服务预约类小程序在2020年之后集中出现了一波开发需求,当时我正好在做一个社区健康服务平台的二次开发,前后经手了三个类似的项目,其中一个就是基于SSM框架加微信小程序的预约系统。这类项目的技术栈选择其实很集中——后端用SSM(Spring + SpringMVC + MyBatis),前端用微信小程序原生开发,数据库用MySQL,部署在Tomcat上。看起来是典型的课程设计配置,但真正做下来会发现,预约类业务里有几个坑是绕不开的:时间槽位的并发控制、用户身份的绑定逻辑、以及后台管理端的数据导出。这篇文章我把整个项目的设计思路、核心代码实现和踩过的坑完整梳理一遍,适合正在做类似课程设计或者需要快速搭建一个预约系统的开发者参考。文中涉及的技术选型和实现方案都来自实际项目,源码结构也一并说明。
1. 项目整体设计与技术选型拆解
1.1 为什么是SSM而不是SpringBoot
这个项目标题里明确写了SSM,我一开始其实想用SpringBoot,毕竟现在新项目基本都是SpringBoot起步。但后来改回SSM的原因有几个:第一,课程设计或毕设场景下,很多学校的指导老师对SSM的接受度更高,答辩时问的也是SpringMVC的请求流程、MyBatis的映射原理这些内容;第二,SSM的分层结构更“显式”,对于刚接触Java Web的学生来说,理解Controller→Service→Dao这条链路比SpringBoot的自动配置要直观得多;第三,项目本身业务不复杂,不需要SpringBoot的自动装配和起步依赖带来的便利。
具体版本选择上,我用的组合是:
| 组件 | 版本 | 说明 |
|---|---|---|
| Spring | 5.2.x | 核心IOC和AOP |
| SpringMVC | 5.2.x | Web层请求处理 |
| MyBatis | 3.5.x | 数据持久层 |
| MySQL | 5.7/8.0 | 数据库 |
| Druid | 1.1.x | 连接池 |
| Tomcat | 8.5/9.0 | Web容器 |
| 微信开发者工具 | 稳定版 | 小程序端开发调试 |
这个组合在2020-2023年间的课程设计里非常常见,稳定性没问题。需要注意的是Spring 5.x对JDK版本的要求是JDK8以上,如果学校机房还在用JDK7,需要降级到Spring 4.x。
1.2 小程序端的技术决策
微信小程序端我没有用uni-app或者Taro这类跨端框架,而是选了原生开发。原因很直接:项目只需要跑在微信端,跨端框架带来的额外抽象层反而会增加调试成本。而且预约类小程序的交互不复杂,原生的小程序框架完全够用。
小程序端的页面结构大致如下:
- 首页:展示预约项目列表(核酸检测、健康咨询、疫苗接种预约等)
- 预约页:选择日期、时间段、填写个人信息
- 我的预约:查看预约记录和状态
- 个人中心:登录授权、信息编辑
- 后台管理(Web端):预约数据查看、导出、时段管理
这里有个设计上的取舍——后台管理端我没有做成小程序页面,而是单独用JSP/HTML+SSM做了个Web管理端。原因是后台涉及表格展示、数据导出这些操作,在小程序里做体验很差,而且管理员通常用电脑操作。
1.3 数据库设计的核心思路
数据库表的设计是整个项目的地基,我在设计时主要考虑了三个维度:用户信息、预约资源、预约记录。
核心表结构:
-- 用户表 CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `openid` varchar(64) DEFAULT NULL COMMENT '微信openid', `nickname` varchar(64) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `id_card` varchar(18) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `idx_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约时段表 CREATE TABLE `time_slot` ( `id` int(11) NOT NULL AUTO_INCREMENT, `date` date NOT NULL, `start_time` time NOT NULL, `end_time` time NOT NULL, `capacity` int(11) DEFAULT 0 COMMENT '容量', `booked` int(11) DEFAULT 0 COMMENT '已预约数', PRIMARY KEY (`id`), KEY `idx_date` (`date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 预约记录表 CREATE TABLE `appointment` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `slot_id` int(11) NOT NULL, `status` tinyint(4) DEFAULT 0 COMMENT '0待确认 1已确认 2已取消 3已完成', `remark` varchar(255) DEFAULT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_user` (`user_id`), KEY `idx_slot` (`slot_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里有几个设计细节值得展开说。time_slot表里我用了capacity和booked两个字段来管理容量,而不是每次查询时去appointment表里count。这样做的原因是预约高峰时段count操作会很频繁,直接读字段性能更好。但代价是需要保证booked字段的准确性,后面会讲怎么处理并发问题。
另外user表的openid字段加了唯一索引,这是必须的。微信授权登录时,同一个用户可能多次点击授权,如果没有唯一约束,会插入多条重复记录。
2. 核心功能模块的细节实现
2.1 微信授权登录的完整流程
小程序端的登录流程是整个系统的入口,也是比较容易出问题的地方。我采用的是wx.login+code换openid的标准方案,没有用手机号快速验证(那个需要企业认证且额外收费)。
流程是这样的:
- 小程序端调用
wx.login()获取临时code - 将
code发送到后端/api/login接口 - 后端用
code+appid+secret调用微信的code2Session接口 - 拿到
openid和session_key - 查询数据库,如果
openid存在则返回用户信息,不存在则创建新用户 - 后端生成一个自定义的token(我用的是JWT)返回给小程序
- 后续请求携带这个token做身份校验
后端核心代码:
@RestController @RequestMapping("/api") public class LoginController { @Value("${wechat.appid}") private String appid; @Value("${wechat.secret}") private String secret; @Autowired private UserService userService; @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appid + "&secret=" + secret + "&js_code=" + dto.getCode() + "&grant_type=authorization_code"; String response = HttpUtil.get(url); JSONObject json = JSON.parseObject(response); String openid = json.getString("openid"); if (openid == null) { return Result.error("微信授权失败"); } User user = userService.findByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); user.setNickname(dto.getNickname()); userService.insert(user); } String token = JwtUtil.generateToken(user.getId()); Map<String, Object> data = new HashMap<>(); data.put("token", token); data.put("user", user); return Result.success(data); } }注意:
session_key不要返回给小程序端,它应该保存在后端(可以存Redis或者直接放在JWT的payload里加密)。很多教程会把session_key一起返回,这是有安全风险的。
2.2 预约时段的管理与展示
预约时段的管理分两部分:后台管理员配置时段,小程序端展示可预约时段。
后台配置时段时,我加了一个批量生成的逻辑。管理员选择日期范围、每天的时间段划分(比如8:00-9:00、9:00-10:00...)、每个时段的容量,然后一键生成。这个功能在实际使用中非常省事,不然一天一天手动添加会疯掉。
@PostMapping("/admin/slot/batch") public Result batchCreateSlot(@RequestBody BatchSlotDTO dto) { List<TimeSlot> slots = new ArrayList<>(); LocalDate start = dto.getStartDate(); LocalDate end = dto.getEndDate(); for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) { for (String[] range : dto.getTimeRanges()) { TimeSlot slot = new TimeSlot(); slot.setDate(Date.valueOf(date)); slot.setStartTime(Time.valueOf(range[0] + ":00")); slot.setEndTime(Time.valueOf(range[1] + ":00")); slot.setCapacity(dto.getCapacity()); slot.setBooked(0); slots.add(slot); } } timeSlotService.batchInsert(slots); return Result.success("生成成功,共" + slots.size() + "个时段"); }小程序端展示时,我按日期分组,用横向滚动的日期选择器加上下方的时段列表。这里有个细节:只展示当前时间之后的时段。比如现在是下午3点,那么今天上午的时段就不应该再显示了。这个过滤逻辑放在SQL里做:
<select id="selectAvailableSlots" resultType="TimeSlot"> SELECT * FROM time_slot WHERE date >= CURDATE() AND CONCAT(date, ' ', start_time) > NOW() AND booked < capacity ORDER BY date, start_time </select>2.3 预约提交的并发控制
这是整个项目里最需要认真处理的地方。预约提交时,如果两个用户同时预约同一个时段的最后一个名额,不做并发控制就会出现超卖。
我一开始用的是最简单的方案:先查询booked < capacity,然后插入预约记录,再更新booked + 1。这个方案在测试环境没问题,但用JMeter模拟50个并发请求时,出现了booked超过capacity的情况。
后来改成了数据库层面的乐观锁:
<update id="incrementBooked"> UPDATE time_slot SET booked = booked + 1 WHERE id = #{slotId} AND booked < capacity </update>然后在Service层判断影响行数:
@Transactional public Result makeAppointment(Integer userId, Integer slotId) { int affected = timeSlotMapper.incrementBooked(slotId); if (affected == 0) { return Result.error("该时段已约满"); } Appointment appointment = new Appointment(); appointment.setUserId(userId); appointment.setSlotId(slotId); appointment.setStatus(0); appointmentMapper.insert(appointment); return Result.success("预约成功"); }这个方案的好处是利用了MySQL的行锁,UPDATE语句本身是原子操作,WHERE booked < capacity保证了不会超卖。affected == 0说明要么时段不存在,要么已经约满。配合@Transactional注解,如果后续插入预约记录失败,booked的更新也会回滚。
实操心得:这个方案在并发量不是特别大的场景下完全够用。如果并发量很大(比如秒杀级别),可以考虑用Redis做预扣减,但预约类业务一般不需要。
2.4 小程序端请求封装
小程序端的网络请求我用了一个统一的封装,主要是处理token携带、错误码统一处理和loading状态。
// utils/request.js const BASE_URL = 'https://your-domain.com/api'; function request(options) { return new Promise((resolve, reject) => { const token = wx.getStorageSync('token'); wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': token ? 'Bearer ' + token : '' }, success(res) { if (res.statusCode === 200) { if (res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { // token过期,重新登录 wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(res.data); } else { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); } } else { wx.showToast({ title: '网络异常', icon: 'none' }); reject(res); } }, fail(err) { wx.showToast({ title: '请求失败', icon: 'none' }); reject(err); } }); }); } module.exports = { request };这个封装用了Promise,调用时可以用async/await,代码清爽很多。注意Authorization头的格式,后端JWT拦截器里要对应解析Bearer前缀。
3. 完整实操流程与关键配置
3.1 后端项目搭建步骤
从零开始搭建这个SSM项目,我按实际操作顺序列一下关键步骤。
第一步,创建Maven项目,pom.xml里引入核心依赖:
<dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.2.8.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.23</version> </dependency> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper</artifactId> <version>5.2.0</version> </dependency> </dependencies>第二步,配置applicationContext.xml,主要是数据源、MyBatis的SqlSessionFactory和事务管理:
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/covid_appointment?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="your_password"/> <property name="initialSize" value="5"/> <property name="maxActive" value="20"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.example.entity"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.example.mapper"/> </bean> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/>第三步,配置spring-mvc.xml,处理请求映射和静态资源:
<context:component-scan base-package="com.example.controller"/> <mvc:annotation-driven/> <mvc:resources mapping="/static/**" location="/static/"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>第四步,配置web.xml,注册DispatcherServlet和字符编码过滤器:
<servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter>这里有个容易踩的坑:web.xml里配置了CharacterEncodingFilter之后,POST请求的中文乱码问题解决了,但GET请求的中文参数在某些Tomcat版本下仍然会乱码。解决办法是在server.xml的Connector标签里加上URIEncoding="UTF-8"。
3.2 JWT拦截器的实现
后端需要拦截需要登录的接口,我用的是SpringMVC的拦截器:
public class JwtInterceptor implements HandlerInterceptor { @Autowired private JwtUtil jwtUtil; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (request.getMethod().equals("OPTIONS")) { return true; } String authHeader = request.getHeader("Authorization"); if (authHeader == null || !authHeader.startsWith("Bearer ")) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}"); return false; } String token = authHeader.substring(7); try { Integer userId = jwtUtil.getUserId(token); request.setAttribute("userId", userId); return true; } catch (Exception e) { response.setStatus(401); response.getWriter().write("{\"code\":401,\"msg\":\"登录已过期\"}"); return false; } } }在spring-mvc.xml里注册拦截器:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/api/**"/> <mvc:exclude-mapping path="/api/login"/> <mvc:exclude-mapping path="/api/slot/list"/> <bean class="com.example.interceptor.JwtInterceptor"/> </mvc:interceptor> </mvc:interceptors>exclude-mapping里的路径是不需要登录就能访问的,比如登录接口和时段列表接口(用户还没登录时也应该能看到有哪些时段可以预约)。
3.3 小程序端的页面跳转与数据传递
小程序的页面跳转传参用的是URL query,但有个限制:传递对象时需要先JSON.stringify,接收方再JSON.parse。
预约页跳转:
// 从首页跳转到预约详情页 wx.navigateTo({ url: `/pages/appointment/detail?slotId=${slot.id}&date=${slot.date}&time=${slot.startTime}-${slot.endTime}` });接收参数:
onLoad(options) { this.setData({ slotId: options.slotId, date: options.date, time: options.time }); }如果参数里包含特殊字符(比如日期里的-),建议用encodeURIComponent处理一下,不然解析可能出问题。
3.4 后台管理端的数据导出
管理员需要把预约数据导出成Excel,方便做统计和上报。我用的是Apache POI,注意POI的版本和JDK版本的兼容性——POI 4.x需要JDK8以上,POI 3.x对JDK6/7支持更好。
@GetMapping("/admin/export") public void export(HttpServletResponse response) throws IOException { List<AppointmentVO> list = appointmentService.listAll(); XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("预约记录"); XSSFRow header = sheet.createRow(0); String[] titles = {"预约编号", "姓名", "手机号", "预约日期", "时间段", "状态"}; for (int i = 0; i < titles.length; i++) { header.createCell(i).setCellValue(titles[i]); } int rowNum = 1; for (AppointmentVO vo : list) { XSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(vo.getId()); row.createCell(1).setCellValue(vo.getNickname()); row.createCell(2).setCellValue(vo.getPhone()); row.createCell(3).setCellValue(vo.getDate().toString()); row.createCell(4).setCellValue(vo.getStartTime() + "-" + vo.getEndTime()); row.createCell(5).setCellValue(getStatusText(vo.getStatus())); } response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment;filename=appointments.xlsx"); workbook.write(response.getOutputStream()); workbook.close(); }注意:导出的文件名如果包含中文,需要URL编码,否则某些浏览器会乱码。可以用
URLEncoder.encode("预约记录.xlsx", "UTF-8")处理。
4. 常见问题排查与避坑经验
4.1 小程序端常见问题
问题一:wx.request请求返回404
这个是最常见的问题,排查顺序是:先确认后端接口是否正常启动(用Postman测一下),然后检查小程序开发者工具里的“不校验合法域名”选项是否勾选。开发阶段这个选项必须勾上,否则localhost的请求会被拦截。上线前需要在微信公众平台配置服务器域名。
问题二:真机调试时请求失败
开发者工具里正常,真机上报错。大概率是域名没有配置HTTPS。微信小程序要求所有请求域名必须走HTTPS,且证书要有效。如果是测试阶段,可以在开发者工具里设置“不校验合法域名”,但真机上这个选项不生效。我当时的解决办法是给测试服务器配了一个免费的SSL证书。
问题三:openid获取失败
code2Session接口返回errcode: 40029,表示code无效。原因通常是小程序端调用wx.login()之后没有及时把code发给后端,或者code被重复使用。code只能用一次,且有效期5分钟。正确的做法是每次登录都重新调用wx.login()。
| 错误码 | 含义 | 解决办法 |
|---|---|---|
| 40029 | code无效 | 重新调用wx.login获取新code |
| 45011 | 频率限制 | 每个用户每分钟100次,降低频率 |
| 40163 | code已被使用 | 不要重复使用同一个code |
| -1 | 系统繁忙 | 稍后重试 |
4.2 后端常见问题
问题一:MyBatis映射失败
报错Invalid bound statement (not found),排查顺序:检查mapperLocations配置的路径是否匹配,检查XML文件的namespace是否对应Mapper接口的全限定名,检查方法名和XML里的id是否一致。这三个地方任何一个不对都会报这个错。
问题二:事务不生效
@Transactional注解不生效的常见原因有三种:一是方法不是public的;二是同类内部方法调用(比如Service的A方法调用B方法,B方法上有事务注解但不会生效);三是没有配置<tx:annotation-driven/>。我遇到过第二种情况,后来把B方法抽到另一个Service里解决。
问题三:日期类型转换异常
前端传的日期是字符串2023-01-01,后端实体类字段是java.util.Date,SpringMVC默认转换会报错。解决办法是在实体类字段上加@DateTimeFormat(pattern = "yyyy-MM-dd")注解。如果是JSON请求,需要在@RequestBody的DTO字段上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")。
4.3 数据库层面的坑
坑一:时区问题
MySQL 8.0的JDBC连接URL必须加serverTimezone参数,否则会报The server time zone value '中国标准时间' is unrecognized。我一般统一用Asia/Shanghai。
坑二:中文乱码
数据库、表、连接都要设置成utf8mb4。特别注意连接URL里的characterEncoding=utf8要写,另外MySQL的my.ini里character-set-server=utf8mb4也要确认。utf8mb4比utf8多支持emoji,用户的昵称里如果有emoji,用utf8会插入失败。
坑三:预约记录的时间显示不正确
如果数据库存的datetime是UTC时间,而用户看到的是北京时间,会有8小时偏差。我统一在JDBC连接里指定serverTimezone=Asia/Shanghai,同时在Java代码里用new Date()生成的时间也要确认JVM的时区设置。可以在启动参数里加-Duser.timezone=Asia/Shanghai。
4.4 部署上线时的注意事项
注意一:Tomcat的context path
本地开发时可能用的是/,部署到服务器后如果加了context path(比如/appointment),小程序端的BASE_URL也要同步修改。我建议本地和线上保持一致,直接用/,省得改来改去。
注意二:静态资源缓存
小程序的代码包有大小限制(主包2M),图片资源建议放CDN或者后端静态目录,不要全部打包进小程序。
注意三:日志配置
SSM项目默认用的是Log4j或者Logback,上线前一定要把日志级别调整为INFO或WARN,DEBUG级别的日志在线上会产生大量日志文件,磁盘很快就满了。我在logback.xml里配置了按天滚动和最大保留天数:
<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/app.%d{yyyy-MM-dd}.log</fileNamePattern> <maxHistory>30</maxHistory> </rollingPolicy> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender>4.5 几个提升开发效率的小技巧
技巧一:用Postman维护接口集合
后端接口写完后,第一时间在Postman里维护好请求示例,包括正确的请求参数和返回结果。这样前端联调时直接分享集合,比口头描述或者写文档快得多。
技巧二:小程序开发者工具的“真机调试”
有些问题只在真机上出现(比如键盘弹起遮挡输入框、滚动穿透),开发者工具的模拟器不一定能复现。真机调试功能可以在手机上实时看到console输出,排查起来很方便。
技巧三:数据库备份脚本
开发过程中数据经常被改乱,建议写一个备份脚本,每次修改表结构之前先备份:
mysqldump -uroot -p covid_appointment > backup_$(date +%Y%m%d_%H%M%S).sql这个习惯在后期改表结构时救过我好几次。
技巧四:接口返回统一格式
后端所有接口的返回格式统一成{code, msg, data},前端只判断code === 200。这样前端处理逻辑最简单,也不容易出现某个接口返回格式不一致导致的解析错误。
4.6 项目文档和源码的组织
课程设计或毕设通常需要提交文档和源码。我的建议是源码目录结构保持清晰:
covid-appointment/ ├── src/main/java/ │ ├── com/example/controller/ │ ├── com/example/service/ │ ├── com/example/mapper/ │ ├── com/example/entity/ │ ├── com/example/dto/ │ ├── com/example/interceptor/ │ └── com/example/util/ ├── src/main/resources/ │ ├── mapper/ │ ├── applicationContext.xml │ ├── spring-mvc.xml │ └── logback.xml ├── src/main/webapp/ │ ├── WEB-INF/views/ │ └── static/ ├── miniprogram/ # 小程序端源码 │ ├── pages/ │ ├── utils/ │ └── app.js └── sql/ └── init.sql # 建表语句文档部分至少包含:需求分析、系统设计(含ER图和架构图)、接口文档、测试用例、部署说明。接口文档可以直接从Postman导出,不用手写。
这套项目做下来,整体代码量不算大,后端核心代码大概3000行左右,小程序端2000行左右。关键在于把预约的并发控制和身份认证这两块做扎实,其他都是常规的增删改查。如果你正在做类似的项目,建议先把数据库设计和接口定义确定下来,再动手写代码,不然后期改起来会非常痛苦。另外,记得在开发阶段就把HTTPS和域名配置好,不要等到最后才处理,那个环节卡住的话很影响进度。