简介:这是一份面向计算机专业毕业生、Java Web初学者及课程设计者的物流配送管理系统毕业论文,围绕基于B/S结构的物流配送平台展开,可用于毕业设计选题参考、论文框架借鉴与SSH项目入门学习。文档从JSP技术、SSH框架和Oracle数据库的技术综述讲起,依次覆盖需求分析、系统架构设计、功能模块描述、系统用例图与领域模型定义,并围绕车辆管理、配送点配送范围管理、配送管理、员工管理、权限管理、订单管理和交接单管理七个模块说明设计思路。资源包共1个doc文件,体积约1.27MB,以论文正文为主体,包含中英文摘要、目录及章节化论述,便于按主题查阅和整理。目前已有625人学习下载,读者可据此了解传统Java Web物流系统的需求分析方式、模块划分方法与论文写作结构,为开题、系统设计和论文撰写提供较完整的参照。
1. 一套 SSH 物流毕设能跑出什么:从车辆台账到交接单的业务闭环
朋友把一套 java物流配送管理系统毕业论文 的源码丢过来时,压缩包里躺着 JSP 页面、Struts 配置、一份 Oracle 建表脚本,还有几张导出的用例图。他只想答辩前把登录跑通。这类 SSH 老项目的代码谈不上新,但它是一条完整业务闭环:省公司维护车辆和配送点,站点把订单按目的地聚成交接单,再挑空闲车辆发车。它适合想弄懂「一张订单怎么变成一趟配送」的人,不适合只想套模板交差的人。物流配送管理系统真正的骨架不在页面上,而在车辆状态、配送范围、交接单这三者怎么互相约束——这一点想明白,后面读代码就顺了。
2. 架构拆解:SSH 四层怎么串起来,双角色权限怎么切
这套系统的结构是教科书式的 SSH:Struts2 管 MVC 分离和请求跳转,Spring 当容器管住 Action 和 DAO 的生命周期,Hibernate 把表操作翻译成对象操作。原文里写得很直白,对象调用流程是jsp -> Action -> Service -> DAO -> Hibernate,中间不绕路。真正决定这套代码好不好改的,是它对「省公司管理员」和「站点管理员」这两个角色的切分,以及每个模块分别挂在哪一层。
2.1 SSH 四层职责与调用链
把四层掰开看,每一层只干一件事。表示层是 JSP 加 Struts2 的 Action,Action 负责接表单参数、调 Service、把结果塞进 request 或 session;业务层是 Service 接口加实现,负责业务规则,比如「配送点在工作中不允许删除」这种判断就该落在这里;持久层是 DAO 接口加 Hibernate 实现,负责把对象落库;域模型层是一堆 POJO,对应数据库表。
浏览器请求 /car_list.action │ ▼ CarManageAction ── 收参数、调 Service、返回结果码 │ ▼ CarManageService ── 业务校验(状态、权限、分页) │ ▼ CarManageDao ── HibernateTemplate 执行 save/update/delete │ ▼ Oracle T_CAR 表分层的好处很实在:想加一个「车辆年检到期提醒」,只需要在 Service 里加一段判断,Action 和 DAO 一行不用动。这也是为什么这类毕设项目虽然页面糙,但结构上是能拿去改的。
2.2 省公司管理员与站点管理员的权限切分
原文把系统分成两个大模块,这不是页面上的两个菜单,而是两套数据边界。
| 角色 | 管什么 | 数据范围 | 典型操作 |
|---|---|---|---|
| 省公司管理员 | 车辆、配送点、权限、员工 | 全公司 | 新增车辆、淘汰车辆、改车辆状态、删除配送点、分配站点管理员权限 |
| 站点管理员 | 配送范围、订单、交接单、配送 | 本配送点 | 录入订单、生成交接单、选车发车、维护本地配送范围 |
关键约束在于:车辆由省公司统一购买,站点只能查、只能选,不能改车辆台账;配送范围只能由站点管理员维护自己站点的;省公司管理员可以查看所有配送点的配送范围,但不能代替站点改。这种「谁拥有数据、谁只能读」的划分,落到代码上就是 Action 里先取 session 里的登录用户,再决定查全量还是查本点。
// 登录成功后把角色和所属站点写进 session public String login() { TEmployee emp = employeeService.login(emp.getLoginName(), emp.getPassword()); if (emp == null) { return "loginFail"; } ActionContext.getContext().getSession().put("loginUser", emp); // role: 1=省公司管理员 2=站点管理员 ActionContext.getContext().getSession().put("role", emp.getRoleId()); ActionContext.getContext().getSession().put("siteId", emp.getSiteId()); return emp.getRoleId() == 1 ? "adminMain" : "siteMain"; }上面这段是登录分流的常见写法,参数说明两点:roleId决定跳哪个主页,siteId决定后续查询的数据边界。站点管理员查订单时把siteId作为条件带进 DAO,省公司管理员则传 null 查全量。
2.3 工程目录结构
按 SSH 约定,源码一般长这样:
src ├─ com.logistics.entity // POJO:TCar、TDistributionSite、TOrder... ├─ com.logistics.action // Struts2 Action:CarManageAction、OrderAction... ├─ com.logistics.service // 业务接口 │ └─ impl // 业务实现 ├─ com.logistics.dao // 持久层接口 │ └─ impl // Hibernate 实现,继承 HibernateDaoSupport ├─ com.logistics.util // PageBean、MD5、日期工具 └─ applicationContext.xml // Spring 配置,声明 bean 与事务 WebRoot ├─ WEB-INF │ ├─ web.xml // Struts2 过滤器、Spring 监听器 │ ├─ struts.xml // action 与 result 映射 │ └─ applicationContext.xml // 有时放这里 ├─ jsp // 各模块页面 └─ css / js / imagesSpring 的配置文件里会把 Action、Service、DAO 都声明成 bean,struts.xml里 Action 的 class 属性填的是 bean 名,靠 Spring 托管。这套约定不复杂,但改包名时三份 XML 和 web.xml 都得同步,少改一处就启动报ClassNotFoundException,这一点后面还会细说。
3. 数据库设计:七张表撑起车辆、配送点与交接单
物流系统的复杂性全在表关系上。原文需求里列了八个功能模块,但落到表上其实是七张核心表加若干中间关系:车辆、配送点、配送范围、订单、交接单、员工、权限。省公司和站点两级管理能不能成立,取决于外键怎么挂、状态字段怎么定义。
3.1 实体关系与表清单
几个关键关系先说清:一个配送点下挂多个配送范围;一个配送点上挂多个员工(站点管理员属于某个点);一张订单归属一个配送点;多张订单按目的地聚成一张交接单;一辆车可对应多张交接单,但在一个班次内只能被一张交接单占用。
表清单大致如下:
| 表名 | 中文名 | 关键字段 | 说明 |
|---|---|---|---|
| T_CAR | 车辆表 | CAR_ID、NOW_STATUS | NOW_STATUS 表示空闲/占用/淘汰 |
| T_DISTRIBUTION_SITE | 配送点表 | SITE_ID、SITE_SCALE | 规模字段用于约束接单量 |
| T_DISTRIBUTION_SCOPE | 配送范围表 | SCOPE_ID、SITE_ID | 外键挂配送点 |
| T_ORDER | 订单表 | ORDER_ID、SITE_ID、ORDER_STATUS | 状态含待配送/已退订 |
| T_TRANSFER | 交接单表 | TRANSFER_ID、CAR_ID、SITE_ID | 一张交接单关联多张订单 |
| T_TRANSFER_ORDER | 交接单订单关系 | TRANSFER_ID、ORDER_ID | 多对多拆成中间表 |
| T_EMPLOYEE | 员工表 | EMP_ID、ROLE_ID、SITE_ID | 角色和所属点决定权限 |
其中T_TRANSFER_ORDER是最容易被忽略的一张表。订单和交接单不是一对多而是多对多——一张订单可能因为分拣被拆到不同班次,但这种简单系统一般按「一单只进一张交接单」处理,于是可以简化为订单表里加TRANSFER_ID外键。原文里没写透这一点,实际选型时按业务复杂度决定,毕设规模下用外键更省事。
3.2 关键表字段说明
建表时字段命名和类型选择会直接决定后面代码写起来别扭不别扭。原文提到「字段要合理命名,在建表语句后面加上字段备注」,这是对的。下面以车辆表为例,给一份 Oracle 风格的 DDL:
-- 车辆表:省公司统一维护,站点只读 CREATE TABLE T_CAR ( CAR_ID VARCHAR2(20) PRIMARY KEY, -- 车牌号/车辆编号 CAR_TYPE VARCHAR2(50) NOT NULL, -- 车型 LOAD_WEIGHT NUMBER(10,2), -- 载重,吨 VOLUME NUMBER(10,2), -- 容积,立方米 MANUFACTURER VARCHAR2(100), -- 厂商 BUY_TIME DATE, -- 购买时间 NOW_STATUS VARCHAR2(10) DEFAULT '0', -- 0空闲 1占用 2淘汰 REMARK VARCHAR2(200) ); COMMENT ON COLUMN T_CAR.NOW_STATUS IS '车辆状态:0空闲 1占用 2淘汰';NOW_STATUS这个字段是全系统耦合最紧的地方之一,生成交接单时要判断车辆是否为空闲,删除车辆时其实不能物理删除,改成把状态置为「淘汰」更符合业务——原文里「淘汰车辆」和「删除车辆」其实是两个概念,落到代码上一个是更新状态,一个是物理 delete,选哪个要看答辩老师问不问数据留存。
订单表的关键是状态流转:
CREATE TABLE T_ORDER ( ORDER_ID VARCHAR2(20) PRIMARY KEY, SITE_ID VARCHAR2(20) NOT NULL, -- 归属配送点 SENDER_NAME VARCHAR2(50), SENDER_PHONE VARCHAR2(20), RECEIVER_ADDR VARCHAR2(200) NOT NULL, -- 收货地址,用于划分配送目的地 GOODS_WEIGHT NUMBER(10,2), ORDER_STATUS VARCHAR2(10) DEFAULT '0', -- 0待配送 1已生成交接单 2已完成 3已退订 CREATE_TIME DATE DEFAULT SYSDATE, TRANSFER_ID VARCHAR2(20) -- 关联交接单,未生成时为空 );参数上注意TRANSFER_ID可为空,这代表订单还没被聚合。生成交接单时先按RECEIVER_ADDR中的目的地字段分组,把同一目的地的待配送订单挑出来,再批量回写TRANSFER_ID。
3.3 Hibernate 映射与实体 POJO
实体和表的对应靠hbm.xml或注解,这类老项目多数还是 XML。以车辆为例:
<!-- TCar.hbm.xml --> <hibernate-mapping> <class name="com.logistics.entity.TCar" table="T_CAR"> <id name="carId" column="CAR_ID" type="string"/> <property name="carType" column="CAR_TYPE" type="string"/> <property name="loadWeight" column="LOAD_WEIGHT" type="big_decimal"/> <property name="volume" column="VOLUME" type="big_decimal"/> <property name="manufacturer" column="MANUFACTURER" type="string"/> <property name="buyTime" column="BUY_TIME" type="date"/> <property name="nowStatus" column="NOW_STATUS" type="string"/> </class> </hibernate-mapping>fact 说明两点:type="big_decimal"对应 Java 的BigDecimal,别写成double,金额和重量这种字段用浮点会丢精度;日期用type="date"而不是timestamp,省得后面格式化多一层。实体类里全部用包装类型而不是基本类型,因为数据库字段可能为空,用int会抛空指针。
4. 核心链路代码:车辆增删改查与订单转交接单怎么落地
前半截是标准 CRUD,后半截是这套系统的业务核心。读这套代码建议从车辆管理入手,因为它链路最完整却没有复杂状态机;看懂之后再去看订单转交接单,那里才有真正的业务判断。
4.1 车辆管理 Action-Service-DAO 三层链路
Action 只做三件事:接参数、调 Service、给结果。别在 Action 里写业务判断,这是后来改起来最痛苦的地方。
public class CarManageAction extends ActionSupport { private CarManageService carManageService; private List<TCar> carList; private TCar car; private int page = 1; private PageBean pageBean; // 新增车辆:省公司管理员操作 public String addCar() { carManageService.addCar(car); return queryCarPage(); // 新增后回到列表页 } // 淘汰车辆:不是物理删除,只把状态置为 2 public String eliminateCar() { car.setNowStatus("2"); carManageService.alterCar(car); return queryCarPage(); } // 分页查询车辆 public String queryCarPage() { pageBean = carManageService.queryForPage(page, 10); return "carList"; } // Spring 依赖注入 public void setCarManageService(CarManageService s) { this.carManageService = s; } // 省略各 getter/setter }Service 接口和实现分开定义,是这套项目最值得保留的习惯:
public interface CarManageService { void addCar(TCar car); void alterCar(TCar car); void deleteCar(TCar car); PageBean queryForPage(int page, int pageSize); }实现里调 DAO,并加上必要的业务校验。比如「车辆被占用时不允许淘汰」,这条逻辑就该落在这里,而不是塞进 Action:
public class CarManageServiceImpl implements CarManageService { private CarManageDao carManageDao; public void alterCar(TCar car) { // 淘汰前检查是否被交接单占用 if ("2".equals(car.getNowStatus())) { int used = carManageDao.countUsingTransfer(car.getCarId()); if (used > 0) { throw new RuntimeException("该车辆正在执行配送任务,不能淘汰"); } } carManageDao.alterCar(car); } // 其他方法略 }DAO 层用 Hibernate 就两行:
public class CarManageDaoImpl extends HibernateDaoSupport implements CarManageDao { public void addCar(TCar car) { getHibernateTemplate().save(car); } public void alterCar(TCar car) { getHibernateTemplate().update(car); } public void deleteCar(TCar car) { getHibernateTemplate().delete(car); } }HibernateDaoSupport的好处是省掉手动开关 session,getHibernateTemplate()会自动绑定 Spring 事务。注意一点:update方法要求对象主键已存在,从页面回填的TCar主键没设对会报StaleStateException,这是新手最常见的翻车点之一。
4.2 分页查询与 PageBean
这套项目用的分页是手写PageBean,不是插件。逻辑不复杂但容易写错:
public class PageBean { private int page; // 当前页码,从 1 开始 private int pageSize; // 每页条数 private int totalCount; // 总记录数 private List<?> list; // 当前页数据 public int getTotalPage() { return (totalCount + pageSize - 1) / pageSize; } public int getStartIndex() { return (page - 1) * pageSize; } // 省略 getter/setter }DAO 里用 HQL 的setFirstResult和setMaxResults分页:
public PageBean queryForPage(int page, int pageSize) { String hql = "from TCar order by carId"; int total = ((Long) getHibernateTemplate() .find("select count(*) from TCar").get(0)).intValue(); List<?> list = getHibernateTemplate().getSessionFactory() .openSession().createQuery(hql) .setFirstResult((page - 1) * pageSize) .setMaxResults(pageSize) .list(); PageBean pb = new PageBean(); pb.setPage(page); pb.setPageSize(pageSize); pb.setTotalCount(total); pb.setList(list); return pb; }这里有两个细节值得点出来:count(*)返回的是Long,不转int直接强转会ClassCastException;openSession()拿到的 session 要记得关,或者干脆用getSession(false)配合HibernateDaoSupport的模板方法,否则连接池用一会儿就耗尽。
4.3 订单生成交接单的业务逻辑
这是整个项目里唯一有「状态机」味道的地方。站点管理员选中一批待配送订单,系统按收货目的地分组,生成交接单,并把订单状态从「待配送」改成「已生成交接单」。核心步骤有四步:
- 查出该站点下所有
ORDER_STATUS='0'的订单; - 按地址里的目的地字段(比如取地址前几个字)分组;
- 每个分组生成一张交接单,写入
T_TRANSFER,并回填订单的TRANSFER_ID; - 更新订单状态为
1。
public String createTransfer() { // 1. 取本站点待配送订单 List<TOrder> orders = orderDao.queryWaitList(siteId); // 2. 按目的地分组 Map<String, List<TOrder>> group = new LinkedHashMap<>(); for (TOrder o : orders) { String dest = splitDest(o.getReceiverAddr()); // 取目的地关键词 group.computeIfAbsent(dest, k -> new ArrayList<>()).add(o); } // 3. 每组一张交接单 for (Map.Entry<String, List<TOrder>> e : group.entrySet()) { TTransfer t = new TTransfer(); t.setTransferId(genTransferNo()); // 自定义单号 t.setSiteId(siteId); t.setDestination(e.getKey()); t.setCreateTime(new Date()); transferDao.add(t); // 4. 回填订单 for (TOrder o : e.getValue()) { o.setTransferId(t.getTransferId()); o.setOrderStatus("1"); orderDao.alter(o); } } return "transferList"; } // 单号:站点号 + 时间戳,够用且不重复 private String genTransferNo() { return siteId + System.currentTimeMillis(); }参数说明:splitDest是取地址的关键词匹配,比如地址里城市名单独存了一个字段就直接用它,没有就用地址前两位截取。这段逻辑本来应该用事务包起来,因为交接单插入和订单更新必须同时成功或同时失败,否则会出现「有交接单但订单还挂在待配送」的脏数据——这一点在 5.3 里会具体展开。
5. 本地部署与常见问题排查:从 Oracle 建库到 Tomcat 启动
这套项目跑不起来,九成不是业务代码的问题,而是环境。Java 老项目对环境版本很敏感,JDK、Tomcat、Oracle 驱动、字符集,任何一环对不上都启动失败或页面乱码。这一章按实际复现顺序写,再单列常见问题。
5.1 环境准备与建库步骤
推荐的环境组合:JDK 8(不要上 11 及以上,Hibernate 和 Struts2 老版本对高版本 JDK 支持不好)、Tomcat 8、Oracle 11g/12c 或者用兼容性更好的数据库替代(但 SQL 语法要改,见 5.3)。MyEclipse 只是 IDE,换成 IDEA 或 Eclipse 都能跑,区别只在部署配置方式。
建库步骤:
- 先用管理员账号建表空间和用户,字符集一定要选
AL32UTF8或者跟系统编码一致,字符集不对后面 JSP 页面中文全是问号; - 用新建的业务用户登录,执行项目里的建表脚本(顺序是车辆、配送点、配送范围、员工、权限、订单、交接单);
- 灌初始数据:一个省公司管理员账号、一个配送点、几个员工;
- 检查
applicationContext.xml里的数据库连接串,<property name="url" value="jdbc:oracle:thin:@localhost:1521:orcl"/>这里的主机名、端口、实例名三个都要改对。
<!-- applicationContext.xml 数据源片段 --> <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/> <property name="url" value="jdbc:oracle:thin:@127.0.0.1:1521:orcl"/> <property name="username" value="logistics"/> <property name="password" value="logistics"/> </bean>连接串的orcl是实例名,不是数据库名,填错报ORA-12505。DriverManagerDataSource每次拿连接都新建,适合调试不适合测试,上线要换连接池。
5.2 配置文件与启动
启动前要确认三处 XML 全部就位:web.xml里配好 Struts2 过滤器和 Spring 监听器,struts.xml里 action 的 class 属性填 Spring bean 名,applicationContext.xml里扫描包或显式声明所有 bean。
<!-- web.xml 关键两段 --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <filter> <filter-name>struts2</filter-name> <filter-class>org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter</filter-class> </filter> <filter-mapping> <filter-name>struts2</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>启动顺序:先起数据库,再启 Tomcat,观察控制台。正常启动的标志是 Spring 日志里所有 bean 初始化完毕,没有NoSuchBeanDefinitionException。页面能打开但点任何按钮都 404,说明struts.xml里 action 没映射上,先看命名空间和后缀。
5.3 常见问题排查
现象一:启动报ClassNotFoundException: oracle.jdbc.driver.OracleDriver。原因:驱动 jar 没放进WEB-INF/lib或 Tomcat 的lib下,或者放的是新版本 ojdbc 而连接串用了老驱动类名。解决:确认 jar 在 classpath,老版本用oracle.jdbc.driver.OracleDriver,新版本用oracle.jdbc.OracleDriver,两者别混用。
现象二:页面中文乱码,数据库里也是乱码。原因:JSP 页面的pageEncoding、请求编码过滤器、数据库字符集三者不一致。解决:JSP 顶部统一写<%@ page contentType="text/html;charset=UTF-8" pageEncoding="UTF-8" %>,web.xml加CharacterEncodingFilter强制 UTF-8,数据库字符集用AL32UTF8。三处必须一致,改一处不够。
现象三:保存时抛org.hibernate.StaleStateException: Batch update returned unexpected row count。原因:页面提交的对象主键为空或者与库里不一致,update找不到对应行。解决:确认表单里带上了隐藏域主键,或者在 Action 里先get(id)再merge,不要拿一个残缺对象直接 update。
现象四:生成交接单后,订单状态没变,产生脏数据。原因:交接单插入和订单更新没有包在同一个事务里。解决:在 Spring 配置里加@Transactional或 XML 方式给 Service 方法配事务通知,把整个createTransfer方法包进去。这一步不做,测试阶段数据越测越乱。
现象五:Tomcat 启动时java.lang.OutOfMemoryError: PermGen space。原因:JDK 8 之前元空间给太小,老项目加载的类多。解决:启动参数加-XX:MaxPermSize=256m(JDK 7 及以下)或-XX:MaxMetaspaceSize=256m(JDK 8),同时确认没在循环里反复openSession不关闭。
6. 二次开发改造:把毕设骨架改成能演示的原型
这套代码拿到手,最省事的改法不是推倒重来,而是挑一个模块做纵深。我的做法是先把车辆管理这条链路跑通——它就是整个系统的模板,改明白它,其他模块照葫芦画瓢。
具体改造顺序建议这样排:先把数据库从 Oracle 换到 MySQL 或 H2,省得答辩现场装数据库。DDL 要动的地方主要是VARCHAR2改VARCHAR、NUMBER改DECIMAL、SYSDATE改NOW(),驱动类和连接串同步换掉,Hibernate 方言改MySQL5Dialect。改完先用schema.sql自动建表跑一遍,表建不起来就别往下走了。
然后是补事务和统一异常处理。原项目在 Service 里直接throw new RuntimeException,用户看到的是 Tomcat 500 页面。加一层 Struts2 的全局异常映射,把业务异常转成一个友好提示页;再把 Service 方法加上事务注解,交接单那种多表写入才不容易留脏数据。
真正的业务增量我建议加两个东西:一是订单状态的时间轴,在订单表加UPDATE_TIME字段,订单状态每变一次记一条流水,答辩演示时点开一张订单能讲清它从录入到发车经历了什么;二是配送范围的可视化约束,把配送范围做成一棵简单的省市区树,录入订单时自动匹配所属站点,比手工选站点更有说服力。
验证方式不用复杂,造三条测试数据就够:一个待配送订单、一辆空闲车辆、一个本配送点员工,从订单录入走到交接单生成再到发车,全程截图留档。跑通这条线,其余模块基本不会出结构性问题。
再补一个我踩过的坑:Hibernate 的openSession别在循环里开,交接单生成那种批量操作一定要在同一个 session 里做,否则连接池用满之后整个应用卡死,现场演示直接翻车。从那以后我每次改批量逻辑,都会在本地把连接池上限压到 2 跑一遍,能跑通才算数。希望帮到你。
本文还有配套的精品资源,点击获取