基于SSM的校园二手交易平台的设计与实现

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 需求分析

通过对校园二手交易场景的梳理,系统需要满足以下需求:

  1. 用户身份与账号管理:用户通过手机号注册与登录,密码需加密存储;支持忘记密码、退出登录。登录后的全部操作(发布、购物车、求购)都基于会话身份。
  2. 商品发布 :卖家填写商品名、成色、单价、数量、详情,并通过三级联动分类(一级 → 二级 → 三级)确定商品归类,同时上传商品图片。
  3. 商品浏览与检索:买家可按分类浏览商城、按关键词进行站内搜索、点击进入商品详情查看完整信息。
  4. 购物车与下单:买家可将商品加入购物车,在购物车中调整数量、勾选结算,并维护收货地址。
  5. 求购通道:买家可以发布求购信息(想买的物品、期望数量与价格),并在求购商城中被其他用户看到;求购信息支持修改与删除。
  6. 个人中心:汇总个人信息(用户名、真实姓名、性别、学号、宿舍号、Email)以及我发布的商品、我发布的求购、我的收藏与购物车。
  7. 商品留言:买家可在商品详情页留言咨询,卖家回复。

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 / 空值判断)、HttpUtilsOCRSaveSession
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 三级分类联动

商品分类设计为「一级 / 二级 / 三级」三层结构,分别存放在 allkindsclassificationspecifickinds 三张表中,后两张分别用 aidcid 指向上层。发布商品时通过三个接口逐级加载,实现下拉框联动:

复制代码
// 获取全部一级分类
@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);
}

商品表中最终只保存三级分类的 idshopinformation.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 数据库准备

  1. 启动 MySQL / MariaDB 服务。

  2. 解密数据库脚本(原 c2c.sql 为 AES 加密文本,参数见 4.1.7),得到明文脚本。

  3. 执行脚本(首行自带 CREATE DATABASE c2c,无需手工建库):

    mysql -uroot -p你的密码 --default-character-set=utf8 < c2c.sql

  4. 确认导入结果:

    USE c2c;
    SHOW TABLES; -- 应有 20 张表
    SELECT COUNT(*) FROM shopinformation; -- 1095

5.2 部署步骤

方式一:直接部署 war 包

  1. 修改 src/main/resources/jdbc.properties,填入自己的数据库账号密码:

    driver=com.mysql.jdbc.Driver
    url=jdbc:mysql://localhost:3306/c2c?useUnicode=true&characterEncoding=utf8
    username=root
    password=你的密码

  2. 打包(若使用现成的 ROOT.war 可跳过):

    mvn clean package -DskipTests

  3. target/ROOT.war(或附件中已构建好的 ROOT.war)复制到 Tomcat 的 webapps/ 目录,启动 Tomcat。

  4. 访问 http://localhost:8080/

重要 :本项目必须部署为 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.propertiesusername / 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.xmlConnectorport

6 总结与不足

6.1 完成的工作

本次课程设计完成了一套功能完整的校园二手交易平台,具体包括:

  1. 需求与设计:完成需求分析、可行性分析、功能模块划分,设计了 20 张数据表并绘制了表关系图。
  2. 后端开发:基于 SSM 架构实现 103 个 Java 类------7 个控制器负责请求分发,36 个 Service 类承载业务规则,20 个 Mapper 接口与 20 个 XML 映射文件完成数据访问,并配套 20 个实体类与 4 个视图封装对象。
  3. 前端开发:使用 Thymeleaf 模板实现 13 个页面,配合 jQuery 完成分类筛选、分页加载、表单提交等异步交互。
  4. 核心功能:实现用户注册登录、商品发布与浏览、三级分类联动、站内搜索、商品详情、购物车、求购发布与浏览、个人信息维护等完整链路。
  5. 测试与部署 :编写并执行 18 项功能测试用例,全部通过;完成 Maven 构建与 Tomcat 部署,输出可运行的 ROOT.war

6.2 主要收获

  • 理解了 SSM 的分层协作:Controller 只管接收与转发,Service 承载业务,Mapper 只做数据访问。分层的价值在调试时体现得最明显------页面数据不对,可以逐层缩小范围,判断是参数没传到、业务没处理,还是 SQL 没查对。
  • 理解了 Spring 的两级容器ContextLoaderListener 加载的父容器与 DispatcherServlet 加载的子容器各管一摊,父容器不应该扫描 Controller,否则会造成事务与 AOP 失效之类的隐性问题。
  • 理解了「显式放行」的必要性DispatcherServlet 映射到 / 会拦截一切请求,静态资源必须手工配置映射;这一点也是本项目部署时最容易踩的坑。
  • 掌握了 Thymeleaf 的使用 :以纯 HTML 为模板,th:eachth:textth:src 直接写在标签上,编辑器里就能预览版式,比 JSP 的开发体验更直观。
  • 掌握了基本的 Web 安全实践:密码不明文存储、会话 Token 防重复提交、未登录拦截、数据库脚本加密保护,这些都是企业开发中的常见做法。

6.3 不足与改进方向

  1. 缺少真正的交易闭环 :当前购物车与订单相关表(orderformgoodsoforderformboughtshop)已设计完成,但页面上尚未打通「结算 → 生成订单 → 订单状态流转」的完整流程。后续应补充下单确认页与订单列表页。
  2. 没有后台管理端admininformationadminoperation 表已建好但未使用。建议补充管理员后台,用于下架违规商品、处理举报、查看操作日志。
  3. 搜索能力较弱 :目前仅按商品名做 like 模糊匹配,无法按价格区间、分类、成色组合筛选,也不支持排序。后续可引入多条件动态查询(MyBatis <where> + <if>)或接入全文检索。
  4. 图片存储在本机磁盘:单机部署时可用,但不利于横向扩展。建议后续替换为对象存储,并在数据库中只保留 URL。
  5. 缺少接口层的自动化测试 :当前测试以手工点击与 curl 为主,JUnit 用例覆盖不足。后续应针对 Service 层补齐单元测试,保证重构时不破坏既有逻辑。

编码不易,有偿分享代码和原文档,有意私聊

相关推荐
许彰午2 小时前
44-useRowSet镜像实现
java·低代码·架构
bamboolm2 小时前
springboot+Ollama整合
java·ollama
我不会起名字3223 小时前
一天一道算法题(34):回溯法的经典例题(子集)
java·数据结构·python·算法·golang·深度优先·力扣
Wang's Blog4 小时前
Java框架快速入门: Spring Security+OAuth2之JWT核心概念与实战
java·spring·log4j
Dreams_l4 小时前
死信队列和延迟队列介绍
java·开发语言
Bs_MoneyMagnet4 小时前
基于springboot+vue的生态果园采摘预约系统的设计与实现 源码+文档
java·vue.js·spring boot·后端·毕业设计·计算机毕业设计
麻瓜code4 小时前
【JUC】AQS enq() 自旋入队
java
Wang's Blog4 小时前
Java框架快速入门: Spring Security+OAuth2之核心角色与授权流程
java·spring·github
hai_android5 小时前
Kotlin Flow combine 源码剖析:一个 Channel 如何优雅合并多条流
android·java·kotlin