1.业务现状调查 1.1 业务背景
简介 天下苦闲鱼久矣。”你在闲鱼上出售闲置时遇到过最恶心的买家是什么样的?“、”你为什么放弃使用闲鱼?“,这些都是知乎上的热点问题。国内类似于闲鱼这样的二手交易平台鱼龙混杂,没有专门针对大学生这一群体。
在大学校园里存在着很多的二手商品,由于信息资源的不流通以及传统二手商品信息交流方式的笨拙,导致了很多仍然具有一定价值或者具有非常价值的二手商品的囤积,乃至被当作废弃物处理。目前广东工业大学完成的二手物品交易九成以上都在微信群中完成,即卖家在微信群发布商品信息,买家联系卖家达成交易,交易完成后卖家发送”已出“信息表明货品已卖出;不足一成为熟人间转让。因此,我计划打造一个面向广东工业大学学生的高质量、高信任度的二手交易信息整合平台
,将线上微信群二手改造成更加高效、便捷的网站线上二手交易。
目标顾客
广东工业大学学生及教师。大学生有很大的交易二手物品的需求,在开学、放假以及毕业时流动范围、流动频率很高,这就意味着有大量不再需要的物品产生,因此这个群体亟需一个高信任度、 高质量的二手交易平台。
政策背景
近年来分享经济的理念渗透、年轻一代二手交易主力的崛起,还有绿色消费的政策助力,二手交易的社会消费理念即将成为主流,互联网巨头纷纷进场:阿里闲鱼、58转转等,然而校园内却没有类似于这些巨头一样的产品,这些产品在校园内难免水土不服,因此校内市场仍未得到很好地开发。
1.2 业务概况
主要工作
总经理:负责整个项目的策划、组织、安排、实施、监控和制约;
市场营销部:负责商品营销、校区联系,与校内各社团机构合作进行推广,吸引买家与卖家;
财务部:负责财务的收支管理;
用户服务部:负责调查用户满意度、解决售后问题。
人事部:人员变动、系统授权;
存在问题
闲鱼创始人曾经谈二手交易的壁垒和盈利问题:“这个壁垒很高,比做一个京东的壁垒高多了。今天你开店做个小生意,货从哪里来?京东可以找商家或厂商去进货,但闲鱼的货来自每一个消费者,必须发动上亿消费者把家里的闲置拿出来卖,这是一个巨大的门槛。交易的过程中,又有各种不确定、不标准充斥交易环节,这意味着要付出巨大的成本才能促成一笔交易,如果没有非常强大的基础建设、交易技术、支付能力、信用产品支撑,没有大量服务创新、交易纠纷解决方案、人工智能和机制设置能力,这个事根本就玩不转。”。从中我们可以大致了解二手交易的四个问题:如何打破用户顾虑,解决交易信任难题、供需匹配难题、配套服务薄弱和平台盈利难。
1.3 业务目标 “让大学生没有难做的生意”。本系统将将聚集大量二手信息,使得无论是买还是卖都更加方便快捷高效,同时达到盈利能够支付运营成本的目标。
本系统核心目标是卖家发布商品信息、买家查询商品信息并发起预约请求,与卖家达成线下交易。为更好的完成该核心功能,需要同时实现其他辅助功能,如成交量查询、人员变动授权、售后调查问卷统计等
1.4 可行性分析 面对整个市场,如何打破用户顾虑,解决交易信任难题
、供需匹配难题
、配套服务薄弱
和平台盈利难
这四个问题要复杂困难得多。但是面对大学生市场,这些问题就有了解决的可能。
首先,用户都是大学生群体。只要把握好身份审核,尽可能与学校达成合作,将有不诚信行为的学生名单反馈给学校等措施,就能够有效解决交易信任难题。
其次,大学生正在逐渐成为不可忽视的一股消费力量,这也意味着大学生有着大量二手物品或者对二手物品的需求,大学生拥有的闲置物品一直都存在释放的需求。
配套服务薄弱,比如物流服务。因此本系统是一个信息整合平台,让买家与卖家线下交易。
如何实现盈利?这是本项目最关键的部分。从二手交易平台的不同类型来看,交易撮合和寄售平台普遍的盈利模式是收取交易佣金,依据不同行业,佣金比例不同。但仅仅靠这个无法或者很难满足营销投入、人员检测、销售人员等成本。因此更多的需要将二手交易衍深其他服务,如二手+社交、二手+售后、二手+消费金融。二手交易常见的盈利方向就是二手+消费金融,因此我们可以为二手交易提供保险、分期付款等服务;二手交易同时也具有电商的基因,因此在后期也可以发展一手交易,进入一手电商市场,如新货买卖分享使用权等新模式。
2 业务需求分析 2.1 涉众分析 2.1.1 涉众概要
编号
涉众名称
涉众说明
期望
CMS001
买家
买家是二手商城系统的主要客户之一,且应当是通过认证的广东工业大学学生。买家在本系统需要收藏商品、预约线下购买商品,必要时请求客服帮助。
【1】预约商品; 【2】管理收藏商品【3】请求客服帮助
CMS002
卖家
卖家是二手商城系统的主要客户之一,且应当是通过认证的广东工业大学学生。卖家在本系统出售自己多余的二手物品,必要时请求客服帮助。
【1】出售商品 【2】管理在售商品【3】请求客服帮助
CMS003
财务部
结算一段周期内的成交量等数据,每月做财务报表。调整、发放每位员工的工资。
【1】通过计算机生成本月财务报表 【2】调整员工工资
CMS004
人事部
管理本系统除卖家和买家之外的人员变动、授权。负责招聘或者裁员。
【1】录入人员信息并授权与收回各部门人员授权(即招聘与裁员)
CMS005
市场营销部
组织发布活动,如毕业季、开学季购物节等活动。与卖家联系,将商品加入活动区域。
【1】发布活动; 【2】管理活动区商品【3】审核卖家加入活动区申请
CMS006
用户服务部
处理买家与卖家之间的纠纷,调查买家与卖家的满意率并每月做报表。
【1】线上调查用户满意率并导出报表 【2】提供客服支持
CMS007
学校
学校提供学生认证功能并处理不诚信学生。
【1】获取有不诚信行为的学生名单 【2】验证学生认证
CMS008
银行
支持线上支付
【1】提供线上支付
2.1.2 涉众简档
涉众
CMS001 买家
涉众代表
用户服务部代表买家提出期望
特点
系统的预期使用者,不可预计计算机应用水平的使用者
职责
【1】收藏商品 【2】向卖家提出线下购买商品申请 【3】向卖家提出取消购买商品申请,并提交取消理由 【4】向用户服务部提交请求帮助申请 【5】向用户服务部提交使用系统反馈 【6】查询以往订单信息
成功标准
【1】按要求正确购买、取消购买商品 【2】按要求正确向用户服务部门提交反馈
参与
不参与系统建设
可交付工件
无
意见/问题
无
涉众
SHM002 卖家
涉众代表
用户服务部代表卖家提出期望
特点
系统的预期使用者,不可预计计算机应用水平的使用者
职责
【1】发布商品 【2】在规定时间内通过或拒绝买家线下购买商品申请 【3】更改商品状态为在售、下架、上架以及商品价格描述等基本信息 【4】向用户服务部提交请求帮助申请 【5】向用户服务部提交使用系统反馈 【6】查询以往订单信息
成功标准
【1】按要求正确发布、下架商品 【2】按要求正确向用户服务部门提交反馈 【3】按要求正确修改商品描述
参与
不参与系统建设
可交付工件
无
意见/问题
无
涉众
CMS003 财务部
涉众代表
财务部部长×××
特点
系统的主要使用者,应具备相应的计算机操作水平,可培训
职责
【1】通过系统导出一段周期的财务报表 【2】修改、发放每位员工的工资
成功标准
【1】在规定时间内制作财务报表 【2】按照要求正确修改、发放员工的工资
参与
不参与系统建设
可交付工件
《×月财务报表》
意见/问题
无
涉众
CMS004 人事部
涉众代表
人事部部长×××
特点
系统的主要使用者,应具备相应的计算机操作水平,可培训
职责
【1】招聘新员工并录入信息 【2】裁员并更新员工信息
成功标准
【1】按要求正确录入、修改员工信息
参与
不参与系统建设
可交付工件
《×月招聘名单》《×月裁员名单》
意见/问题
无
涉众
CMS005 市场营销部
涉众代表
市场营销部部长×××
特点
系统的主要使用者,应具备相应的计算机操作水平,可培训
职责
【1】发布新促销活动 【2】管理促销区商品
成功标准
【1】按要求发布活动 【2】审核卖家加入活动申请
参与
不参与系统建设
可交付工件
《促销活动申请表》《关于商家参加×××促销活动的规则说明》
意见/问题
无
涉众
CMS005 用户服务部
涉众代表
用户服务部×××
特点
系统的主要使用者,应具备相应的计算机操作水平,可培训
职责
【1】收集用户评价反馈 【2】处理买家和卖家的客服帮助申请 【3】处理卖家与买家的纠纷
成功标准
【1】按月发布用户反馈报表 【2】提供客服帮助 【3】将有不诚信行为的学生名单上报学校
参与
不参与系统建设
可交付工件
《×月用户满意率报表》
意见/问题
无
涉众
SHM005 学校
涉众代表
学校教务系统负责人×××
特点
系统的非预期使用者,仅将不诚信学生名单的纸质文件上报给学校; 提供学生认证接口无需学校使用本系统。
职责
【1】提供学生认证接口 【2】接收不诚信学生名单文件
成功标准
【1】能够进行学生认证 【2】获取到不诚信学生名单并作出处理
参与
不参与系统建设
可交付工件
无
意见/问题
无
涉众
SHM008 银行
涉众代表
微信支付
特点
系统的非预期使用者
职责
【1】提供在线支付接口
成功标准
【1】能够正常在线支付
参与
不参与系统建设
可交付工件
无
意见/问题
无
2.2 业务边界 本报告准备实现以下两个业务:用户购买商品业务
和卖家出售商品业务
2.3 业务用例分析 2.3.1 获取业务用例
详细的业务用例
2.3.2 业务用例场景实现
2.3.3 业务用例规约
用例名称
购买商品
用例描述
买家注册登录后,浏览商品并下单,线下取货
执行者
买家
前置条件
1. 注册并登录 2. 学生认证审核通过
后置条件
确认收货或者取消订单
主过程描述
1. 用户填写个人信息注册,请求学生认证接口。认证成功执行2
,认证失败执行异常过程2.1
。 2. 买家浏览商品,与卖家交谈,谈拢成交执行3
,未成交用例结束。 3. 线上下单,将订单信息提交给卖家。 4. 线下交易,买家满意则确认收货,同时执行分支过程4.1
。买家不满意则取消订单。 5. 服务评价
分支过程描述
4.1 订单信息提交给财务部门,用于制作财务报表
异常过程描述
2.1 学生认证未通过,用例结束。
业务规则
买家必须为注册用户,即必须通过学生认证
设计的业务实体
买家档案,订单信息
用例名称
出售商品
用例描述
卖家注册登录后,发布商品,等待买家购买,线下发货
执行者
卖家
前置条件
1. 注册并登录 2. 学生认证审核通过 3. 发布了有效的商品
后置条件
商品售出或者订单被取消
主过程描述
1. 用户填写个人信息注册,请求学生认证接口。认证成功成为卖家后执行2
,认证失败执行异常过程2.1
。 2. 卖家发布商品,等待买家咨询。 3. 买家咨询,如果双方均同意则由买家提交订单,线下交易;如果有一方或者两方不满意则用例结束。 4. 线下见面交易,买家满意则确认收货,并同时进行分支过程4.1
。买家不满意则取消订单。 5. 卖家对服务进行评价
分支过程描述
4.1 订单信息提交给财务部门,用于制作财务报表
异常过程描述
2.1 学生认证未通过,用例结束。
业务规则
卖家必须为注册用户,即必须通过学生认证
设计的业务实体
卖家档案,订单信息
2.3.4 业务对象交互模型 用户档案与多个业务部门有关系,这些业务部门关心的和处理的数据又各有不同,因此有必要建立一个用户档案的模型,描述清楚用户档案的构成,以及档案各部分与各业务部门之间的存取关系。
用户注册时填写必需信息,从而建立用户基本档案
用户服务部门可能会修正用户的资料,比如审核学生认证后改变用户资料。
订单信息也与多个业务部门有关系,这些业务部门关心的和处理的数据也不同,因此同样有必要建立一个订单信息的模型。
用户下单时,建立订单基本档案
当订单状态改变,如用户取消订单或者确认收货,亦或是卖家取消订单,都会造成订单状态的改变,为订单添加了其他资料。
从以上分析中提取出用户基本资料、用户其他资料、订单基本资料、订单其他资料等领域对象
2.3.5 业务规则
编号
名称
描述
标志
日期
备注
001
安全性要求
本系统的所有用户都必须是广东工业大学师生
创建
2020.05
002
安全性要求
本系统的所有操作都必须登录
创建
2020.05
002.1
安全性要求
本系统的所有操作都必须登录,但是发布在主页上的系统公告可以被匿名用户浏览
创建
2020.05
交互规则
在前文的前置条件和后置条件中已经写出
内禀规则
实体名称
用户资料
实体描述
用户档案中的基本资料,通过他能识别系统使用者的姓名、联系方式等
属性名称
类型
精度
说明
用户编号
字符
20
地区编号(6位)+学号(10位)+流水号(4位)
联系方式
字符
11
手机号
实体名称
订单资料
——–
————————————————
实体描述
订单的基本资料,通过它能够得知订单价格、商品ID等
属性名称
类型
精度
说明
——–
—-
—-
—————————————–
订单编号
字符
20
地区编号(6位)+日期(8位)+流水号(6位)
实体名称
商品资料
——–
——————————————–
实体描述
商品的基本资料,通过它能够得知商品的基本情况
属性名称
类型
精度
说明
——–
—-
—-
——————————————
商品编号
字符
30
用户编号(20位)+年份(4位)+流水号(6位)
3 概念模型构建 基于上述主要业务用例进一步抽象出概念用例,绘制概念用例场景图, 找到关键类对象,构建概念模型,明晰对象间关系和交互场景。本部分必须包括概念用例图、分析类视图、分析类场景图,可采用活动图、时序图、协作图等来展示。
3.1 概念用例分析 二手商城系统比较复杂,但是其最主要最核心的业务是发布商品、出售商品、购买商品,这就是撑起二手信息聚合平台业务的主线,几乎所有的业务用例都围绕这条主线展开。
因此提炼出以下关键业务:
3.2 分析概念用例
关键业务用例挑选出来之后,根据业务主线的需要,为这些业务用例找出概念用例。
3.3 分析类场景图
卖家出售商品用例
3.4 分析类对象交互模型
4 系统分析 4.1 系统边界与系统用户 4.1.1 系统边界 由于在没有引入计算机系统之前的业务就是在微信群线上完成,分析原来的业务用例后可以发现,边界没有改变。原先所有的业务在引入计算机系统后都可以实现。
4.1.2 用户分析 用户是不同于涉众的抽象概念,一般是涉众的代表,是实实在在参与系统且需要编程实现的。
用户名称
用户概况和特点
使用系统方式
代表涉众
买家
买家是二手商城系统的主要客户之一, 且是通过认证的广东工业大学学生。
【1】注册、登录系统 【2】修改个人信息 【3】浏览商品; 【4】下单商品 【5】管理收藏商品 【6】请求客服帮助
买家
卖家
卖家是二手商城系统的主要客户之一, 且是通过认证的广东工业大学学生。
【1】注册、登录系统 【2】修改个人信息 【3】发布商品 【4】查看订单 【5】管理在售商品 【6】请求客服帮助
卖家
用户
买家
用户代表
管理学院某学生小王
说明
广东工业大学学生
特点
系统的预期使用者,不可预计计算机应用水平的使用者
职责
【1】注册登录系统 【2】编辑个人信息 【3】浏览商品 【4】向卖家提出线下购买商品申请 【5】向卖家提出取消购买商品申请,并提交取消理由 【6】向用户服务部提交请求帮助申请 【7】向用户服务部提交使用系统反馈 【8】查询以往订单信息
成功标准
【1】按要求正确购买、取消购买商品 【2】按要求正确向用户服务部门提交反馈
参与
页面设计
可交付工件
网站页面设计稿
意见/问题
无
用户
卖家
用户代表
管理学院学生小张
说明
广东工业大学学生
特点
系统的预期使用者,不可预计计算机应用水平的使用者
职责
【1】注册、登录系统 【2】发布商品 【3】在规定时间内通过或拒绝买家线下购买商品申请 【4】更改商品状态为在售、下架、上架以及商品价格描述等基本信息 【5】向用户服务部提交请求帮助申请 【6】向用户服务部提交使用系统反馈 【&】查询以往订单信息
成功标准
【1】按要求正确发布、下架商品 【2】按要求正确向用户服务部门提交反馈 【3】按要求正确修改商品描述
参与
页面设计
可交付工件
网站页面设计稿
意见/问题
无
4.2 系统用例分析
4.3 系统对象交互模型
4.4 系统用例规约
用例名称
注册账号
用例描述
用户录入资料进行注册
执行者
买家或者卖家
前置条件
用户是广东工业大学学生
后置条件
用户成功注册并自动登陆系统
主事件描述
用户进入注册界面,输入基本信息,校验通过后即注册成功,并自动登录进入首页;校验失败进入异常事件1.1
分支事件描述
无
异常事件描述
1.1
用户输入的信息校验不通过,如学号不正确、用户已注册过,不予通过注册,用例结束。
业务规则
a. 根据用户信息查看是否已经注册,若已经注册,则结束用例 b. 校验用户输入是否正确,不正确则结束用例
涉及的实体
用户档案
用例名称
登录账号
用例描述
用户输入登录凭证进行登录
执行者
买家或者卖家
前置条件
用户是广东工业大学学生且已经注册过,拥有本系统的登录凭证。
后置条件
用户成功登陆系统
主事件描述
用户进入登录界面,输入登录凭证,校验通过后即登录成功并跳转首页;校验失败进入异常事件1.1
分支事件描述
无
异常事件描述
1.1
用户输入的登录凭证校验不通过,不予通过登录,用例结束。
业务规则
a. 判断登录凭证是否合法,合法则登陆成功,不合法给出提示
涉及的实体
用户档案
用例名称
浏览商品
用例描述
买家登录后浏览商品
执行者
买家
前置条件
买家已经登录
后置条件
买家能够查看商品
主事件描述
买家进入搜索商品界面,输入关键词,返回相关商品。如果未登录,进入异常事件1.1
分支事件描述
无
异常事件描述
1.1
买家未登录,结束用例
业务规则
判断买家是否登录,如果已经登录则正常返回商品搜索结果,否则跳转登录界面,用例结束
涉及的实体
商品信息
用例名称
发布商品
用例描述
卖家登录后发布商品
执行者
卖家
前置条件
卖家已经成功登陆
后置条件
卖家成功发布商品信息
主事件描述
卖家进入后台管理界面,填写商品信息表后提交,成功发布商品。如果商家未登录,进入异常事件1.1
分支事件描述
无
异常事件描述
1.1
卖家未登录,跳转登录界面,用例结束。
业务规则
判断卖家是否登录,如果已经登录,则正常发布商品流程;如果未登录,则跳转登录界面,结束用例。
业务规则
1.1
卖家未登录,跳转到登录界面,结束用例
涉及的实体
商品信息
用例名称
下架商品
用例描述
卖家登录后下架已经发布的商品
执行者
卖家
前置条件
卖家已经成功登陆且商品已经成功发布
后置条件
卖家成功下架商品
主事件描述
卖家进入后台管理界面,查询商品信息,点击下架功能键下架商品。如果商家未登录,进入异常事件1.1
分支事件描述
无
异常事件描述
`1.1卖家未登录,跳转登录界面,用例结束。
业务规则
判断卖家是否登录,如果已经登录,则请求商品清单,然后处理卖家的下架请求;如果未登录,则跳转登录界面,结束用例。
业务规则
1.1
卖家未登录,跳转到登录界面,结束用例
涉及的实体
商品信息
用例名称
下订单
用例描述
买家登录后下订单
执行者
买家
前置条件
买家已经成功登陆
后置条件
成功生成订单
主事件描述
买家进入商品详情页,点击下单,生成订单;如果未登录,进入异常流程1.1
分支事件描述
无
异常事件描述
1.1
买家未登录,跳转登录页面,用例结束
业务规则
判断买家是否登录,如果已经登录,则正常下单;如果未登录,则跳转登录界面,结束用例。
涉及的实体
商品信息,订单信息
用例名称
支付商品费用
用例描述
买家登录后查询已经下过的单并支付
执行者
买家
前置条件
买家已经成功登陆且已经下过单
后置条件
成功支付
主事件描述
用户进入个人中心,查询到已经下过的单,点击立刻支付进行支付;如果未登录,进入异常流程1.1
分支事件描述
无
异常事件描述
1.1
买家未登录,跳转登录页面,用例结束
业务规则
判断买家是否登录,如果已经登录,则正常查询订单、支付订单;如果未登录,则跳转登录界面,结束用例。
涉及的实体
订单信息
用例名称
查看订单
用例描述
卖家登录后查询买家下的订单
执行者
卖家
前置条件
卖家成功登陆
后置条件
查询订单成功
主事件描述
卖家进入个人中心,查询到买家下过的单;如果未登录,进入异常流程1.1
分支事件描述
无
异常事件描述
1.1
卖家未登录,跳转登录页面,用例结束
业务规则
判断卖家是否登录,如果已经登录,则正常查询订单;如果未登录,则跳转登录界面,结束用例。
涉及的实体
订单信息
用例名称
请求客服帮助
用例描述
用户遇到问题后请求客服帮助
执行者
卖家或买家
前置条件
无
后置条件
问题被解决
主事件描述
用户在操作本系统时遇到无法解决的问题,寻求客服帮助
分支事件描述
无
异常事件描述
无
业务规则
无论有无登录,均可以联系在线客服寻求帮助
涉及的实体
工单信息
5 系统设计 5.1 系统架构与部署设计 建议采用四层架构:表示层、控制层、业务逻辑层和数据持久层
订单管理:
商品管理
5.2 包设计(项目目录结构)
6 系统实现 6.1 系统开发环境及配置
Windows下开发,部署在CentOS 7服务器上
使用Spring Boot + Vue作为后端和前端框架
SSM环境的配置录了一个视频:哔哩哔哩
后期项目将会上传GIthub,因此具体配置文件不在此贴出
6.2 数据库实现 建立数据库gdutmall
,字符编码为utf8mb4
,排序规则为utf8mb4_unicode_ci
。初步设计有四张表:
6.3 主要模块核心代码实现 状态码枚举类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 package xyz.swzdl.gdutmall.statuscode;public enum StatusCodeEnum { SUCCESS("0" , "" ), SYSTEM_ERROR("1" , "SYSTEM_ERROR" ), ACCOUNT_NOT_REGISTER("1001" , "用户未注册" ), ACCOUNT_ALREADY_REGISTERED("1002" , "用户已注册" ), PASSWORD_NOT_CORRECT("1003" , "密码错误" ), VERIFICATION_CODE_NOT_CORRECT("1004" , "验证码错误" ), REGISTER_SUCCESS("1005" , "注册成功" ), REGISTER_FAIL("1006" , "注册失败" ), USER_NOT_LOGIN("1007" ,"尚未登录" ), COMMODITY_NOT_EXIST("2001" ,"商品不存在" ); private final String code; private final String desc; StatusCodeEnum(String code, String desc) { this .code = code; this .desc = desc; } public String getCode () { return code; } public String getDesc () { return desc; } @Override public String toString () { return "ErrorCodeEnum{" + "code='" + code + '\'' + ", desc='" + desc + '\'' + '}' ; } }
用户实体类(使用建造者模式):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 package xyz.swzdl.gdutmall.entity;import com.fasterxml.jackson.databind.ObjectMapper;import io.swagger.annotations.ApiModelProperty;import org.springframework.security.core.GrantedAuthority;import org.springframework.security.core.userdetails.UserDetails;import java.io.Serializable;import java.util.Collection;import java.util.Date;public class User implements Serializable , UserDetails { @ApiModelProperty("用户ID,此ID由数据库自动生成并递增") private Integer userId; @ApiModelProperty("用户登录密码") private String userPassword; @ApiModelProperty("用户昵称") private String userNickName; @ApiModelProperty("真实姓名") private String userName; private String userStudentId; @ApiModelProperty("用户性别") private Integer userGender; @ApiModelProperty("用户手机号") private String userPhone; @ApiModelProperty("用户手机号") private String userMail; @ApiModelProperty("用户QQ") private String userQq; @ApiModelProperty("用户微信") private String userWeChat; @ApiModelProperty("用户宿舍") private String userDormitory; @ApiModelProperty("用户校区") private String userCampus; @ApiModelProperty("用户注册时间") private Date userRegisterTime; @ApiModelProperty("用户状态(如封号)") private Integer userStatus; private static final long serialVersionUID = 1L ; @Override public String toString () { return new ObjectMapper().createObjectNode() .put("userId" ,userId) .put("userPassword" ,userPassword) .put("userNickName" ,userNickName) .put("userName" ,userName) .put("userStudentId" ,userStudentId) .put("userGender" ,userGender) .put("userPhone" ,userPhone) .put("userMail" ,userMail) .put("userQq" ,userQq) .put("userWeChat" ,userWeChat) .put("userDormitory" ,userDormitory) .put("userCampus" ,userCampus) .put("userRegisterTime" , String.valueOf(userRegisterTime)) .put("userStatus" ,userStatus) .toString(); } public User () { } private User (Builder builder) { this .userPassword = builder.userPassword; this .userNickName = builder.userNickName; this .userName = builder.userName; this .userStudentId = builder.userStudentId; this .userGender = builder.userGender; this .userPhone = builder.userPhone; this .userMail = builder.userMail; this .userQq = builder.userQq; this .userWeChat = builder.userWeChat; this .userDormitory = builder.userDormitory; this .userCampus = builder.userCampus; this .userRegisterTime = builder.userRegisterTime; this .userStatus = builder.userStatus; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null ; } @Override public String getPassword () { return this .userPassword; } @Override public String getUsername () { return this .userPhone; } @Override public boolean isAccountNonExpired () { return true ; } @Override public boolean isAccountNonLocked () { return true ; } @Override public boolean isCredentialsNonExpired () { return true ; } @Override public boolean isEnabled () { return this .userStatus == 0 ; } public static class Builder { private final String userPassword; private final String userNickName; private String userName; private String userStudentId; private Integer userGender; private final String userPhone; private String userMail; private String userQq; private String userWeChat; private String userDormitory; private String userCampus; private final Date userRegisterTime; private final Integer userStatus; public Builder (String userPassword, String userNickName, String userPhone, Date userRegisterTime, Integer userStatus) { this .userPassword = userPassword; this .userNickName = userNickName; this .userPhone = userPhone; this .userRegisterTime = userRegisterTime; this .userStatus = userStatus; } public Builder userName (String userName) { this .userName = userName; return this ; } public Builder userStudentId (String userStudentId) { this .userStudentId = userStudentId; return this ; } public Builder userGender (int userGender) { this .userGender = userGender; return this ; } public Builder userMail (String userMail) { this .userMail = userMail; return this ; } public Builder userQq (String userQq) { this .userQq = userQq; return this ; } public Builder userWeChat (String userWeChat) { this .userWeChat = userWeChat; return this ; } public Builder userDormitory (String userDormitory) { this .userDormitory = userDormitory; return this ; } public Builder userCampus (String userCampus) { this .userCampus = userCampus; return this ; } public User build () { return new User(this ); } } public Integer getUserId () { return userId; } public String getUserPassword () { return userPassword; } public String getUserNickName () { return userNickName; } public String getUserName () { return userName; } public String getUserStudentId () { return userStudentId; } public Integer getUserGender () { return userGender; } public String getUserPhone () { return userPhone; } public String getUserMail () { return userMail; } public String getUserQq () { return userQq; } public String getUserWeChat () { return userWeChat; } public String getUserDormitory () { return userDormitory; } public String getUserCampus () { return userCampus; } public Date getUserRegisterTime () { return userRegisterTime; } public Integer getUserStatus () { return userStatus; } public static long getSerialVersionUID () { return serialVersionUID; } }
Spring Security
配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 package xyz.swzdl.gdutmall.configuration;import com.fasterxml.jackson.databind.ObjectMapper;import lombok.extern.slf4j.Slf4j;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;import org.springframework.security.config.annotation.web.builders.HttpSecurity;import org.springframework.security.config.annotation.web.builders.WebSecurity;import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;import org.springframework.security.crypto.password.NoOpPasswordEncoder;import org.springframework.security.crypto.password.PasswordEncoder;import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;import xyz.swzdl.gdutmall.filter.CustomUsernamePasswordAuthenticationFilter;import xyz.swzdl.gdutmall.service.serviceimpl.UserServiceImpl;import xyz.swzdl.gdutmall.statuscode.StatusCodeEnum;@Configuration @Slf4j public class WebSecurityConfig extends WebSecurityConfigurerAdapter { final UserServiceImpl userService; public WebSecurityConfig (UserServiceImpl userService) { this .userService = userService; } @Bean PasswordEncoder passwordEncoder () { return NoOpPasswordEncoder.getInstance(); } @Override protected void configure (AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } @Override protected void configure (HttpSecurity http) throws Exception { http.addFilterAt(customUsernamePasswordAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); http.authorizeRequests() .antMatchers("/api/v1/user/getAllUsers" ).authenticated().anyRequest().permitAll().and().formLogin() .loginProcessingUrl("/api/v1/user/login" ) .successHandler((request, response, authentication) -> { response.setContentType("application/json;charset=utf-8" ); final var writer = response.getWriter(); writer.write(new ObjectMapper().writeValueAsString(authentication.getPrincipal())); writer.close(); }) .failureHandler((request, response, exception) -> { response.setContentType("application/json;charset=utf-8" ); final var writer = response.getWriter(); writer.write(new ObjectMapper().writeValueAsString(exception.getMessage())); writer.close(); }).and().logout() .logoutUrl("/api/v1/user/logout" ) .logoutSuccessHandler((request, response, authentication) -> { response.setContentType("application/json;charset=utf-8" ); final var writer = response.getWriter(); writer.write(new ObjectMapper().createObjectNode().put("errorCode" , "200" ).put("errorMessage" , "注销登录成功" ).toString()); writer.close(); }).permitAll().and() .csrf().disable() .exceptionHandling().authenticationEntryPoint((request, response, exception) -> { response.setContentType("application/json;charset=utf-8" ); final var writer = response.getWriter(); writer.write(new ObjectMapper().createObjectNode().put("errorCode" , StatusCodeEnum.USER_NOT_LOGIN.getCode()).put("errorMessage" , StatusCodeEnum.USER_NOT_LOGIN.getDesc()).toString()); writer.close(); }); } @Bean CustomUsernamePasswordAuthenticationFilter customUsernamePasswordAuthenticationFilter () throws Exception { CustomUsernamePasswordAuthenticationFilter filter = new CustomUsernamePasswordAuthenticationFilter(); filter.setAuthenticationSuccessHandler((request, response, authentication) -> { response.setContentType("application/json;charset=utf-8" ); final var writer = response.getWriter(); writer.write(new ObjectMapper().writeValueAsString(authentication.getPrincipal())); writer.close(); }); filter.setAuthenticationFailureHandler((request, response, exception) -> { response.setContentType("application/json;charset=utf-8" ); final var writer = response.getWriter(); writer.write(new ObjectMapper().writeValueAsString(exception.getMessage())); writer.close(); }); filter.setFilterProcessesUrl("/api/v1/user/login" ); filter.setAuthenticationManager(authenticationManagerBean()); return filter; } @Override public void configure (WebSecurity web) { web.ignoring().antMatchers("/js/**" , "/css/**" , "/img/**" , "/fonts/**" , "/index.html" ); } }
Druid
等的配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 mybatis: check-config-location: true config-location: classpath:mybatis-config.xml mapper-locations: classpath:mapper/*Mapper.xml spring: datasource: druid: url: jdbc:mysql://localhost:3306/gdutmall?serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: 123456 initial-size: 5 max-active: 8 min-idle: 3 pool-prepared-statements: false validation-query: select 'x' validation-query-timeout: 1 test-on-borrow: false test-on-return: false test-while-idle: true filters: web-stat-filter: enabled: true url-pattern: /* exclusions: - '*.js' - '*.gif' - '*.jpg' - '*.png' - '*.css' - '*.ico' - /druid/* session-stat-enable: true session-stat-max-count: 1000 principal-session-name: Mr.Sheng profile-enable: true stat-view-servlet: enabled: true url-pattern: /druid/* reset-enable: true login-username: druid login-password: druid type: com.alibaba.druid.pool.DruidDataSource server: port: 8000 debug: false logging: level: root: debug
日志配置(logback+slf4j):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 <?xml version="1.0" encoding="UTF-8" ?> <configuration scan ="true" scanPeriod ="3 seconds" > <statusListener class ="ch.qos.logback.core.status.OnConsoleStatusListener" /> <appender name ="STDOUT" class ="ch.qos.logback.core.ConsoleAppender" > <encoder > <pattern > %d{HH:mm:ss.SSS} [%thread] %-5level %logger{32} - %msg%n</pattern > </encoder > </appender > <appender name ="FILE" class ="ch.qos.logback.core.rolling.RollingFileAppender" > <File > log/logFile.log</File > <rollingPolicy class ="ch.qos.logback.core.rolling.TimeBasedRollingPolicy" > <FileNamePattern > log/logFile.%d{yyyy-MM-dd_HH-mm}.log.zip </FileNamePattern > </rollingPolicy > <encoder > <pattern > %d{HH:mm:ss.SSS} [%thread] %-5level %logger{32} - %msg%n</pattern > </encoder > </appender > <root > <level value ="DEBUG" /> <appender-ref ref ="STDOUT" /> </root > </configuration >
Mybatis Generate
配置自动生成Mapper类和Mapper的xml:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 <!DOCTYPE generatorConfiguration PUBLIC "-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN" "http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd" > <generatorConfiguration > <properties resource ="application.properties" /> <context id ="GDUTMall" targetRuntime ="MyBatis3Simple" > <property name ="autoDelimitKeywords" value ="true" /> <property name ="javaFileEncoding" value ="utf-8" /> <property name ="beginningDelimiter" value ="`" /> <property name ="endingDelimiter" value ="`" /> <property name ="javaFormatter" value ="org.mybatis.generator.api.dom.DefaultJavaFormatter" /> <property name ="xmlFormatter" value ="org.mybatis.generator.api.dom.DefaultXmlFormatter" /> <plugin type ="org.mybatis.generator.plugins.SerializablePlugin" /> <plugin type ="org.mybatis.generator.plugins.ToStringPlugin" /> <commentGenerator > <property name ="suppressAllComments" value ="false" /> <property name ="suppressDate" value ="true" /> </commentGenerator > <jdbcConnection driverClass ="${spring.datasource.driver}" connectionURL ="${spring.datasource.url}" userId ="${spring.datasource.username}" password ="${spring.datasource.password}" /> <javaTypeResolver > <property name ="forceBigDecimals" value ="true" /> </javaTypeResolver > <javaModelGenerator targetPackage ="xyz.swzdl.gdutmall.entity" targetProject ="src/main/java" /> <sqlMapGenerator targetPackage ="mapper" targetProject ="src/main/resources" > <property name ="enableSubPackages" value ="false" /> </sqlMapGenerator > <javaClientGenerator type ="XMLMAPPER" targetPackage ="xyz.swzdl.gdutmall.dao" targetProject ="src/main/java" > <property name ="enableSubPackages" value ="false" /> </javaClientGenerator > <table tableName ="user" enableCountByExample ="true" enableUpdateByExample ="true" enableDeleteByExample ="true" enableSelectByExample ="true" enableInsert ="true" enableSelectByPrimaryKey ="true" enableDeleteByPrimaryKey ="true" enableUpdateByPrimaryKey ="true" > <property name ="useActualColumnNames" value ="false" /> </table > <table tableName ="commodity" enableCountByExample ="true" enableUpdateByExample ="true" enableDeleteByExample ="true" enableSelectByExample ="true" enableInsert ="true" enableSelectByPrimaryKey ="true" enableDeleteByPrimaryKey ="true" enableUpdateByPrimaryKey ="true" > <property name ="useActualColumnNames" value ="false" /> </table > <table tableName ="image" enableCountByExample ="true" enableUpdateByExample ="true" enableDeleteByExample ="true" enableSelectByExample ="true" enableInsert ="true" enableSelectByPrimaryKey ="true" enableDeleteByPrimaryKey ="true" enableUpdateByPrimaryKey ="true" > <property name ="useActualColumnNames" value ="false" /> </table > <table tableName ="order" enableCountByExample ="true" enableUpdateByExample ="true" enableDeleteByExample ="true" enableSelectByExample ="true" enableInsert ="true" enableSelectByPrimaryKey ="true" enableDeleteByPrimaryKey ="true" enableUpdateByPrimaryKey ="true" > <property name ="useActualColumnNames" value ="false" /> </table > </context > </generatorConfiguration >
前端参数校验:
1 2 3 4 5 6 7 8 9 registerCheckRules : { phoneNumber : [], VerificationCode : [{ validator : validateCode, trigger : "blur" }], name : [{ validator : validateName, trigger : "blur" }], password : [{ validator : validatePassword, trigger : "blur" }], checkPass : [{ validator : validatePass2, trigger : "blur" }] },
用户名验证规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 const validateName = (rule, value, callback ) => { const that = this ; if (value === "" ) { return callback(new Error ("请输入用户名!" )); } else { that.$http .post( "/user/checkUserNicName" , { userName : value }, { headers : { "Content-Type" : "application/json" } } ) .then(function (response ) { if (response.data.errorCode === 200 ) { callback(); } else { return callback(new Error (response.data.errorMessage)); } }) .catch(function ( ) { return callback(new Error ("检测用户名是否可用错误,请稍后重试!" )); }); } };
7 系统测试 主页(未登录):
关于页面:
登陆页面:
注册页面:
主页(已经登陆):
搜索结果页:
底部的分页处理:
Druid
控制台:
API
文档:
8 总结
在使用Spring Security
的过程中遇到很多问题也学到了很多,比如Spring Security
默认使用前后端不分离的形式,在修改其为前后端分离的时候,遇到POST
传递登录名和登陆密码,但是Spring Security
获取到null
的问题:
1 2 3 11:22:08.505 [http-nio-8000-exec-10] DEBUG o.m.s.t.SpringManagedTransaction - JDBC Connection [com.mysql.cj.jdbc.ConnectionImpl@51d41223] will be managed by Spring 11:22:08.505 [http-nio-8000-exec-10] DEBUG x.s.g.d.U.getUserByPhone - ==> Preparing: select user_id, user_password, user_nick_name, user_name, user_student_id, user_gender, user_phone, user_mail, user_qq, user_we_chat, user_dormitory, user_campus, user_register_time, user_status from user where user_phone = ?
通过查阅源码找到获取用户名密码是UsernamePasswordAuthenticationFilter
这个类,源码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public Authentication attemptAuthentication (HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST" )) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } String username = obtainUsername(request); String password = obtainPassword(request); if (username == null ) { username = "" ; } if (password == null ) { password = "" ; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); setDetails(request, authRequest); return this .getAuthenticationManager().authenticate(authRequest); }
而从request
中获取username
的方法obtainUsername
为:
1 2 3 4 5 6 7 8 9 10 11 12 13 @Nullable protected String obtainUsername (HttpServletRequest request) { return request.getParameter(usernameParameter); }
发现这是从form
表单中取值,但是使用axios
将参数放在body
则取不到值,因此可以重写获取用户名和密码的方法,自定义一个过滤器,继承UsernamePasswordAuthenticationFilter
类,然后重写其获取用户名密码的方法,再在spring
配置中进行注册该自定义过滤器,并将该过滤器注册到Spring Security
的过滤器链中。
Spring Security
认证使用传统的cookie-session
模式,当从单服务变为多服务时候,应当使用JSON Web Token (JWT)
小程序端未开发完成
对于一个完整的电商项目而言,数据库设计很糟糕。没有完善的考虑商品SKU
、SPU
等的处理,距离真正落地可商用项目还很大。另外应当独立开发一个CMS
,很多内容都是写死在程序里,后期如果需要更改就需要修改源代码,非常不合理,应该能够通过CMS
进行修改。
短信验证码由于无法在腾讯云申请短信签名的缘故,因此无法发送验证码,暂时使用000000
代替
没有设计统一异常处理