SpringBoot+Vue+MyBatis:美发门店管理系统源码与实战拆解 这两年做门店数字化的项目不算少零售、餐饮、美业都碰过我得承认美发是一个特别容易把人和钱的关系搞复杂的行业。这套美发门店管理系统完整源码技术选型很清晰SpringBoot 做后端服务Vue 做管理后台MyBatis 负责数据库访问MySQL 存全量业务数据四样东西拧成一套可以直接落地的项目。它不是为了凑一堆 CRUD 页面而是要解决门店里最头疼的会员卡金、预约排队、员工提成、库存耗材这套真实运营链路。很多刚接触这类项目的朋友第一反应是“这不就是个进销存加会员管理嘛”真拿这套东西去门店跑一遍就发现光“卡金余额怎么算准”这一个点就能让收银员和老板吵起来。我写这篇博文不打算把源码从头到尾贴一遍而是把当初设计这套系统的思路、模块边界、表结构、后端前端的关键实现以及上线后踩过的坑完整梳理出来。不管你是想接单做同类系统还是公司内部要自研一套门店 SaaS都可以直接拿这套拆解去对照。1. 项目到底在做什么业务拆解与模块规划1.1 美发门店管理的真实痛点美发门店和便利店、餐饮店最大的不同在于它的收入结构里“预收款”占了大头。顾客办一张 3000 元的充值卡本质上门店是先收了钱、后提供服务。只要这个前提存在会员余额、开卡折扣、赠送金额、划卡记录就会纠缠在一起。再加上每家店都有几位发型师而发型师通常关心“我这个月做了多少业绩、能拿多少提成”前台关心“今天收了多少钱、划了多少卡”老板关心“库存的烫发水染发膏到底消耗到哪里去了”。用 Excel 记账时这些数据是分裂的前台记一套余额发型师自己记一套业绩库管月底盘点又发现账实不符。这个项目的目标就是把这些分裂的数据收拢到一个系统里。它不是单纯把“纸单变电子单”而是让一次消费动作同时驱动三条链路会员卡金扣款、员工提成计算、产品库存扣减。这三条链路只要任何一条断了系统就没有实际价值。1.2 功能清单与业务闭环拆开来看系统核心功能模块可以划分成下面几块预约管理顾客来电或到店预约发型师和时间段前台可以查看当天预约看板避免撞单和空档。会员管理会员资料、开卡充值、卡金余额、消费记录、等级折扣支持家庭卡或多人共用一张卡。收银消费选择会员、选择服务项目或商品、自动计算金额、选择扣卡金或现金扫码支付生成消费小票。员工与提成发型师档案、服务项目提成比例、按月统计业绩和提成。产品与库存染发剂、烫发剂、护理产品的采购入库、领用出库、库存预警。经营统计每日营收、卡金充值、划卡消耗、员工业绩排行、项目销售占比。系统权限老板、店长、收银员、发型师不同角色看到不同菜单操作留痕。这里最关键的设计是“业务闭环”。顾客到店后前台在收银台选择会员和服务项目点击结算系统在一个事务里完成三件事检查并扣减会员卡余额、生成消费订单和明细、按项目提成比例生成发型师提成记录。如果选用了实物产品还要同时扣减库存。这套闭环跑通之后老板每天看的报表不再是几个手工拼出来的数字而是每一次操作自然沉淀的结果。1.3 这套系统里的“企业级”到底指什么“企业级”这三个字在技术圈经常被误解好像不搞微服务、不搞消息队列就算不上企业级。放到门店管理系统这个语境里我理解的企业级是指三件事权限边界清晰、资金数据可追溯、系统可扩展。权限边界清晰意味着收银员只能看到收银相关菜单不能看到全店利润发型师可以看到自己的业绩但看不到其他员工工资。这个通过后端接口鉴权加前端菜单权限双重控制来实现。资金数据可追溯意味着每一笔卡金变动都有流水记录谁操作的、什么时间、变动前余额多少、变动后余额多少全部写进流水表。门店一旦发生纠纷直接查流水就行不用靠互相撕扯。系统可扩展则要求在数据库设计阶段就预留门店编号、员工归属等字段将来从单店升级成多店连锁不必推倒重来。2. 技术架构与关键选型SpringBoot Vue MyBatis 背后怎么想的2.1 前后端分离开发但部署可以很轻项目整体采用前后端分离架构这是最成熟的方案。前端工程独立开发和构建后端通过接口对外提供服务前后端只有 JSON 数据往来。团队成员可以并行开发互不阻塞。但这个架构在部署时有一个特别实用的玩法前端打包出的 dist 目录可以直接扔进 SpringBoot 项目的 static 目录里随 jar 包一起运行。也就是说你既可以按传统方式把前端部署到 Nginx也可以把前端静态文件放进 SpringBoot最后只启动一个 Java 进程整个系统就跑起来了。对单店场景来说这种方式最省运维成本老板不需要理解什么是反向代理。真正需要 Nginx 的时候多半是后面接了微信小程序、App 端或者需要同一台服务器部署多个站点时才引入。2.2 SpringBoot 版本不能盲目追新在选型上我特别想讲一个坑SpringBoot 版本不是越高越好。早期这套项目用 SpringBoot 2.7.x 配合 JDK 8为什么不用最新的 3.x因为 3.x 对 JDK 版本有硬性要求同时大量第三方组件的包名从 javax 迁移到 jakarta很多老教程、老配置直接失效。我见过不止一个团队把项目升级到 SpringBoot 3 之后一连串配置报错最后实在搞不定又退回 2.x。对门店管理系统这种以业务稳定性为首要目标的项目我会优先选社区资料多、大家踩坑经验足的 SpringBoot 2.7.x。如果你是从零开始学也用 2.7.x 起步等到对底层机制足够熟悉再研究 3.x 的差异也不迟。这个选择背后不是技术保守而是控制交付风险。客户门店每天都有营业数据系统上线后每挂一分钟都可能在丢钱。2.3 Vue 技术栈Vue2 还是 Vue3 要看维护成本前端这块目前存量门店管理系统里 Vue 2 加 Element UI 的组合占了大头。这套源码按 Vue 2 组织原因很直接Element UI 组件生态成熟表格、表单、弹窗、日期选择器这些后台管理常用的组件拿来即用文档齐全会 Vue 的人基本都能快速上手。当然新项目完全可以走 Vue 3 加 Element Plus 的路线组合式 API 写起来更清爽性能也更好。但从选型角度看团队维护能力才是最关键的。如果接手的人都是 Vue 2 出身硬切到 Vue 3 只会增加沟通和排错成本。我实际的做法是先围绕业务梳理页面清单再去选前端框架而不是先定框架再套页面。页面多、逻辑重的后台系统稳定和效率远比“技术新”重要。2.4 MyBatis MySQLSQL 掌握在自己手里持久层选了 MyBatis 而不是 Spring Data JPA核心原因是门店系统的报表类查询特别多日营收报表、员工业绩报表、项目销售占比每一个都涉及多表关联、条件拼接、分组统计。这类 SQL 用 JPA 写起来很容易变成一段很难维护的“魔法字符串”反而用 MyBatis 的 XML Mapper 文件一条 SQL 清清楚楚摆在眼前后期优化起来也方便。配合 MySQL 8 使用需要注意几个基础设置数据库和表统一用 utf8mb4 字符集否则存不了 emoji 表情和生僻字时间字段用 datetime统一存 Asia/Shanghai 时区订单表、流水表这类高频查询的表要在门店ID、会员ID、创建时间上建好索引。MySQL 并不是装完就能直接用的字符集、时区、连接参数这三项配置不对开发环境再正常上线后也会出各种怪问题。3. 数据库设计从门店到会员的全链路表结构3.1 核心业务表数据库是整个系统的地基我的习惯是先画清楚核心业务表再开始写接口。这套系统的主要表包括门店表门店编号、名称、地址、联系电话、营业时间、状态。员工表姓名、手机号、职位、所属门店、入职日期、提成比例。会员表姓名、手机号、等级、卡金余额、累计充值、累计消费、所属门店。会员卡表卡号、卡类型、开卡时间、到期时间、余额、押金、是否挂失。卡金流水表会员ID、变动类型、变动前余额、变动金额、变动后余额、操作人、备注。服务项目表项目名称、类型、原价、会员价、预计耗时、提成比例、状态。产品表产品名称、条码、分类、规格、进价、售价、库存数量、预警阈值。预约表会员ID、员工ID、服务项目、预约时间、状态、备注。消费订单表订单号、会员ID、员工ID、订单金额、实收金额、支付方式、状态、创建时间。订单明细表关联订单、项目或产品、单价、数量、金额、提成比例、提成金额。提成记录表员工ID、订单ID、提成金额、计算状态、结算状态。这个表设计有一条主线用户、资金、商品、服务四个圈层。会员和员工属于人和组织卡金流水和消费订单属于钱的流动产品和库存属于实物流转预约和订单属于服务履约。理清这条主线后再去写业务代码就不会东一榔头西一棒子。3.2 两张核心表结构示例会员表和消费订单表是最常被查询的两张表贴一下自己的建表风格。这种写法不一定追求极致性能但胜在字段语义清楚接手的人一眼能看懂。CREATE TABLE member ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, shop_id bigint NOT NULL COMMENT 所属门店ID, name varchar(50) NOT NULL COMMENT 会员姓名, phone varchar(20) NOT NULL COMMENT 手机号, level tinyint NOT NULL DEFAULT 1 COMMENT 会员等级 1普通 2黄金 3铂金, balance decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 卡金余额, total_recharge decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT 累计充值, total_consume decimal(12,2) NOT NULL DEFAULT 0.00 COMMENT 累计消费, created_time datetime NOT NULL COMMENT 创建时间, PRIMARY KEY (id), KEY idx_shop_phone (shop_id, phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员表;CREATE TABLE consume_order ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, order_no varchar(32) NOT NULL COMMENT 订单号, shop_id bigint NOT NULL COMMENT 门店ID, member_id bigint DEFAULT NULL COMMENT 会员ID散客为空, employee_id bigint NOT NULL COMMENT 服务员工ID, total_amount decimal(10,2) NOT NULL COMMENT 订单原价总额, discount_amount decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 优惠金额, pay_amount decimal(10,2) NOT NULL COMMENT 实收金额, pay_type tinyint NOT NULL COMMENT 支付方式 1现金 2微信 3支付宝 4卡金, status tinyint NOT NULL DEFAULT 0 COMMENT 状态 0待支付 1已支付 2已退款, remark varchar(255) DEFAULT NULL COMMENT 备注, created_time datetime NOT NULL COMMENT 创建时间, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_member_time (member_id, created_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT消费订单表;3.3 表关系与设计细节这里有几个设计细节值得展开说。第一卡金余额冗余存储在会员表里同时每一步变动都写入流水表。有人会问为什么不直接查流水算余额原因很简单门店每天有大量查询需要高频读取余额每次实时汇总在性能上不划算。余额冗余和流水记录并存属于典型的“以空间换性能”做法但前提是必须在事务里保证两者同步不能只改余额不写流水。第二提成单独建一张记录表而不在订单明细里靠报表实时算。原因是要考虑“结算后修改”的场景。比如订单已经生成月末老板发现某个项目提成比例设置错了如果提成是实时算出来的改比例会直接影响历史报表单独建表后历史提成记录保持原样新订单才用新比例账目更稳。第三预约表里的会员ID允许为空因为门店存在大量散客到店直接剪发的场景。设计上不要为了强关系约束把手脚绑死否则每次接单都要先注册会员前台效率会大打折扣。4. 后端核心实现SpringBoot 接口与 MyBatis 实战4.1 工程结构与分层后端工程采用经典的四层结构controller、service、mapper、entity。controller 只做参数接收和结果返回service 写业务逻辑mapper 是 MyBatis 的数据访问接口XML 文件放 SQL。实际项目中我还会加一层 dto用来接收前端传参和返回前端视图对象。有些朋友喜欢在 controller 里堆一堆业务代码接口几百行看着也能跑但后面维护时真的痛苦。业务规则和 HTTP 层混在一起想复用逻辑只能复制粘贴。这个项目在 controller 层非常薄比如创建订单的接口controller 里只有参数校验和调用 service 两行真正的扣款、提成、库存逻辑都在 service 事务里。这样做的好处是将来如果增加微信小程序端只需要复用 service 层前端怎么调接口不影响核心逻辑。4.2 登录认证与权限控制门店系统的角色不算复杂我用 JWT 加拦截器来做接口鉴权。用户登录成功后后端生成一个带角色信息的 token前端后续请求在请求头里携带 token拦截器校验通过后放行并把当前登录用户信息放入 ThreadLocal。对老板这种只有一个超级管理员角色的场景完全够用没必要上 Spring Security 那一整套过滤器链。核心逻辑大概是这样public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { throw new BusinessException(未登录或登录已过期); } LoginUser user JwtUtil.parseToken(token); UserContext.set(user); return true; }拦截器只负责身份认证具体的菜单权限和接口权限我用一个更简单直接的方式给每个接口配置一个权限标识比如order:create登录用户拥有的权限集合里包含这个标识才允许访问。权限配置放在数据库的菜单表里用角色关联菜单前端登录后根据角色菜单生成动态路由后端接口再做二次校验。前后端双重控制能避免“改了前端源码就能绕过按钮”这种尴尬情况。4.3 核心业务接口与动态 SQL会员列表查询是典型的多条件分页场景姓名、手机号、等级都可能作为筛选条件。这种查询用 MyBatis 的动态 SQL 非常顺手一段if判断即可。XML 里的写法大概是select idselectMemberPage resultTypecom.example.entity.Member SELECT * FROM member where if testname ! null and name ! AND name LIKE CONCAT(%, #{name}, %) /if if testphone ! null and phone ! AND phone #{phone} /if if testlevel ! null AND level #{level} /if /where ORDER BY created_time DESC /select再重点说下单事务的实现。卡金扣款和订单生成这一串操作必须在一个事务里。我写过一个版本一开始图省事在 controller 里分别调用两个 service 方法结果出现“订单生成了但余额没扣”的极端情况排查半天发现是事务边界问题。后来统一收口到一个 service 方法加上Transactional扣余额、写流水、生成订单、写明细、算提成、扣库存全部要么成功要么回滚。Transactional(rollbackFor Exception.class) public void createOrder(CreateOrderRequest request) { Member member memberMapper.selectByIdForUpdate(request.getMemberId()); if (member.getBalance().compareTo(request.getPayAmount()) 0) { throw new BusinessException(会员卡余额不足); } memberMapper.deductBalance(request.getMemberId(), request.getPayAmount()); memberFlowMapper.insert(FlowRecord.build(request)); orderMapper.insert(order); orderItemMapper.insertBatch(order.getItems()); commissionMapper.insertBatch(CommissionRecord.build(order)); }这里还有一个并发扣款的细节selectByIdForUpdate显式加行锁就是为了防止两个收银台同时操作同一个会员。门店高峰期前台两台电脑同时收银的情况太常见了如果不用锁余额完全可能扣成负数。4.4 MyBatis 缓存、TypeHandler 与常见性能点MyBatis 的一级缓存默认是开启的作用范围是一次 SqlSession同一个查询在事务内执行两次会命中缓存。二级缓存默认关闭门店管理系统一般不建议打开因为缓存失效策略一旦控制不好很容易读到脏数据尤其涉及金额的表宁可少一点缓存也要保证数据绝对正确。TypeHandler 是个容易被忽略但很实用的功能。比如会员表里有些扩展属性存成了 JSON 字符串Java 实体类里对应一个 Map 字段自定义一个 TypeHandler把数据库里的 JSON 串和 Java Map 自动互转就不用每次手动序列化反序列化。这个功能在维护系统配置、会员标签这类字段时非常省心。分页方面我在工程里整合了 PageHelper。它内部会对 SQL 做拦截改写自动拼接 LIMIT。要注意的是 PageHelper 有个经典坑分页参数必须紧跟查询语句中间隔着其他查询就会分页错乱。所以我在 Mapper 代码规范里明确约定需要分页时查询方法内部只写一条 SQL不掺入关联子查询避免 PageHelper 识别错对象。5. 前端核心实现Vue 页面、路由与数据可视化5.1 环境准备与工程搭建前端工程是标准的 Vue 项目。环境配置这里提醒一下Node 版本不要盲目追求最新Vue CLI 对某些过新版本的 Node 可能报兼容性错误。装 Vue CLI 脚手架后进入项目目录执行npm install依赖安装阶段容易遇到网络和版本锁问题大部分情况下把package-lock.json删掉重新安装就能解决。后台管理页面的组件我这里主要依赖 Element UI 的表格、表单、对话框、标签页几类组件。拿到一套新页面时不要急着写代码先把路由、菜单、接口请求层搭好。我的习惯是先在src/api目录里按模块建接口文件登录、会员、订单、员工分开再写页面这样的项目结构后面横向扩展非常舒服。5.2 Axios 请求封装与拦截器前端所有接口请求统一走一个封装好的 axios 实例。拦截器里主要做三件事请求发出前带上 token响应返回后统一处理 HTTP 错误码和业务错误码特殊场景统一弹出提示。门店前台操作场景多错误提示如果不统一就会出现“接口报错了但用户完全不知道怎么回事”的问题。service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use(res { const code res.data.code if (code 200) { return res.data } if (code 401) { router.push(/login) } Message.error(res.data.msg) return Promise.reject(res.data) })这个封装看起来基础实际上解决了大量重复代码。我最早做项目时每个页面都手动写 token 拼接和错误处理后来发现一改后端错误码几十个页面都要跟着动教训非常明显。5.3 动态路由与菜单权限配合登录后需要根据角色动态生成可访问的路由。传统做法是把所有路由一次性注册再用按钮权限去隐藏菜单不够安全也不够干净。更合理的做法是前端维护一套完整的路由配置每个路由带上权限标识登录后根据用户权限集合过滤出可用路由再用路由的addRoutes动态注册进去。这里有个经验动态路由不要按角色字符串写死判断比如“如果是老板就加这些路由”而应该按权限标识判断。因为门店角色是可配置的今天叫老板明天改成“店长”只要权限标识不变路由逻辑就不用动。我踩过按角色名写死的坑后期加角色时前端逻辑越补越乱重构成权限标识驱动才彻底解决。5.4 收银台、预约看板与经营统计页面落地前端最核心的页面是收银台。这个页面交互很密集左边搜索会员中间选择服务项目或商品右边展示当前单据能计算会员折扣能选择支付方式结算成功后自动打印小票。这个页面做得好坏直接影响前台效率。我用了比较多的表单联动选择会员后自动带出卡金余额选择服务项目后自动计算金额和折扣每一步都给操作人员明确反馈。预约看板推荐用时间轴布局按员工维度展示一天的预约情况某个时间段被占用了对应格子就有颜色标记。这样前台排班和发型师接单都一目了然。经营统计页面用的是 ECharts充值趋势、消费趋势、员工业绩排行、项目销售占比各做一个图表老板看报表不需要懂数据库图表就是最好的人机交互。6. 环境搭建、打包、部署全流程6.1 数据库初始化与后端启动拿到源码先做的事情不是跑代码而是准备数据库。我建议用 MySQL 8.x安装完成后把初始化 SQL 脚本导入。数据库字符集一定要检查建库时可以显式写上utf8mb4避免各种中文乱码问题。连接数据库的工具用官方 MySQL Workbench 或 Navicat 都可以顺便把账号权限建好不要再拿 root 账号跑业务。后端配置文件application.yml里重点检查几项数据库连接、端口、时区、文件上传路径。时区问题不显眼但危害大很多系统上线后发现时间差八小时就是因为连接串里没指定serverTimezoneAsia/Shanghai。文件上传路径要预先规划好图片存本地目录还是对象存储先在配置里定下来后面移动存储时业务代码不用改。6.2 Maven 打包与 Java 进程启动后端使用 Maven 管理依赖打包前把application.yml里的环境相关配置确认一遍执行mvn clean package生成目标 jar 包。启动直接用java -jar xxx.jar如果服务器内存不大可以加-Xms256m -Xmx512m限制一下堆内存。门店管理系统平时的并发量并不高没必要给 JVM 分太多内存反而更要注意日志是否完整。日志方面我建议至少保留info级别并独立输出到文件。尤其是收银、充值这类资金敏感操作最好单独打印一条操作日志线上排查问题时非常有用。日志文件要做滚动切割避免单文件无限增大把磁盘撑爆。6.3 前端构建与 SpringBoot 静态资源整合前端构建执行npm run build生成 dist 目录。这里有两种部署路线。如果你把前端直接放进 SpringBoot就把 dist 里的文件复制到src/main/resources/static目录再重新打包一个 jar 包全部跑干净。这种方式启动后访问服务器 IP 加端口看到的就是登录页。如果想要更规范的站点管理就单独部署 Nginx。生产环境我更推荐先 Nginx 后 SpringBoot 的组合Nginx 负责静态文件服务和 HTTPS 终结后端只处理api开头的请求。配置大概是server { listen 80; server_name yourdomain.com; location / { root /opt/app/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里try_files $uri $uri/ /index.html;这一行很关键它解决的是 Vue 路由在 history 模式下刷新页面 404 的问题。我见过太多项目因为漏掉这一行用户一刷新就白屏。6.4 部署后必须检查的清单系统上线前我会按固定清单过一遍第一数据库是否做了定时备份备份策略可以用系统 crontab 定期执行 mysqldump第二服务器时间是否准确偏差太大会影响订单时间展示第三初始管理员密码是否已经修改第四图片上传目录是否存在且权限正确第五检查后端日志路径是否可写。这些细节看起来琐碎但每一项都是真实线上事故的来源。7. 常见问题与排查技巧实录7.1 MyBatis、Vue、MySQL 高频问题速查表现象常见原因解决办法MySQL 连接报 SSL 连接错误连接串未关闭 SSL 校验jdbc URL 加useSSLfalse中文插入后变乱码数据库字符集不是 utf8mb4库、表、连接串统一 utf8mb4查询返回字段全是 null实体类属性名与表列名不匹配开启map-underscore-to-camel-casetrue或写 resultMap前端接口请求跨域前端域名和后端端口不一致后端配置 CORS 或通过 Nginx 同域代理Vue history 路由刷新 404Nginx 没做 SPA fallback配置try_files $uri $uri/ /index.htmlPageHelper 分页错乱分页参数后跟了多条 SQL确保紧跟查询语句并在 Mapper 方法内只放一条 SQL时间显示差 8 小时数据库连接未指定时区serverTimezone 设为 Asia/Shanghai打包构建过程报内存溢出Node 或 Maven 构建内存不足调整 Node 内存参数或 Maven 设置 MAVEN_OPTS这张表尽量按“现象、原因、办法”三列来维护团队内部也可以不断补充。排查问题时先看日志里有没有明确报错再看请求参数是否异常最后怀疑配置问题不要一上来就乱翻代码。7.2 典型故障会员余额高峰期被扣成负数这是我在真实门店上线后遇到的一个比较典型的资金问题。系统运行了两周财务对账发现某个会员的卡金余额变成了负数但这笔异常并不是某一次操作造成的而是两个收银台几乎同时对这个会员发起扣款在没加锁的情况下两个请求都读到余额 30 元一个扣 20、一个扣 20数据库最终余额变成 10 元但两笔订单都成功了相当于门店多付出了 10 元的额度。当时的排查过程是先查操作日志确认两次操作时间靠得很近再回看代码发现查询余额没有加锁。修复方案就是在查询余额时改成SELECT ... FOR UPDATE把这一行锁住第二个请求必须等第一个事务提交后才能继续余额自然不会为负。经过这个案例后我在所有涉及金额变动的业务代码里都加了一条硬性要求先锁行再比较再更新。7.3 几个值得长期坚持的工程习惯最后分享几个我在这个项目里长期坚持的工程习惯。第一资金类字段一律用 decimal 而不是 float 或 double浮点数的精度问题在钱上面是不可容忍的。第二每个接口都写清楚业务错误码和错误信息不直接抛 Java 异常给前端。第三MyBatis 的 SQL 在 XML 里写不依赖注解拼字符串至少团队 review 代码时能快速看到完整 SQL。第四版本控制提交信息要规范。这个项目从第一行代码开始就规定feat、fix、refactor前缀后期追查问题从 git 记录里能很快定位到改动范围。系统上线不是终点后面还有无数需求变更和 bug 修复一个结构清楚、行为规范的代码库比多写一万行代码更有价值。