news 2026/11/5 5:39:54

Unieap Java小项目实战:企业应用骨架与分页查询链路

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Unieap Java小项目实战:企业应用骨架与分页查询链路

简介:Unieap简单小项目是一份面向Java初学者与业务流程开发入门者的示例工程,基于Unieap 3.4版本,围绕工作流引擎、表单设计、数据模型和权限控制等核心能力展开,帮助读者理解如何借助该框架降低编码量、搭建企业级审批与协作流程。资源以7z压缩包分发,约82.8MB;平台当前记录的文件总数为0,类型明细暂无有效数据,具体以压缩包实际内容为准。已有387人学习,说明其对入门者具有一定参考价值。项目重点呈现示例工程的基础结构、流程定义、业务逻辑与测试用例等学习素材,读者可据此了解Unieap项目组成、流程建模思路及权限与数据访问的整合方式,适合作为熟悉框架主要功能、开展小型实践的起步样例。

1. 交接包里只有六个字:Unieap 简单小项目能跑出什么

上个月帮朋友看一个离职交接的压缩包,文件名就叫「Unieap简单小项目」,解压出来一个 war、一份 SQL、几个配置文件,README 里只写了「JAVA 写的,先跑起来再说」。这类东西最磨人——不是不会写代码,是不知道这套东西跑到哪一步算正常。真跑起来之后我发现,它其实是一套很典型的企业应用骨架:登录鉴权、菜单权限、数据字典、带分页条件的列表查询、表单新增修改,一套 CRUD 从前到后全串通了。它适合两类人:一是接手老系统维护、只需要在既有平台上加模块的开发;二是想搞明白企业级 JAVA 平台到底怎么把 Spring、JSP、JDBC 缝在一起的人。麻雀虽小,链路是全的,把它跑通一次,后面加功能基本就是复制粘贴改字段。

2. 技术栈与目录结构:一个 Unieap JAVA 简单项目的骨架

这套项目最容易被误判的地方,是看着像普通 SSH/SSM 项目,实际上多了一层平台底座。业务代码只有薄薄一层,剩下全靠平台 jar 撑着。所以先别急着看业务类,先把「平台提供什么、业务写什么」这条界线划清楚,不然后面改配置会一直处在云里雾里。

2.1 运行时组成:平台 jar 与业务代码的边界

从依赖上看,这是一个典型的「容器 + Spring + JSP + JDBC」组合。Servlet 容器负责跑 war,Spring 负责 Bean 装配和事务,JSP 负责渲染,数据访问走的是手写 SQL 加连接池,没有用重量级 ORM。平台层以一组私有 jar 的形式出现,一般放在WEB-INF/lib下或者在私服里,业务侧只依赖它的接口。

层次常见组件谁提供改动频率
容器Tomcat 7/8 一类 Servlet 容器部署环境基本不动
平台底座平台 core、权限、字典、页面标签库平台 jar不动,只调用
框架Spring 容器、事务管理平台 jar 带入偶尔调配置
视图JSP + JSTL + 平台标签业务自己写常改
数据访问JDBC + 连接池 + 手写 SQL业务自己写常改
日志Log4j 或平台封装的日志门面平台 jar 带入排错时调级别

判断一个类属于哪一层,有个很土但有效的办法:打开 import,如果引的是平台包名,那就是平台能力,别去改它的源码;如果引的是java.sql、org.springframework,那基本是业务或框架胶水,可以放心动。

pom.xml里通常长这样,注意平台依赖一般scope是system或者干脆不在 Maven 里,靠手工拷 jar:

<dependencies> <!-- 平台底座:多数情况下不在公共仓库,需要本地私服或手工放进 lib --> <dependency> <groupId>com.platform</groupId> <artifactId>platform-core</artifactId> <version>${platform.version}</version> <scope>system</scope> <systemPath>${project.basedir}/lib/platform-core.jar</systemPath> </dependency> <!-- Web 层 --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> </dependency> <!-- 数据访问:连接池 + Spring JDBC --> <dependency> <groupId>commons-dbcp</groupId> <artifactId>commons-dbcp</artifactId> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> </dependency> </dependencies>

这里scope=system加systemPath是历史遗留做法,好处是打包时不会漏,坏处是换机器必须保证lib/目录跟着走。我一般会在 README 里单独列一份lib清单,写清楚每个 jar 是谁带进来的、少一个会报什么错,这比事后翻堆栈省事得多。

2.2 目录职责:每个文件夹坏了会出什么症状

这类项目的目录结构大多长得差不多,src/main/java放业务,src/main/resources放配置,src/main/webapp放页面和WEB-INF。真正有信息量的是WEB-INF下面那堆 xml 和页面放在哪。

路径职责出问题时的典型症状
src/main/java/.../action请求入口,取参、调服务、选视图页面 404 或参数拿不到
src/main/java/.../service业务逻辑、事务边界数据只写了一半
src/main/java/.../daoSQL 与结果映射查询有结果但字段全是 null
src/main/resources/configSpring、数据源、日志、平台参数启动就抛异常
src/main/webapp/WEB-INF/web.xml容器入口,监听器与过滤器顺序整个应用起不来
src/main/webapp/WEB-INF/viewsJSP 页面视图解析不到
src/main/webapp/staticjs、css、图片页面样式全丢
db/或sql/建表与初始化数据空库跑不起来

一个血泪经验:WEB-INF下的 JSP 不能被浏览器直接访问,只能由 Action 转发。所以看到「页面 404 但文件明明在」,先确认它是不是被放到了WEB-INF外面又被权限过滤器拦了,而不是去怀疑 Tomcat。

2.3 启动链路:容器按什么顺序把 Spring 拉起来

启动顺序搞不清楚,排错就只能靠猜。这个项目的启动链基本是固定的:容器读web.xml→ 加载 Spring 上下文 → 初始化数据源 → 平台初始化(菜单、字典、权限缓存)→ 业务 Bean 就绪。

<!-- web.xml 关键片段:顺序决定一切 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:config/applicationContext*.xml</param-value> </context-param> <!-- 1. Spring 上下文先起来,业务 Bean 才能注入 --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 2. 编码过滤器必须排在业务过滤器之前 --> <filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <!-- 3. 平台初始化 Servlet:负责把菜单和字典灌进缓存 --> <servlet> <servlet-name>platformInit</servlet-name> <servlet-class>com.platform.web.InitServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet>

contextConfigLocation用了通配符applicationContext*.xml,意味着你可以按模块拆成applicationContext-dao.xml、applicationContext-service.xml,加载顺序按文件名排序,这直接影响 Bean 的依赖能不能注入上。load-on-startup设成 1 表示容器启动时就实例化,别设成 0 或负数,否则第一次访问才初始化,菜单缓存会晚一步就绪,表现成「第一次进首页菜单是空的,刷新一下又有了」这种玄学问题。

启动成功与否,看日志就够了:Spring 打完Root WebApplicationContext: initialization completed,平台再打一条初始化耗时,最后容器打Server startup in xxx ms。这三条任意一条没出现,就往回找,别往下走。

3. 核心链路拆解:一次分页查询从 JSP 到数据库

挑一条最典型的链路:列表页按条件分页查询。这条线把视图、控制、服务、数据访问四层全串上了,看懂了它,新增、删除、修改基本是同一套模板换 SQL。

3.1 视图层:表单参数怎么传出去

页面用的是 JSP 加平台标签,表单提交到 Action 配置的路径。这里最容易踩的是参数名对不上——页面上叫orderNo,后端接的是order_no,结果查询条件永远为空,看着有数据其实没过滤。

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <!-- 查询条件区:name 必须和后端取参的 key 完全一致 --> <form id="queryForm" action="${ctx}/order/query.do" method="post"> <input type="text" name="orderNo" value="${param.orderNo}" placeholder="订单号"/> <select name="status"> <option value="">全部</option> <option value="1" ${param.status == '1' ? 'selected' : ''}>已受理</option> <option value="2" ${param.status == '2' ? 'selected' : ''}>已完成</option> </select> <!-- 分页参数:由翻页组件回填,不要手写死 --> <input type="hidden" name="pageNo" value="${page.pageNo}"/> <input type="hidden" name="pageSize" value="${page.pageSize}"/> <button type="submit">查询</button> </form> <!-- 结果区:字段名来自 DAO 返回的 Map key,大小写敏感 --> <table> <c:forEach items="${page.rows}" var="row"> <tr> <td>${row.orderNo}</td> <td>${row.statusName}</td> <td>${row.createTime}</td> </tr> </c:forEach> </table>

${ctx}是上下文路径,通常由平台的标签或者<c:set>注入,写死成/项目名换环境必翻车。param.xxx回填是为了查询后条件不丢,这属于体验细节但很影响验收。结果区的${row.orderNo}依赖 DAO 返回的 Map key,如果 SQL 里写了select order_no,这里就必须是row.order_no,要么就在 SQL 里用as orderNo别名统一掉——我一般选后者,别名比改页面省事。

3.2 控制层:取参、调用、选视图

Action 的职责很薄,不该有业务判断。它做三件事:把请求参数收成一个查询对象、调用 Service、把结果放到 request 域并返回视图名。

public class OrderQueryAction extends PlatformAction { private OrderService orderService; // setter 注入,Spring 装配时调用 public void setOrderService(OrderService orderService) { this.orderService = orderService; } @Override public String execute() { // 1. 分页参数要有兜底,前端不传时不能直接 NPE int pageNo = getIntParam("pageNo", 1); int pageSize = getIntParam("pageSize", 20); if (pageSize > 200) { pageSize = 200; // 防止有人手动改 URL 拉全表 } // 2. 条件对象:字段与页面 name 一一对应 OrderQuery query = new OrderQuery(); query.setOrderNo(getParam("orderNo")); query.setStatus(getParam("status")); query.setPageNo(pageNo); query.setPageSize(pageSize); // 3. 调服务,拿分页结果 PageResult<Map<String, Object>> page = orderService.queryByPage(query); getRequest().setAttribute("page", page); // 4. 返回逻辑视图名,由平台视图解析器拼成 /WEB-INF/views/order/list.jsp return "order/list"; } }

getIntParam和getParam是平台基类封装的取值方法,比request.getParameter多了空值和类型转换处理。pageSize上限那三行别嫌多余,老项目里真有人把pageSize改成 999999 导全表,数据库直接被打挂。返回的"order/list"是逻辑视图名,前缀后缀在平台配置里,找到那个配置就知道 JSP 该放哪。

3.3 服务与数据层:SQL 与分页怎么配合

Service 层是事务边界,DAO 层只管 SQL。分页这里有个绕不开的问题:总数和明细要两条 SQL,或者用数据库方言的窗口函数。

public class OrderServiceImpl implements OrderService { private OrderDao orderDao; public void setOrderDao(OrderDao orderDao) { this.orderDao = orderDao; } @Override public PageResult<Map<String, Object>> queryByPage(OrderQuery query) { // 先查总数,再查明细;两条 SQL 必须用同一套 where 条件 int total = orderDao.countByCondition(query); PageResult<Map<String, Object>> result = new PageResult<>(); result.setTotal(total); result.setPageNo(query.getPageNo()); result.setPageSize(query.getPageSize()); if (total == 0) { result.setRows(Collections.emptyList()); return result; // 没数据就别再查一次明细了 } result.setRows(orderDao.queryByCondition(query)); return result; } }

total == 0提前返回,是两个好处:少一次数据库往返,也避免某些方言在offset超出总数时抛异常。DAO 那边用JdbcTemplate拼条件,重点是条件拼接要用参数占位符,不能字符串拼值。

public class OrderDao extends JdbcDaoSupport { private static final String WHERE = " where 1=1 " + " and (#{orderNo} is null or order_no like #{orderNo}) " + " and (#{status} is null or status = #{status}) "; public int countByCondition(OrderQuery q) { String sql = "select count(1) from t_order" + WHERE; Map<String, Object> args = buildArgs(q); Integer n = getJdbcTemplate().queryForObject(sql, args, Integer.class); return n == null ? 0 : n; } public List<Map<String, Object>> queryByCondition(OrderQuery q) { // 别名必须和 JSP 里的取值保持一致 String sql = "select order_no as orderNo, status, create_time as createTime " + "from t_order" + WHERE + " order by create_time desc " + " limit #{offset}, #{pageSize} "; Map<String, Object> args = buildArgs(q); args.put("offset", (q.getPageNo() - 1) * q.getPageSize()); args.put("pageSize", q.getPageSize()); return getJdbcTemplate().queryForList(sql, args); } private Map<String, Object> buildArgs(OrderQuery q) { Map<String, Object> args = new HashMap<>(); // 模糊查询的 % 在 Java 侧拼,别在 SQL 里拼 args.put("orderNo", isBlank(q.getOrderNo()) ? null : "%" + q.getOrderNo().trim() + "%"); args.put("status", isBlank(q.getStatus()) ? null : q.getStatus()); return args; } }

#{orderNo} is null or ...这种写法是让同一段 where 同时支持「有值过滤」和「无值不过滤」,省掉动态拼 SQL 的麻烦,代价是索引有时用不上。数据量小几十万行无所谓,上百万就得改成真正的动态 SQL。limit #{offset}, #{pageSize}是 MySQL 方言,换成别的库要跟着改,这也是老项目跨库迁移最烦的一处。

3.4 数据源与事务:配在哪儿、配错是什么表现

数据源不在业务代码里,在 Spring 配置文件里。这块配错的表现特别迷惑:应用能启动,日志也没报错,一查数据就超时或者报「连接已关闭」。

<!-- applicationContext-dao.xml --> <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close"> <property name="driverClassName" value="${jdbc.driver}"/> <property name="url" value="${jdbc.url}"/> <property name="username" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <!-- 这几个参数决定高峰期会不会假死 --> <property name="initialSize" value="5"/> <property name="maxActive" value="50"/> <property name="maxWait" value="6000"/> <property name="validationQuery" value="select 1"/> <property name="testWhileIdle" value="true"/> </bean> <!-- 事务:切在 Service 层,不要切在 Action 或 DAO --> <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="txManager"/>

maxWait单位是毫秒,设成 6000 表示拿不到连接最多等 6 秒就抛异常,比无限等待好排查。validationQuery是防止数据库连接被中间设备掐断后还留在池子里——这个坑很隐蔽,表现是「每天早上第一次访问必超时,之后正常」,加上testWhileIdle就好了。事务切在 Service 层是原则问题,切在 DAO 上会导致一个业务操作里多个 DAO 各自提交,中途失败就留下脏数据。

4. 跑起来:从空库到页面出数据的完整步骤

链路看明白了,剩下就是照着做。这部分我尽量按能直接抄的顺序写,中间不给「自行配置」这种糊弄话。

4.1 环境与依赖补齐

先把环境对齐,版本不匹配是 80% 启动失败的直接原因。

项要求检查方式
JDK与平台 jar 编译版本一致(老平台多为 7/8)java -version看主版本
容器Tomcat 7/8 一类,别上太新的启动日志里无Unsupported major.minor
数据库与 SQL 脚本方言一致能执行select 1
构建Maven 或直接手工打 warmvn -v或能压出 war
平台 jarlib/下齐全启动无ClassNotFoundException

JDK 版本这条最容易被忽略。平台 jar 如果用 JDK 7 编译,你拿 JDK 17 去跑,会在类加载阶段直接报版本号不支持,而这个报错看起来像缺包,很多人会去补 jar,越补越乱。

4.2 建库与初始化脚本

SQL 一般分两部分:建表和初始化数据。初始化数据里往往还包含菜单、字典、权限这些平台表,漏了就会出现「能登录但左边菜单是空的」。

-- 1. 业务表 create table t_order ( id bigint primary key auto_increment, order_no varchar(32) not null comment '订单号', status varchar(8) not null default '1' comment '状态', create_time datetime not null default current_timestamp, key idx_order_no (order_no), key idx_create_time (create_time) ) engine=InnoDB default charset=utf8mb4 comment='订单表'; -- 2. 平台菜单表:少这一条,功能写完了也点不进去 insert into t_menu (menu_id, parent_id, menu_name, menu_url, sort_no, visible) values ('M_ORDER', 'M_ROOT', '订单管理', '/order/query.do', 10, '1'); -- 3. 权限关联:不给角色授权,菜单渲染时会被过滤掉 insert into t_role_menu (role_id, menu_id) values ('R_ADMIN', 'M_ORDER');

表名、字段名都以项目脚本为准,别自己重命名,页面和 SQL 都是硬编码的。utf8mb4是为了兼容特殊字符,如果老脚本写的是utf8,跟数据库默认字符集不一致时中文会出现问号。

4.3 改配置:连接串、编码、日志级别

配置文件通常是jdbc.properties加几个 Spring xml。改的时候一项一项来,改完一次启动一次,别一口气全改。

# jdbc.properties jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/demo_db?useUnicode=true&characterEncoding=UTF-8&useSSL=false jdbc.username=root jdbc.password=你的密码 # 排错期把日志开到 DEBUG,跑通后记得调回 INFO,不然日志能把磁盘写满 log.level=DEBUG

连接串里的useUnicode=true&characterEncoding=UTF-8不能省,少了它中文会以乱码存进去,事后修数据非常痛苦。密码别留在提交记录里,本地用一个jdbc-local.properties覆盖,或者在容器里配环境变量,这是基本操作。

4.4 打包部署与自检

打包、扔进容器、看日志、访问。每一步都有明确的成功标志,别跳过。

# 1. 打包(跳过测试,老项目测试类经常跑不过,但不影响运行) mvn clean package -DskipTests # 2. 部署:war 扔进容器的 webapps 目录,或者用解压目录方式部署 cp target/demo.war $CATALINA_HOME/webapps/ # 3. 启动并盯日志 tail -f $CATALINA_HOME/logs/catalina.out # 4. 验证:先探活,再访问业务页 curl -I http://127.0.0.1:8080/demo/ curl -s "http://127.0.0.1:8080/demo/order/query.do?pageNo=1&pageSize=10"

自检顺序建议是:根路径能不能打开(证明 war 解压成功)→ 登录页能不能出(证明 Spring 和数据库通了)→ 菜单能不能显示(证明平台初始化数据和权限对了)→ 列表能不能出数据(证明业务链路通了)。这四步任意一步卡住,就去对应的那一层看,不要一上来就翻业务代码。

5. 避坑与排查:Unieap 小项目最常翻车的五处

下面这五条都是我实际遇到过、并且不止一次遇到的,按「现象 → 原因 → 解决」记下来,下次再撞上直接对号入座。

5.1 启动报 ClassNotFoundException,补了 jar 还是报

现象:日志里java.lang.ClassNotFoundException,指向某个平台类,往lib里补了对应 jar 依然报同样的错。 原因:多半是WEB-INF/lib和 Maven 依赖里同时存在同名不同版本的 jar,类加载器加载了旧的那个;或者 jar 本身是好的,但被provided作用域排除在 war 之外。 解决:把 war 解开,ls WEB-INF/lib逐个对照,同名 jar 只留一份;用mvn dependency:tree找出冲突来源加exclusions。补 jar 前先确认容器加载的是哪个路径下的类,别盲目加。

5.2 中文全是乱码,改了 JSP 编码也没用

现象:页面输入的中文存进数据库变成???,或者从库里读出来显示成方块。 原因:编码问题不止一处——JSP 页面编码、请求过滤器、数据库连接串、表字符集,四个地方任意一处不一致就会乱。只改页面是最常见的无效操作。 解决:按顺序排。JSP 顶部pageEncoding="UTF-8";web.xml里编码过滤器排在所有业务过滤器之前;连接串带characterEncoding=UTF-8;建表用utf8mb4。四个都对了才彻底不乱。判断当前卡在哪一环,看数据是「写进去就坏」还是「读出来才坏」,能省一半时间。

5.3 数据源连不上,或者跑一会儿就假死

现象:启动时报连接超时;或者白天正常,第二天早上第一次访问必等十几秒然后报错。 原因:前者是连接串、驱动或库没起来;后者是连接池里的空闲连接被网络设备或数据库自身超时掐断,池子还以为它是活的。 解决:连不上先telnet目标和端口,再确认驱动版本和数据库版本是否匹配。假死问题在连接池配置里加validationQuery=select 1、testWhileIdle=true,并把maxWait设成有限值,避免请求线程全卡在等连接上。

5.4 功能写完了,菜单里根本点不进去

现象:Action、Service、JSP 都写好了,直接敲 URL 能访问,但左侧菜单里找不到入口。 原因:菜单是靠平台表驱动的,不插t_menu就没有入口;插了但没给角色授权,渲染时被权限过滤掉;还有一种情况是平台有菜单缓存,插完数据不重启不生效。 解决:先查菜单表有没有记录、menu_url是否和 Action 配置的路径完全一致(多一个斜杠都匹配不上),再查角色菜单关联表。数据补完重启一次或调用平台的缓存刷新,别指望热生效。

5.5 事务不生效,出错了数据还是写进去了

现象:Service 方法里先插入再更新,第二步抛异常,但第一步的数据已经落库。 原因:两种最常见。一是 Service 是被new出来的,不是 Spring 注入的,代理根本没生效;二是注解扫描的包路径没覆盖到当前类,或者事务管理器配的是另一个数据源。 解决:在 Service 实现类里打断点,看this是不是代理对象(类名带$Proxy或$$)。不是的话,检查 Action 里是不是写了new XxxServiceImpl(),改成 setter 注入;再检查component-scan的包名和tx:annotation-driven是否都在。

6. 二次开发:照着加一个业务模块并验证

真正落到改代码,我一般不打无准备的仗:先按「表 → 查询对象 → DAO → Service → Action → 页面 → 菜单」这个顺序推一遍,每完成两步编译一次,别等到最后一起编译,报错会堆成一坨看不出因果。

假设要加一个「客户台账」查询,最小的改动集合是这样的:

-- 第一步:建表 + 注册菜单,数据库先准备好 create table t_customer ( id bigint primary key auto_increment, cust_name varchar(64) not null, cust_level varchar(8) default 'C', create_time datetime default current_timestamp ) engine=InnoDB default charset=utf8mb4; insert into t_menu (menu_id, parent_id, menu_name, menu_url, sort_no, visible) values ('M_CUST', 'M_ROOT', '客户台账', '/customer/query.do', 20, '1'); insert into t_role_menu (role_id, menu_id) values ('R_ADMIN', 'M_CUST');
// 第二步:Action 只做转发,逻辑全部下沉到 Service public class CustomerQueryAction extends PlatformAction { private CustomerService customerService; public void setCustomerService(CustomerService customerService) { this.customerService = customerService; } @Override public String execute() { CustomerQuery query = new CustomerQuery(); query.setCustName(getParam("custName")); query.setCustLevel(getParam("custLevel")); query.setPageNo(getIntParam("pageNo", 1)); query.setPageSize(getIntParam("pageSize", 20)); getRequest().setAttribute("page", customerService.queryByPage(query)); return "customer/list"; // 对应 /WEB-INF/views/customer/list.jsp } }

Spring 配置里补一行 Bean 声明,如果项目用的是注解扫描就加@Service,两种方式别混着来:

<bean id="customerQueryAction" class="com.demo.customer.action.CustomerQueryAction"> <property name="customerService" ref="customerService"/> </bean> <bean id="customerService" class="com.demo.customer.service.CustomerServiceImpl"> <property name="customerDao" ref="customerDao"/> </bean>

改完之后怎么验证,我有一套固定动作,比点点点靠谱:

验证项操作通过标准
编译mvn clean package -DskipTests无编译错误,war 正常产出
启动重启容器看日志无BeanCreationException
菜单登录后看左侧导航出现「客户台账」,点击能跳转
权限换个低权限账号登录菜单不显示,直接敲 URL 被拦
查询空条件查一次返回默认分页,总数正确
条件只填名称查一次结果按条件过滤,总数同步变
边界把pageSize手动改成 99999被后端限制到上限,不拉全表

权限那项特别值得单独测。很多小项目在本地开发时用管理员账号一路顺畅,上线换普通账号才发现 URL 裸奔——菜单藏了但接口没拦。这类问题在验收时基本发现不了,属于必须主动构造的用例。

改造时我最容易犯的错是「顺手优化」:看到旧代码 SQL 拼得糙就改,看到命名不规范就重命名,结果牵一发动全身。老项目的每处怪写法背后大概率有个已经踩过的坑,先照着现有模式抄一份能跑通的,验证通过之后再考虑重构,顺序反了就是给自己找麻烦。从那以后我每次动这种接手项目,都强制走一遍「先复制一份能跑的、再改字段、最后才动结构」,少了很多后悔药要吃。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/11/5 5:38:10

ECShop老商城接入支付宝免签约即时到账实战与避坑

简介&#xff1a;面向 ECShop V2.7.3 商城系统的支付宝免签约即时到帐接口&#xff0c;适合没有支付宝签约资质、又希望快速接入即时到账支付的中小站长与开发者使用。资源包共 7 个文件&#xff0c;以 6 个 PHP 支付模块、回调接收及中英文语言文件为主&#xff0c;另附 1 个 …

作者头像 李华
网站建设 2026/11/5 5:36:32

免费去ai痕迹方法4个,手改改不动的再用去i迹免费降AI 1000字

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/5 5:35:24

从 F429 说起基于Keil5搭建STM32工程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/11/5 5:34:54

光缆色序零配置自动换型 基于 OCR 与几何特征的色谱自匹配方案

1. 多规格混线的模板选错风险 光缆行业的生产模式正在发生结构性变化。过去一条线动辄跑几万公里同一规格&#xff0c;如今订单碎片化&#xff0c;多是小批量、多品种&#xff1a; 上午跑 12 芯标准色谱&#xff08;蓝橙绿棕灰白、红黑黄紫粉青&#xff09;&#xff1b;下午切到…

作者头像 李华
网站建设 2026/11/5 5:34:42

项目工作总结

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华