最近在给一家纺织品企业做财务管理系统,技术栈选了Java + SpringBoot + Vue3 + MyBatis,数据库用MySQL,前后端分离方式开发。这个项目从零搭建到交付,踩了不少坑,也积累了一些心得。想着很多同行也在做中小企业管理系统,特别是有财务核算这类相对严肃的业务场景,就把整个设计和实现过程整理出来,给后来人一个参考。
文章会覆盖技术选型、数据库设计、后端事务和报表处理、Vue3前端落地、以及我从开发到上线遇到的一堆经典问题(比如MySQL SSL连接报错、MyBatis的typeHandler工作逻辑、SpringBoot版本过高带来的兼容性坑)。适合正在做管理系统、供应链系统,或者想用前后端分离快速搭一套企业内部系统的朋友。我尽量把关键细节讲透,同时保留可直接复用的代码片段。
1. 项目整体设计与技术选型
1.1 为什么坚持前后端分离
纺织品企业的财务系统,表面看是“记账、管应收应付、出报表”,实际使用场景往往比想象中复杂。工厂财务要同时处理原材料采购、坯布销售、染整加工费、代销结算等多种业务,电脑端之外还存在老板随时看报表、财务部多人同时录入凭证的情况。如果按照传统单体开发,用服务端渲染页面,后期想给移动端复用接口,就得重写一套,很被动。
前后端分离解决的不只是开发效率的问题。前端只专注交互和展示,后端只提供Restful API,两边可以并行开发。纺织企业的业务规则变动频繁,比如费用项调整、科目变更、报表模板修改,分离后前端界面调整不影响后端接口,后端规则改动也不阻塞前端迭代。更重要的一点,财务系统的数据安全等级高,API化之后,权限校验、操作日志、审计追踪都能统一在网关层处理,比在页面里嵌入一大堆判断要干净得多。
很多人担心前后端分离增加部署成本,但就我的实践来看,SpringBoot打包成jar,前端build成静态文件扔到Nginx,再配上反向代理,反而比传统的war包部署更简单。团队里一个前端、一个后端,用接口文档约定好数据结构,开发效率直接翻倍。
1.2 技术栈选型和版本搭配的深层思考
这个项目我最终定下的组合是:SpringBoot 2.7.x + MyBatis 2.3.x + MySQL 8.0 + Vue3 + Vite + Element Plus + Pinia。这套组合是参考了近几年社区里大量企业级后台管理系统的常用配置,也规避了不少兼容性雷区。
先说说SpringBoot版本。我前期调研时注意到很多新项目直接上了SpringBoot 3.x,但3.x有两点容易让人措手不及:一是强制要求Java 17,二是javax包名改成了jakarta。如果团队里还有人习惯用Java 8,或者项目要集成一些老牌三方库,兼容成本会很高。我最终选择2.7.x,因为它的LTS预定支持周期足够长,MyBatis官方starter在2.7.x下跑得非常稳定,不需要额外处理命名空间迁移。如果你的项目团队已经全面拥抱Java 17,那可以直接用3.x,但千万别迷信版本越新越好。
Vue3就不用多解释了,组合式API带来的逻辑复用能力确实比Vue2的Options API适合大型后台系统。我之前用过一个Vue2写的旧系统,几百行代码堆在一个methods里,看着就想重构。Vue3的setup语法让每个业务模块的state、computed、watcher都能集中组织,财务这种带大量表单和状态判断的场景写起来很舒服。
至于为什么选MyBatis而不是JPA,核心原因是财务系统的查询极其依赖手写SQL。比如利润表要跨多张表聚合,资产负债表要按科目方向做余额折算,这些复杂查询用JPA的Specification拼起来又难看又难调优。MyBatis让你对SQL有绝对的控制权,一条慢SQL可以直接在XML里改写,执行计划一目了然。再加上财务模块天然需要强事务和高数据准确性,手写SQL能大大减少ORM映射带来的意外结果。老实说,MyBatis的XML写起来是啰嗦,但它写在明面上,审计核查时清清楚楚,这个特质在财务场景下反而是加分项。
数据库选MySQL主要考虑的是部署和成本。纺织品企业绝大部分是中小型规模,财务数据虽然敏感但量级有限,MySQL的InnoDB引擎在事务支持和崩溃恢复上足够可靠。我用了MySQL 8.0,因为官方对utf8mb4的支持比5.7更完善,能直接存储生僻字和emoji,避免很多中文乱码问题。
1.3 系统核心模块拆解
财务管理系统不是做一个“账单本”,必须要覆盖企业财务的基本账务闭环。我按这个拆法来做模块:
-
基础资料模块:会计科目、客户档案、供应商档案、仓库信息、产品与物料信息。虽然听起来基础,但后头所有凭证都依赖这些档案的引用,一旦允许界面随意录入非档案客户,后续对账就全乱套。
-
凭证管理模块:记账凭证的填制、审核、过账、反过账。这里是财务信息的源头,所有报表数据都从凭证层提取,不允许绕过凭证直接改余额。
-
应收应付模块:应收账款登记、收款核销、应付账款登记、付款核销,包括账龄分析和逾期提醒。纺织行业里赊销比例很高,应收管理直接影响企业现金流。
-
成本核算模块:按订单归集原材料、人工、制造费用,计算出坯布或成品的单位成本。这个模块业务定制性强,每个工厂的计算逻辑都不太一样,我预留了自定义分摊接口。
-
账务处理模块:期末调汇、结转损益、生成科目余额表、试算平衡表。
-
报表中心:资产负债表、利润表、现金流量表,以及管理层需要的自定义报表。纺织企业很看重客户对账单,所以还做了一个简易版“对账单导出”。
这六个模块不是拍脑袋拆的,而是在参考了财务软件常见功能后,针对纺织品企业“生产+贸易”并存的特点做了裁剪。如果只做一个纯通用记账软件,纺织行业的工序成本和款式订单完全体现不出来,最后肯定会被业务方吐槽。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与权限模型
2.1 财务核心表结构怎么设计才不至于埋雷
数据库设计是整个项目的地基,我一开始就明确了几个底线:金额字段一律用 decimal(18,2),Java侧配BigDecimal,杜绝float/double的精度误差;所有表都带主键、创建时间和更新时间;逻辑删除字段要有,但财务表尽量不删数据,只做反记。
凭证相关的三张表是财务系统的命脉:
凭证主表 voucher(存凭证号、记账日期、附件张数、审核人、过账状态、备注),凭证明细表 voucher_detail(存每条科目分录的摘要、科目ID、借方金额、贷方金额、往来单位ID),科目表 account_subject(存科目编码、科目名称、科目类型、余额方向、上级科目)。
为什么凭证要拆主表和明细表?因为一张凭证包含多条分录,比如借:原材料,贷:应付账款,必须成对出现。拆表后主表只存一次性的凭证头信息,明细表可以随便关联多行,还方便以后查“某个科目的所有凭证分录”。
| 字段 | 类型 | 说明 |
|---|---|---|
| subject_code | varchar(20) | 科目编码,如1001/2202 |
| subject_name | varchar(50) | 科目名称 |
| subject_type | tinyint | 1资产/2负债/3权益/4成本/5损益 |
| balance_direction | tinyint | 借方方向:1为借、2为贷 |
| parent_code | varchar(20) | 上级科目编码,空表示一级科目 |
应收回款和应付付款的业务不能简单在明细表里再插一行“收钱”,我设计了独立的 receivable_payment表,记录每一笔核销时间、核销金额、关联的凭证明细ID。这样后续账龄分析直接从这个表统计,不需要去拆凭证历史。
往来单位(客户/供应商)尽量抽成独立表,不要只存个名称。纺织企业可能有同名客户,只用名称关联迟早会出事。客户表用统一ID和客户编码做唯一标识,坏账风险等级、信用额度这些也一起维护。
还有一个特别要说的:科目余额表不要每次实时去汇总凭证明细。虽然数据量不大时汇总很快,但会随着数据增长越来越慢,而且复杂查询很难优化。我加了一张 subject_balance表,在凭证过账时同步更新科目余额。这样查询资产负债表明细可以秒出,代价是过账逻辑必须严格事务控制。这也是后面讲数据一致性时的重要基础。
2.2 财务数据一致性:事务是底线
财务系统最忌讳的就是出现“半条账”。举个例子:新增一张凭证,主表插入了,明细表也插入了,然后更新科目余额时因为某张子表加锁失败,系统报错回滚。如果事务没控制好,就会出现凭证有记录但余额没更新,或者明细少了关键分录,对账永远对不平。
SpringBoot里用 @Transactional 绝大多数场景没问题,但有几个隐藏坑我踩过:同一个类内部方法直接调用,事务注解会失效,因为事务是基于AOP代理的不经过代理的自调用不会生效。想让自调用也走事务,可以把两个方法拆分到不同Service,或者注入自身代理对象、或使用 AopContext.currentProxy()。异常被try-catch吞掉也会让事务失效,因为事务只在异常传播到代理方法时才回滚。还有,事务里别做IO操作,比如请求外部接口、发短信,不然数据库连接一直被占用,高并发时容易爆连接池。
过账操作必须严格判断凭证状态:只有已审核的凭证才能过账,过账后凭证状态变为“已过账”,不能再被修改。这些状态判断也要放在同一个事务里,通过数据库更新条件 where status='已审核' 来保证并发下也不会重复过账。
还有一个小技巧:更新余额时使用“乐观锁”或“先查询再从应用层计算”的方式百分之百会产生并发覆盖问题。我最终是在一条SQL里完成余额更新:UPDATE subject_balance SET balance = balance + #{amount} WHERE subject_id = #{subjectId},这种原子更新把计算交给数据库,避免了多线程读取旧值再回写覆盖的风险。
2.3 RBAC权限与数据隔离:纺织企业特别在意分厂和部门
权限我用了典型的RBAC模型:用户表、角色表、菜单表、用户角色关联表、角色菜单关联表。登录后用户拿到角色对应的权限标识列表,前端根据权限动态渲染菜单和按钮,后端接口在Spring Security的Filter里校验权限码。
但只有菜单权限远远不够,纺织企业经常有多个分厂、多个业务部门,财务人员只能看自己分厂的数据,如果所有用户都能查全局,机密性就没了。为此我给核心业务表统一加了 dept_id字段,在后端查询是强制按当前用户的部门维度过滤。具体做法是在MyBatis的拦截器或SQL拼接中注入过滤条件,前端拿到的数据永远是被部门隔离后的结果。
这里提醒一句:前端隐藏菜单只能防君子,真正严格的数据权限必须放在后端SQL层。只在前端判断的话,别人拿接口工具直接请求就能绕过,等于没设防。
3. 后端核心实现与踩坑记录
3.1 项目分层和MyBatis配置的几个要点
后端工程我按常见的分包方式:controller、service、mapper、entity、dto、vo、config。controller只做参数接收、调用service、结果封装,绝对不写业务逻辑。service负责事务和业务规则。mapper只做数据库操作。这样一旦财务规则有问题,定位起来很清爽。
MyBatis配置有个细节:开启下划线转驼峰最容易踩坑。SpringBoot的 application.yml 里需要设置:
yaml复制mybatis:
configuration:
map-underscore-to-camel-case: true
如果不设,查询结果里 subject_code 就无法映射到 subjectCode 属性上,返回给前端就成了null。以前我用老版本MyBatis时还喜欢在XML里写 resultMap,现在基本都是开启驼峰转换,只对特殊统计结果写 resultMap。
另外打印SQL在开发阶段很关键,配置:
yaml复制logging:
level:
com.example.mapper: debug
这样真正执行的SQL会打在控制台。遇到SQL数据不符,先把实际SQL拷出来扔到Navicat里跑一次,会省掉很多瞎猜时间。
3.2 凭证录入、过账与反过账的实务逻辑
凭证录入是财务系统最常用的功能,也是业务难点。界面上用户要动态增加多行分录,每行选择会计科目、填金额(必须选借方或贷方),最后要求“有借必有贷,借贷必相等”。这个逻辑后端起关键作用:
java复制@Override
@Transactional(rollbackFor = Exception.class)
public Long createVoucher(VoucherForm form) {
// 1.参数校验
if (CollectionUtils.isEmpty(form.getDetails())) {
throw new BizException("凭证明细不能为空");
}
BigDecimal debitTotal = BigDecimal.ZERO;
BigDecimal creditTotal = BigDecimal.ZERO;
for (VoucherDetailForm detail : form.getDetails()) {
if (detail.getDebitAmount() != null) {
debitTotal = debitTotal.add(detail.getDebitAmount());
}
if (detail.getCreditAmount() != null) {
creditTotal = creditTotal.add(detail.getCreditAmount());
}
if (detail.getDebitAmount() == null && detail.getCreditAmount() == null) {
throw new BizException("借贷金额至少填写一项");
}
}
if (debitTotal.compareTo(creditTotal) != 0) {
throw new BizException("借贷不平衡,请检查");
}
// 2.插入凭证主表
Voucher voucher = new Voucher();
voucher.setVoucherDate(form.getVoucherDate());
voucher.setStatus(0); // 0草稿 1审核 2过账
voucher.setAttachmentCount(form.getAttachmentCount());
voucherMapper.insert(voucher);
// 3.插入明细
for (VoucherDetailForm detail : form.getDetails()) {
VoucherDetail entity = new VoucherDetail();
entity.setVoucherId(voucher.getId());
entity.setSubjectId(detail.getSubjectId());
entity.setSummary(detail.getSummary());
entity.setDebitAmount(detail.getDebitAmount());
entity.setCreditAmount(detail.getCreditAmount());
voucherDetailMapper.insert(entity);
}
return voucher.getId();
}
这段代码看着简单,实际上藏着财务系统最重要的“借贷平衡”校验。很多人认为前端校验过就够了,但我坚持后端再做一次。财务数据不能信任任何单一的校验层面,否则前端出个漏洞就是脏数据。
过账逻辑比新增复杂得多,因为要同时更新科目余额。我把它拆成这几步:更新凭证状态 -> 校验凭证未过账 -> 遍历明细更新科目余额 -> 如果是应收应付凭证还要生成应收/应付流水。这四步全部放在一个事务里,只要任一步失败,整张凭证就保持原状态。
反过账则是过账的逆操作,必须区分“已有后续往来的凭证不允许反过账”,比如某张应收凭证已经被收款核销了,那反过账会把核销关系弄乱。我在代码里加了一个判断:如果凭证关联的 receivable_payment 表存在已核销记录,就禁止反过账。这种业务规则最好写在数据库中,至少在service层校验,不能依赖开发人员自觉。
3.3 复杂报表SQL和Excel导出的性能优化
纺织企业的财务报表和普通企业有差异,比如需要按“纱线、坯布、成品面料”分类统计成本,还要算每一个订单的毛利率。这种查询如果老用ORM一个个表去查,再在Java里拼接,效率很低且代码一团糟。我用MyBatis在XML中写了两条核心报表SQL,一条是科目余额表汇总,一条是利润区间统计。
写复杂SQL时有个经验:先手写逻辑清晰、单表join的SQL,用Navicat跑通数据后,再搬到XML里。不要试图直接在XML里边写边调,那样遇到问题根本分不清是SQL问题还是映射问题。用好 <where> 标签和 <if> 标签来动态拼条件,避免在Java里手动拼SQL导致注入风险。
Excel导出方面,最初的版本我用的是Apache POI的 Workbook,结果数据量超过一万行时内存直接飙升,一次导出要四五秒,生产环境内存一紧张就OOM。后来换成 SXSSFWorkbook(XSSF的流式版本),改为边生成边往文件写,内存占用明显下降。如果你只需要导一页几千行,普通Workbook也够,但别在小内存服务器上作死。
至于有人问“java poi word能生成图表吗”,我用实践说话:POI原生对Word图表支持很弱,要生成带图表的Word报表,用Freemarker或Aspose做模板填充更靠谱。我后来做月度财务分析报告时,直接在前端用ECharts出图,后端生成Excel数据源,这样既灵活又不用折腾POI的图表API。
3.4 登录鉴权和跨域统一处理
登录我用JWT(JSON Web Token),配合Spring Security做认证和授权。流程很简单:用户输入账号密码,后端校验通过后生成带角色权限信息的token,返回给前端存储(我放在Pinia和localStorage里)。之后每次请求都在Authorization头带上token,后端过滤器解析token,把用户信息放进当前上下文。
这里有个坑:Spring Security默认会拦截所有请求,如果你不配置放行登录接口、验证码接口,前端第一次联调就全是401。放行要根据实际情况配置,比如:
java复制.antMatchers("/auth/login", "/captcha", "/file/**").permitAll()
.anyRequest().authenticated()
生产部署跨域上,本地开发我用Vite的proxy把/api下的请求代理到后端,规避跨域。上线后直接用Nginx反向代理,把前端静态文件和后端接口放在同一个域名下的不同路径,这样甚至可以不单独配置跨域,浏览器的同源策略也拦不住。如果你确实要纯跨域请求,可以定义CorsConfigurationSource,但记住一句,CORS开得太宽实际上等于把自己的接口敞在别人面前,尽量限制来源。
4. 前端Vue3项目实操
4.1 Vue3安装和项目搭建的细节坑
前端我用Vite快速初始化,比Webpack快得多。命令也很传统:npm create vite@latest finance-web -- --template vue,然后安装并启用路由、Pinia、Element Plus。
很多人在第一步“vue3安装scss”就卡住。网上很多老教程会让你装 node-sass,那个东西编译需要依赖Python和C++,环境一有问题就跪。我建议直接装 sass(即Dart Sass):
bash复制npm install -D sass
Vite天然支持 .scss 文件,只要安装好 sass,加个 <style lang="scss"> 就能用,根本不需要额外的scss-loader配置。我在做主题定制时把颜色变量都放在 src/styles/variables.scss 里,通过 cssPreprocessOptions 全局注入,这样每个组件直接用变量就行,不用到处 @import。
4.2 Pinia状态管理:用户态和菜单权限的最佳姿势
后台管理系统绕不开“用户信息”和“菜单权限”的全局状态。Vue3里我用Pinia写了一个store:
javascript复制// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
token: localStorage.getItem('token') || '',
userInfo: null,
permissions: [],
menus: []
}),
actions: {
async fetchUserInfo() {
this.userInfo = await api.user.getInfo()
this.permissions = this.userInfo.permissions
this.menus = generateMenus(this.userInfo.menus)
},
logout() {
this.token = ''
this.userInfo = null
this.permissions = []
localStorage.removeItem('token')
}
}
})
为什么用Pinia而不是Vuex?因为Pinia的TypeScript支持更友好,代码量也更少,没有那些无谓的mutations,直接能在action里改state。刚开始团队里有人习惯Vuex,但用了两天Pinia就回不去了。
菜单权限我推荐动态生成,不要让前端把所有菜单都写死在静态路由里。登录后根据后端的菜单树数据,动态 addRoute 到路由实例,有些没有权限的路由压根就不存在,这比单独用指令隐藏菜单按钮更彻底。
4.3 财务表单的可复用封装和动态校验
财务录入界面几乎都是“大表单+弹窗+明细表格”,我花了不少时间封装通用组件。比如金额输入框,需要自动保留两位小数、不允许负数、粘贴时自动过滤非法字符。这个需求在凭证、报销、付款弹窗里反复出现,我就封了一个 AppDecimalInput 组件,内部用 v-model 接收字符串,再通过计算属性转BigDecimal,避免显式调用 .toFixed(2) 时产生额外模型更新。
凭证明细表是典型的动态行,我用了Element Plus的 el-table,每行的单元格用的是 el-input-number 或 el-select。动态行有个经典Bug:输入金额后行内数据不刷新,或者新增行时把上一行的校验状态带过来。根本原因是v-for渲染时没有给每行唯一且稳定的 :row-key,我直接给每行加一个 row.tempId,随手生成UUID,不加这个就是纯粹折磨。
跨表单校验也别全用正则硬怼,Element Plus的表单支持 async-validator 自定义规则,比如“借贷方向选借且金额大于0,或者选贷且金额大于0,不允许两个方向同时填”。我把这个规则写成函数,挂在每行明细上,用户体验很顺。
还有一个体验优化:金额合计直接放在表格下方,用计算属性实时求和,只要借贷不平衡就亮红色提示。把校验前置到界面,减少后端报错。但这不意味着后端可以不校验,前后端两层校验缺一不可。
4.4 Axios封装:统一loading、异常提示和token过期
所有请求我都通过封装的axios实例发起。封装要做三件事:请求拦截器里自动追加token;响应拦截器里统一解包 {code,data,message} 结构;遇到401就弹出登录过期提示并跳转登录页。
javascript复制// utils/request.js
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const service = axios.create({
baseURL: '/api',
timeout: 10000
})
service.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) config.headers.Authorization = `Bearer ${token}`
return config
})
service.interceptors.response.use(
res => {
const { code, data, message } = res.data
if (code === 200) return data
ElMessage.error(message || '请求异常')
return Promise.reject(new Error(message))
},
err => {
if (err.response?.status === 401) {
localStorage.removeItem('token')
router.push('/login')
} else {
ElMessage.error(err.message || '网络错误')
}
return Promise.reject(err)
}
)
这套封装看着简单,但真的避免了每个页面都在 catch 里重复处理错误。我做项目时特别强调错误提示必须来自后端message,前端自己编的错误文案往往会误导。
5. 常见问题与排查技巧实录
5.1 MyBatis的typeHandler到底是什么流程
看网上很多人面试会被问“mybatis中typehandler的工作流程图”。聊点实际的:typeHandler负责在PreparedStatement里设置SQL参数时,把Java类型转换成JDBC类型;以及在ResultSet读取数据时,把JDBC类型转换成Java类型。它本质是JDBC与Java对象之间的“翻译官”。
在财务系统里最常见的场景是处理枚举,比如借贷方向 Debit/Credit,我更倾向用Integer存储,而不是字符串。给一个枚举字段加自定义typeHandler时需要实现 BaseTypeHandler<T>,重写四个方法:
setNonNullParameter:在写SQL时把Java枚举转为IntegergetNullableResult(三个重载):从Result集里读取Integer并转为枚举
配置时在mybatis-config里注册typeHandler,或者在字段上指定 @TableField(typeHandler = ...)。同事曾经在这个地方踩坑,他定义的typeHandler在新增时生效了,但查询时一直返回null,原因是数据库列与Java属性类型不匹配,他注册的handler只实现了其中两个重载。改成所有getNullableResult都实现后就好了。
我的建议是:财务系统里尽量别用太花哨的枚举映射,直接用Integer缓存字典,然后前端根据字典翻译成文字。因为后续报表组件很多是基于Map读数的,枚举反而增加序列化复杂性。
5.2 MySQL SSL连接错误和时区问题的解法
这个坑几乎每个人都会遇到。新装MySQL 8.0之后,用SpringBoot启动项目,日志会报:
code复制java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed
或
SSL connection error: unable to get private key
原因有两个:第一,MySQL 8.0默认启用SSL,但本地开发库经常没有配置合法证书,JDBC驱动在不安全的链路下就炸了。第二,连接需要拿公钥解密,客户端默认不允许,得在jdbc url里明确开。
我的解决方案是统一在数据库连接串上加上这三个参数:
ini复制jdbc:mysql://localhost:3306/finance?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
useSSL=false 开发环境可以放心,生产环境如果是云主机内网或者有防火墙,也可以保持false。如果对安全有要求,就在MySQL侧配好证书并放行SSL,但那样会涉及Java信任证书库的处理,成本比较高。
serverTimezone 必须设置,不然连接MySQL 8.0会差8个小时。还有一点,MySQL驱动从5.x升级到8.x后,驱动类变成了 com.mysql.cj.jdbc.Driver,千万记得别再用旧的 com.mysql.jdbc.Driver,否则直接ClassNotFound。
5.3 SpringBoot版本太高导致的“版本战争”
我起初照着最新文档用了SpringBoot 3.0,结果遇到几个bug排查花了一整天。最典型的是Spring Security 6.0对WebSecurityConfigurerAdapter废弃,写法整个变了。而网上90%的教程还是SpringBoot 2.x的写法,照着抄后编译都过不去。加上MyBatis starter的2.3.x版本只兼容SpringBoot 2.x,3.x需要引入单独的 mybatis-spring-boot-starter 新坐标,两者管理方式都换了。
如果团队没有强需求必须用SpringBoot 3,我建议小中型项目直接停留在2.7.x,这个版本当前用得非常广,技术资料也最全。等团队把坑趟明白了再考虑升级也不迟。别因为“新版本更酷”去给业务方埋雷,稳定性才是财务系统的第一位。
5.4 MyBatis缓存惹出的数据一致性问题
这里必须明确一个原则:财务系统不要开MyBatis二级缓存。MyBatis一级缓存作用在SqlSession内,默认开启,但多数时候一个请求就会新建一个SqlSession,所以一级缓存只在同一事务内有效。二级缓存则是跨SqlSession的全局缓存,看起来能给查询提提速,但财务数据是实时变化的,一旦缓存了科目余额或未过账数据,再去查可能查到旧值,直接影响对账和报表。
我遇到过这么一次:一个同事在某个Mapper上开了二级缓存,启动后测单个接口没问题,但只要有人改了凭证,另一个用户查余额还是旧值,追了半天愣是以为事务有问题。后来排查到缓存才恍然大悟。所以财务业务我全部关掉二级缓存,宁可多花零点几秒查库,也不要承担数据过期风险。如果真要缓存,就借助Redis,而且要设置合理的key和过期策略,绝不能让缓存层做业务正确性的假设。
5.5 前后端联调和部署中的CORS与404的问题
开发模式下前后端分离通常会遇到跨域问题。我已提到最好的做法是Vite配置proxy:
js复制// vite.config.js
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true
}
}
}
这时候前端请求 /api/user/list,代理到后端 http://localhost:8080/api/user/list,完全不需要后端开CORS。但需要注意,当页面里直接访问文件上传地址或WebSocket,也走代理的话要额外配置 ws: true。
上线部署后如果出现“404”,一般不是路径错,而是SPA路由不是真实文件。Nginx需要配置 try_files $uri $uri/ /index.html;,把你的所有无后缀请求都重写到index.html,再交给VueRouter自行匹配。我刚开始没加这段,页面一刷新就404,后来加上这句立马解决。
还有一个容易忽略的点:前后端分离后,如果接口返回404,先看看URL前缀有没有忘。我见过好几次,前端写的是 /user/list,后端base路径是 /api,代理配置里又没加前缀,结果一直404。这种基础问题建议直接用curl测一下接口,确认后端路径是通的,再回头看前端配置。
一点个人体会放在最后:财务系统的开发,比起技术炫技,更看重严谨和稳定。我这次项目里最大的收获不是用熟了Vue3和SpringBoot,而是真正体会到“业务规则优先级高于代码实现”这句话。数据库锁、事务边界、权限控制、缓存要不要开,每一项决定都会直接影响财务数据的可信度。如果你正在做类似的项目,建议先把表结构和事务边界想清楚,再动手写代码。这样后面遇到的坑会少很多,收尾也会顺很多。
