☰
宽带业务管理系统实战:SpringBoot+Vue+MyBatis全流程开发
2026/10/16 10:14:47 网站建设 项目流程

1. 项目概述与业务场景拆解

1.1 宽带业务管理系统到底解决什么问题

先说清楚这个系统是干什么的。宽带业务管理系统,说白了就是给宽带运营商或驻地网公司用的内部业务支撑平台,用来管理从用户报装申请开始,到派单施工、设备开通、费用收缴、报障维修、续费停机的完整业务闭环。别小看这个场景,宽带业务和普通电商或者内容管理系统有个很大的区别:它涉及强流程状态流转,单子从“申请”到“竣工”要走好几个环节,每一步都要留痕、可追溯,所以系统设计的核心不是简单的增删改查,而是状态机驱动的流程管理。

我在实际做这类项目的时候,最深的体会是:宽带业务系统真正难的不是CRUD,而是业务规则的梳理。比如一个用户申请装宽带,系统得先校验地址是否覆盖、端口是否空闲、身份证是否重复登记,这些都校验通过了才能生成工单。工单派给施工人员之后,施工人员回填设备SN号、光功率值,系统要校验这些数据的格式和合理性,然后才能标记竣工。竣工之后才开始计费周期,产生账单,用户缴费之后状态才能变为正常。这些规则如果不梳理清楚,代码写得再漂亮也是空中楼阁。

所以这篇博文我不会只讲一个空的框架代码,而是结合宽带业务的实际场景,把完整的业务闭环拆开讲清楚:数据库怎么设计、后端接口怎么实现、前端页面怎么交互、部署的时候有哪些坑。项目采用SpringBoot + Vue前后端分离架构,数据层用MyBatis操作MySQL,这几个技术选型大家都耳熟能详,但把它们组合起来做好一个业务系统,中间是有不少门道的。适合正在做毕设的同学参考,也适合刚工作不久、想独立完整做一套业务系统的Java开发新人看看。

1.2 为什么选前后端分离而不是传统JSP

这里先回答一个很多人纠结的问题:为什么不用SpringBoot直接渲染Thymeleaf或者JSP页面,非要拆成Vue前端?我在做这套系统之前也犹豫过,后来坚定选了前后端分离,原因有三。

第一是维护边界清晰。宽带业务系统的前端交互密度不低,比如工单状态流转要做时间轴展示,账单数据要做图表统计,装维人员要在大屏上看着任务列表实时刷新。这些交互用服务端模板渲染写起来非常别扭,动不动就得整页刷新或者写一堆Ajax拼接DOM,代码一多完全没法维护。拆成Vue以后,前端只管交互和渲染,后端只吐结构化JSON数据,两边通过接口文档协作,各自演进互不干扰。

第二是多人协作的现实需求。这种项目往往是前端一个人、后端一个人配合开发,如果前后端耦合在一起,前端改一个按钮样式都可能要重启应用,效率太低。前后端分离之后,前端开发可以完全用Mock数据跑在自己的开发服务器上,后端用Swagger或者Postman调试接口,联调阶段才需要把两边的服务跑起来对接。

第三是部署和扩展的灵活性。前端构建完就是一堆纯静态文件,扔到Nginx就能跑,后端打包成JAR独立部署,两边可以分开扩容。宽带业务系统高峰期并发都在查询和工单提交上,前端静态资源完全可以交给CDN加速,后端做集群负载均衡,这些在前后端分离的架构下都很顺手。传统JSP方案虽然简单,但做到这一步就会很痛苦。

1.3 核心功能模块全景

宽带业务管理系统整体拆成六大模块,每个模块对应标题里提到的完整业务闭环。别急着上来就写代码,先把模块边界划清楚,后面实现的时候才不会东一榔头西一棒子。

客户管理模块:维护宽带用户的基础信息,包括姓名、身份证号、联系电话、安装地址、宽带账号、套餐类型、开通日期、到期日期、状态等。这个模块是其他所有模块的数据基础,工单要引客户、收费要引客户、统计也要引客户,所以数据质量和唯一性校验特别重要。

套餐管理模块:维护宽带套餐的定价和规格信息,包括带宽大小(100M/200M/500M/1000M)、月费标准、合约周期、初装费、包含的增值服务(比如IPTV、云盘)等。套餐一旦被订单或者用户引用,就不能随意删除或修改费率,否则历史账单会乱套,这一点在实现时要用逻辑删除加归档机制。

工单管理模块:这是整个系统的核心,也是最体现业务复杂度的模块。工单类型包括新装工单、移机工单、故障报修工单、拆机工单,每种工单都有独立的流程状态。比如新装工单的流程是:待受理 → 待派工 → 施工中 → 待验收 → 已竣工,中间任意环节都可以回退。工单还需要关联客户、关联套餐、关联施工人员、记录处理时长,这些字段的流转逻辑很考验后端的设计能力。

费用与账单模块:负责宽带费用的计算和收缴,包括每月定时生成账单、记录实收费用、开通收据打印功能、支持预存款抵扣、处理欠费停机与复机。这个模块看起来独立,实则和工单模块、客户模块深度耦合,因为新装工单竣工的次月才开始计费,拆机工单完工当天要生成截止账单,这些联动规则是系统里最容易出bug的地方。

统计报表模块:给管理层看的经营数据看板,包括新装用户趋势、工单完成率、装维人员工作量排行、欠费用户列表、收入月度对比等。报表的数据来自前面几个模块的历史数据聚合,所以查询量大、维度多,既要考虑接口性能,又要考虑图表的易读性。

系统管理模块:包含用户登录认证、操作员账号管理、角色权限分配、操作日志记录。宽带业务系统的使用者角色包括营业员、装维人员、主管、财务、系统管理员,不同角色能看到的菜单和数据范围完全不同,这个模块做不好,前面的业务模块做得再好也会被用户骂“系统难用”。

2. 技术选型深度解析与项目结构规划

2.1 SpringBoot + MyBatis是这套组合的黄金搭档吗

标题里明确写了SpringBoot + MyBatis,这是国内中小型业务系统里最常见的技术组合。有人会问,现在MyBatis-Plus都这么流行了,为什么还要用纯MyBatis?说实话,纯MyBatis写起来确实繁琐,但Mapper接口加XML映射的方式有一个明确的好处:SQL完全由开发者掌控,任何复杂查询都不会被框架的wrapper机制裹挟。这套系统里有很多多表关联查询和报表聚合SQL,手写XML反而更直接。

SpringBoot 2.7.x是目前兼容性最稳的版本,用来做单体业务系统非常合适,再高版本虽然新特性多,但和大量第三方库的兼容性需要额外操心。用SpringBoot的好处不用多说,自动配置、内嵌Tomcat、一站式依赖管理,大幅缩减了配置成本。以前SSH时代光是配置Spring和Hibernate就得折腾一天,现在一个启动类全搞定。

MyBatis在这套系统里主要负责数据库访问层的工作。我在建工程的时候会采用Mapper接口 + XML文件双目录结构,接口负责定义方法签名,XML负责SQL语句。为什么要把SQL写在XML而不是注解里?因为宽带业务系统的查询条件非常灵活,比如客户列表要支持按姓名、电话、地址、状态、日期范围组合筛选,这种动态SQL在XML里用<if>标签处理,比注解里硬拼接字符串不知道优雅多少倍。

数据库选的是MySQL 8.0,这是当下最主流的选择,8.0的窗口函数、JSON类型支持给报表统计和配置扩展提供了不少便利。字符集统一utf8mb4,排序规则utf8mb4_general_ci,基本不会有中文乱码问题。存储引擎用InnoDB,事务安全和行级锁对工单和账单这类核心业务表是必需品。

2.2 Vue侧的技术栈组合

前端用的是Vue 2 + Element UI + Axios + ECharts。这套组合虽然不算最新潮,但胜在生态成熟、资料多、踩坑成本低。Vue 2的响应式原理虽然不如Vue 3的Proxy那么优雅,但胜在稳定,社区里能搜到的每一个问题几乎都有答案。Element UI的表格、表单、日期选择器这些组件配合业务系统简直天衣无缝,不需要自己造轮子。

有一点我要特别强调:Element UI默认的样式风格偏向后台管理,和宽带营业厅的现场使用场景虽然匹配,但注意表格列太多的场景要开启横向滚动,表单校验的触发时机要设置好。Axios拦截器是前端的命脉,统一处理token注入、响应状态码判断、401跳转登录这些逻辑。ECharts用来做统计报表的可视化展示,折线图画新装用户趋势,柱状图画收入对比,饼图画套餐占比,都是现成图表类型,改改配置就能出效果。

前端项目用Vue CLI 5搭建,目录结构按照api / assets / components / router / store / views / utils拆分,组件按页面模块建立文件夹。路由使用Vue Router,采用动态路由模式,根据用户角色动态注册可访问的菜单路由,这个是权限控制的关键实现点。

2.3 工程目录结构与代码组织方式

项目采用的Maven多模块结构,这个在单体系统里虽然不强制,但建议还是拆一下,因为把代码按照controller / service / mapper / entity / dto / vo / common / config分层组织,后续排查问题会非常舒服。拆分模块的方式如下:

broadband-system/ ├── broadband-common/ # 公共模块:统一返回结果、常量、异常、工具类 ├── broadband-system/ # 后台管理模块:登录认证、权限、日志 ├── broadband-business/ # 业务模块:客户、套餐、工单、账单、报表 └── broadband-admin/ # 启动模块:入口Application、配置文件

很多初学者不理解为什么要拆模块,把所有代码堆在一个目录里不是更简单吗?但实际项目一跑起来你就知道,业务模块和系统管理模块混在一起,代码量上了五千行之后就是灾难。现在这样拆,每个模块各司其职,依赖关系也是一目了然。broadband-common被所有模块依赖,里面定义的Result统一返回体、ResultCode错误码枚举、BusinessException全局异常,这些是每个接口都要用的基础设施,必须优先做好。

后端接口统一返回Result<T>结构,格式是{ code, message, data },这个是前后端联调的规范,比直接返回裸数据要科学得多。全局异常处理器用@RestControllerAdvice统一捕获业务异常和数据校验异常,这样前端收到的永远是结构化的错误信息,不会出现后端某个地方忘记try-catch导致返回一堆堆栈信息的情况。

2.4 为什么强制要求先设计接口文档再写代码

这个习惯是我踩过大坑之后总结出来的。宽带业务系统涉及几十个接口,每个接口的请求参数、返回结构和可能的异常码如果不提前约定好,前端和后端各自为战,联调阶段会变成灾难。比较好的做法是后端用Swagger自动生成在线接口文档,前端根据文档Mock数据并行开发。

我在实际开发时会在Controller层加Swagger注解,描述每个接口的作用和参数含义。有人觉得Swagger注解侵入性强,但比起联调时扯皮吵架,多写几个注解真不算什么。另一个做法是在项目根目录维护一份API.md,按照模块列出所有接口的路径、方法、参数、返回示例,实时更新,这比Swagger更好用,因为可以写更详细的业务说明和边界条件。

3. 数据库设计与核心表结构说明

3.1 核心业务表的字段和关系

数据库设计是这套系统真正的“地基”,地基打得不好,后面任何阶段都会返工。我按业务域把表分成了三组:基础数据表、流程数据表、财务数据表。

首先是客户表t_customer,这张表是系统的核心主数据表。字段包括id、customer_name、id_card、phone、address、broadband_account、status、package_id、open_date、expire_date、create_time、update_time。其中package_id外键关联套餐表,status用0-未开通 / 1-正常 / 2-欠费停机 / 3-已销户四个值表示。

其次是套餐表t_package,字段包括id、package_name、bandwidth、monthly_fee、contract_months、setup_fee、service_items、status、create_time。bandwidth用整数存,100就代表100M,monthly_fee用DECIMAL(10,2),千万注意货币字段不能设计成浮点,Java侧的实体类对应用BigDecimal,否则后面算账会有精度问题。

然后是工单表t_work_order。这张表是整个系统里最复杂的表,字段包括id、order_no、customer_id、order_type、status、operator_id、handler_id、content、appoint_time、finish_time、remark、create_time。order_no是业务单号,格式是WO + 日期 + 流水号,这个是给营业员核对订单用的,必须有且唯一。order_type用1-新装 / 2-移机 / 3-报障 / 4-拆机区分工单类型,status按照工单类型有不同的状态枚举,这些在数据库层用int存,具体含义在Java常量类里定义清楚。

账单表t_bill和缴费记录表t_payment_record是财务域的两张核心表。t_bill字段包括id、bill_no、customer_id、package_id、period、amount、status、create_time,其中period表示账期,比如2025-04,每个月一个用户一条记录。t_payment_record记录每一笔实收资金,字段包括id、customer_id、bill_id、pay_amount、pay_type、pay_time、operator_id。这两个表之间存在一对多关系,一张账单允许分多次缴费,比如用户先交一半,过两天再补另一半。

3.2 表之间如何保证数据一致性

宽带业务系统里最要命的问题就是数据一致性。举个具体例子:用户申请新装宽带,系统生成了工单,但工单还没竣工之前用户的宽带账号是不能激活的。如果你在代码里把“工单已竣工”和“客户状态变为正常”这两个操作分开写在两个方法里,中间一旦出现异常,就会出现工单显示竣工但客户还是未开通的脏数据。

解决这个问题的标准做法是数据库事务 + 状态机校验。在后端Service层,一个完整的业务流程必须放在一个事务方法里执行,保证多个数据表要么全部更新成功,要么全部回滚。Spring的@Transactional注解就是干这个的。我的经验是,事务粒度要控制在业务用例级别,比如completeWorkOrder()这个方法里完成了工单状态更新、客户状态激活、初始化首月账单三个操作,这三个操作必须同生共死。

另一个一致性问题是外键约束。MyBatis框架本身不强制外键,很多开发者因此就不建外键,这是极其错误的习惯。工单表关联客户,客户表关联套餐,在DDL(数据定义语言)层面一定要建外键索引。虽然应用层代码保证了大部分一致性,但外键是最后一道防线,防止有人手动改数据库或者写了一段没走ORM的SQL把数据搞坏。

3.3 初始化SQL脚本的准备要点

项目启动时需要一套完整的初始化SQL脚本,包括建库、建表、插入初始管理员账号和演示数据。这套脚本要做得尽可能完善,因为后面所有功能调试都依赖它。脚本名称按版本管理,比如init_v1.0.sql,以后数据结构变更就新增upgrade_v1.1.sql,不要直接改旧的,否则已经在跑的环境没法平滑升级。

演示数据要结合宽带业务场景来造,比如创建20个不同类型的客户,其中10个正常在网、3个欠费停机、2个已销户、5个未开通。套餐数据要有低带宽和高带宽的区分,价格有高低差异。工单数据要覆盖不同的状态节点,包括待受理、施工中、已竣工的都要有几条。账单数据要生成最近三个月的,有些已缴费,有些未缴费。这样测试的时候各种情况都能覆盖到,不会出现数据不足无法验证功能的情况。

4. 后端核心模块实现与关键代码解读

4.1 登录认证与权限控制的落地实现

登录认证用的是JWT(JSON Web Token)方案。用户输入账号密码,后端校验通过后生成一个token返回,前端把它存到localStorage,后续每个请求都在Authorization头带上这个token。后端用一个拦截器(HandlerInterceptor)统一校验token有效性,校验不通过就直接返回401状态码。

JWT生成的代码并不复杂,选择io.jsonwebtoken库来处理。token里携带的关键信息有用户id、用户名、角色编码,签名密钥放在配置文件里,过期时间设置为24小时。有个重要的安全细节:token里不要放敏感字段,比如身份证号、手机号这些信息,因为JWT的payload只是Base64编码,不是加密,任何人拿到token都能解码看到内容。业务需要用户信息时,用拦截器解析出userId,再去数据库查询最新数据,而不是直接从token读。

权限控制采用RBAC(基于角色的访问控制)模型,数据库里有t_user / t_role / t_menu / t_user_role / t_role_menu五张表。后端使用Spring Security结合JWT实现认证和授权,但考虑到这只是一个单体博客项目规模的系统,我实际使用的是自定义拦截器 + 注解鉴权的方式,更轻量易懂。每个Controller方法上加@RequirePermission("workOrder:create")这样的权限标识,在拦截器里判断当前用户角色是否拥有这个权限码。这种方式比写一堆ConfigurerAdapter配置类要直白得多,重点逻辑一目了然。

4.2 客户信息管理与列表分页

客户管理模块的核心难点在于条件组合查询。营业员在前端页面上可以同时按姓名、电话、宽带账号、状态、开通时间段来筛选客户,这些条件可能填一个可能全填,后端必须能动态拼SQL。老实说这里的实现路径在MyBatis里就是一套<where>配合<if>标签的动态SQL,逻辑和功能都很清晰。

<select id="selectCustomerPage" resultType="com.example.entity.Customer"> SELECT c.*, p.package_name AS packageName FROM t_customer c LEFT JOIN t_package p ON c.package_id = p.id <where> <if test="query.name != null and query.name != ''"> AND c.customer_name LIKE CONCAT('%', #{query.name}, '%') </if> <if test="query.phone != null and query.phone != ''"> AND c.phone LIKE CONCAT('%', #{query.phone}, '%') </if> <if test="query.status != null"> AND c.status = #{query.status} </if> <if test="query.startDate != null and query.endDate != null"> AND c.open_date BETWEEN #{query.startDate} AND #{query.endDate} </if> </where> ORDER BY c.create_time DESC </select>

分页方案用的是PageHelper(MyBatis的物理分页插件),在Service层调用PageHelper.startPage(pageNum, pageSize),紧接着执行的查询会自动拼接limit子句,返回的List会被包装成Page对象,拿到total和pages信息转成统一PageVO再返回给前端。用PageHelper要注意一个坑:它只对紧邻的下一条查询语句生效,所以中间不能插入其他数据库操作,否则会拦截错SQL。

新增或者修改客户信息时,数据校验不能只靠前端。后端必须用@Validated注解配合@NotNull/@Pattern校验DTO字段,比如手机号要匹配^1[3-9]\d{9}$,身份证要过18位校验。校验规则写在后端是底线,前端校验只是用户体验优化,两者都不该缺失。

4.3 工单流程的状态机设计

工单模块是整个系统里价值最高、难度也比较大的部分。开始写码前想清楚一件事:工单状态流转的本质是一个状态机,常见的有新装工单的待受理 → 待派工 → 施工中 → 待验收 → 已竣工。如果在代码里到处写散落的if判断来推进状态,后面加一个新状态或者调整流程,简直就是噩梦。

我的方案是在Service层用一个WorkOrderStateMachine类,维护一个状态流转的Map,每一组“当前状态 → 目标状态”的合法转移都提前定义好。流转时先查Map,如果非法就抛异常,合法才执行更新。这样做的好处是流程规则集中管理,代码可读性大大提升。

private static final Map<Integer, List<Integer>> NEW_INSTALL_FLOW = new HashMap<>(); static { NEW_INSTALL_FLOW.put(0, Lists.newArrayList(1)); // 待受理可以派工 NEW_INSTALL_FLOW.put(1, Lists.newArrayList(2, 0)); // 已派工可以施工或回退受理 NEW_INSTALL_FLOW.put(2, Lists.newArrayList(3, 1)); // 施工中待验收或退单 NEW_INSTALL_FLOW.put(3, Lists.newArrayList(4)); // 验收通过即竣工 }

工单创建的时候需要执行一系列业务校验,比如新装工单要校验客户地址是否在覆盖范围内、客户名下有没有未竣工的工单。这些校验放在Service层的createOrder()方法里,会在事务内执行,有任何一个条件不满足就抛出业务异常并整体回滚。异常消息要写得人话化,比如“该客户已有未竣工的新装工单”,营业员看到才知道该怎么办,而不是强制性抛一句“操作失败”。

工单流转的操作层代码还有权限控制的讲究:派工操作只有主管能做,验收操作只有装维人员或者主管能做。这些规则落在业务层校验,拦截器只做角色粗粒度校验,细粒度的操作权限必须放在方法里面查工作流定义。

4.4 账单生成与费用计算

账单生成是这个系统里我最想展开讲的一个模块,因为这里藏着很多业务细节。宽带的计费方式一般是月初生成整月账单,但不同用户的开通日期不同,所以第一个月要按实际天数折算。比如一个用户15号开通,月费100元,那当月账单就不能按100元收,而是按100 / 30 × 15这样折算到天数。

我实现账单生成用的是定时任务,Spring的@Scheduled注解每天凌晨1点触发。任务逻辑是这样的:先查出所有状态为正常的用户,对每个用户检查本月是否已有账单,有则跳过,没有则计算金额并生成。金额计算的逻辑是:如果用户的开通日期在当月,则用当月剩余天数折算;如果是老用户,则直接按套餐月费生成。这个月生成账单的逻辑是整个系统里最容易出bug的地方,所以要有足够的单元测试覆盖不同场景。

private BigDecimal calMonthFee(Customer customer, Package pkg, YearMonth period) { if (isSameMonth(customer.getOpenDate(), period)) { int totalDays = period.lengthOfMonth(); int usedDays = customer.getOpenDate().getDayOfMonth() - 1; int billableDays = totalDays - usedDays; return pkg.getMonthlyFee() .multiply(BigDecimal.valueOf(billableDays)) .divide(BigDecimal.valueOf(totalDays), 2, RoundingMode.HALF_UP); } return pkg.getMonthlyFee(); }

缴费处理时,系统要支持整月缴费和部分缴费。用户交一笔钱,系统先判断对应账单的已缴金额,如果本次缴费加上之前的已缴金额达到账单金额,账单状态改成已缴清,如果没达到,则保持部分缴费状态。同时要更新客户的账户余额和状态,如果是欠费停机状态缴清费用后要自动复机,这些操作都在一个事务里完成。

欠费停机逻辑这边也值得讲讲:每月账单生成之后要做一次扫描,把生成账单超过30天还未缴清的用户批量置为欠费停机状态。停机之后用户不能上网,但账单还在继续生成累计欠费,直到缴费或者销户为止。这个规则让宽带经营的成本控制成为可能,也是账单模块和客户模块联动的关键场景。

4.5 统计报表的常用SQL与实现

报表模块是管理层的门面工程,但技术本质就是多表聚合查询。以月度收入统计为例,思路是:查某个月的缴费记录表,按日期分组求和,得到每天的收入,然后用ECharts画成折线图或者柱状图。

新装用户趋势的统计:查客户表,按开通日期分组,统计每天的开通量。工单完成率的统计:查工单表,统计某个时间段内各类型工单的总数和完成数,完成率就是两者相除。

写这类报表SQL时有个经验:不要把所有统计都塞进一条大SQL里。真实场景中更推荐分维度查询,在Service层组装结果,一次Controller调用内部执行几个简单的聚合查询,再在Java代码里合并数据。这样做代码好维护,SQL出错的概率也低,性能上因为每个查询都是走索引的简单聚合,实际比复杂嵌套子查询要快得多。

MySQL 8.0的窗口函数在报表查询里也有用武之地。比如要统计每个月的累计收入,直接用SUM(pay_amount) OVER (ORDER BY month)就是一条SQL的事,比在Java里循环累加要优雅得多。

5. Vue前端页面设计与交互细节

5.1 前端项目的初始化和工程搭建

前端项目用Vue CLI创建,创建时的选项建议选择Router、Vuex、CSS预处理器,这三样是后台管理系统的标配。项目创建完成之后,先别急着写页面,先把基础设施搭好:配置Axios实例、封装统一的请求方法、设置路由守卫、建立菜单布局框架。

一个要特别养成的习惯就是用代码生成器去搭联调基础。做一个后台管理系统,页面结构高度相似:顶部栏显示用户信息和退出按钮,左侧菜单显示功能列表,中间是主内容区的router-view。这一步搭建通用布局骨架,后面每个页面都往里填内容就行。

先看一下axios封装的核心代码:

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( response => { const code = response.data.code if (code === 0) { return response.data } else { Message.error(response.data.message) return Promise.reject(new Error(response.data.message)) } }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

5.2 登录页与动态路由的实现逻辑

登录页实现相对简单,一个表单加一个登录按钮,提交后调后端登录接口,拿到token存进localStorage,然后跳转到首页。关键逻辑在路由守卫里。

系统采用动态路由的方式做权限控制,意思是:用户登录前,路由表里只有登录页和404页;登录后,前端根据用户角色调用后端获取菜单列表接口,然后动态router.addRoutes添加这个角色有权限访问的页面路由。这样做的好处是没权限的页面不仅菜单不显示,连路由都不存在,直接输URL也访问不了。

路由守卫的实现逻辑是:

router.beforeEach((to, from, next) => { if (to.path === '/login') return next() const token = localStorage.getItem('token') if (!token) return next('/login') if (!store.state.hasLoadedMenus) { store.dispatch('loadMenus').then(() => { next({ ...to, replace: true }) }) return } next() })

有个细节容易被忽略:动态添加路由之后,直接刷新页面会把动态路由清空,所以要在刷新之后重新执行加载菜单的动作。上面的代码用hasLoadedMenus这个状态判断,重新加载后通过next({ ...to, replace: true })重新触发导航,这个模式要记住,不然刷新就白屏。

5.3 客户列表页面的组件封装方式

客户列表页是后台系统最典型的CRUD页面,我建议把它抽象成一个搜索表单 + 表格 + 分页的模板结构。搜索表单单独封装组件,表格列单独配置,这样不同的业务模块可以复用同样的页面骨架,只需要替换不同的API和列配置。

在Element UI里表格分页的处理方式,就是在el-table下面加el-pagination组件,current-page和page-size绑定表格查询参数,current-change或者size-change事件触发搜索。这些是每个列表页通用的,建议抽成一个PageMixin,每个页面只需定义自己的搜索条件和列配置,避免重复劳动。

客户表格里有一个开关切换客户状态的场景:用户状态在“正常”和“已停机”之间切换。这个交互表面是改一个字段,实际涉及的业务联动比较多,不能直接在前端把状态改了就算完。正确做法是点击开关后调用后端的变更状态接口,由后端去做关联操作并校验是否允许变更,前端等接口返回成功之后再刷新数据。这个经验适用于所有看似简单的字段修改场景——只要这个字段的变化会影响其他模块的数据,就必须走后端接口。

5.4 工单处理页面的操作交互设计

工单处理页面是整个系统前端交互最复杂的页面。营业员在这个页面上要能看到工单列表的实时状态、操作按钮根据状态动态显示、点击工单行能展开详情。

操作按钮的动态显隐规则要从前端硬编码逻辑中解放出来。我在后端工单详情的返回结构里增加了allowedOperations字段,是一个字符串数组,包含当前状态下允许执行的操作编码。前端直接把这个数组渲染成按钮列表,不需要自己判断状态和角色的组合。这样做有一个特别大的好处:当业务流程调整时,只需要改后端返回的操作数组,前端页面不用动,也不会出现前后端判断不一致的问题。

工单详情页还应该包含处理历史的时间轴组件,用Element UI的el-timeline展示每一步操作的操作人、操作时间、操作内容和备注,配合后端提供的history接口渲染。时间轴对营业员和装维人员来说价值很高,他们需要通过历史记录迅速判断这个工单卡在谁那里、为什么卡住。

6. 完整部署流程与运维避坑记录

6.1 后端项目打包与线上环境配置

后端项目打包非常简单,在项目根目录执行mvn clean package -DskipTests,然后会在broadband-admin/target目录下生成可执行的JAR包。JAR包建议直接用java -jar broadband-admin.jar启动,不建议弄成WAR包再丢外置Tomcat,纯粹多此一举。

线上配置和本地开发配置要分开。SpringBoot的application.yml里用spring.profiles.active指定当前环境,本地用dev,线上用prod。application-prod.yml要配置线上的数据库连接、JWT密钥、日志路径。配置文件涉及数据库密码等敏感信息,不要把真实值直接提交到Git仓库,用环境变量或者启动参数覆盖,比如java -jar app.jar --spring.datasource.password=${DB_PASSWORD}。

线上启动后一定要看日志确认有没有启动报错。我配置了logback日志,输出到/data/logs/broadband/目录,按天滚动,保留30天。日志格式包含时间、线程、级别、Logger名、消息,这样排查问题才能定位到具体模块。

6.2 前端构建与Nginx反向代理配置

前端部署相对简单,执行npm run build之后会在dist/目录下生成纯静态文件,把这个目录上传到Nginx的Web根目录即可。重点在于Nginx的配置,要处理好两个问题:一是前端路由History模式需要try_files重写;二是API请求需要反向代理到后端服务,避免跨域。

History模式的路由配置必须写,否则用户点击刷新按钮会404:

location / { root /data/www/broadband; index index.html index.htm; try_files $uri $uri/ /index.html; }

API代理配置如下:

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; }

注意proxy_pass路径结尾,如果写http://127.0.0.1:8080/(带斜杠),会把/api/前缀替换成/,也就是前端请求/api/customer/list会被转发到后端的/customer/list。如果后端Controller的请求映射是从/api/customer/list开始的,那就不带斜杠,这个细节错了很容易排查半天结果发现是代理规则问题。

6.3 服务器环境准备与数据库初始化

服务器建议直接装Linux系统(CentOS 7或者Ubuntu 22.04都可以),然后按顺序安装JDK 8、MySQL 8.0、Nginx、Redis(如果用了缓存)。JDK选择上,JDK 8或者11都行,SpringBoot 2.7完全兼容这两个版本。MySQL装好后要执行初始化脚本,导入建库建表语句和初始数据。

MySQL建库执行SQL的顺序不能乱:先建数据库、再创建用户、授权、然后导入表结构。中文乱码问题是老生常谈,要在/etc/my.cnf的[mysqld]段配置character-set-server=utf8mb4,确保新建的库表默认就是utf8mb4字符集。连接串里也要加characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai,时区不设置的话,Java和MySQL之间的时间会有八小时偏差,排查数据时间问题很折磨人。

还有一个经验是数据库连接池参数要显式配置。SpringBoot默认的HikariCP性能很好,但初始连接数、最大连接数、连接超时这些参数要根据线上并发预估来调整。宽带业务系统营业厅内部使用,并发不高,但报表统计的查询可能比较耗时,所以连接数要稍微富余一些,最大连接数建议50左右。

7. 高频报错与问题排查经验汇总

7.1 经典报错场景

场景一:前端请求跨域报错。浏览器控制台报Access-Control-Allow-Origin相关错误,原因是前后端域名或端口不一致。处理方式是开发环境下用Vue项目的proxy代理机制解决,生产环境下通过Nginx反向代理解决跨域。不要让后端随手在代码里加@CrossOrigin注解加上allowCredentials=true,生产环境还开着全放开跨域,是有安全风险的。

场景二:MyBatis查询返回字段全部为null。表象是接口返回的data里每个字段都是null但列表长度正常。原因是Java实体类属性名和数据库列名映射不上,比如数据库列customer_name,实体属性customerName,MyBatis默认关闭驼峰映射。解决方式是在application.yml配置map-underscore-to-camel-case: true,这是一个极其常见的新手问题。

场景三:金额字段出现精度不准。账单金额本该是100.00,数据库里却变成99.9999999。原因是Java实体类用了Double或者Float来接收金额字段。核心规则很简单:货币一律用BigDecimal,数据库层面用DECIMAL,两个都不妥协。前后端传输时BigDecimal序列化为数值或者字符串都可以,但字符串方式精度保持更可靠。

场景四:JWT登录后一段时间接口全部401。这个大概率是因为token过期了,前端Axios拦截器没有做自动续期处理。处理方式是把token过期时间设长一些(比如7天),或者实现刷新token机制。后台管理系统一般安全要求不高,用7天有效期加每次请求时静默滑动续期(即将过期时刷新),够用了。

场景五:定时任务没有按预期执行。排查路径是这样的:先确认@EnableScheduling是否加了,再确认SpringBoot启动类所在包有没有覆盖定时器所在包,否则定时器类不会被扫描进去,然后确认Cron表达式含义。我遇到过Cron写反的情况,配置成了每天凌晨1点和中午各执行一次,还在奇怪为什么每天会有两次账单生成。报表型的定时任务跑完后最好打一条固定格式的日志,方便追溯。

7.2 权限校验踩过的两个大坑

第一个坑是前端菜单控制正确但后端接口没做保护。操作员虽然看不到菜单,但他只要直接调API仍然能提交数据。我的原则是:菜单显隐只是用户体验,后端每个增删改接口必须有权限校验,前后端双重校验才能真正保证安全。

第二个坑是角色权限变更后,已登录用户的权限不刷新。装维人员登录了系统,管理员调整了他的角色,他如果不重新登录,token里携带的旧角色信息还能继续访问原来的接口。解决方案是在角色变更时强制该用户重新登录,或者后端每次请求都去数据库查最新权限而不是只管token里的信息。后台管理系统这类型业务,我更推荐后者,数据量小、实时性好,基本上多一次Redis内存查询或者数据库查询不是瓶颈。

7.3 系统性能优化记录

宽带业务系统虽然并发量不大,但有几个性能关键点要注意。报表查询是最容易拖慢系统的地方,因为要聚合大量历史数据。优化措施是给关键查询字段建复合索引,比如客户表的status + open_date组合索引,工单表的customer_id + status组合索引,账单表的customer_id + period组合索引。这些索引基本覆盖了所有高频查询场景。

第二个优化的点是前端表格大数据渲染。当客户的列表数据超过500条时,一次性渲染所有DOM会让页面卡顿,因为Element UI的表格渲染开销不小。解决方式就是后端分页,每页不超过20条,前端懒加载数据。如果需求里确实需要看大数据量的排行,推荐用虚拟滚动组件(如el-table的v-loadmore或单独引入虚拟滚动库),我做的这套系统里用分页就够了。

第三个优化的点是图片和静态资源的加载,因为装维工单中可能会包含现场照片,一张照片动辄好几兆,直接塞数据库或者前端直接加载会拖垮页面。处理方式是后端用文件服务单独保存照片,数据库只存URL,前端用懒加载或缩略图展示。宽带业务系统场景里这是刚需,因为施工验收环节基本都要上传现场照片。

8. 项目复盘与后续扩展建议

8.1 这套系统的价值点和我的体会

做了这个项目之后我一个很深的体会是:一个业务系统的难度从来不取决于用了什么框架,而取决于你对业务场景的理解深度。SpringBoot + Vue只是工具,宽带业务里的工单状态流转、计费规则、欠费停机联动这些业务逻辑才是让一个系统真正值钱的部分。框架大家都在学,但能把业务规则抽象得清清楚楚的开发者,才是项目中真正不可替代的人。

对于想拿这个项目做毕业设计或者面试项目的同学,我的建议是不要只会说“我做了客户管理、工单管理、报表统计”这种空话。你要能讲清楚:工单状态机是怎么设计的?为什么新增客户的同时要校验身份证唯一性?账单金额的折算规则是怎么实现的?欠费停机的联动逻辑是什么?把这些业务细节讲透,面试官会明显感觉到你是真正做过这个项目的,而不是照着教程敲了一遍。

8.2 技术上可以优化的下一步方向

实话实说,目前这套系统的架构还是标准的单体应用,性能瓶颈在于报表查询和大量数据聚合。后续如果有余力,可以做这几个方向的优化。

缓存层引入Redis,把客户基本信息、套餐列表这些读多写少的数据缓存起来,减少数据库压力。工单详情页因为包含流转历史,也可以做缓存,但要注意工单状态变更时必须清理对应缓存。

报表模块可以引入定时任务预生成机制,每晚批量把当天的报表数据聚合好存成结果表,用户次日查询时直接读结果表,而不是实时跑聚合SQL。这个优化对报表系统的体验提升是最明显的。

部署方式可以考虑引入Docker,把后端、前端、数据库分别打成容器镜像,配合Docker Compose一键启动。对于要交付给甲方部署的项目来说,容器化能省去很多环境配置的麻烦,也是现在工业界部署的主流做法。把这些方向说明白,项目的技术深度就会上一个台阶。

8.3 对接真实生产环境还需要补齐什么

如果这套系统要真正商用到生产环境,还需要补齐几块:数据库读写分离、操作日志审计的细化、短信通知网关(比如工单派单时给装维人员发短信)、对账系统对接支付渠道。这些虽然不是核心开发工作,但在真实场景下都是必不可少的完整拼图。宽带业务系统牵扯到收费和用户数据,上线前安全等保评测和功能测试阶段的工作量甚至比开发还要大。

真实生产环境中还有一个必须考虑的问题是数据迁移与历史数据清洗。老系统可能跑了很多年,里面积累了十万级的用户数据和几十万级的缴费记录。迁到新系统时要考虑历史账单如何处理、老用户的到期日期怎么平移、身份证号和手机号格式怎么统一。这些工作看起来很不起眼,但一旦在迁移过程中弄丢一条缴费记录,后患无穷。

最后说一句掏心窝的话:这样一套完整系统做完,最大的收获不是学会了SpringBoot的自动配置或者Vue的双向绑定,而是真正理解了一个业务系统从零到一的全过程。需求分析怎么拆解、数据库怎么设计、前后端怎么协同、部署怎么安排、线上问题怎么排查,这些能力就是程序员最核心的工程素养。把这个项目吃透,你以后遇到任何业务系统,都有底气说一句:这玩意儿我能做。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询