资讯动态

基于SSH框架与SQL Server的供应链管理系统设计与实现

发布时间:2026/9/18 18:48:14 来源:尧图企业网站定制
简介面向计算机专业学生及需要完成毕业设计、课程设计的开发者这份基于Java的供应链管理信息系统设计与实现文档提供了从课题背景到系统测试的完整内容。系统采用JSP页面技术、SSH框架与SQL Server数据库细分出系统用户管理、供应商信息、制造商信息、分销商信息、商品信息、登录与退出等模块各模块相互独立又彼此协作并针对旧系统数据处理弱、扩展性差、操作复杂等问题提出了明确改进方案。文档共1个文件为docx格式压缩包大小约730KB包含中英文摘要、目录、绪论、开发背景、技术介绍与测试结论结构完整。已有54人浏览学习。读者可从中获得完整的设计思路、模块划分和实现细节便于独立复现或二次开发也能深入理解SSH框架与SQL Server在供应链管理中的应用。1. 为什么还在用 SSH 搭供应链系统我在一家做制造业软件的公司接维护过一套供应链系统业务方最头疼的不是算法而是供应商、制造商、分销商的数据散落在 Excel 和纸质单据里对账要翻半天库存和发货记录经常对不上。这套基于 Java 的供应链管理信息系统用 JSP 做页面、SSHSpringSpringMVCHibernate做服务端、SQL Server 做存储目标就是把这些环节统一收口让管理员能维护基础资料让商品和发货记录可查询、可追溯。它适合两类人一是需要快速交付的团队技术栈常见、招人容易二是想理解 SSH 整合原理的学生或初级工程师。下面从数据库设计、登录权限到核心模块实现完整拆一遍。2. 供应链系统的模块边界与数据库表设计2.1 先理清业务模块的边界供应链信息管理系统不是一个单一大对象而是多个角色围绕“商品流转”协作。原系统的目录里列了系统用户管理、供应商信息、制造商信息、分销商信息、商品信息、登录和退出模块。实际拆解时我会把它们归成几类认证与权限用户、登录、退出、基础资料供应商、制造商、分销商、核心业务商品信息、供应发货、产品销售、系统维护数据备份、个人资料。每个模块在代码里是独立的包在数据库里则是独立的表模块之间只能通过主外键关联不允许直接跨表访问业务字段。这样设计的好处是后期加一个“物流商”模块时不需要改动已有的供应商、商品代码只要新增表和对应的 Service、Controller 即可。原系统里提到的“模块相互独立又彼此协作”本质就是数据库层面通过外键解耦应用层面通过 Service 互相调用。2.2 SQL Server 下的核心表结构这个系统的后端存储是 SQL Server 2008虽然现在很多人用 MySQL但 SQL Server 在 Windows 生态里的管理工具、身份验证、镜像和备份机制都很成熟对中小型管理系统来说完全够用。表结构设计上我建议每张业务表都要有自增主键、创建时间、更新时间这三个基础字段后续排查数据问题会省很多事。下面给出核心表的字段设计。表名字段类型说明sys_useridint identity(1,1)用户ID主键sys_userusernamenvarchar(50)登录名唯一sys_userpasswordnvarchar(64)MD5 哈希后的密码sys_userrolenvarchar(20)角色admin / usersupplieridint identity(1,1)供应商IDsuppliernamenvarchar(100)供应商名称suppliercontactnvarchar(50)联系人supplierphonenvarchar(20)联系电话suppliercategorynvarchar(50)产品类别productidint identity(1,1)商品IDproductcodenvarchar(50)商品编码应唯一productnamenvarchar(100)商品名称productcategorynvarchar(50)分类productpricedecimal(18,2)销售单价productstockint当前库存supply_shipmentidint identity(1,1)发货单IDsupply_shipmentsupplier_idint外键对应 supplier.idsupply_shipmentproduct_idint外键对应 product.idsupply_shipmentquantityint发货数量supply_shipmentship_datedatetime发货日期supply_shipmentstatusnvarchar(20)状态在途 / 已收货这里要注意product 表里的 stock 是冗余字段每次发货或销售都要同步更新它。常见做法是在供应发货和产品销售的服务方法里用事务一起更新商品表避免单独改库存导致的数据不一致。外键约束一定要建否则应用层删除供应商时可能留下悬空的发货记录。制造商和分销商的表结构与供应商基本一致只是业务含义不同这里不再重复列出。2.3 SSH 整合的配置要点SSH 整合的关键在数据源和 SessionFactorySpring 统一管理 Hibernate 的会话与事务。下面是我惯用的 Hibernate 配置文件数据库方言和连接参数是核心。hibernate-configuration session-factory property namehibernate.connection.driver_classcom.microsoft.sqlserver.jdbc.SQLServerDriver/property property namehibernate.connection.urljdbc:sqlserver://localhost:1433;DatabaseNamescm_db/property property namehibernate.connection.usernamesa/property property namehibernate.connection.passwordyour_password/property property namehibernate.dialectorg.hibernate.dialect.SQLServer2008Dialect/property property namehibernate.show_sqltrue/property property namehibernate.hbm2ddl.autoupdate/property /session-factory /hibernate-configuration注意url 里的DatabaseName是 SQL Server 独有的写法写错会报“无法打开登录所请求的数据库”。hibernate.dialect指定为 SQLServer2008Dialect分页 SQL 才能正确生成。hbm2ddl.auto在开发时用 update生产环境建议改成 validate 或 none避免 Hibernate 自动改表结构。2.4 为什么在众多 ORM 里仍然选 Hibernate有人会问现在 MyBatis 这么流行为什么这套系统还用 Hibernate。我的看法是供应链管理后台的绝大多数操作是单表 CRUD 和简单的关联查询Hibernate 的对象关系映射可以让我直接操作Product对象而不用为每个字段写 ResultMap。Hibernate 的自动建表和缓存机制对快速开发很友好。但它也有短板多表聚合统计、复杂动态 SQL 写起来不如 MyBatis 直观。所以如果业务里大量是报表我才会考虑换成 MyBatis 或者用 Hibernate 的 Native SQL 查询。选型不是越新越好而是匹配团队和业务场景。3. 登录与权限从 JSP 页面到 SpringMVC 控制器3.1 登录模块的完整流程登录是整个系统的入口也是最容易被忽略安全细节的地方。原系统需求里明确写了游客可以注册登录时校验验证码密码用 MD5 加密存储超级管理员和普通用户登录后看到不同的界面。实际实现时流程可以拆成五步用户提交用户名、密码、验证码前端先做非空和格式校验后端先核对验证码再根据用户名查库把提交的密码 MD5 之后与库中值比对比对成功后将用户对象写入 Session并根据角色跳转到不同主页最后用拦截器拦截受保护的 URL未登录或角色不匹配直接跳回登录页。3.2 表单与控制器代码用户登录页面是 JSP核心表单如下form action${pageContext.request.contextPath}/login methodpost input typetext nameusername placeholder用户名 required / input typepassword namepassword placeholder密码 required / input typetext namecaptcha placeholder验证码 required / img src${pageContext.request.contextPath}/captcha onclickthis.srcthis.src?tMath.random() / button typesubmit登录/button /form表单里的action指向/login由 SpringMVC 的DispatcherServlet接管。captcha图片每次点击都会加随机参数刷新这是防止浏览器缓存验证码的常用手段。用户名和密码的required只是前端最基本的拦截真正判断必须在后端重新做因为任何人可以绕过页面直接构造 HTTP 请求。对应的控制器代码Controller public class LoginController { Autowired private UserService userService; Autowired private CaptchaService captchaService; RequestMapping(value /login, method RequestMethod.POST) public String login(String username, String password, String captcha, HttpSession session, Model model) { // 1. 校验验证码忽略大小写 String sessionCaptcha (String) session.getAttribute(captcha); if (sessionCaptcha null || !sessionCaptcha.equalsIgnoreCase(captcha)) { model.addAttribute(error, 验证码错误); return login; } // 2. 调用Service验证用户名密码 User user userService.login(username, password); if (user null) { model.addAttribute(error, 用户名或密码错误); return login; } // 3. 登录成功写入Session角色区分 session.setAttribute(loginUser, user); if (admin.equals(user.getRole())) { return redirect:/admin/home; } return redirect:/user/home; } }这里login方法的参数由 SpringMVC 绑定请求中的username、password、captcha会自动映射到同名参数。Model用于携带错误信息回显到页面。注意验证码判断必须在用户名密码之前因为验证码错了就没必要再去查数据库能减少无效的数据库压力。角色判断放在 Session 之后后续拦截器才能正确读取。3.3 Service 层的验证与 MD5 处理密码不能明文存储原系统里提到了 MD5。MD5 虽然不算强哈希但对于这类内部系统足够。实现时我会在 Service 里把输入的密码转成 MD5再与数据库比较。代码片段Service public class UserServiceImpl implements UserService { Autowired private UserDao userDao; Override public User login(String username, String password) { User user userDao.findByUsername(username); if (user null) { return null; } String md5Password MD5Util.md5(password); if (user.getPassword().equals(md5Password)) { return user; } return null; } }MD5Util.md5内部使用MessageDigest完成加密这一步放在 Service 而不是 Controller是为了保持控制器的纯净也方便单元测试。数据库里存储的是定长 32 位十六进制字符串所以sys_user.password字段类型设计为 nvarchar(64) 是够用的。这里有个坑如果数据库字段长度不够MD5 值会被截断登录永远失败排查这类问题时要先检查字段长度。3.4 基于拦截器的权限控制区分超级管理员和普通用户除了登录跳转不同更需要一个全局的拦截面。SpringMVC 的HandlerInterceptor正好做这件事。配置如下mvc:interceptors mvc:interceptor mvc:mapping path/admin/**/ bean classcom.scm.interceptor.AuthInterceptor/ /mvc:interceptor /mvc:interceptors拦截器实现public class AuthInterceptor extends HandlerInterceptorAdapter { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } if (!admin.equals(user.getRole())) { response.sendError(HttpServletResponse.SC_FORBIDDEN, 无权访问); return false; } return true; } }mvc:mapping path/admin/**/的意思是只有 URL 以/admin/开头的请求才走这个拦截器。这样普通用户就算猜到管理员路径也会被拦截。这里要特别注意response.sendRedirect后一定要return false否则请求还会继续向下执行。退出模块的实现同样简单从 Session 中移除loginUser属性再重定向到登录页即可。4. 供应商、商品与发货模块的增删改查实现4.1 商品信息管理的列表与条件搜索商品信息管理是所有业务模块的核心供应商发货、产品销售都围绕商品编码展开。原系统需求里要求“商品信息列表将数据库的商品表以列表的形式呈现给管理员”并且支持关键字查询。用 Hibernate 实现时我倾向于用 HQL 或者 Criteria 查询而不是直接拼 SQL。看一个带分页的 DAO 实现Repository Transactional public class ProductDaoImpl implements ProductDao { Autowired private SessionFactory sessionFactory; Override public ListProduct findProducts(String keyword, int pageNo, int pageSize) { Session session sessionFactory.getCurrentSession(); Criteria criteria session.createCriteria(Product.class); if (keyword ! null !keyword.trim().isEmpty()) { criteria.add(Restrictions.or( Restrictions.like(code, keyword.trim(), MatchMode.ANYWHERE), Restrictions.like(name, keyword.trim(), MatchMode.ANYWHERE) )); } criteria.setFirstResult((pageNo - 1) * pageSize); criteria.setMaxResults(pageSize); return criteria.list(); } }createCriteria是 Hibernate 的 QBC 查询Restrictions.like默认区分大小写但 SQL Server 的排序规则一般不分问题不大。setFirstResult和setMaxResults用来分页Hibernate 会根据方言自动生成top或offset语句。这里的关键参数是pageNo从 1 开始如果前端传来的页码是 0第一页就会查空。我会在 Controller 里统一做pageNo Math.max(1, pageNo)处理。4.2 供应商发货管理中的事务与级联操作供应发货涉及供应商、商品、发货单三个表。添加一张发货单时还要同步减少商品库存。这个操作必须放在一个事务里否则库存更新失败时发货单已经落库对账就会出问题。Spring 的声明式事务可以解决Service public class ShipmentServiceImpl implements ShipmentService { Autowired private ShipmentDao shipmentDao; Autowired private ProductDao productDao; Override Transactional(rollbackFor Exception.class) public void addShipment(Shipment shipment) { // 1. 保存发货单 shipmentDao.save(shipment); // 2. 扣减库存 Product product productDao.findById(shipment.getProductId()); product.setStock(product.getStock() - shipment.getQuantity()); productDao.update(product); } }Transactional(rollbackFor Exception.class)表示任何异常都回滚包括运行时异常和受检异常。如果不加rollbackForSpring 默认只对 RuntimeException 回滚受检异常比如业务校验抛出的 Exception会导致发货单保存成功但库存没扣。这是供应链系统里非常经典的事务边界问题。注意productDao.update不一定需要手动调用因为 Hibernate 的一级缓存会检测到持久化对象状态改变在事务提交时自动 flush。但为了代码可读性我还是保留显式 update。4.3 参数绑定的防注入写法在实现商品搜索时有人图省事会直接把关键字拼接进 HQLString hql from Product where name like % keyword %; Query query session.createQuery(hql);这样写非常危险关键字里如果包含单引号或 HQL 关键字轻则查询报错重则被构造恶性查询。正确做法是用参数绑定String hql from Product where code like :kw or name like :kw; Query query session.createQuery(hql); query.setParameter(kw, % keyword.trim() %);参数名:kw可以复用setParameter会自动处理特殊字符。HQL 层面的参数绑定不仅能防注入还能让数据库复用执行计划。对于登录查询、商品搜索这类高频操作性能差别不大但安全性差别巨大。原系统需求里提到的“通过关键字查询”都必须用这种写法。4.4 懒加载与 N1 查询的坑Hibernate 的关联映射默认是懒加载比如Shipment里关联Product当你遍历发货单列表并访问shipment.getProduct().getName()时Hibernate 会逐条发起 SQL100 条发货单就会产生 101 条 SQL这就是 N1 问题。解决办法是在 DAO 层用join fetch一次把关联对象查出来String hql from Shipment s join fetch s.product join fetch s.supplier;join fetch会把 Product 和 Supplier 通过一条 SQL 用左连接查出来放进同一级缓存。代价是查询结果集变宽但对发货单这种列表页完全值得。另外注意懒加载在事务外访问会抛出LazyInitializationException我一般会把 Service 方法直接返回已经初始化好的 DTO 或 VO避免在 JSP 里直接访问懒加载代理对象。这个习惯能省下大量调错时间。5. 系统测试与几个容易忽略的坑5.1 登录和添加操作的单元测试原系统测试章节写了单元测试和集成测试但很多开发者在真实项目里只测接口好不好用忽视了 Service 层的回归测试。我用 Spring Test 和 JUnit 写登录用例RunWith(SpringJUnit4ClassRunner.class) ContextConfiguration(classpath:applicationContext.xml) public class LoginServiceTest { Autowired private UserService userService; Test public void testLoginSuccess() { User user userService.login(admin, 123456); Assert.assertNotNull(user); Assert.assertEquals(admin, user.getRole()); } Test(expected Exception.class) public void testLoginWithWrongPassword() { userService.login(admin, wrong); // 期望Service抛异常或返回null这里根据实现约定 Assert.fail(密码错误时应提示异常); } }注意ContextConfiguration里的配置路径必须包含 Spring 核心配置尤其是sessionFactory和数据源。测试数据库最好单独建一个不要动生产库。测试方法里的Assert.assertNotNull是最基本的校验更完整的做法是构造多个用户和商品跑一遍“添加供应商→生成发货单→扣库存”的流程断言库存值变化是否符合预期。这类集成测试能暴露事务边界和级联问题。5.2 SQL Server 连接与乱码排查SSH 项目连接 SQL Server 最常见的两个报错一是“通过端口 1433 连接失败”二是 JSP 页面中文乱码。前者先检查 SQL Server 是否启用了 TCP/IP 协议以及 Windows 防火墙是否放行 1433 端口。连接字符串里最好加上sendStringParametersAsUnicodefalse对 varchar 类型能减少不必要的 Unicode 转换但这只是性能优化不是乱码的根因。乱码问题九成出在请求和页面编码不一致。JSP 页面头部要统一% page languagejava contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%同时 Spring 的 CharacterEncodingFilter 要配置在 web.xml 最前面filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping没有这个过滤器POST 提交的中文在 Controller 里就会变成问号。排查时先看浏览器 Network 面板的请求头是否带了charsetUTF-8再确认数据库表的字段类型是否nvarchar。如果字段是varchar且数据库默认排序规则是中文 GBK也会出现存储乱码。5.3 一个技巧用声明式事务的传播属性避免数据覆盖最后一个值得说的技巧是事务的传播属性。供应链系统里经常出现“保存发货单的同时调用其他模块更新数据”的嵌套场景。比如添加发货单时系统要同时更新商品库存、生成一条产品销售记录如果这两个方法各自带有Transactional默认传播属性是REQUIRED会共用外层事务任何一个失败都会让整个发货单回滚。这符合业务预期。但如果你在某个方法上用了REQUIRES_NEW内层事务会单独提交外层后来失败时内层已经落库数据就“半成功”了。所以除非有明确的审计需求不要在供应链主链路事务里使用 REQUIRES_NEW。我在代码里会专门加一行注释提醒后续维护的人// 注意保持REQUIRED不允许单独提交库存扣减。这种细节只有被线上对账坑过的人才写出来。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价