使用springboot创建商城系统
首先,恭喜自己(伪)独立完成了一个包括后台管理和前端商城的(简陋至极的)商城系统。
在这一过程中,我最主要的问题是——呃,什么都不会!
没错,真的是什么都不会。一个月前,我所拥有的对这个项目唯一有帮助的基础是:使用eclipse自学过两周java编程算法。除此之外,没有任何web项目经验,没有任何spring经验,没有任何J2EE经验。在学校里只学过c++,而且仅限于算法。
老实说,当我面对这个项目命题时,我感到了深深的绝望和对以往荒废时光的惋惜。于是,我开始做的第一件事就是,登录b站,打开点击最高的spring,springmvc,mybatis,springboot视频。。。。。
咳咳,闲话扯远了,回到正题!
这篇博客我将尽可能完整地展示一个springboot商城系统的构建思路和业务逻辑。当然,项目还存在一些不够友好的bug,以及功能单薄的缺点(可我实在懒得改了)。博客的发布,最主要目的是我对过去一个月的思考和知识的巩固汇总,所以不会一一列举所有的项目内容(比如对对象的增删改查操作等),尤其是我自己觉得很简单的部分,所以可能会显得不够全面,如果你需要全面的代码,那么可以去git上搜索靠前的原码。
那么问题来了,依靠这篇博客你可以成功完成一个项目吗?
答案显然是no。因为我只是总结一些个人在项目中遇到的重点,我自己本身也是通过视频,博客,git等途径逐步学习完成的整个项目。也就是说,这不是一篇教学类的博客。
如果你要完成一个自己的项目,你需要看更多的视频,学更多的基础知识,看更多的博客,这只是一个参考,并且不一定是你能用到的参考。仅此。
基本来说,我将采取每天(???)更新的方式,逐步完成。
技术栈
因为初学者,所以项目使用的都是最基础的技术框架。如下:
(懒得手敲,直接ppt截图)
其中springboot括号里的组件是我在完成基本功能后,通过度娘添加补充的。
需求分析
这是项目开始之初,首先要做的工作。当然,我这一步做的就不好,直到项目差不多了,我才整理好需求分析,可以说是很不专业了。
用户前台需求分析
用户中心:
1注册与登录
2用户个人信息(包括地址和积分信息)的查看与修改
3用户购物车的查看与修改
4个人订单的查看、取消、下单
5积分兑换商品记录查询
商城系统:
1商品展示
2商品分类及展示
3商品详情
4商品加入购物车以及下单
订单系统:
1商品下单
2支付订单以及退单
积分商城:
1积分商城
2积分获取规则
3积分兑换商品
管理员后台需求分析
管理员登录
用户管理:
1用户信息搜索查询
2用户信息增删改
3地址信息搜索查询
4用户地址信息增删改
商品管理:
1商城商品增删改查
2积分商品增删改查
3商品类别增删改查
订单管理:
1订单的分类查询
2订单的删改查
积分管理:
1用户积分查询与修改
2积分兑换记录查询
思维导图
可以看见,思维导图就是在需求分析后整理成可视化的图形结构,使得整个项目功能一目了然,同时还可以使用标记来记录自己的完成情况,就很舒服。同样地当然,我这一步做的也不太好,开始时列举功能不够详细,思路不清晰,也可以看出,我的功能相当单薄(所以说,菜就是原罪啊!)。所以,思维导图的列举,一定要具体详细,每完成一项就做一个标记,成就感满满!
项目实战(开始撸代码)
这一大模块,我将采用自己完成项目的时间步骤书写。基本来说,就是管理平台后端——管理平台前端——商城后端——商城前端的顺序。其中,前端部分我花了五分之三还多的时间,后端花费时间较少(前端实在是不会啊!!!)。由于很多内容是简单的复用(比如后台对用户,商品的管理操作),所以不会全部列举。
在这里,首先列举一下数据表和项目结构。其他具体内容以后慢慢展示。
数据库表
项目结构
配置文件application.yml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: ######
password: ######
url: jdbc:mysql://localhost:3306/miaosha?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC
driver-class-name: com.mysql.cj.jdbc.Driver
Jackson配置
jackson:
default-property-inclusion: NON_NULL
time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
mvc:
view:
suffix: .html
prefix: /
resources:
#classpath:/META-INF/resources/,classpath:/resources/,classpath:/static/,classpath:/public/
#static-locations: classpath:/css/, classpath:/image/, classpath:/js/
#static-path-pattern: /static/*
thymeleaf:
prefix: classpath:/templates/
suffix: .html
encoding: UTF-8
cache: false
配置驼峰映射
configuration:
map-underscore-to-camel-case: true
mybatis配置
mybatis:
typeAliasesPackage: com.pf.businessdemo.dataobject
mapperLocations: classpath:mapping/*.xml
configuration:
map-underscore-to-camel-case: true
(这里是一个最简单的配置,学过一点基础的想必都懂。)
除此之外的依赖注入我也不再列举,都是一个web项目所必需的jar包。
后台用户模块
一、登录注册
用户实体类
public class UserDo {
private Integer id;
private String name;
private String gender;
private Integer age;
private String telphone;
private String registerMode;
private String thirdPartyId;
private String receiverAddress;
private Integer integral;
getset省略
service层接口
public interface UserService{
/**
*@Description: 通过id获取用户信息
*@Param: id
*@return: usermodel用户领域模型
*/
UserModel getUserById(Integer id);
/**
*@Description:通过id删除用户信息
*@Param: id
*@return: void
*/
void deleteUser(Integer id);
/**
*@Description:通过name删除用户
*@Param:userdo
*@return:
*/
int deleteByName(UserDo userDo );
/**
*@Description:通过id更改用户信息
*@Param: 要更改的用户信息
*@return: void
*/
void updateUserInfo(UserDo userDo);
/**
*@Description:完善用户信息
*@Param:用户填写的具体信息
*@return:
*/
UserDo insertUserInfo(UserDo userDo);
/**
*@Description:查询获取所有用户信息
*@Param:
*@return:list<userdo>
*/
List<UserDo> findUserAll();
/**
*@Description:联合查询用户及密码信息
*@Param:
*@return:
*/
List<UserPassWordDo> findDouble();
/**
*@Description:通过名字搜索用户
*@Param:用户信息
*@return:
*/
List<UserDo> findUserByName(UserDo userDo);
/**
*@Description: 用户注册接口
*@Param: 用户领域模型
*@return:
*/
void register(UserModel userModel)throws BusinessException;
/**
*@Description:用户带校验的登陆接口
*@Param: 用户手机,用户加密密码
*@return:
*/
UserModel validateLogin(String telphone,String encrptPassword) throws BusinessException;
这里列举了所有的用户service接口,目前只需要关注登录注册相关的即可。
UserServiceImpl
@Override
public List
return userDoMapper.findUserByName(userDo);
}
@Override
@Transactional
public void register(UserModel userModel) throws BusinessException {
if (userModel==null){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
}
ValidationResult result=validator.validate(userModel);
if (result.isHasError()){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,result.getErrMsg());
}
//实现model->dataobject方法
UserDo userDo=convertFromDataObject(userModel);
try {
userDoMapper.insertSelective(userDo);
}catch (DuplicateKeyException ex){
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR,"手机号已重复注册");
}
userModel.setId(userDo.getId());
UserPassWordDo userPassWordDo=convertPasswordFromDataObject(userModel);
userpasswordDoMapper.insertSelective(userPassWordDo);
return;
}
@Override
public UserModel validateLogin(String telphone, String encrptPassword) throws BusinessException {
//通过用户手机获取用户信息
UserDo userDo=userDoMapper.selectByTelphone(telphone);
if (userDo==null){
throw new BusinessException(EmBusinessError.USER_LOGIN_FAIL );
}
UserPassWordDo userPassWordDo=userpasswordDoMapper.selectByUserId(userDo.getId());
UserModel userModel=convertFromDataObject(userDo,userPassWordDo);
//比对用户信息内加密的密码是否和传输进来的密码相匹配
if (!StringUtils.equals(encrptPassword,userModel.getEncrptPassword())){
throw new BusinessException(EmBusinessError.USER_LOGIN_FAIL );
}
return userModel;
}
private UserPassWordDo convertPasswordFromDataObject(UserModel userModel){
if (userModel==null){
return null;
}
UserPassWordDo userPassWordDo=new UserPassWordDo();
userPassWordDo.setEncrptPassword(userModel.getEncrptPassword());
userPassWordDo.setUserId(userModel.getId());
return userPassWordDo;
}
private UserDo convertFromDataObject(UserModel userModel){
if (userModel==null){
return null;
}
UserDo userDo=new UserDo();
BeanUtils.copyProperties(userModel,userDo);
return userDo;
}
private UserModel convertFromDataObject(UserDo userDo, UserPassWordDo userpasswordDo){
if(userDo==null){
return null;
}
UserModel userModel=new UserModel();
BeanUtils.copyProperties(userDo,userModel);
if(userpasswordDo!=null){
userModel.setEncrptPassword(userpasswordDo.getEncrptPassword());
}
return userModel;
}
1首先是register(注册)方法:
在这里需要说明一下,我的用户表是不包含用户密码的,而是单独写了一张user_password表,如下
接着用一个用户领域模型UserModel来包含用户所有字段
public class UserModel {
private Integer id;
@NotBlank(message = "用户名不能为空")
private String name;
@NotNull(message = "性别为必填选项")
private String gender;
@NotNull(message = "年龄为必填选项")
@Min(value = 0,message = "年龄必须大于0岁")
@Max(value = 150,message = "年龄必须小于150岁")
private Integer age;
@NotNull(message = "手机号不能为空")
private String telphone;
private String registerMode;
private String thirdPartyId;
@NotNull(message = "密码不能为空")
private String encrptPassword;
@NotNull(message = "手机号不能为空")
private String receiverAddress;
private Integer integral;
getset省略
因为是对两张表操作,所以自定义了类似convertPasswordFromDataObject的方法,逻辑非常简单,用到了BeanUtils.copyProperties(a,b)(将a赋给b)。
对代码逐行分析。首先是对UserModel的判空处理,定义异常,使用ExceptionHandler捕获,接着是对注册信息的输入校验,这里使用了HibernateValidator。
接下来,调用用户的mapper接口UserDoMapper和密码的mapper接口UserPasswordDoMapper的insert方法,这里逻辑比较简单,不再赘述。
2登录validateLogin方法
登录的逻辑是,首先通过用户手机号得到用户信息,接着对比用户加密密码是否和传输进来的密码相匹配。具体实现看代码即可。
UserController
/**
*@Description:用户登录接口
*@Param:用户登录信息
*@return:
*/
@RequestMapping("/index")
public String index() {
return "admin/adminHomepage";
}
@RequestMapping(value = "/login")
@ResponseBody
public CommonReturnType login(@RequestParam(name = "telphone") String telphone,
@RequestParam(name = "password") String password) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
//入参校验
if (org.apache.commons.lang3.StringUtils.isEmpty(telphone) ||
StringUtils.isEmpty(password)) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR);
}
//用户登录服务,用来校验用户登录是否合法
UserModel userModel = userService.validateLogin(telphone, this.EncodeByMd5(password));
//将登录凭证加入到用户登陆成功的session内
this.httpServletRequest.getSession().setAttribute("IS_LOGIN", true);
this.httpServletRequest.getSession().setAttribute("LOGIN_USER", userModel);
String url = "/shop/home";
return CommonReturnType.create(url);
}
/**
*@Description:用户注册接口
*@Param:注册信息
*@return:
*/
@RequestMapping(value = "/register", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType register(@RequestParam(name = "telphone") String telphone,
@RequestParam(name = "otpCode") String otpCode,
@RequestParam(name = "name") String name,
@RequestParam(name = "gender") String gender,
@RequestParam(name = "password") String password,
@RequestParam(name = "age") Integer age,
@RequestParam(name = "receiverAddress") String receiverAddress) throws BusinessException, UnsupportedEncodingException, NoSuchAlgorithmException {
//验证手机号和对应的otpCode相符合
String inSessionOtpCode = (String) this.httpServletRequest.getSession().getAttribute(telphone);
if (!com.alibaba.druid.util.StringUtils.equals(otpCode, inSessionOtpCode)) {
throw new BusinessException(EmBusinessError.PARAMETER_VALIDATION_ERROR, "短信验证码不符合");
}
//用户的注册流程
UserModel userModel = new UserModel();
userModel.setName(name);
userModel.setGender(gender);
userModel.setAge(age);
userModel.setTelphone(telphone);
userModel.setRegisterMode("byphone");
userModel.setEncrptPassword(this.EncodeByMd5(password));
userModel.setReceiverAddress(receiverAddress);
userService.register(userModel);
return CommonReturnType.create(null);
}
1登录controller
Controller里的代码写的非常清楚(最开始的index()方法可以忽略,这是我写的跳转后台首页),同样是入参校验(这里是仅是判空),接着调用service层的登录方法,最后将登录信息存入session,方便之后的状态控制。要特别说明的是最后的CommonReturnType类型,直接上代码:
private String status;
private Object data;
private String url;
/**
*@Description:定义一个通用的创建方法
*@Param:
*@return:
*/
public static CommonReturnType create(Object result){
return CommonReturnType.create(result ,"success");
}
public static CommonReturnType create(String url){
return CommonReturnType.create(null,"success",url);
}
public static CommonReturnType create(Object result,String status){
CommonReturnType type=new CommonReturnType();
type.setStatus(status);
type.setData(result);
return type;
}
public static CommonReturnType create(Object result,String status,String url){
CommonReturnType type=new CommonReturnType();
type.setStatus(status);
type.setData(result);
type.setUrl(url);
return type;
}
getset省略
这个类放在前面的response包,表示一个通用的返回类型,主要由状态status,数据data构成(最后的属性url是因为我的登录页面是静态页面,而我后来其他页面用的是模板页面写的,所以强行前后端不分离,如果你要写前后端分离,可以不加)。类里面定义了一个creat方法,如果入参为null则默认status为success,接着重写方法将入参填入即可。
2注册controller
使用@RequestParam注解将参数注入,接着验证手机号和对应的otpCode是否符合(otp短信验证码后面讲),最后set属性。这里涉及到Md5加密,我在同一个类里定义了一个EncodeByMd5方法,如下:
@ResponseBody
public String EncodeByMd5(String str) throws NoSuchAlgorithmException, UnsupportedEncodingException {
//确定计算方法
MessageDigest md5 = MessageDigest.getInstance("MD5");
BASE64Encoder base64en = new BASE64Encoder();
//加密字符串
String newstr = base64en.encode(md5.digest(str.getBytes("utf-8")));
return newstr;
}
接下来是用户获取otp短信接口:
@RequestMapping(value = "/getotp", method = {RequestMethod.POST}, consumes = {CONTENT_TYPE_FORMED})
@ResponseBody
public CommonReturnType getOtp(@RequestParam(name = "telphone") String telphone) {
//需要按照一定的规则生成otp验证码
Random random = new Random();
int randomInt = random.nextInt(99999);
randomInt += 10000;
String otpCode = String.valueOf(randomInt);
//将otp验证码同对应用户的手机号关联,使用httpsession的方式绑定用户手机号与otpcCode
httpServletRequest.getSession().setAttribute(telphone, otpCode);
//将otp验证码通过短信通道发送给用户,省略
System.out.println("telphone = " + telphone + " &otpCode = " + otpCode);
return CommonReturnType.create(null);
}
我这里是将otp验证码打印到控制台,只为模拟短信验证流程。(这部分内容网上有很多,我也是照猫画虎copy的)
关于用户后台模块的其他内容我觉得很没有必要写,因为用户后台模块都是一些简单的crud操作,网上的资料实在太多了,非常简单也很容易掌握。然后这里的登录注册涉及到了异常捕获和入参校验,这两部分你要觉得麻烦完全可以不要,直接写登录注册逻辑,但是这样的设计是很不完善的。这两部分内容网上同样有很多资料,我将不再赘述。