做这类“综合小区管理系统”的项目,我前前后后接手过至少五个版本——毕业设计、物业公司的小型内部系统、老旧系统改造,都有。技术栈来来去去,最后基本都落在SpringBoot、Vue、MySQL、MyBatis这一套上。它不一定是性能最极端的方案,但胜在成熟、稳定、资料丰富,很契合中小型管理系统的真实场景。今天这篇不聊空泛架构,直接从数据库设计、后端接口、前端权限到部署避坑,把整个系统怎么从零搭起来讲明白。如果你正打算做类似项目,或者拿到一份“完整源码”不知道怎么梳理,这篇文章可以帮你节省不少时间。
1. 项目整体设计与技术选型
1.1 为什么又是SpringBoot+Vue+MySQL+MyBatis
很多同学会问,现在微服务这么火,为什么小区管理系统还要用这么“传统”的组合?我的答案是:场景决定选型,不为了技术炫技。
小区管理系统本质是一个典型的业务信息系统,核心是增删改查、权限管理、流程流转。它的并发量不大,数据量在几万到几十万级别,系统使用人数多则几百人。这种规模下,SpringBoot内嵌Tomcat够用,MySQL单库也毫无压力,完全没必要引入微服务、消息队列这类重型组件。
SpringBoot的真正价值在于“开箱即用”。它把Spring的配置简化到极致,内嵌web容器,项目里依赖配好,直接java -jar就能跑。对于需要快速交付的毕业设计或小公司系统,这是巨大的时间优势。
Vue负责前端交互。选择Vue不仅因为组件化开发维护方便,更因为生态里有Element Plus这类成熟组件库,表格、表单、弹窗、分页这些管理系统的高频页面,几乎不需要自己手写样式。前后端分离,后端只需要返回JSON数据,前端按接口渲染,联调起来思路清晰。
MySQL加MyBatis的组合,是让很多人又爱又恨的部分。MySQL本身稳定,事务可靠,适合业务系统。MyBatis比JPA更直观的地方在于,SQL是你自己写的,复杂查询、多表关联、动态条件都能精确控制;系统一旦出现慢查询,优化SQL也比琢磨框架自动生成的语句省事得多。再加上MyBatis-Plus这个增强包,单表CRUD连XML都不用写,进一步压缩了开发时间。
提示:如果项目里用到了MyBatis-Plus,数据层开发效率会明显提高,但复杂SQL还是建议写XML,不要指望它全自动搞定。
1.2 核心功能拆解与角色权限设计
“综合”这两个字,意味着系统不只是简单记录房产,而是要把业主、房产、车位、缴费、报修、公告这些信息串在一起。我在做需求梳理时,一般先拆成六个核心模块:
- 基础数据:小区信息、楼栋单元房屋信息、业主档案、租户信息。
- 车位管理:车位编号、位置、绑定业主、变更记录。
- 缴费管理:物业费、水费、电费账单生成、缴费状态查询、账单详情。
- 报修管理:业主提交报修、物业接单、处理进度、评价闭环。
- 公告管理:物业发布通知公告,业主端查看。
- 系统管理:登录认证、用户管理、角色权限分配。
这六个模块看着不多,但彼此之间有复杂的关联关系。比如业主关联房屋,房屋关联车位,房屋又关联缴费记录;报修单既关联业主,也关联具体房产。如果一开始不把这些关系理清,后面写SQL就会越写越乱。
权限设计上,我不建议给业主、物业、管理员各做一套独立的用户表。管理起来太痛苦,改个密码要同步三张表。更好用的做法是统一一张用户表,用user_type或role字段区分身份,再用前端路由和后端拦截器双重控制访问范围。
实际项目中我习惯分成三个角色:
- 系统管理员:拥有全部权限,负责用户管理、基础数据配置、账单生成。
- 物业人员:可以查询业主信息、处理报修、发布公告,但没有系统配置权限。
- 业主:只能查看自己的房产信息、车位信息,提交报修,查看自己的账单并缴费。
后端拦截器只需要判断登录状态和路径权限,前端根据角色渲染动态菜单,两边配合,基本能覆盖所有业务场景。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:核心表结构与那几个容易踩的坑
2.1 核心表结构拆解
数据库是整个系统最不能偷懒的部分。表结构设计的质量,直接决定后续开发是流畅还是天天返工。我画一下最核心的表,以及它们之间的关联关系。
| 表名 | 核心字段 | 说明 |
|---|---|---|
user_info |
id, username, password, name, phone, user_type, deleted | 所有系统用户统一存放,user_type区分管理员、物业、业主 |
house_info |
id, building_no, unit_no, room_no, area, owner_id, deleted | 房屋信息,owner_id关联user_info |
parking_info |
id, parking_no, park_type, house_id, status, deleted | 车位信息,可与房屋绑定,也可单独管理 |
bill_info |
id, house_id, bill_type, amount, status, due_time, pay_time | 缴费账单,bill_type区分物业费/水费/电费 |
repair_info |
id, house_id, user_id, content, status, assign_time, finish_time | 报修工单,state简化,也可以拆成更细的字段 |
notice_info |
id, title, content, publish_user_id, publish_time | 公告信息 |
config_info |
id, config_key, config_value, remark | 全局配置,比如物业费单价、水电费单价 |
字段设计上有几个细节,比表本身更重要:
- 金额字段必须用
decimal(10,2),别用float或double。二进制浮点数存金额会有精度误差,虽然几秒钟看不出问题,但利润表对账时一定会出乱子。 - 所有业务表保留
deleted逻辑删除字段。真实系统几乎不会物理删数据,标记删除可以保留历史记录,出了问题能找回。 - 时间字段统一用
datetime,并设置默认值CURRENT_TIMESTAMP。create_time放在每张表里,排查数据的时候非常有用。
house_info.owner_id这个字段我特意单独说明。一个业主名下可能有多套房产,所以房屋与业主是典型的多对一关系。最常见的做法就是在房屋表上直接加owner_id,查询业主名下的房屋时,一句SQL就能拿到。但这个冗余字段也有风险,如果业主退房、换房没同步更新,数据就会错乱。所以我在更新服务层都会写一个额外方法,专门保证owner_id变更时,相关车位和账单也能跟着调整。
2.2 建表时最容易踩的坑
第一坑:逻辑删除字段与唯一索引冲突。比如业主表里的手机号需要唯一,逻辑删除后如果同一个手机号重新注册,就会和已删除的那条记录冲突。解决思路有两种:唯一索引改成(phone, deleted),同时把deleted在删除时设置成该行的id值而不是固定1;或者干脆不建唯一索引,而是在应用层用手机号查询时过滤掉deleted=1的记录。
第二坑:楼栋单元信息贪方便全部塞到一个字符串字段里。比如house_info里存地址3幢2单元501室,查询、统计、筛选都变得很痛苦。正确做法是拆成building_no、unit_no、room_no三个字段,既方便筛选,也方便按楼栋统计缴费率。
第三坑:账单表没有唯一约束。生成账单时如果定时任务重复执行,或者管理员手动触发了两遍,同一笔物业费可能生成两条记录。我习惯在bill_info上建立联合唯一索引(house_id, bill_type, due_time),从数据库层面保证同一房屋、同一费种、同一个账期只能存在一条账单。这个设计在并发时非常管用。
3. 后端实现:SpringBoot与MyBatis的落地细节
3.1 项目分层与基础配置
后端如果不用分层,业务代码会越写越乱。我一般固定使用五层结构:
code复制com.example.community
├── controller // 接收请求,参数校验,返回结果
├── service // 业务逻辑
├── mapper // MyBatis数据访问层接口
├── entity // 数据库实体
└── common // 统一返回、异常处理、工具类
pom.xml里的核心依赖,按这套组合往下填就够:
xml复制<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.20</version>
</dependency>
为什么特意用Druid而不是HikariCP?Druid自带监控页面、SQL防注入和慢查询日志,排查问题非常方便。如果系统是纯生产环境追求极致性能,HikariCP默认就很好,但中小项目我更倾向Druid的可观测性。
application.yml里,最容易被忽略的是MyBatis配置:
yaml复制spring:
datasource:
url: jdbc:mysql://localhost:3306/community?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
map-underscore-to-camel-case这个配置一开,数据库create_time字段自动映射成createTime,省去一大半字段映射的重复工作。log-impl在开发阶段一定要开,否则SQL和参数都看不见,排查问题时就像蒙着眼睛找针。等上线后再关闭SQL打印也不迟。
3.2 登录鉴权与权限控制
小区系统的登录逻辑不复杂,但安全细节不能少。密码要用BCrypt加密存储,不能明文入库。登录成功后签发JWT令牌,前端每次请求带着令牌,后端用拦截器校验。
JWT的生成可以简单封装:
java复制public class JwtUtil {
private static final String SECRET = "your-secret-key";
public static String generateToken(Long userId, String userType) {
return Jwts.builder()
.setSubject(userId.toString())
.claim("userType", userType)
.setExpiration(new Date(System.currentTimeMillis() + 2 * 60 * 60 * 1000))
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
}
public static Claims parseToken(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}
}
拦截器里只需要做两件事:从请求头读取token,解析成功后把用户信息放入ThreadLocal或请求上下文;解析失败直接返回401。权限控制不需要特别复杂,如果是管理员专用接口,路径前缀加/admin/,拦截器里按路径判断角色即可。
注意:JWT无状态,登出后只要token没过期,理论上依然可以访问。如果对安全性要求高,可以把token加入Redis黑名单,或者用Redis保存token值,拦截器每次都查询一下Redis。一般小型小区系统,无状态JWT已经够用。
3.3 MyBatis使用的核心技巧
MyBatis在这个系统里的核心价值,就是处理各种动态查询和复杂更新。
动态SQL是日常开发中最常用的特性。比如业主列表的查询条件包括姓名、手机号、楼栋号,用户可能填也可能不填,用<if>标签可以无脑拼接:
xml复制<select id="selectOwnerList" resultType="com.example.community.entity.OwnerVO">
SELECT u.id, u.name, u.phone, h.building_no, h.unit_no, h.room_no
FROM user_info u
LEFT JOIN house_info h ON h.owner_id = u.id AND h.deleted = 0
<where>
<if test="name != null and name != ''">
AND u.name LIKE CONCAT('%', #{name}, '%')
</if>
<if test="phone != null and phone != ''">
AND u.phone = #{phone}
</if>
<if test="buildingNo != null and buildingNo != ''">
AND h.building_no = #{buildingNo}
</if>
</where>
AND u.deleted = 0
</select>
分页这块,我建议直接用PageHelper或者MyBatis-Plus自带的分页插件。自己手写LIMIT并不难,麻烦的是要同时统计总条数,多写一遍count查询容易出错。PageHelper的用法很简单,查询前一行设置分页参数即可:
java复制PageHelper.startPage(pageNum, pageSize);
List<OwnerVO> list = ownerMapper.selectOwnerList(query);
PageInfo<OwnerVO> pageInfo = new PageInfo<>(list);
MyBatis缓存是另一个高频话题。一级缓存在SqlSession内部有效,同一个SqlSession查询同一SQL,第二次不会真正访问数据库,这是默认行为,基本不用管。二级缓存我强烈建议不要在业务系统里乱开,尤其多表关联的时候,一旦数据更新缓存没有正确失效,就会出现脏数据。与其调缓存,不如把SQL优化好。很多同学在网上看到“MyBatis缓存面试题”就来问怎么配,我的回答通常都是:除非你非常清楚缓存失效条件,否则别开。
批量操作也是刚需。比如缴费模块要给所有未缴费房屋批量生成下月账单,如果用循环单条插入,几百条数据还能接受,几千条就有明显耗时。用foreach批量插入,性能提升非常明显:
xml复制<insert id="batchInsertBill">
INSERT INTO bill_info (house_id, bill_type, amount, status, due_time)
VALUES
<foreach collection="list" item="bill" separator=",">
(#{bill.houseId}, #{bill.billType}, #{bill.amount}, 0, #{bill.dueTime})
</foreach>
</insert>
3.4 核心业务模块的实现思路
报修模块的难点不在CRUD,而在状态流转。我的状态设计很简单:0待接单,1处理中,2已完成,3已取消。业主提交报修时状态为0,物业接单时执行一条更新语句:
java复制int updated = repairMapper.updateStatusById(repairId, 1, 0);
这条SQL必须带上旧状态0作为条件,防止两个物业人员同时接了同一单。updated如果等于0,说明当前状态已经不是待接单,直接提示“该工单已被处理”。这个方案比“先查再改”更安全,是并发控制里最简单便宜的乐观锁思路。
账单生成模块,我的做法是在bill_info上加联合唯一索引,同时生成前先查一下目标账期是否已经有数据。但没有唯一索引兜底时,依然可能重复。所以我的顺序是:先尝试批量插入,如果捕获到DuplicateKeyException,就说明已有账单,直接提示“本月账单已生成”。这一段逻辑虽然小,但在实际交付中很加分。
4. 前端实现:Vue从初始化到打包全流程
4.1 项目初始化与Axios封装
前端如果选Vue3,我习惯用Vite构建,启动速度比Webpack快好几倍。不过如果你的项目源码是基于Vue2,也不用焦虑,核心逻辑没有本质区别。下面这套设计,Vue2和Vue3都能参考。
创建项目建议用官方脚手架:
bash复制npm create vite@latest community-web -- --template vue
cd community-web
npm install
npm install axios vue-router@4 pinia element-plus
前端目录结构要清晰,至少包含:
code复制src
├── api // 接口请求方法
├── router // 路由配置
├── store // 全局状态管理
├── views // 页面组件
├── layout // 通用布局
└── utils
└── request.js // Axios封装
Axios封装是整个前端的入口。统一处理baseURL、token注入、错误提示,能避免每个页面重复写一堆判断逻辑:
javascript复制import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const service = axios.create({
baseURL: '/api',
timeout: 15000
})
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
service.interceptors.response.use(
response => {
const res = response.data
if (res.code !== 200) {
ElMessage.error(res.message || '请求失败')
return Promise.reject(new Error(res.message))
}
return res.data
},
error => {
if (error.response && error.response.status === 401) {
localStorage.removeItem('token')
router.push('/login')
}
ElMessage.error(error.message || '网络异常')
return Promise.reject(error)
}
)
export default service
这里有个容易被坑的点:baseURL用的是/api还是完整地址。开发阶段通过Vite代理把/api转发到后端8080端口,生产阶段则由SpringBoot统一拦截/api路径。这样前端代码不需要因为环境不同而改动,体验最好。
4.2 路由权限与动态菜单
前后端分离系统的权限,不只是控制按钮显隐,更重要的是路由级别的访问控制。我的做法是前端先写死基础路由(登录页、首页),再根据当前用户的角色,动态注册对应权限路由。
以Vue3为例,登录成功后把用户信息保存到Pinia,在路由守卫里判断:
javascript复制router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (!token && to.path !== '/login') {
next('/login')
return
}
if (token && to.path === '/login') {
next('/')
return
}
next()
})
动态菜单的核心是router.addRoute()。管理员能看到“楼栋管理”“账单生成”“系统配置”,业主只能看到“我的房产”“我的维修”“我的账单”。这不能只靠隐藏菜单栏——用户直接输入URL依然可能访问未授权页面,所以后端接口也要做同样的权限校验。前端控制的是体验,后端控制的才是安全。
这块代码有一个高频问题:页面刷新后动态路由丢失。原因很简单,Pinia里的数据是内存态,刷新就没有了。解决方式是在Pinia中写一个generateRoutes方法,刷新时重新拉取用户角色并重新注册路由。只要把这个逻辑挂在“刷新后初始化应用”的位置,就不会再有刷新404或白屏。
4.3 页面实现与组件复用
管理系统页面大多是表格+表单+弹窗的组合。Element Plus能省掉大量样式工作,但组件复用依然值得花时间。
以房产管理为例,楼栋单元树和房屋表格是典型联动场景。左侧是树形楼栋选择,右侧是根据选中节点刷新的房屋列表。这个功能拆成两个组件:BuildingTree.vue负责树形数据,HouseTable.vue负责表格展示,通过事件传递选中节点。这样以后车位管理、维修管理都能复用同一个树组件,改动只集中在数据接口层。
表单校验建议统一用rules配置,而不是每个输入框写一堆if判断。比如添加业主时,手机号校验:
javascript复制const rules = {
phone: [
{ required: true, message: '请输入手机号', trigger: 'blur' },
{ pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确', trigger: 'blur' }
],
name: [
{ required: true, message: '请输入业主姓名', trigger: 'blur' }
]
}
小区系统里经常需要上传图片,比如录入报修单拍个现场照片、业主上传证明材料。Element Plus的el-upload默认返回上传后的URL,把URL保存到表单对应字段即可。如果不上传文件到云存储,可以后端写一个本地文件上传接口,把文件存到指定目录,再返回访问路径。
4.4 打包后放进SpringBoot这个经典操作
前端开发完成后,下一步就是把npm run build生成的dist目录放进SpringBoot。这一步有几个同学问过我最多的问题——刷新后404、图片路径不对、接口请求不到。
最简方案是复制dist目录里的所有内容,放到后端的src/main/resources/static下,然后重新打包后端。但这里必须处理一个问题:Vue的路由如果是history模式,直接访问http://localhost:8080/house/list刷新时,SpringBoot本身没有这个路由,会返回404。
解决办法是加一个转发规则,把所有非/api的请求都转发到index.html:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/{path:[^\\.]*}").setViewName("forward:/index.html");
}
}
这段配置的含义是:只要路径中不包含点号,就转发到index.html,交给Vue路由处理。同时接口路径统一带/api前缀,前端请求都是/api/...,就不会被转发规则误拦截。
提示:这个方案适合单体部署。如果前后端分开部署到Nginx或OSS,就用Nginx的
try_files规则实现同样效果。但做成一个小系统,把前端打进jar包,带来的最大好处是部署时只需要一个jar文件,不需要单独维护前端进程。
5. 部署过程与常见问题排查
5.1 从零到能跑起来的部署流程
如果是在本地开发环境跑通,步骤很固定:
- 安装JDK8或JDK11,配置
JAVA_HOME。 - 安装Maven,配置
settings.xml里的阿里云镜像,加速依赖下载。 - 安装MySQL,初始化
community.sql脚本,建库建表。 - 启动后端:
mvn spring-boot:run。 - 前端安装依赖:
npm install,启动开发服务器:npm run dev。 - 浏览器访问前端地址,开始联调。
这里默认流程自然会遇到一个经典问题:数据库连接报SSL连接错误。原因在于新版本MySQL连接器默认使用SSL,而本地MySQL通常没启用或证书不受信任。解决办法就是在JDBC连接串里加useSSL=false。如果还报时区错误,把serverTimezone=Asia/Shanghai加上。
MySQL字符集也一定要在初始化时设置成utf8mb4,不用这个字符集,名字里有生僻字或者表情符号就会报错或变成乱码。建库语句我一般固定写成:
sql复制CREATE DATABASE community DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
5.2 常见问题速查表
这些是我在实际项目中反复遇到、每次带新人都需要解释一遍的问题,整理成表,直接收藏即可。
| 现象 | 原因 | 解决方案 |
|---|---|---|
| MySQL连接报SSL错误 | MySQL驱动与服务器SSL协商失败 | 连接URL加useSSL=false |
| 控制台不打印SQL | MyBatis日志配置没开 | mybatis.configuration.log-impl设置成StdOutImpl |
| MyBatis报BindingException | mapper接口和XML的namespace绑定错误 | 检查mapper-locations路径和namespace全类名 |
| 前端请求跨域 | 前后端端口不同 | 后端配CorsFilter,或前端代理转发 |
| SpringBoot启动端口被占用 | 8080被其他程序占用 | 修改server.port或结束占用进程 |
| 打包后前端刷新404 | history路由未被后端处理 | 使用ViewController转发到index.html |
| 打包后图片/字体不显示 | 静态资源路径错误 | Vitebase设置成./,或按实际部署路径设置 |
| 中文乱码 | 连接字符集问题 | URL加characterEncoding=utf8mb4,数据库也统一utf8mb4 |
MyBatis查出来是null很多字段 |
驼峰映射没开 | 开启map-underscore-to-camel-case: true |
| PageHelper分页不生效 | PageHelper依赖冲突 | 版本对齐,确保使用官方starter对应版本 |
除了表格里的问题,还有一个比较隐蔽的坑:MyBatis中方法的参数只有一个List或自定义对象时,XML里引用参数名没有加@Param会报错。通常我的习惯是,不管几个参数都用@Param("xxx")明确指定名称,这样XML里引用永远不会出问题。
5.3 部署到服务器时要注意的内存和存储问题
本地跑通不代表服务器能顺利部署。如果服务器内存只有1G或2G,默认JVM参数很容易导致内存不足。启动命令里限制一下堆内存:
bash复制java -Xms256m -Xmx512m -jar community-server.jar
MySQL在Linux上安装后,默认lower_case_table_names可能和Windows不一样,导致之前写的表名大小写不一致而报错。保险做法是执行SQL初始化时表名和下划线命名规范保持统一,我全部小写表名,避免踩坑。
数据库备份别忘了。小区系统虽然不大,但账单、业主信息一旦丢失就是灾难。写个简单的crontab定时任务,每天凌晨备份一次:
bash复制0 2 * * * mysqldump -uroot -p123456 community > /backup/community_$(date +\%Y\%m\%d).sql
保留最近30天备份文件,就足够应急。
6. 项目扩展与实操心得
6.1 还能往哪些方向扩展
这个系统做完后,稳定运行只是开始,后续扩展空间其实很大。最容易想到的是文件存储改造:项目早期上传的维修图片、证明材料都保存在本地目录,一旦重启或扩容就面临丢失风险。引入MinIO做对象存储,把上传接口替换成MinIO客户端上传,返回的文件URL改为MinIO地址,前端几乎不用改动。SpringBoot集成MinIO不复杂,加依赖、配连接、写一个MinioService类就够了。
如果需要做小区公告推送、短信通知,可以引入Redis存验证码和token,再做接口给小程序端调用。现在很多物业公司都想要一个业主小程序,业主在小程序里直接交物业费、报修,后端接口是完全复用的,只需要给小程序单独做一套登录对接和接口鉴权。
再进一步,如果后续要做视频监控或社区直播,会碰到播放m3u8流的问题。前端可以在浏览器里用hls.js,不需要安装任何额外插件,就能直接播放m3u8格式的视频流。这个能力跟当前系统的Vue技术栈完全兼容,是低成本高感知的功能扩展。
还有一个小细节:接口文档。多人协作或者自己后来接手时,如果没有文档,光靠翻Controller代码效率太低。给项目集成springdoc-openapi或者knife4j,生成在线接口文档,前后端联调时直接看接口定义,能少写很多无谓的沟通。
6.2 做完这个项目后,我个人最深的几点体会
第一,数据库设计的优先级永远高于接口设计。我在一个早期项目里,因为急于把接口写出来,把房屋和业主关系设计成了两条互相独立的记录,结果后面写“业主名下房产列表”时硬生生拆成三张表去查,还时不时出现数据不一致。后来我养成了习惯:编码前先花半天时间把所有核心表关系画清楚,理不顺就坚决不写代码。
第二,逻辑删除和唯一索引的冲突,是业务系统几乎必踩的坑。你如果不提前设计方案,上线后第三天就会遇到“被删除的用户占用手机号,新用户注册不上”的投诉。这个点在面试和答辩中也经常被问到,值得多花十分钟想清楚。
第三,分页和批量写入一定要提前封装。很多初学者都是一个页面写一套分页逻辑,一个接口写一条插入SQL,看着也能跑,但维护起来极其痛苦。把分页参数封装成公共对象,把批量插入统一用XML的<foreach>实现,不仅仅是为了性能,更是为了后续扩展少改代码。
第四,前后端分离项目,接口约定要在开工前统一。字段命名、返回格式、错误码、时间格式,任何一点没约定好,联调时都会浪费大量时间。我的做法是先写好一份简单接口文档模板,固定返回{ code, message, data }三层结构,所有接口都按这个来。前后端并行开发就都不会乱。
最后说个小技巧:项目交付时,源码里除了代码,一定要放一个完整的数据库初始化脚本、一个README说明文档、一个Postman接口测试集合。这三个东西,能让接手的人少问十几个问题。你自己如果隔了半年再回来看项目,也会感谢当初留下这些资料。
