SpringBoot+Vue3纺织品企业财务系统开发实战:从单据流到业财闭环
发布时间:2026/9/30 11:49:28来源:尧图网络
做纺织品企业财务系统最怕的不是写代码而是把行业业务理解错。这个项目我前后折腾了三个多月从最初只想做个简单记账页面到最终落地成一套覆盖采购、销售、库存、成本核算、应收应付和报表的业务闭环系统。技术栈用的是国内中小型管理系统里最普遍的一套Java 17 SpringBoot 3.x MyBatis MySQL 8 Vue3 Vite Element Plus前后端完全分离。如果你正准备做类似的行业管理系统或者正在纠结 SpringBoot 和 Vue3 到底怎么配合才顺畅这篇就当一份过来人的全程记录看。这套系统要解决的问题说白了就是纺织品企业那种账实对不上、成本算不清、应收没人催的老毛病。纱线多少钱一吨买的、坯布多少米入库的、染费怎么摊到每米布上全是财务和业务来回对账的焦点。用传统 Excel 管到两百张单据之后基本就乱了。所以这套系统从设计第一天就不是财务软件模板而是围绕纺织行业真实的单据流来做业务闭环让每一笔业务都能落到凭证上让每个科目余额都能追到原始单据。适合谁参考一类是刚入行想做业务系统的开发者另一类是手头刚好有企业信息化需求的实施者。下面按我实际开发的顺序从选型、库表设计、后端实现、前端页面、部署排查五个维度展开。踩过的坑和想明白的道理都在里面有些是花了好几个通宵才趟平的。1. 项目整体设计与技术选型思路1.1 先看清纺织品企业的财务痛点再动手写代码之前最重要的一件事不是搭框架而是搞懂业务。纺织品企业的财务核算和一般商贸公司差别很大至少有这么几个绕不开的特点品种规格多。同一款面料可能有几十个色号、克重、门幅纱线还分棉纱、涤纶纱、混纺纱物料档案动辄上千条。单位换算乱。采购按吨、库存按公斤、销售按米坯布还经常按匹入库一匹多少米还不固定。财务对账时单位不一致是最常见的扯皮点。成本构成复杂。面料的成本包括原料、染化料、水电汽、人工、机物料而且不同订单、不同批次成本差异很大要做按订单、按品种的成本归集。账期长、应收压力大。面料贸易基本都是月结30天、60天甚至90天应收款占用资金特别大账龄管理做不好企业现金流很容易出问题。上下游对账频繁。采购有折扣、返利、补差销售有退换货、次品折价这些业务要么在单据层面体现要么在财务层面调整系统必须两头都能接住。所以系统定位不是出个报表给老板看而是要支撑业务人员开单、财务人员记账、管理者看经营这三个层次。我最终把模块定为基础资料、采购、销售、库存、生产领料与成本、应收应付、总账与报表七大块业务单据审核后自动或半自动生成记账凭证形成单据驱动凭证的闭环。1.2 为什么是 SpringBoot Vue3 MyBatis MySQL 这套组合选型的时候我也纠结过一阵子比如后端要不要用 JPA、前端要不要上 React、数据库要不要上 PostgreSQL。最后都放弃了原因很实际SpringBoot 3.x 选它是因为内嵌 Tomcat、起步依赖统一版本、社区资料极多。财务系统讲究稳定和可审计用一套被长期验证过的生态比追逐新技术更安全。更关键的是招人容易后续维护的人接手成本低。MyBatis 而不是 MyBatis-Plus更不是 JPA这一点要说清楚。财务系统的报表查询非常依赖手写 SQL比如账龄分析要 CASE WHEN 分段、成本汇总要 GROUP BY 多维度、凭证查询要 JOIN 科目表JPA 在这种复杂查询下写起来非常别扭性能调优也无从下手。MyBatis 把 SQL 显式暴露在 XML 里DBA 可以直接优化改一条查询不用重新编译理解。MyBatis-Plus 适合纯 CRUD 场景但到了报表层该写 XML 还是得写没必要多引入一层抽象。Vue3 选它是因为 Composition API 对后台管理系统这种复杂页面实在太友好。表格页、筛选窗、弹窗表单这些业务逻辑复用用组合式函数可以拆得很干净不会像 Options API 那样到处是 mixin 的地雷。配套的 Element Plus 组件覆盖了表格、表单、弹窗、日期选择这些后台刚需开发速度能快一大截。MySQL 8 则是基于数据量评估。中小企业财务系统的核心数据一年几十万张单据顶天了单库 MySQL 配合 InnoDB 事务和合理索引完全够用没必要为了显得高级引入更重的数据库。成本考虑也很现实一个懂 MySQL 的运维遍地都是省心。1.3 前后端分离的代价与应对前后端分离的好处不用多说独立开发、独立部署、前端负载交给 Nginx、后端只需要跑 jar。但它的代价也很直接跨域问题、接口契约维护、两边联调成本。我这边从项目一开始就定了三条规矩第一所有接口统一返回{ code, message, data }结构前端只认这一种格式第二前端只在 axios 拦截器统一处理错误提示页面里禁止散落弹窗第三开发环境前端起 Vite用代理转发接口生产环境前端打包成 dist 交给 Nginx 反代整个生命周期不启用后端 CORS。后面联调基本没为为什么跨域了这种事浪费过时间。2. 核心功能模块与数据库设计2.1 单据流驱动记账的模块闭环财务系统最怕的就是做的是一堆孤立的增删改查页面。我的做法是先画一张单据流转图然后按业务流分模块。纺织企业的核心链路是这样的采购环节采购订单 → 到货后开入库单 → 收到供应商发票 → 形成应付账款。销售环节销售订单 → 发货出库 → 开票 → 形成应收账款。生产环节领料单、生产报工单 → 归集人工和制造费用 → 按订单或品种结转成本。最后所有业务单据审核通过后由系统按预设的科目对照表自动生成记账凭证财务人员只需要检查和微调。这样做的好处是每个财务数字都有上游单据支撑审计追溯时点开凭证就能看到来源单据。模块之间靠单号和数据状态关联而不是靠人肉对 Excel。2.2 核心表结构与字段设计要点库表设计是整个项目的地基我贴一下核心表结构和字段设计逻辑表名用途关键字段t_user系统用户username, password, real_name, role_id, statust_customer客户档案customer_name, tax_no, contact, credit_limit, credit_dayst_supplier供应商档案supplier_name, tax_no, contact, payable_balancet_material物料档案material_code, material_name, spec, base_unit, sale_unit, cost_methodt_purchase_order采购订单主表order_no, supplier_id, order_date, total_amount, statust_purchase_order_item采购订单明细order_id, material_id, qty, unit, price, amountt_sales_order销售订单主表order_no, customer_id, order_date, total_amount, statust_inventory_ledger库存流水表material_id, warehouse_id, change_type, qty, unit_cost, biz_not_voucher记账凭证主表voucher_no, voucher_date, period, status, total_amountt_voucher_item凭证分录表voucher_id, account_code, account_name, debit_amt, credit_amt, summary设计上几个关键决策供参考金额字段一律用DECIMAL(18,2)数量用DECIMAL(18,4)。布料按米可能有小数纱线的公斤数也有三位小数数量精度给足成本计算后面统一四舍五入避免每个行项目单独舍入造成合计对不上。主表和明细表必须分离。主表只存汇总金额和单据状态明细表存行项目。查列表走主表查详情走明细 join 主表性能和逻辑都清楚。每张业务表都带create_time、update_time、creator方便做审计追溯。财务系统必备不只是规范而是出了问题能查是谁在什么时候改了哪张单。状态字段用tinyint而不是字符串。1 草稿、2 已审核、3 已记账、4 已作废。状态流转全部在 service 层校验绝对不允许前端直接改。比如一张已记账的采购单必须先生成红字冲销单才能关闭不能直接删除。索引方面所有业务表建(biz_date, status)联合索引报表查询基本都按时间范围加状态条件走这样能覆盖绝大多数统计需求。2.3 纺织品行业特有的单位换算与成本精度处理这个点非纺织行业的人很难体会。一张采购单上买的是5吨棉纱到了库存模块要记成5000公斤到了生产领料按公斤出库最后成本分摊到每米布上又是元/米。单位不统一财务月底结账时库存账和实物账永远对不上。我的方案是在物料表里加base_unit库存标准单位和unit_list可选业务单位JSON 或关联表存储换算率。单据录入时允许选业务单位保存时后端统一换算成base_unit数量再写库存流水。比如吨转公斤前端只做展示换算全部在后端完成避免两边标准不一致。成本精度更是个坑。面料的成本单价如果直接保留两位小数遇到数量是 1234.5 米、成本是 3.333 元/米时单行金额和汇总金额总差几分钱。我的处理是成本单价在中间计算时保留 6 位小数金额先按行累加不进位全部加完再统一保留两位。这个规则写死在成本核算工具类里所有模块共用绝不各自实现。3. 后端实现要点SpringBoot 与 MyBatis 的配合3.1 工程结构、统一响应与全局异常后端工程我按功能拆分包结构不搞微服务一个单体应用足够。目录大概是com.company.finance ├── common // Result、ResultCode、BusinessException、GlobalExceptionHandler ├── config // WebMvcConfig、JacksonConfig、MyBatisConfig ├── controller // 各模块接口 ├── service │ └── impl ├── mapper // Mapper接口 XML ├── entity // 数据库实体 ├── dto // 入参数据传输 ├── vo // 出参视图对象 └── util // JWT、金额、单位换算等工具统一响应包装类是整个项目的地基所有接口不管是成功还是失败都返回同一个结构public class ResultT { private int code; private String message; private T data; public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT fail(int code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }业务异常统一抛 BusinessException全局异常处理器兜底RestControllerAdvice public class GlobalExceptionHandler { ExceptionHandler(BusinessException.class) public ResultVoid handleBusiness(BusinessException e) { return Result.fail(e.getCode(), e.getMessage()); } ExceptionHandler(Exception.class) public ResultVoid handleException(Exception e) { log.error(system error, e); return Result.fail(500, 系统异常请稍后重试); } }这样做的直接好处是 controller 里不用到处写 try-catch参数校验失败、业务规则不满足、系统未知错误三层异常各归各的位置日志里能一眼定位是业务问题还是系统 bug。特别是财务系统的数据校验特别多比如收款金额不能大于应收余额科目不能越级记账这种业务校验全部放在 service 层抛 BusinessException前端弹提示、后端记日志非常清晰。3.2 JWT 登录认证与接口权限落地登录认证我没有上完整的 Spring Security说实话这种后台管理系统用 Spring Security 太重了配置多、学习成本高出问题还不好查。我用的是 JWT HandlerInterceptor 的方案轻量且够用用户密码用 BCrypt 加密存储登录时校验通过后签发 JWT。JWT 里只放 userId 和 roleCode过期时间设 8 小时不存密码等敏感信息。写一个 JwtInterceptor 注册到拦截器链白名单放行登录接口和静态资源。核心拦截器大概长这样Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod)) { return true; } String token request.getHeader(Authorization); if (StringUtils.isBlank(token) || !JwtUtils.verify(token)) { throw new BusinessException(ResultCode.UNAUTHORIZED, 登录已过期请重新登录); } Long userId JwtUtils.getUserId(token); UserContext.set(userId); return true; } Override public void afterCompletion(...) { UserContext.clear(); } }权限方面我做的是一版简化 RBAC用户表带角色ID角色和菜单权限关联表。后端在需要权限的接口上用自定义注解标记拦截器里校验角色权限。比如凭证审核接口只有财务主管角色能调这个控制放在后端而不是前端隐藏按钮——前端隐藏只是体验真正的安全边界在后端。这里有个很容易踩的坑JWT 是无状态的用户被禁用或角色变更后已签发的 token 在过期前依然有效。我的处理方式是登录时把用户状态和角色版本号放进 token关键操作审核、记账、删除每次请求都查一次数据库校验用户是否仍可用。财务系统的操作要留痕宁可多查一次库也不能让一个离职账号继续用旧 token 操作。3.3 MyBatis 动态 SQL 与复杂报表查询实战MyBatis 在这个项目里最大的价值不是 CRUD而是那些财务对账用的复杂查询。举个真实的例子应收账龄分析表财务每个月都要看未到期的、逾期30天的、逾期60天的、逾期90天以上的各有多少。这个 SQL 用 JPA 写能把人写疯用 MyBatis 就很自然select idselectReceivableAging resultTypemap SELECT c.customer_name, SUM(CASE WHEN DATEDIFF(NOW(), r.biz_date) lt; 30 THEN r.outstanding_amt ELSE 0 END) AS age_30, SUM(CASE WHEN DATEDIFF(NOW(), r.biz_date) gt; 30 AND DATEDIFF(NOW(), r.biz_date) lt; 60 THEN r.outstanding_amt ELSE 0 END) AS age_60, SUM(CASE WHEN DATEDIFF(NOW(), r.biz_date) gt; 60 AND DATEDIFF(NOW(), r.biz_date) lt; 90 THEN r.outstanding_amt ELSE 0 END) AS age_90, SUM(CASE WHEN DATEDIFF(NOW(), r.biz_date) gt; 90 THEN r.outstanding_amt ELSE 0 END) AS age_over_90, SUM(r.outstanding_amt) AS total_amt FROM t_receivable r JOIN t_customer c ON r.customer_id c.id where if testcustomerId ! null and customerId ! AND r.customer_id #{customerId} /if if teststartDate ! null AND r.biz_date gt; #{startDate} /if /where GROUP BY c.customer_id, c.customer_name /select这个查询有几个细节值得说第一XML 里比较符号、必须用gt;、lt;转义不然 XML 解析直接报错这是新手最容易卡住的点。第二where标签会自动处理首行的 AND不需要担心用户没传条件时WHERE后面直接跟AND的语法错误。第三#{customerId}是预编译占位符防 SQL 注入必须用它。${}这种字符串拼接只能用在动态表名、排序字段等场景而且用之前必须做白名单校验这个原则我写进了团队开发规范。第四分页用的 PageHelper不需要每个查询手写 LIMIT offset而且 PageHelper 的 Total 自动查询能直接驱动前端分页组件。像这样的报表 SQL 项目里还有不少销售按客户按月汇总、库存收发存汇总、成本结转明细表。每一条我都放进 XML 而不是注解里理由很简单XML 里 SQL 可以加注释、可以对齐格式化以后接手的人包括三个月后的我自己看着不费劲。MyBatis 官方文档也一直强调复杂 SQL 用 XML 可读性最好。3.4 事务边界与财务数据一致性财务系统的核心诉求就是一笔操作要么全部成功要么全部失败。比如采购入库审核这个动作逻辑上要做四件事把采购单状态改为已审核、写库存流水、生成应付账款、生成记账凭证。这四件事必须在一个事务里。我在 service 实现类上统一这样写Transactional(rollbackFor Exception.class) public void approvePurchaseOrder(Long orderId) { PurchaseOrder order purchaseOrderMapper.selectById(orderId); // 状态校验只有草稿状态才能审核 if (order.getStatus() ! 1) { throw new BusinessException(当前单据状态不允许审核); } // 写库存流水 inventoryLedgerService.addLedger(order); // 生成应付 payableService.createPayable(order); // 生成记账凭证 voucherService.createFromPurchase(order); // 更新单据状态 purchaseOrderMapper.updateStatus(orderId, 2); }这里面有几个坑必须点名Transactional默认只对 RuntimeException 回滚如果代码里抛出的是 checked Exception事务不会回滚。所以必须写成rollbackFor Exception.class。同类内部方法调用不走 Spring 代理你在这个类里调另一个Transactional方法事务是不生效的。我一开始就吃了这个亏approvePurchaseOrder里调voucherService.createFromPurchase是跨类的没问题但后来写退款逻辑时同类里调同类死活不回滚查了半天才发现是自调用问题。解决办法很简单拆成单独的 service 或者用TransactionTemplate。并发审核的问题是另一个隐蔽的坑。两个用户同时点审核同一张采购单都检查了 status1都可能继续往下走。我的处理是更新语句带上状态条件int rows purchaseOrderMapper.conditionalUpdateStatus(orderId, 1, 2); if (rows 0) { throw new BusinessException(单据已被他人处理); }利用数据库行锁保证只有一个请求能成功修改状态其他请求更新影响行数为 0 就直接报已被处理。这套乐观锁思路在财务单据上够用不用上分布式锁。4. 前端实现要点Vue3 后台管理界面4.1 Vite 工程搭建与目录组织前端我用的 Vite 构建配 Vue3 是官方推荐组合。创建工程和装依赖的命令先贴出来npm create vitelatest finance-web -- --template vue cd finance-web npm install vue-router4 pinia axios element-plus echarts sass目录结构一开始就规划好不要等代码多了再重构src ├── api │ ├── request.js // axios 封装和拦截器 │ ├── auth.js │ ├── finance.js │ ├── purchase.js │ └── sale.js ├── views │ ├── login │ ├── dashboard // 首页看板ECharts 图表 │ ├── base // 客户、供应商、物料档案 │ ├── purchase // 采购订单、入库、采购发票 │ ├── sale // 销售订单、出库、销售发票 │ ├── inventory // 库存台账、流水 │ └── finance // 凭证、应收应付、报表 ├── components │ ├── TablePage.vue // 表格页通用封装 │ ├── CurrencyInput.vue // 金额输入组件 │ └── FormDialog.vue // 弹窗表单封装 ├── router ├── store └── utils ├── auth.js // token 存储 └── format.js // 金额、日期格式化开发环境代理配置在vite.config.jsexport default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // 如果后端有 context-path则开启 rewrite // rewrite: path path.replace(/^\/api/, ) } } } })关于开发代理有两个重要提醒代理配置改完必须重启 Vite 服务才生效不是热更新能解决的如果后端接口路径本身带/api那 rewrite 就不需要改之前先确认后端context-path设的是什么。这里最烦的坑是前后端都觉得自己路径没错结果 404 了两个人互相甩锅后来我干脆把后端 context-path 清掉所有后端接口统一走/api前缀前端代理原样转发口诀就一句前端/api/xxx后端就接/api/xxx两边完全一致。4.2 路由守卫与请求拦截后台管理系统的登录态控制前端必须做两层路由守卫控制页面访问axios 拦截器控制接口请求。路由守卫的写法很直接router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.public) { next() } else if (token) { next() } else { next({ path: /login, query: { redirect: to.fullPath } }) } })axios 拦截器这是整个前端最重要的文件不能只处理 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, message, data } res.data if (code 200) { return data } if (code 401) { localStorage.clear() router.push(/login) return Promise.reject(new Error(登录已过期)) } ElMessage.error(message || 请求失败) return Promise.reject(new Error(message)) }, err { ElMessage.error(err.response?.data?.message || 网络异常) return Promise.reject(err) } )实践中的重点业务 code 是 401 时JWT 过期前端要做两件事——清掉本地 token 并跳转登录页同时不要把旧的错误提示弹给用户不然用户一脸懵。另外后端的 500 错误尽量不要直接把Internal Server Error英文抛给用户统一转成系统异常请联系管理员真实错误信息记在后端日志里就行。4.3 表格页、筛选表单和凭证录入的实现套路后台管理系统 90% 的页面都是同一个套路搜索条件 表格 分页 弹窗表单。我把这套抽成了一个可复用的 TablePage 组件每个列表页只需要传配置搜索区是客户下拉、日期范围、状态下拉表格列配置是字段名、格式化函数、操作按钮分页参数自动带上 current 和 size。这样一个页面从零写只需要半天后面维护也只改配置。筛选表单有个细节日期范围选择器绑定的是一个数组提交时拆成startDate和endDate两个字段后端 MyBatis 里用gt;和lt;做范围查询这个前后端约定要写进接口文档不然经常出现时间条件没生效的 bug。凭证录入是整个系统交互最重的页面因为财务凭证的格式是固定的摘要、科目、借方金额、贷方金额可以有多行借方合计必须等于贷方合计。我的实现是用表格动态行el-table :datavoucherItems el-table-column label摘要 template #default{ row } el-input v-modelrow.summary placeholder必填 / /template /el-table-column el-table-column label借方金额 template #default{ row } CurrencyInput v-modelrow.debitAmt :disabledrow.direction C / /template /el-table-column el-table-column label贷方金额 template #default{ row } CurrencyInput v-modelrow.creditAmt :disabledrow.direction D / /template /el-table-column /el-table借方和贷方单行只能填一个方向用列里的小按钮切换。对借贷是否平衡做实时校验watch(voucherItems, () { const debitTotal Math.round(voucherItems.reduce((sum, r) sum Number(r.debitAmt || 0), 0) * 100) / 100 const creditTotal Math.round(voucherItems.reduce((sum, r) sum Number(r.creditAmt || 0), 0) * 100) / 100 balanceState.value Math.abs(debitTotal - creditTotal) 0.005 }, { deep: true })借贷不平的时候保存按钮置灰同时给出差额提示。这里算合计一定不能直接对格式化后的字符串求和我一开始用toFixed(2)先格式化再累加结果出现0.1 0.2不等于0.3的浮点误差。正确姿势是先加原始数字再统一四舍五入。4.4 金额格式化与浮点精度处理前端处理金额我必须强调一个总原则展示层格式化计算层用原始值。界面上一律显示千分位分隔符用户录入金额时保留两位小数但内部存储和传给后端的永远不是被破坏精度的字符串。金额输入组件 CurrencyInput 是我封装得最勤快的组件核心思路输入框聚焦时显示原始数字失焦时显示带千分位的格式化字符串。只允许数字和小数点位数限制由精度属性控制。不允许负数财务输入金额为负数应该走红字冲销流程而不是直接录负数。如果输入了超过两位的小数自动四舍五入但要在验证提示里告知用户。还有一点后端返回的 BigDecimal 金额我强烈建议 Jackson 序列化时转成字符串。原因很现实JS 的 Number 类型对超过 2^53 的整数不安全而 Java 的 Long 类型 ID 很容易就超了。金额虽然不会那么大但主键 ID 会。我在 Jackson 配置里做了全局处理Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - { builder.serializerByType(Long.class, ToStringSerializer.instance); builder.serializerByType(Long.TYPE, ToStringSerializer.instance); builder.serializerByType(BigDecimal.class, new ToStringSerializer()); }; } }这样前端拿到的 Long 类型 ID 和 BigDecimal 金额都是字符串精度不会丢展示的时候再按需格式化。这个配置是我项目上过线之后才补的原因是一个客户反馈打开客户列表两个客户 ID 一模一样其实就是超精度了很典型。5. 前后端联调、打包部署与常见问题排查5.1 接口契约BigDecimal、时间与字段命名前后端联调顺畅的秘诀就是提前把格式约定死我总结了三条第一时间格式统一yyyy-MM-dd HH:mm:ss。数据库 datetime 读取出来是 Timestamp如果不配置 Jackson 时间格式默认序列化出来是一长串数字前端 dayjs 虽然能解析但看着头疼。我直接在 application.yml 里全局配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第二BigDecimal 转字符串Long 转字符串理由前面已经说过。再补充一句如果查询返回的是 VO 而不是实体建议 VO 里的金额字段用 String 类型接收从源头避免精度问题。第三字段命名的下划线与驼峰映射。MyBatis 配置里设置了map-underscore-to-camel-case: true所以数据库customer_name自动映射到customerName。但要注意XML 里写resultType为 Map 时不会自动转驼峰查出来的 key 还是下划线。做报表的时候我想省事用了 Map前端拿到的字段名是customer_name不是customerName和我文档写的对不上联调时来回确认花了不少时间。后来统一规则强类型查询用 VO弱类型报表用 Map 时后端自己做过一次 key 转换避免前端感知到数据库命名。5.2 前端打包与 Nginx 反向代理部署部署环节我的做法是前端npm run build生成 dist扔到服务器/opt/finance-web/dist后端打包成 jar跑在 8080 端口Nginx 监听 80把/api请求反代到后端其余路径指向前端静态资源。Nginx 配置核心部分server { listen 80; server_name your-domain.com; root /opt/finance-web/dist; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location / { try_files $uri $uri/ /index.html; } }try_files那一行至关重要。Vue Router 用的是 history 模式直接访问/finance这种路径时服务器上根本没有这个文件必须 fallback 到 index.html 让前端路由接管。如果不写这行刷新页面就 404这是前后端分离部署最常见的坑。后端 jar 我用 systemd 管起来比 nohup 靠谱开机自启、崩了自动拉起[Unit] Descriptionfinance-server Afternetwork.target [Service] Userroot ExecStart/usr/bin/java -jar /opt/finance-server/finance-server.jar --spring.profiles.activeprod Restartalways RestartSec5 [Install] WantedBymulti-user.target数据库这块有个必须提醒的建库的时候字符集一定用 utf8mb4不是 utf8。utf8 在 MySQL 里是 utf8mb3存 emoji 和生僻汉字会报错。纺织企业的客户名、品名里经常有生僻字吃过一次亏就记住了。5.3 高频踩坑排查实录一张表帮你省三个通宵项目开发和上线过程遇到的典型问题整理成下面这张表遇到现象直接对着查现象根因解决方案MySQL 连接报 SSL 连接错误Connector/J 8 默认校验 SSL 证书JDBC URL 加useSSLfalseallowPublicKeyRetrievaltrue内网环境足够前端 id 精度丢失多条记录 ID 相同Long 超过 JS 安全整数Jackson 将 Long 序列化为 String或 VO 里用 String 接收LocalDateTime 反序列化报错缺少 jsr310 支持引入jackson-datatype-jsr310并配置 JavaTimeModule 和格式No qualifying bean of type MapperMapper 接口没被扫描启动类加MapperScan(com.company.finance.mapper)Transactional不生效、异常不回滚默认只回滚 RuntimeException注解写rollbackFor Exception.class同类自调用要拆类Vite 代理改了没反应代理配置修改后未重启改vite.config.js后必须重启 npm 服务金额合计差一分钱每行单独四舍五入后累加先累加精度值最后统一舍入计算不要对格式化字符串做El-table 大数据卡顿一次性渲染行数太大后端分页每页保持 20 行以内确需大批量用虚拟滚动刷新页面 404history 路由没有 fallbackNginx 配置try_files $uri $uri/ /index.html中文乱码数据库或连接字符集问题库表字符集 utf8mb4JDBC URL 加characterEncodingutf8这里面我要单独拎出来说两个。一个是 MySQL SSL 错误。新装的 MySQL 8 用默认 JDBC 连接一启动就报Public Key Retrieval is not allowed不懂的人会以为是密码错了。解决方案就是连接参数里加两条allowPublicKeyRetrievaltrue和useSSLfalse。内网部署完全没问题不要一听到关闭 SSL就觉得不安全内网数据传输本来就走私有网络。另一个是金额精度问题。我强调过很多次但每次都要再说一遍Java 后端用 double 算金额是重罪必须用 BigDecimal前端拿金额当 number 做计算也尽量少最好以字符串接收、展示时格式化。财务系统的报表对不上账十次有八次是精度问题这不是技术洁癖是业务底线。5.4 上线后值得继续落地的几个扩展方向系统跑通之后我陆续补了几个扩展都是纺织企业实际提的需求你可以根据情况考虑第一个是附件管理。采购发票、入库验收单、客户回款水单这些纸质单据以前是财务大姐抽屉里的一摞。我用 MinIO 搭了个简单的附件服务业务单据表加attachment_group_id上传后先传 MinIO 再存附件记录。好处是凭证联查能看到原始票据扫描件审计时不用翻柜子。第二个是报表导出。网页上看报表没问题但财务月底要交 Excel 给老板和税务。我用 Apache POI 封装了导出工具把账龄表、销售汇总表、库存收发存表导出成带固定表头的 Excel。这里顺便回应一个常见疑问POI 生成 Word 图表是可以的通过XWPFChart实现但实际财务场景里 Excel 导出用得远比 Word 多所以我把主要精力放在 Excel 导出上。第三个是应收到期提醒。每天定时任务扫描应收账款的账期到期前三天生成待办通过站内信推给业务员。这个功能上线后被销售部门夸了好几次说以前都是月底被财务催现在系统提前提醒客户关系也好处理了。第四个是权限细化为菜单级、按钮级 RBAC。初期只有角色区别后面前端加了v-permission指令控制按钮显示后端加接口权限校验两头发力。财务系统的操作越敏感权限粒度就得越细。最后聊点做这个项目的真实体会整个项目做下来我最深的感受是财务管理系统最大的坑不在代码在业务口径。比如金额到底是含税还是不含税这个问题不写死采购和财务各算各的月底对账必然打架。我的经验是动手写表之前先跟财务人员把三件事确认清楚并且写成文档金额口径含税/不含税、成本算法移动加权/月末一次加权、科目对照规则哪个业务类型生成哪些凭证分录。这三件事定了后面所有模块才不会返工。另一个经验给所有做前后端分离管理系统的朋友宁可前期多花两天把前后端接口契约、错误码、时间格式、金额格式统一约定好也不要前期赶进度埋下一堆 联调时各改各的 的地雷。我这次前松后紧的教训很明显改过几个字段类型之后前端组件、后端 VO、数据库字段三处对齐花的精力远超想象。还有个小技巧分享开发期间我会在本地起一个假的测试数据源用application-dev.yml里配置 H2 内存库灌一批模拟的纺织业务数据前端开发和接口调试完全不依赖真实的 MySQL 环境。这样开发互相不阻塞等联调阶段再切回 MySQL。这个小习惯帮我省了不少等环境的时间。系统上线到现在跑了大半年最让我欣慰的不是技术多炫而是财务大姐说了一句这个月的账我三天就结完了以前要一周。做业务系统的人听到这种话比收到什么都强。写这篇算是把整个过程的思路、选型、代码要点和坑都摊开讲了一遍希望对正准备做同类系统的你有参考价值。
网站建设高端定制企业官网