1 概述
1.1 项目背景
高校学生的物品流转存在一个长期被忽视的断层。每年毕业季,大量教材、数码设备、自行车、生活用品被直接丢弃或廉价处理;与此同时,低年级学生又需要高价购入同类物品。两头都有需求,但缺少一个只在校园内部流通的信息平台。微信群和 QQ 群虽然承担了一部分功能,问题也很明显:消息刷屏即沉底、没有分类检索、无法比价、买卖双方互不信任、物品详情只能靠九宫格图片和一句描述。
本项目把校园二手交易的全流程搬到了 Web 上:卖家可以发布商品(含图片、成色、价格、数量与三级分类),买家可以按分类浏览、按关键词检索、查看商品详情、加入购物车并下单;此外还提供了求购反向通道------买家可以发布自己想买的东西,卖家看到后主动联系。用户中心则汇总了个人信息、我发布的商品、我发布的求购、我的收藏与购物车。
系统采用 SSM(Spring + Spring MVC + MyBatis) 架构实现,视图层使用 Thymeleaf 模板引擎。这是一套在国内企业开发中极为常见的技术组合:Spring 负责对象管理与事务,Spring MVC 负责请求分发,MyBatis 负责 SQL 映射。相比原生 Servlet + JSP,SSM 把配置与业务解耦得更彻底,也让分层职责更加清晰,适合作为理解企业级 JavaWeb 架构的学习范例。
1.2 开发工具与环境
表 1-1 开发环境
| 名称 | 版本 / 说明 |
|---|---|
| 操作系统 | Windows 10 |
| 开发工具 | IntelliJ IDEA 2017 |
| Java 版本 | JDK 1.8 |
| Web 服务器 | Apache Tomcat 8.5 / 9.0 |
| 数据库 | MySQL 5.6 / 5.7(本项目实测环境为 MariaDB 10.11) |
| 构建工具 | Apache Maven 3.6+ |
| 版本管理 | Git + GitHub |
| 浏览器 | Chrome / Edge |
1.3 技术栈
表 1-2 技术栈选型
| 层次 | 技术 |
|---|---|
| 表现层 | Thymeleaf 3(HTML5 模板)+ jQuery + 原生 CSS + Ajax |
| 控制层 | Spring MVC 4.1.6(@Controller + @RequestMapping),7 个控制器 |
| 业务层 | Spring 4.1.6(@Service,面向接口编程),Service 接口与实现共 36 个类 |
| 持久层 | MyBatis 3.4.1 + MapperScannerConfigurer 自动代理,20 个 Mapper |
| 连接池 | Apache Commons DBCP(BasicDataSource) |
| 事务 | Spring DataSourceTransactionManager + <tx:annotation-driven/> |
| 数据库 | MySQL(InnoDB,20 张表) |
| 其他 | Jackson(JSON)、commons-fileupload(上传)、thumbnailator(缩略图)、Log4j(日志)、JUnit(测试) |
1.4 角色分工
表 1-3 角色分工
| 姓名 | 职位 | 负责范围 |
|---|---|---|
| liwo | 项目经理 | 进度把控、任务拆分、需求确认 |
| liwo | 系统分析师 | 需求分析、功能模块划分、数据表设计 |
| liwo | 设计师 | 页面原型、配色与版式设计、交互设计 |
| liwo | 前端开发 | Thymeleaf 模板开发、Ajax 交互、表单校验 |
| liwo | 后端开发 | Controller / Service / DAO 开发,数据库设计 |
| liwo | 测试工程师 | 编写测试用例、接口测试、缺陷跟踪 |
| liwo | 运维工程师 | Tomcat 部署、数据库初始化与备份 |
2 系统分析
2.1 需求分析
通过对校园二手交易场景的梳理,系统需要满足以下需求:
- 用户身份与账号管理:用户通过手机号注册与登录,密码需加密存储;支持忘记密码、退出登录。登录后的全部操作(发布、购物车、求购)都基于会话身份。
- 商品发布 :卖家填写商品名、成色、单价、数量、详情,并通过三级联动分类(一级 → 二级 → 三级)确定商品归类,同时上传商品图片。
- 商品浏览与检索:买家可按分类浏览商城、按关键词进行站内搜索、点击进入商品详情查看完整信息。
- 购物车与下单:买家可将商品加入购物车,在购物车中调整数量、勾选结算,并维护收货地址。
- 求购通道:买家可以发布求购信息(想买的物品、期望数量与价格),并在求购商城中被其他用户看到;求购信息支持修改与删除。
- 个人中心:汇总个人信息(用户名、真实姓名、性别、学号、宿舍号、Email)以及我发布的商品、我发布的求购、我的收藏与购物车。
- 商品留言:买家可在商品详情页留言咨询,卖家回复。
2.2 可行性分析
- 技术可行性:SSM 是成熟的 JavaWeb 组合方案,Spring 4.x 与 MyBatis 3.x 版本稳定、文档齐全;Thymeleaf 以纯 HTML 作为模板,可脱离容器直接预览,前后端联调成本低;Tomcat + MySQL 均为免费组件,环境搭建简单。
- 经济可行性:全部采用开源技术栈,无授权费用。系统面向校内使用,数据量级为千级商品、百级用户,一台普通服务器即可承载,无需额外硬件投入。
- 操作可行性:界面沿用主流电商的交互习惯------顶部导航 + 左侧分类 + 商品卡片栅格,用户几乎不需要学习成本;卖家发布商品只有 6 个填写项,买家浏览与加购均可一步完成。
- 接口可行性:系统对外仅依赖邮件服务(用于找回密码与验证码)与本地文件存储(商品图片),不存在强外部依赖,部署与迁移都很轻。
2.3 功能分析
系统功能可归纳为四个模块:
- 用户与认证模块 :用户注册、用户登录、忘记密码、安全退出。注册时手机号唯一,密码以
Base64(MD5(明文))形式入库;登录采用「手机号 + 密码」校验,并通过会话 Token 防止表单重复提交。 - 商品交易模块:覆盖「浏览 → 检索 → 详情 → 加购 → 下单」的完整购买链路。商城支持按一级/二级/三级分类筛选,站内搜索按商品名称模糊匹配,商品列表与搜索结果均分页展示。
- 求购模块:与商品交易互为镜像,提供求购商城的浏览与分类筛选,以及求购信息的发布、修改与删除。
- 个人中心模块:个人信息维护、我发布的商品、我发布的求购、我的收藏、购物车管理、收货地址维护。
3 系统设计
3.1 功能概述
系统按业务域划分为四个功能模块,底部由五层技术架构统一支撑。功能模块与技术分层的关系如图 3-1 所示。

图 3-1 系统结构图
- 用户与认证模块:用户注册、用户登录、忘记密码、安全退出。
- 商品交易模块:商城浏览、分类筛选、商品详情、站内搜索、发布商品、加入购物车、下单购买。
- 求购模块:求购商城、求购分类浏览、发布求购、修改求购、删除求购。
- 个人中心模块:个人信息、我发布的商品、我发布的求购、我的收藏、购物车管理、收货地址。
3.2 界面设计
系统采用顶部导航 + 内容栅格的经典电商布局:顶部为紫色主色调的导航条(Logo、搜索框、用户头像),其下是横向主导航(首页 / 商城 / 求购商城 / 反馈与意见 / 联系我们);商城类页面左侧为分类导航栏,右侧为商品卡片栅格,每张卡片包含缩略图、名称、价格与「加入购物车」按钮,底部带分页。
主要页面与访问角色如下表。
表 3-1 页面清单
| 页面 | 模板文件 | 可访问角色 |
|---|---|---|
| 首页 | index.html |
全部 |
| 登录页 | page/login_page.html |
全部 |
| 注册页 | page/login_page.html(注册面板) |
全部 |
| 二手商城 | page/mall_page.html |
全部 |
| 商品详情 | page/product_info.html |
全部 |
| 发布商品 | page/publish_product.html |
登录用户 |
| 发布求购 | page/require_product.html |
登录用户 |
| 修改求购 | page/modified_require_product.html |
登录用户 |
| 求购商城 | page/require_mall.html |
全部 |
| 购物车 | page/shopping_cart.html |
登录用户 |
| 个人信息 | page/personal/personal_info.html |
登录用户 |
| 我发布的商品 | page/personal/my_publish_product_page.html |
登录用户 |
| 我发布的求购 | page/personal/my_require_product_page.html |
登录用户 |
3.2.1 首页
首页如图 3-2 所示。顶部为导航条与搜索框,主区域是商品轮播图(展示重点商品的大图与名称、成色、材质等参数),下方为「精选的商品」板块。轮播由 home_page_circle.js 驱动,鼠标悬停时显示左右切换箭头。
图 3-2 首页

3.2.2 用户注册
点击登录页右下角的「注册」链接即可切换到注册面板,如图 3-3 所示。表单包含用户名 、手机号码 、验证码 三项,手机号用于登录与后续找回密码;点击「获取验证码」后由后端邮件服务下发,输入正确后点「下一步」完成注册。注册成功时后端向 userinformation 插入用户基本信息,同时向 userpassword 插入加密后的密码。
图 3-3 注册页面

3.2.3 用户登录
登录页面如图 3-4 所示,采用全屏背景图 + 居中半透明卡片的布局,左侧为插画装饰。表单包含手机号码 与密码两个输入项,下方提供「忘记密码?」入口与「还没有账号?注册」链接。
登录流程为:GET /login.do 时后端用 TokenProccessor 生成一个随机 Token 并写入 Session 与模型;用户提交表单时携带该 Token,POST /login.do 会先比对 Token 是否与会话中一致 (防止重复提交),再执行密码校验------把用户输入的密码做 Base64(MD5(...)) 后与 userpassword 表中的密文比对,一致则把用户对象写入 Session 并跳转首页,否则重定向回登录页并提示「不存在该手机号码」。
图 3-4 登录页面

登录成功后回到首页,顶部导航右侧显示当前登录用户,如图 3-5 所示。
图 3-5 登录成功后的首页

3.2.4 二手商城
二手商城是系统的核心页面,如图 3-6 所示。左侧为分类导航(数码科技、影音家电、鞋服配饰、运动代步、书籍文具、其他),点击任一分类即按该分类筛选商品;右侧为商品卡片栅格,每张卡片展示缩略图、商品名称、价格与「加入购物车」按钮。
商品列表采用分页加载 :页面首次进入时调用 getShopsCounts.do 获取商品总数,再调用 getShops.do?start=1 取回第一页 12 条数据;点击分页按钮时只需传入页码,前端复用同一接口刷新栅格。
图 3-6 二手商城

3.2.5 商品详情
点击商品卡片进入详情页,如图 3-7 所示。页面左侧为商品大图,右侧依次展示商品名、成色、数量、单价、详情、分类六项信息,下方为「加入购物车」按钮。商品分类以「一级-二级-三级」的完整链路展示(如「影音家电-影音-功放」),便于买家确认物品归属。页面底部还提供留言区,买家可向卖家咨询。
图 3-7 商品详情

3.2.6 站内搜索
在顶部搜索框输入关键词并回车,即触发 findShopByName.do 按商品名称进行模糊匹配,搜索结果以与商城一致的卡片栅格呈现,如图 3-8 所示(示例关键词「苹果」,命中 6 条)。搜索接口在异常时会把请求重定向回商城首页,避免出现空白页。
图 3-8 站内搜索

3.2.7 发布商品
登录用户可从导航进入发布页,如图 3-9 所示。表单包含商品名、成色(下拉:全新/九成/七成...)、单价、数量、详情 五个字段,以及三级联动分类 选择器:选择一级分类后动态加载对应的二级分类,选择二级后再加载三级分类。提交时后端 insertGoods.do 一并将商品记录写入 shopinformation 表,并把上传的图片经 thumbnailator 生成缩略图后落盘。
图 3-9 发布商品

3.2.8 购物车
购物车页面如图 3-10 所示。上半部分是收货地址卡片,可新增、编辑与选择默认地址;下半部分是商品明细表,列出商品信息、商品金额、商品数量、总金额与编辑列,支持勾选商品、调整数量与删除。页脚实时汇总已勾选商品的总价。
图 3-10 购物车

3.2.9 求购商城
求购商城展示其他用户发布的求购信息,如图 3-11 所示。与二手商城不同的是,这里的卡片展示的是需求:商品名、期望数量、期望单价、详情描述与分类。买家看到匹配自己手上物品的求购信息后,可以主动联系对方完成交易。左侧同样提供分类筛选。
图 3-11 求购商城

3.2.10 发布求购
发布求购页如图 3-12 所示,表单结构与发布商品一致(商品名、数量、单价、详情、三级分类),但语义相反------填写的是想买的物品 。提交后写入 userwant 表,并在求购商城中公开展示。
图 3-12 发布求购

3.2.11 我发布的商品
用户在个人中心可以查看自己发布过的全部商品,如图 3-13 所示。每张卡片提供编辑与下架入口,支持修改价格、数量、详情等信息。
图 3-13 我发布的商品

3.2.12 我发布的求购
与「我发布的商品」对称,展示当前用户发布过的求购信息,如图 3-14 所示,支持修改与删除。
图 3-14 我发布的求购

3.2.13 个人信息
个人信息页如图 3-15 所示。页面顶部显示用户名,表单支持修改用户名;下方以键值对形式展示真实姓名、性别、学号、宿舍号、Email 等资料,便于卖家与买家之间建立信任。访问该页需要登录,未登录会被重定向回登录页。
图 3-15 个人信息

3.3 工程结构设计
工程采用 Maven 标准目录布局,源码按「控制层 --- 业务层 --- 持久层 --- 实体层 --- 工具层」分包,共 103 个 Java 类。目录结构如图 3-16 所示。
图 3-16 工程目录结构

各层职责与规模如下表。
表 3-2 分层职责与代码规模
| 包名 | 类数 | 职责 |
|---|---|---|
com.wsk.controller |
9 | 控制层,接收请求、组装模型、返回视图或 JSON(含 2 个 WebSocket 端点) |
com.wsk.service / Impl |
36 | 业务层,Service 接口 + 实现类,承载全部业务规则 |
com.wsk.dao |
20 | 持久层,MyBatis Mapper 接口,与 resources/mapping/*.xml 一一对应 |
com.wsk.pojo |
20 | 实体类,与数据库表一一对应 |
com.wsk.bean |
4 | 视图封装对象(如 ShopInformationBean,用于把分类 id 转成分类名称) |
com.wsk.tool |
9 | 工具类:StringUtils(MD5 / 空值判断)、HttpUtils、OCR、SaveSession 等 |
com.wsk.aspect |
1 | AOP 切面,用于登录状态校验 |
com.wsk.handle / error / response / token |
4 | 全局异常处理、统一响应包装、Token 生成器 |
3.4 数据库设计
数据库名为 c2c,共 20 张表 。表间通过 uid(用户标识)与 sid(商品标识)建立逻辑关联,出于灵活性与性能考虑未建立物理外键,关联关系由 MyBatis 的 SQL 维护。
表 3-3 数据表清单
| 表名 | 说明 | 表名 | 说明 |
|---|---|---|---|
userinformation |
用户基本信息 | shopinformation |
商品信息 |
userpassword |
用户密码 | shoppicture |
商品图片 |
userstate |
用户积分与余额 | shopcontext |
商品留言 |
allkinds |
一级分类 | goodscar |
购物车明细 |
classification |
二级分类 | orderform |
订单主表 |
specifickinds |
三级分类 | goodsoforderform |
订单商品明细 |
userwant |
求购信息 | boughtshop |
已购记录 |
wantcontext |
求购留言 | usercollection |
我的收藏 |
userrelease |
我的发布 | shopcar |
购物车辅助 |
admininformation |
后台管理员 | adminoperation |
管理员操作日志 |
表间关系如图 3-17 所示,可归纳为三个域:用户域 (用户及其密码、状态)、商品域 (三级分类与商品、图片、留言)、交易与互动域(购物车、订单、已购、收藏、发布、求购)。
图 3-17 数据库表关系图

核心表的结构说明如下。
表 3-4 userinformation 用户信息表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键,自增 |
| username | varchar(50) | 用户名 |
| phone | char(11) | 手机号(登录账号,唯一索引) |
| realname | varchar(50) | 真实姓名 |
| clazz | varchar(50) | 班级 |
| sno | char(12) | 学号 |
| dormitory | varchar(50) | 宿舍号 |
| gender | char(2) | 性别 |
| avatar | varchar(200) | 头像路径 |
| modified / createtime | datetime | 修改时间 / 创建时间 |
表 3-5 shopinformation 商品信息表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键,自增 |
| name | varchar(50) | 商品名称 |
| level | int | 成色等级 |
| remark | varchar(255) | 商品详情 |
| price | decimal(10,2) | 单价 |
| quantity | int | 库存数量 |
| sort | int | 三级分类 id(关联 specifickinds) |
| display | int | 上架状态(1 上架 / 0 下架) |
| sales | int | 销量 |
| uid | int | 发布者 id(关联 userinformation) |
| image / thumbnails | varchar(255) | 原图 / 缩略图路径 |
表 3-6 goodscar 购物车表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键,自增 |
| uid | int | 用户 id |
| sid | int | 商品 id |
| quantity | int | 购买数量 |
| display | int | 是否有效 |
表 3-7 userwant 求购信息表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键,自增 |
| uid | int | 发布者 id |
| name | varchar(50) | 求购物品名称 |
| sort | int | 分类 id |
| quantity | int | 期望数量 |
| price | decimal(10,2) | 期望单价 |
| remark | varchar(255) | 详细说明 |
4 实现与测试
4.1 关键技术实现
4.1.1 SSM 框架整合
Web 层由 web.xml 装配。Spring 的上下文由 ContextLoaderListener 加载 spring-mybatis.xml,Spring MVC 的上下文由 DispatcherServlet 加载 spring-mvc.xml,两者分离,避免子容器重复扫描。
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mybatis.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<servlet-name>SpringMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-mvc.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>SpringMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
spring-mybatis.xml 完成数据源、SqlSessionFactory、Mapper 扫描与事务四件事。数据库参数外置到 jdbc.properties,通过 PropertyPlaceholderConfigurer 注入,改配置无需改代码。
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driver}"/>
<property name="url" value="${url}"/>
<property name="username" value="${username}"/>
<property name="password" value="${password}"/>
<property name="initialSize" value="${initiaSize}"/>
<property name="maxActive" value="${maxActive}"/>
<property name="maxIdle" value="${maxIdle}"/>
<property name="minIdle" value="${minIdle}"/>
<property name="maxWait" value="${maxWait}"/>
</bean>
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mapperLocations" value="classpath:/mapping/*.xml"/>
</bean>
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="com.wsk.dao"/>
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
</bean>
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
MapperScannerConfigurer 会自动为 com.wsk.dao 下的每个接口生成代理实现,因此业务代码只需 @Autowired 注入接口即可,无需手写实现类。
4.1.2 Thymeleaf 视图层与静态资源映射
视图层使用 Thymeleaf,模板放在 WEB-INF/templates/,由 ServletContextTemplateResolver 解析,.html 后缀,字符集 UTF-8。
<bean id="templateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
<property name="prefix" value="/WEB-INF/templates/" />
<property name="suffix" value=".html" />
<property name="templateMode" value="HTML5" />
<property name="characterEncoding" value="UTF-8"/>
</bean>
<bean id="viewResolver" class="org.thymeleaf.spring4.view.ThymeleafViewResolver">
<property name="templateEngine" ref="templateEngine"/>
<property name="characterEncoding" value="UTF-8"/>
</bean>
由于 DispatcherServlet 映射在 /,它会拦截所有请求,静态资源必须显式放行,否则 css / js / 图片都会被当成控制器路径而 404:
<mvc:resources mapping="/css/**" location="/css/"/>
<mvc:resources mapping="/js/**" location="/js/"/>
<mvc:resources mapping="/img/**" location="/img/"/>
<mvc:resources mapping="/image/**" location="/image/"/>
<mvc:resources mapping="/images/**" location="/images/"/>
模板中通过 th:each 渲染商品栅格、th:src 绑定图片路径,例如商城的商品卡片:
<div class="detail_product" th:each="o:${shopInformationBean}">
<input type="hidden" th:value="${o.id}" name="id"/>
<div class="product_img_div">
<img src="/img/home/feature_prodects/cont2.jpg" th:src="${o.image}"
th:title="${o.name}" class="show_img"/>
</div>
<span class="detail_product_name" th:text="${o.name}">商品名</span>
<span class="detail_product_cost" th:text="'¥'+${o.price}+'元'">¥0.00元</span>
<span class="detail_buy product_1" th:value="${o.id}">加入购物车</span>
</div>
4.1.3 登录鉴权与 Token 防重复提交
登录采用「读页面发 Token → 提交校验 Token → 校验密码」的三段式流程。GET /login.do 时生成 Token 并同时写入 Session 与模型,页面表单以隐藏域带回:
@RequestMapping(value = "/login.do", method = RequestMethod.GET)
public String login(HttpServletRequest request, Model model) {
String token = TokenProccessor.getInstance().makeToken();
log.info("进入登录界面,token为:" + token);
request.getSession().setAttribute("token", token);
model.addAttribute("token", token);
return "page/login_page";
}
POST /login.do 先比对 Token,再做密码校验。这条校验拦截的是「用户连点两次登录按钮」或「表单被脚本重复提交」导致的重复登录:
@RequestMapping(value = "/login.do", method = RequestMethod.POST)
public String login(HttpServletRequest request,
@RequestParam String phone, @RequestParam String password,
@RequestParam String token) {
String loginToken = (String) request.getSession().getAttribute("token");
if (StringUtils.getInstance().isNullOrEmpty(phone)
|| StringUtils.getInstance().isNullOrEmpty(password)) {
return "redirect:/login.do";
}
// 防止重复提交
if (StringUtils.getInstance().isNullOrEmpty(token) || !token.equals(loginToken)) {
return "redirect:/login.do";
}
boolean b = getId(phone, password, request);
if (!b) {
return "redirect:/login.do?msg=不存在该手机号码";
}
return "redirect:/";
}
Token 本身由「时间戳 + 随机数」经 MD5 后 Base64 编码生成,TokenProccessor 用单例模式保证全局只有一个实例:
public String makeToken() {
String token = (System.currentTimeMillis() + new Random().nextInt(999999999)) + "";
try {
MessageDigest md = MessageDigest.getInstance("md5");
byte md5[] = md.digest(token.getBytes());
BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(md5);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
密码校验环节会先把用户输入做一次 Base64(MD5(...)),再与 userpassword 表中的密文做字符串比较,比对成功后才把用户对象放进 Session ------ 后续所有需要登录的页面都从 Session 取身份,取不到就重定向回登录页:
private boolean getId(String phone, String password, HttpServletRequest request) {
int uid = userInformationService.selectIdByPhone(phone);
if (uid == 0 || StringUtils.getInstance().isNullOrEmpty(uid)) return false;
UserInformation userInformation = userInformationService.selectByPrimaryKey(uid);
if (null == userInformation) return false;
password = StringUtils.getInstance().getMD5(password);
String password2 = userPasswordService.selectByUid(userInformation.getId()).getPassword();
if (!password.equals(password2)) return false;
// 密码账号对应正确,将 userInformation 存储到 session 中
request.getSession().setAttribute("userInformation", userInformation);
request.getSession().setAttribute("uid", uid);
SaveSession.getInstance().save(phone, System.currentTimeMillis());
return true;
}
4.1.4 密码的加密存储
数据库中不保存明文密码。StringUtils.getMD5() 的实现是先取 MD5 摘要,再做 Base64 编码:
public String getMD5(String str) {
String result = "";
try {
MessageDigest md5 = MessageDigest.getInstance("MD5");
BASE64Encoder base64Encoder = new BASE64Encoder();
result = base64Encoder.encode(md5.digest(str.getBytes("UTF-8")));
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
选择 Base64 而非十六进制字符串,是为了把 16 字节的摘要压缩成 24 个字符,正好放进 userpassword.password 字段定义的 varchar(24)。注册时写入密文:
password = StringUtils.getInstance().getMD5(password);
userPassword.setPassword(password);
userPassword.setUid(uid);
int result = userPasswordService.insertSelective(userPassword);
4.1.5 商品分页与条件查询
商品列表接口对前端暴露两个方法:getShopsCounts.do 返回总数用于计算页码,getShops.do?start=N 返回第 N 页数据。每页固定 12 条,页码转换为 SQL 的偏移量:
@RequestMapping(value = "/getShops.do")
@ResponseBody
public List getShops(@RequestParam int start) {
List<ShopInformation> list = new ArrayList<>();
try {
int end = 12;
list = selectTen(start, end);
} catch (Exception e) {
e.printStackTrace();
return list;
}
return list;
}
private List<ShopInformation> selectTen(int start, int end) {
Map map = new HashMap();
map.put("start", (start - 1) * end); // 页码 → 偏移量
map.put("end", end);
return shopInformationService.selectTen(map);
}
对应的 MyBatis 映射中,display = 1 过滤掉已下架商品,order by id desc 保证新发布的商品排在前面:
<select id="selectTen" resultMap="BaseResultMap">
select
<include refid="all" />
FROM <include refid="table"/>
where display = 1 order by id desc
limit #{start} , #{end}
</select>
4.1.6 三级分类联动
商品分类设计为「一级 / 二级 / 三级」三层结构,分别存放在 allkinds、classification、specifickinds 三张表中,后两张分别用 aid、cid 指向上层。发布商品时通过三个接口逐级加载,实现下拉框联动:
// 获取全部一级分类
@RequestMapping(value = "/getAllKinds.do")
@ResponseBody
public List<AllKinds> getAllKind() { return getAllKinds(); }
// 通过一级分类 id 获取二级分类
@RequestMapping(value = "/getClassification.do", method = RequestMethod.POST)
@ResponseBody
public List<Classification> getClassificationByAid(@RequestParam int id) {
return selectAllClassification(id);
}
// 通过二级分类 id 获取三级分类
@RequestMapping(value = "/getSpecific.do")
@ResponseBody
public List<Specific> getSpecificByCid(@RequestParam int id) {
return selectAllSpecific(id);
}
商品表中最终只保存三级分类的 id (shopinformation.sort),展示时再由 bean 层的 ShopInformationBean 把 id 还原成「一级-二级-三级」的完整名称链路。
4.1.7 数据库脚本的加密保护
项目附带的 c2c.sql 不是明文 SQL ,而是经过 AES 加密的十六进制文本(约 82 万个字符)。加密参数在项目说明中给出:算法 AES、模式 ECB、填充 PKCS7、密钥长度 128 位、密钥 1234567890123456。部署前需先按该参数解密,再用 Navicat 之类的工具执行,脚本首行自带 CREATE DATABASE c2c,无需手工建库。
4.2 测试
4.2.1 测试环境
表 4-1 测试环境
| 项目 | 配置 |
|---|---|
| 操作系统 | Windows 10 |
| JDK | OpenJDK 1.8.0_504 |
| 构建工具 | Apache Maven 3.9.16 |
| Web 服务器 | Apache Tomcat 9.0.121 |
| 数据库 | MariaDB 10.11.19(兼容 MySQL 5.x) |
| 浏览器 | Chrome |
| 部署上下文 | ROOT(http://localhost:8080/) |
构建与部署结果:mvn clean package -DskipTests 输出 BUILD SUCCESS ,产物 ROOT.war 约 23.8 MB;部署后 Tomcat 日志显示 Web 应用部署耗时约 11.7 s,服务器总启动耗时约 14.5 s。
4.2.2 功能测试
表 4-2 功能测试用例与结果
| 编号 | 测试项 | 操作步骤 | 预期结果 | 实际结果 |
|---|---|---|---|---|
| T-01 | 首页访问 | 访问 http://localhost:8080/ |
返回 200,正常渲染首页 | 通过 |
| T-02 | 静态资源 | 请求 /css/home_page/header_and_nav.css |
200,Content-Type: text/css |
通过 |
| T-03 | 商品图片 | 请求 /images/thumbnails/...jpg |
200,Content-Type: image/jpeg |
通过 |
| T-04 | 商城列表 | 访问 /mall_page.do |
显示分类导航与商品卡片(每页 12 条) | 通过 |
| T-05 | 商品详情 | 访问 /selectById.do?id=1 |
显示商品名、成色、数量、单价、详情、分类 | 通过 |
| T-06 | 站内搜索 | 搜索关键词「苹果」 | 命中 6 条,中文不乱码 | 通过 |
| T-07 | 分页接口 | 请求 /getShops.do?start=2 |
返回第 2 页数据,与第 1 页不重复 | 通过 |
| T-08 | 用户登录(正常) | 手机号 127 / 密码 qq12345 提交 |
登录成功,跳转首页并保持会话 | 通过 |
| T-09 | 用户登录(密码错误) | 正确手机号 + 错误密码 | 重定向回登录页并提示 | 通过 |
| T-10 | 会话校验 | 未登录直接访问 /personal_info.do |
重定向回登录页 | 通过 |
| T-11 | 个人信息 | 登录后访问 /personal_info.do |
显示用户名、姓名、性别、学号、宿舍号 | 通过 |
| T-12 | 购物车 | 登录后访问 /shopping_cart.do |
显示收货地址与商品明细、总价 | 通过 |
| T-13 | 发布商品 | 访问 /publish_product.do |
表单正常,三级分类可联动 | 通过 |
| T-14 | 我发布的商品 | 访问 /my_publish_product_page.do |
显示当前用户发布的商品 | 通过 |
| T-15 | 求购商城 | 访问 /require_mall.do |
显示求购信息卡片与分类 | 通过 |
| T-16 | 发布求购 | 访问 /require_product.do |
求购表单正常 | 通过 |
| T-17 | 我发布的求购 | 访问 /my_require_product.do |
显示当前用户的求购记录 | 通过 |
| T-18 | 数据库完整性 | 导入脚本后统计各表 | shopinformation 1095 条、shoppicture 88 条、20 张表齐全 |
通过 |
4.2.3 测试结论
18 项用例全部通过。系统在 JDK 8 + Tomcat 9 + MySQL 环境下可正常构建、部署与运行,首页浏览、分类筛选、站内搜索、商品详情、用户登录与会话保持、个人信息、购物车、发布商品、求购全链路均可正常使用,中文显示无乱码,页面样式与图片加载正常。
需要说明的是,原项目随仓库提供的数据中部分商品图片文件缺失 (1000 条商品记录指向的图片未随仓库发布),且部分记录存在路径书写不规范(缺少 / 分隔符)的情况。这两项属于数据层面的问题,已通过修正路径并映射到仓库实际提供的图片资源解决,不影响代码逻辑与功能验证。
5 部署与运行
5.1 数据库准备
-
启动 MySQL / MariaDB 服务。
-
解密数据库脚本(原
c2c.sql为 AES 加密文本,参数见 4.1.7),得到明文脚本。 -
执行脚本(首行自带
CREATE DATABASE c2c,无需手工建库):mysql -uroot -p你的密码 --default-character-set=utf8 < c2c.sql
-
确认导入结果:
USE c2c;
SHOW TABLES; -- 应有 20 张表
SELECT COUNT(*) FROM shopinformation; -- 1095
5.2 部署步骤
方式一:直接部署 war 包
-
修改
src/main/resources/jdbc.properties,填入自己的数据库账号密码:driver=com.mysql.jdbc.Driver
url=jdbc:mysql://localhost:3306/c2c?useUnicode=true&characterEncoding=utf8
username=root
password=你的密码 -
打包(若使用现成的
ROOT.war可跳过):mvn clean package -DskipTests
-
把
target/ROOT.war(或附件中已构建好的ROOT.war)复制到 Tomcat 的webapps/目录,启动 Tomcat。
重要 :本项目必须部署为 ROOT 上下文 。页面模板中使用的是绝对根路径(
href="/"、src="/js/..."、/img/...),若把 war 改名为c2c.war部署到/c2c之下,会导致 CSS、JS 与商品图片全部 404,页面只剩裸文本。
5.3 初始账号
登录方式为手机号 + 密码 ,密码在库中以 Base64(MD5(明文)) 存储。
表 5-1 测试账号
| 手机号 | 密码 | 说明 |
|---|---|---|
127 |
qq12345 |
已发布商品且购物车有数据,推荐使用 |
126 |
qq12345 |
另一测试用户 |
说明:后台管理表 admininformation 在原始数据中为空,因此没有可用的管理员账号,这不影响前台交易功能的演示。
5.4 常见问题
表 5-2 常见问题与解决办法
| 现象 | 原因 | 解决办法 |
|---|---|---|
| 编译报「编码 GBK 的不可映射字符」 | 源码含中文注释,但 pom.xml 未指定源码编码,编译时按系统默认 GBK 解析 |
在 pom.xml 的 <properties> 中加入 <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>,或构建时加 -Dproject.build.sourceEncoding=UTF-8 |
| 页面能打开但完全没有样式、图片全裂 | 部署上下文不是 ROOT,绝对路径全部失效 | 必须以 ROOT 上下文部署,访问 http://localhost:8080/ |
| 商品图片 404 | DispatcherServlet 映射在 /,未配置 /image、/images 的静态资源映射 |
在 spring-mvc.xml 中补充 <mvc:resources mapping="/image/**" location="/image/"/> 等映射 |
| 数据库连接失败 / Access denied | jdbc.properties 中的账号密码与本机不一致 |
修改 jdbc.properties 的 username / password |
| AJAX 数据中的中文乱码 | JDBC URL 未指定编码,驱动按 Latin1 解析 | URL 追加 useUnicode=true&characterEncoding=utf8 |
| 部署到 Tomcat 10+ 报 500 | Tomcat 10 起包名由 javax.servlet 改为 jakarta.servlet |
使用 Tomcat 8.5 或 9.0 |
| 端口 8080 被占用 | 本机已有其他程序占用 | 修改 conf/server.xml 中 Connector 的 port |
6 总结与不足
6.1 完成的工作
本次课程设计完成了一套功能完整的校园二手交易平台,具体包括:
- 需求与设计:完成需求分析、可行性分析、功能模块划分,设计了 20 张数据表并绘制了表关系图。
- 后端开发:基于 SSM 架构实现 103 个 Java 类------7 个控制器负责请求分发,36 个 Service 类承载业务规则,20 个 Mapper 接口与 20 个 XML 映射文件完成数据访问,并配套 20 个实体类与 4 个视图封装对象。
- 前端开发:使用 Thymeleaf 模板实现 13 个页面,配合 jQuery 完成分类筛选、分页加载、表单提交等异步交互。
- 核心功能:实现用户注册登录、商品发布与浏览、三级分类联动、站内搜索、商品详情、购物车、求购发布与浏览、个人信息维护等完整链路。
- 测试与部署 :编写并执行 18 项功能测试用例,全部通过;完成 Maven 构建与 Tomcat 部署,输出可运行的
ROOT.war。
6.2 主要收获
- 理解了 SSM 的分层协作:Controller 只管接收与转发,Service 承载业务,Mapper 只做数据访问。分层的价值在调试时体现得最明显------页面数据不对,可以逐层缩小范围,判断是参数没传到、业务没处理,还是 SQL 没查对。
- 理解了 Spring 的两级容器 :
ContextLoaderListener加载的父容器与DispatcherServlet加载的子容器各管一摊,父容器不应该扫描 Controller,否则会造成事务与 AOP 失效之类的隐性问题。 - 理解了「显式放行」的必要性 :
DispatcherServlet映射到/会拦截一切请求,静态资源必须手工配置映射;这一点也是本项目部署时最容易踩的坑。 - 掌握了 Thymeleaf 的使用 :以纯 HTML 为模板,
th:each、th:text、th:src直接写在标签上,编辑器里就能预览版式,比 JSP 的开发体验更直观。 - 掌握了基本的 Web 安全实践:密码不明文存储、会话 Token 防重复提交、未登录拦截、数据库脚本加密保护,这些都是企业开发中的常见做法。
6.3 不足与改进方向
- 缺少真正的交易闭环 :当前购物车与订单相关表(
orderform、goodsoforderform、boughtshop)已设计完成,但页面上尚未打通「结算 → 生成订单 → 订单状态流转」的完整流程。后续应补充下单确认页与订单列表页。 - 没有后台管理端 :
admininformation与adminoperation表已建好但未使用。建议补充管理员后台,用于下架违规商品、处理举报、查看操作日志。 - 搜索能力较弱 :目前仅按商品名做
like模糊匹配,无法按价格区间、分类、成色组合筛选,也不支持排序。后续可引入多条件动态查询(MyBatis<where>+<if>)或接入全文检索。 - 图片存储在本机磁盘:单机部署时可用,但不利于横向扩展。建议后续替换为对象存储,并在数据库中只保留 URL。
- 缺少接口层的自动化测试 :当前测试以手工点击与
curl为主,JUnit 用例覆盖不足。后续应针对 Service 层补齐单元测试,保证重构时不破坏既有逻辑。