简介:基于Java和SQLServer2008实现的Web网上书店管理系统,是面向高校计算机专业课程设计或Java Web初学者的完整项目。系统针对在线图书销售和书店信息管理需求,设计了游客浏览检索注册、会员登录维护购物车下单评论、管理员图书分类会员订单新闻评论等前后台功能模块,覆盖电商网站核心业务环节。压缩包共460个文件,大小9.89MB,主要文件为JSP动态页面、Java源码及编译后的class文件、SQL数据库脚本,辅以GIF/JPG图片资源和配置文件。这些文件共同构成了系统的页面展示、业务逻辑与数据存储三层结构,便于直接部署和二次开发,内容预览中的若干Java类对应订单、用户、图书等核心对象,可帮助快速定位关键代码。已有376人学习下载,适合作为课程设计答辩项目或Java Web实战练手案例,尤其适用于学习和验证基于JSP+Servlet+SQLServer的经典开发模式。
1. 网上书店管理系统:基于 Java+SQLServer2008 的 Web 课程设计到底考你什么
网上书店管理系统是我见过最典型的 Java+SQLServer2008 Web 课程设计题,前后台分离、三种角色、从注册登录一路做到订单发货,几乎把 JSP+Servlet+JDBC 该考的点全考了一遍。这份资源里没有完整源码,而是编译好的 class 文件和 JSP 页面,第一次打开的时候像个黑匣子。但把 UserLoginBean、BookManage、GouWu 这些类名一拆,整个项目结构就清晰了:前台是游客逛书、会员买书,后台是管理员管书、管单、管新闻。适合两类人:一类是拿到题没思路、想看完整业务链路怎么搭的学生;另一类是快答辩了手头项目跑不起来、需要对照 class 部署排查的。本篇会从角色权限与数据库、注册登录、购物车订单流转、后台管理到部署排错把项目讲透。
2. 角色权限与数据表设计:三种身份的边界与八张表的结构
2.1 三张脸,三个权限边界
这个系统的需求拆开后其实就三句话:游客能看不能买,会员能看能买还能评,管理员能管一切。权限边界如果不清晰,就会出现会员删书、游客下单这种逻辑漏洞,答辩时也最容易被追问。
游客的权限被限定在「浏览」这一层:打开首页看到图书列表,按分类翻书,用关键字搜书名,最后能做的动作只有注册。这里的设计意图很明确——把游客的入口收窄到只读,所有写操作都必须在登录之后才能触发。会员在游客基础上多了五件事:维护资料、加购物车、提交订单付款、查订单状态、发表评论。管理员则完全在另一个后台界面操作:管图书分类、管会员账号、审核订单发货、发新闻、删评论。
从 Web 项目开发的角度看,这不只是功能清单,更是面向对象编程里角色建模的基本功。通常的做法是三种身份各建一个域对象——游客没有对应实体,会员对应 member 表,管理员对应 admin 表,两者字段完全不同,没必要硬塞进一张表。登录时的会话里分别存不同的身份标记,页面再根据标记决定显示哪套导航和操作按钮。
2.2 数据表设计:八张表把业务拆干净
SQLServer2008 是这套系统最合理的数据库选型,因为课程设计的环境大多用 Windows + SQLServer 组合,而且 SQLServer2008 的图形化管理工具对建库建表非常友好。网上书店的业务数据按功能域拆,通常需要下面这些表支撑。
| 表名 | 用途 | 关键字段 |
|---|---|---|
| member | 会员信息 | id, username, password, realname, sex, email, tel, regdate |
| admin | 管理员账号 | id, username, password, addtime |
| book | 图书信息 | id, bookname, category, author, publisher, price, cover, intro, addtime |
| category | 图书分类 | id, categoryname |
| orders | 订单主表 | id, orderno, memberid, totalprice, paytype, status, createtime, sendtime |
| orderitem | 订单明细表 | id, orderid, bookid, bookname, price, quantity |
| comment | 图书评论 | id, bookid, memberid, content, createtime |
| news | 新闻公告 | id, title, content, createtime |
member 表里 password 字段在课程设计级别一般直接存明文,因为登录逻辑简单,答辩也不会深究加密。book 表和 category 表通过 category 字段关联而不是外键,这是学生项目里常见的取舍,好处是改分类名时不用动图书,坏处是分类删除时得先检查有没有书挂在下面。orders 和 orderitem 是一对多关系,一个订单对应多条明细,这个拆分是必须的,因为同一订单里每本书的数量和单价都要独立记录。
订单状态字段 status 是整张表里最核心的字段,这套系统用的是一组整数状态值:0 代表未支付,1 代表已支付未发货,2 代表已发货。会员删除订单的权限就依赖这个状态——只有 status 小于 2 时允许删;管理员删除订单则只看 status 是否为 0。状态值写死在代码里而不是用枚举,课程设计可以接受,但最好是写个常量类集中管理,免得 JSP 页面上到处写魔法数字。
2.3 购物车为什么放 Session 而不是数据库
这个设计点是指导老师最爱问的问题。购物车如果建一张 cart 表,每个会员的每次加购操作都要读写数据库,购物车本身是临时数据,放数据库反而得不偿失。常见做法是把购物车对象存进 Session,用 HashMap 或者一个 List 结构保存,key 是图书编号,value 是购买数量。
// 购物车在 Session 中的存取结构 Map<String, Integer> cart = (Map<String, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<String, Integer>(); }这里用泛型 Map 存的是图书 ID 到数量的映射,Session 在用户浏览器关闭前一直有效,所以整个购书过程中不需要反复查库。等到提交订单时,一次性把购物车遍历出来写入 orders 和 orderitem 两张表,然后清空 Session 里的车。这个方案的问题也明显:用户关浏览器车就没了,但课程设计场景下这是完全可以接受的行为,老师问起来你就答「购物车是会话级临时数据,落库是下单时的事」。
3. 注册登录链路:从 RegUtil 正则校验到 UserLoginBean 会话管理
3.1 RegUtil:一行正则挡住无效注册信息
注册页提交的信息包括用户名、密码、真实姓名、性别、邮箱、电话,这些字段如果不做校验,垃圾数据会直接灌进 member 表。RegUtil 这个工具类承担的就是前端校验之外的后端兜底校验,常见实现是封装一组静态正则方法。
import java.util.regex.Pattern; public class RegUtil { // 用户名:字母开头,4-16 位字母、数字、下划线 public static boolean checkUsername(String username) { String regex = "^[a-zA-Z]\\w{3,15}$"; return Pattern.matches(regex, username); } // 密码:6-16 位字母、数字、下划线 public static boolean checkPassword(String password) { String regex = "^\\w{6,16}$"; return Pattern.matches(regex, password); } // 邮箱:标准邮箱格式 public static boolean checkEmail(String email) { String regex = "^\\w+@\\w+(\\.\\w+)+$"; return Pattern.matches(regex, email); } // 手机号:1 开头,11 位数字 public static boolean checkTel(String tel) { String regex = "^1\\d{10}$"; return Pattern.matches(regex, tel); } }这套正则的核心逻辑是「前端提示一次,后端再拦一次」。前端 JS 校验只能挡住手滑的用户,懂技术的人抓包直接绕过前端也能提交数据,所以 RegUtil 里的 checkUsername 这类方法必须在注册处理的 Servlet 或 Bean 里再调一遍。参数设计上故意把规则放宽到 \w,避免中文用户名被误杀,因为课程设计一般不要求处理国际化或特殊字符。
调用方式很直接:注册信息接收后,用 if (!RegUtil.checkUsername(username)) 这样的判断逐字段检查,任何一个失败就返回注册页并把错误信息塞进 request,JSP 页面上用 EL 表达式回显。注意返回时不要清空用户已填的字段,否则用户得重填一遍,这是体验细节也是动手实现时容易漏的地方。
3.2 UserLoginBean 与登录状态保持
UserLoginBean 从命名就能看出职责:接收 JSP 页面提交的账号密码,查 member 表做验证,验证通过后把用户信息写进 Session。登录逻辑用 PreparedStatement 做参数化查询,这既是防 SQL 注入的基本功,也是答辩时能讲的代码亮点。
public boolean login(String username, String password) { String sql = "SELECT * FROM member WHERE username=? AND password=?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, username); pstmt.setString(2, password); ResultSet rs = pstmt.executeQuery(); if (rs.next()) { this.id = rs.getInt("id"); this.username = rs.getString("username"); this.realname = rs.getString("realname"); return true; } } catch (SQLException e) { e.printStackTrace(); } return false; }这段代码的逻辑核心是「查得到就是登录成功,查不到就是用户名密码错误」。参数绑定用的是 setString,比起字符串拼接的好处是,即使有人在输入框填了' OR '1'='1这类内容,驱动也会把它当成纯字符串处理,不会改变 SQL 语义。审核订单、删会员这些管理员操作也走同样的 PreparedStatement 思路。
登录成功后通常有两个动作:一是把当前用户 ID 存进 Session,key 叫 userid 或者 memberid,后续购物车下单、发表评论都要用它定位用户;二是把 UserLoginBean 实例本身也放进 Session,JSP 页面要显示「欢迎 xxx」或者回显个人资料时直接取。退出登录更简单,Session 调 invalidate() 把所有会话数据清掉,然后重定向回首页。
3.3 会员资料维护:改完密码别忘了同步 Session
会员修改资料这块,常见实现是一个 update 方法,前端表单展示当前值,用户改了之后点提交,后端执行 update 语句。密码、真实姓名、性别、邮箱、电话这五个字段里,密码要单独处理,因为用户不一定要改密码。
public boolean updateMember(int id, String password, String realname, String sex, String email, String tel) { String sql = "UPDATE member SET password=?, realname=?, sex=?, email=?, tel=? WHERE id=?"; // 参数绑定:password 若为空串则保持原密码,否则用新值覆盖 }这里要处理的坑是密码字段留空。很多用户只是想改个邮箱,密码框空着提交,后端如果把空字符串写进数据库,用户下次登录就会失败。常规做法是提交前判断:密码框非空才更新密码,为空就沿用 session 里的老值。资料更新成功后,必须把新的 realname、email 等字段重新写回 Session 里的 UserLoginBean 对象,否则页面右上角显示的仍是旧资料,看起来很别扭,得有这个意识。
4. 图书检索、购物车与订单流转:从关键字查询到状态机
4.1 BookManage:图书浏览与关键字查询
BookManage 这个类承担了图书的前台展示和后台管理双重职责。前台浏览图书是典型的无条件查询,而关键字搜索就是按书名做模糊匹配。SQLServer2008 里的模糊查询用 LIKE,但要注意参数里能不能直接拼%关键字%。
public List<BookBean> searchBooks(String keyword) { String sql = "SELECT * FROM book WHERE bookname LIKE ? ORDER BY addtime DESC"; List<BookBean> list = new ArrayList<BookBean>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement pstmt = conn.prepareStatement(sql)) { pstmt.setString(1, "%" + keyword + "%"); ResultSet rs = pstmt.executeQuery(); while (rs.next()) { BookBean bean = new BookBean(); bean.setId(rs.getInt("id")); bean.setBookname(rs.getString("bookname")); bean.setPrice(rs.getDouble("price")); bean.setCover(rs.getString("cover")); list.add(bean); } } catch (SQLException e) { e.printStackTrace(); } return list; }这里的逻辑拆成两步:先把关键字包装成带百分号的模糊条件,再放进 PreparedStatement 参数里绑定。百分号放在 setString 里面而不是拼进 SQL 串,是为了保持参数化查询的完整性。分类浏览是同样的套路,把 WHERE 条件换成category=?或者category IN (SELECT id FROM category WHERE categoryname=?),前者性能更好,后者更灵活。
首页图书列表的分页在课程设计里经常被省略,整表查出全部图书也不是不能用,但图书一多页面就拖沓。加分做法是给查询加ORDER BY addtime DESC并做简单分页,SQLServer2008 里用ROW_NUMBER() OVER (ORDER BY addtime DESC)取第 N 到 M 条,比一次性查全部数据要稳妥。
4.2 购物车:Session 里的加仓、改数与清空
GouWu 这个类从拼音就能看出来是购物车业务。购物车的操作主要有四个:加入、查看、修改数量、清空。加入操作的实现核心就是操作 Session 里的 Map。
// 加入购物车 @SuppressWarnings("unchecked") public void addToCart(HttpSession session, String bookId, int quantity) { Map<String, Integer> cart = (Map<String, Integer>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<String, Integer>(); } Integer oldCount = cart.get(bookId); if (oldCount == null) { cart.put(bookId, quantity); } else { cart.put(bookId, oldCount + quantity); } session.setAttribute("cart", cart); }参数 bookId 是图书主键,quantity 是本次加入的数量。关键分支在「这本书是不是已经在车里」:没加过就直接放进去,加过了就把新旧数量累加。这个逻辑如果不做累加而是直接覆盖,用户点了两次「加入购物车」结果数量还是 1,就是明显的业务缺陷。修改购物车操作也在这上面做文章,用户把某个数量从 2 改成 5,就执行 cart.put(bookId, newCount),清空则直接 session.removeAttribute("cart")。
购物车页面展示时,需要拿 Map 里所有图书 ID 去 book 表批量查书名和单价,再在 JSP 页面上算出合计金额。这里可以暴露一个拿到商品详情的接口,循环查库没问题,数据量不大,但别在循环里执行太多次 SQL,课程设计阶段可以接受 N 次查询,提前有「连接要复用」的意识就行。
4.3 订单生成与状态流转:一次事务写入两张表
订单生成是整个系统里最需要严谨处理的环节。会员从购物车点提交订单,系统要做的事是把订单主表 orders 和订单明细表 orderitem 一起写入,而且必须保证两张表同时成功或者同时失败,这就是事务控制要解决的场景。
public int createOrder(int memberId, String payType, Map<String, Integer> cart, List<BookBean> books) { String orderNo = new SimpleDateFormat("yyyyMMddHHmmssSSS").format(new Date()); String insertOrder = "INSERT INTO orders(orderno, memberid, totalprice, paytype, status, createtime) " + "VALUES(?, ?, ?, ?, 0, GETDATE())"; String insertItem = "INSERT INTO orderitem(orderid, bookid, bookname, price, quantity) " + "VALUES(?, ?, ?, ?, ?)"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); PreparedStatement psOrder = conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); psOrder.setString(1, orderNo); psOrder.setInt(2, memberId); psOrder.setDouble(3, totalPrice); psOrder.setString(4, payType); psOrder.executeUpdate(); ResultSet keys = psOrder.getGeneratedKeys(); int orderId = 0; if (keys.next()) { orderId = keys.getInt(1); } for (BookBean book : books) { // 遍历购物车明细,写入 orderitem PreparedStatement psItem = conn.prepareStatement(insertItem); psItem.setInt(1, orderId); psItem.setString(3, book.getBookname()); // ... 绑定剩余字段 psItem.executeUpdate(); } conn.commit(); return orderId; } catch (SQLException e) { e.printStackTrace(); } return 0; }这段代码的要点有三个。第一,setAutoCommit(false) 关闭自动提交,让两条 insert 处于同一事务里,任何一条失败都能回滚。第二,Statement.RETURN_GENERATED_KEYS 配合 getGeneratedKeys 拿新订单的自增主键,不先查一遍数据库就知道刚才插入的订单 ID。第三,总价 totalPrice 要在循环里累加出来,从购物车明细算,不要相信前端传来的隐藏域——用户改一下页面金额就能虚假下单,这也算 web 安全 里最基本的输入校验原则。
订单状态机的完整链路是:会员提交订单后 status=0(未支付),会员在购物车页选择付款方式并确认付款后 status=1(已支付未发货),管理员在后台看到 status=1 的订单确认发货后 status=2(已发货)。会员能删除订单的前提是 status 为 0 或 1,管理员能删除订单的前提是 status=0(未支付),这两条删除规则要在数据访问层写清楚,否则会出现把已发货订单删掉的情况。会员下单后订单详情页展示 BookBean 和 OrderBean 的字段,发货状态由 status 值翻译成文字:未付款 / 已发货 / 已完成。
5. 避坑排查:让 SQLServer2008 老项目跑起来的五个典型问题
5.1 class 文件没放对位置,页面报 ClassNotFoundException
现象是启动 Tomcat 后打开首页,JSP 正常渲染但一提交表单就报 java.lang.ClassNotFoundException,异常堆栈里指向某个自定义类。原因是这些 class 文件必须按包路径放进WEB-INF/classes目录,比如类包名是 com.bookstore.bean,就得放到WEB-INF/classes/com/bookstore/bean/BookBean.class。解决方法是先看 JSP 的 import 或 useBean 标签里的包名,再对照 class 文件目录一层层摆好,WEB-INF/classes 下面就是包路径的根。放好之后记得重启 Tomcat,因为类加载发生在启动阶段。
5.2 SQLServer 连接超时:不是代码问题,是 TCP/IP 被关了
现象是启动不报错,但第一次访问数据库就卡住,等几秒钟后抛 com.microsoft.sqlserver.jdbc.SQLServerException: 与主机 localhost 的 TCP/IP 连接失败。原因是 SQLServer2008 默认可能没开启 TCP/IP 协议,或者 1433 端口没启用。解决方法是打开「SQL Server Configuration Manager」,在 SQL Server 网络配置里找到 SQLEXPRESS 的协议,把 TCP/IP 启用,然后在 IP 地址选项卡里确认 1433 端口已启用,最后重启 SQLServer 服务。连接串里如果写的是jdbc:sqlserver://localhost:1433;DatabaseName=bookstore,本地测试用 localhost 就行,不需要动防火墙。
5.3 中文乱码一条龙:页面、请求、数据库三处编码要统一
现象是首页中文正常,但往数据库插入书名或会员姓名后变成问号,或者从数据库读出来乱码。原因是 JSP 页面编码、请求编码和 SQLServer 排序规则不一致,最常见的是 JSP 页面用了 UTF-8,SQLServer 2008 的默认排序规则是 Chinese_PRC_CI_AS,插入时没有显式转码。解决方法是把 JSP 顶部的pageEncoding="UTF-8"确认好,在注册和登录处理的 Servlet 里加request.setCharacterEncoding("UTF-8"),数据库连接串里加;sendStringParametersAsUnicode=true或者建库时选 UTF-8 排序规则。排查顺序是:先看页面源码中文是否正常,再看请求参数有没有乱,最后看数据库里的已存数据,哪一层坏了修哪一层。
5.4 端口被占用,Tomcat 反复启动失败
现象是启动 Tomcat 提示Port 8080 was already in use或者直接闪退。原因大概率是另一个服务占了 8080,或者上次没正常关闭 Tomcat。解决方法是命令行执行netstat -ano | findstr 8080找到占用进程的 PID,然后taskkill /PID 进程号 /F杀掉,或者直接改 Tomcat 的 server.xml 里 Connector port 改为 8081。改端口的话要同步改项目里所有跳转路径,不然可能出现页面 404。顺手把 JAVA_HOME 环境变量检查一遍,老项目 JDK 版本不要追新,JAVA_HOME 指向 JDK 1.8 和 Tomcat 7/8 是最稳的组合,这套组合跑 SQLServer2008 的老包基本不需要额外配置。
5.5 SmartUpload 上传图书封面,普通表单字段全变 null
现象是用 SmartUpload 处理封面上传后,request.getParameter("bookname")取出来全是 null,书名、价格都写不进数据库。原因是 SmartUpload 拦截了 multipart 表单,数据流被它读走之后,原始 request 里就拿不到普通字段了。解决方法是改用 SmartUpload 封装的请求对象取普通字段,即smart.getRequest().getParameter("bookname");另一个办法是表单里单独把普通字段和文件分开,但 JSP 页面一旦设置了enctype="multipart/form-data",整个表单都会走流式处理,所以最省事的做法就是统一从 smart 对象里取参数。上传文件保存的路径要用真实路径,String path = getServletContext().getRealPath("/upload"),别自己拼相对路径,否则图片可能存到 Tomcat 临时目录里,重启就丢。
6. 验收演示路径:把订单状态机从头走到尾
答辩和验收时,演示顺序比功能完整性更重要。我一般走的路径是:先用游客身份逛首页、按分类翻书、搜索一本书——把只读功能带一遍;然后现场注册一个新会员,用 RegUtil 校验故意填错一次邮箱,让页面提示错误,再填正确信息注册成功——这一步既展示了校验又展示了数据库写入。登录进去之后立刻改一下真实姓名和电话,刷新页面看右上角姓名变了,证明资料维护和 Session 同步都正常。
接下来是重头戏:挑两三本书加入购物车,改一次数量再删掉一本,把购物车的三个操作全演示一遍,然后提交订单选付款方式,此时订单状态是「未付款」。回到「我的订单」页面确认能看到这笔订单,再模拟付款让状态变成「已支付未发货」,接着切到管理员后台,在订单列表里看到这笔单,执行发货,再切回会员视角,订单变成「已发货」。这一步把 0→1→2 完整走了一遍,状态机闭环了。最后在图书详情页提交一条评论,切到后台把评论删掉——前台后台的权限差异也覆盖到了。
演示时最容易被老师问的三个点:购物车为什么放 Session、订单为什么要拆 orders 和 orderitem 两张表、SQL 为什么用 PreparedStatement。这三个问题正好对应第 2 章的表设计和第 4 章的事务代码,回答时把「会话级临时数据」「一对多明细结构」「防 SQL 注入」三个词说清楚就够了。自从带过的项目里出现过好几次「演示到发货一步才发现订单状态逻辑没写完」的情况,从那以后我每次去验收前都强制把这条路径从头到尾走一遍,尤其是删除订单的边界条件:status 不等于 2 才让删、管理员只能删未支付订单,很多程序就是这样翻车的。希望帮到你——拿到资源先别急着改功能,让这条状态机先完整转一圈,整个项目的骨架就立住了。
本文还有配套的精品资源,点击获取