前言

宠物乐园这个项目是为xxx公司开发o2o模式一个项目。它是以宠物为中心,提供宠物领养,寻主的基本功能,还提供了宠物服务,相关物品购买,宠物相关知识学习等功能一个综合性平台。它主要有组织机构,用户,服务,宠物,订单, 支付等模块。它是使用前后端分离开发模式,前端使用的是vue技术栈,后台使用的是springboot+ssm+docker。

一、人员组成

共11人 周期:半年
项目经理:管人管项目 1
架构师: 负责项目架构+技术选型+疑难问题解决+培训 1
UI: 设计界面 1
H5: 前端开发工程师 2
后台: 后台开发工程师 4
测试: 测试人员 1
运维人员: 搭建开发公共环境,线上环境 1。

二、负责模块

1.用户模块

1.1注册

用户注册分为邮箱注册和手机号注册,由于逻辑都是一样的,只是激活方式不一样,所以这里只以手机号为例

1.1.1基本流程

手机号注册:
1)输入手机号
2)获取验证码并且输入
3)输入密码和确认密码
4)完成注册
手机发送短信验证码流程

1.1.2 表的设计

(1) 1张用户信息表t_user
注意:
1.如果是手机号注册,用户的默认状态就是激活状态。如果是邮箱注册,需要给用户发送邮件,让用户登录邮件进行激活。目的是防止用户随便写一个不可用的邮箱注册账号。但是目前大多数新项目都不用了,都用手机号。
2.前台用户注册的密码不能是明文的形式存储在数据库,需要进行加密处理,防止用户信息泄露。
(2)1张登录信息表t_logininfo
表里面有一列栏位type,用于区分是后台工作人员登录还是前台用户登录. 因为我的设计就算你是管理员,你想消费,你也要注册用户,意味着员工表和用户表中分别有记录。 也就会造成logininfo有相同电话或用户名或email等两个记录,通过type进行区分。

1.1.3 技术准备

(1)工具类StrUtils,生成随机字符串,作为验证码

    public static String getComplexRandomString(int length) {String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";Random random = new Random();StringBuffer sb = new StringBuffer();for (int i = 0; i < length; i++) {int number = random.nextInt(62);sb.append(str.charAt(number));}return sb.toString();}

(2)md5不可逆加密,用来加密用户登录密码
盐值salt:同一种加密算法,由于不同的盐值,加密出来就不一样。
1)整个系统使用同一个盐值,多个用户共用-定义一个常量
每个用户都有自己盐值,就算是相同的密码,两个用用户加密出来也不一样

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;public class MD5Utils {/*** 加密* @param context*/public static String encrypByMd5(String context) {try {  MessageDigest md = MessageDigest.getInstance("MD5");md.update(context.getBytes());//update处理  byte [] encryContext = md.digest();//调用该方法完成计算  int i;  StringBuffer buf = new StringBuffer("");  for (int offset = 0; offset < encryContext.length; offset++) {//做相应的转化(十六进制)  i = encryContext[offset];  if (i < 0) i += 256;  if (i < 16) buf.append("0");  buf.append(Integer.toHexString(i));  }  return buf.toString();} catch (NoSuchAlgorithmException e) {// TODO Auto-generated catch block  e.printStackTrace();return  null;}  }

(3)引入redis数据库

        <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency>

(4)短信发送接口
找三大运营,或者中间商,我们项目中使用网建短信通
链接: http://sms.webchinese.com.cn/Rates.shtml
导包,并准备发送短信的工具类

     <dependency><groupId>commons-httpclient</groupId><artifactId>commons-httpclient</artifactId><version>3.1</version></dependency>

1.1.4 后端

短信发送:

 @Overridepublic AjaxResult sendSmsCode(Map<String, String> param) {String phone = param.get("phone");if(!StringUtils.hasLength(phone)) {return AjaxResult.result().setMessage("请输入手机号");}User user = userMapper.loadByPhone(phone);if(user != null){return AjaxResult.result().setMessage("该手机号已被注册");}String key = "registerCode" + phone;String value =(String)redisTemplate.opsForValue().get(key);String code = "";//如果value为null,说明是第一次或者过期了,则生成随机验证码,并且下面会存放到redis里面去if(!StringUtils.hasLength(value)){code = StrUtils.getComplexRandomString(4);}else {//如果value不为空,说明存在验证码,判断是否过了重发事件,还是用同一个验证码String time = value.split(":")[1];if(System.currentTimeMillis()-Long.parseLong(time) < 1*60*1000){return AjaxResult.result().setMessage("请不要频繁发送");}code = value.split(":")[0];}//存放到redis,有效时间3分钟.设置value的格式为 验证码:存入时间int expireTime = 3;redisTemplate.opsForValue().set(key,code +":"+ System.currentTimeMillis(),expireTime,TimeUnit.MINUTES);//发送短信验证码给该手机号System.out.println("已经给"+phone+"发送了验证码:"+code +" 发送时间:"+ new Date().toLocaleString());//SmsUtil.sendSms(phone,"验证码为:"+code+",请在"+expireTime+"分钟之内使用!当前时间:"+//                new Date().toLocaleString());return AjaxResult.result();

完成注册:

 @Overridepublic AjaxResult register(UserDto userDto) {if (StringUtils.isEmpty(userDto.getPhone()) ||  StringUtils.isEmpty(userDto.getSmsCode()) ||StringUtils.isEmpty(userDto.getPassword()) || StringUtils.isEmpty(userDto.getConfirmPwd())){return AjaxResult.result().setMessage("请完善必填信息");}if (!userDto.getPassword().equals(userDto.getConfirmPwd())){return AjaxResult.result().setMessage("两次密码不一致");}User user = userMapper.loadByPhone(userDto.getPhone());if (user != null){return AjaxResult.result().setMessage("该手机号已被注册");}Object value = redisTemplate.opsForValue().get("registerCode" + userDto.getPhone());if (value == null){return AjaxResult.result().setMessage("验证码未获取或过期,请重新获取验证码");}String codeOnRedis = (String)value;String code = codeOnRedis.split(":")[0];if (!userDto.getSmsCode().equalsIgnoreCase(code)){return AjaxResult.result().setMessage("请输入正确的验证码");}//把注册信息转为LoginInfo对象,然后保存数据库,返回自增的id给User对象用来新增LoginInfo loginInfo = userDto2LoginInfo(userDto);infoMapper.save(loginInfo);User userTmp = loginInfo2User(loginInfo);userMapper.save(userTmp);return AjaxResult.result();

1.2登录

基本分析:后台登录(管理员账号),前台登录(用户账号/手机验证码/三方登录)

1.2.1流程设计

由于是前后端分离,所以不能用传统的cookie保存用户.所以这里我们采用的是token方案, 完全抛弃session,自动过期30分钟,用redis来替代
怎么让token每次都携带过去?
1)登录成功后把token存放到浏览器 localStorage
2)每次对后端的请求,都从浏览器获取token并且携带过去
对后端请求都是使用axios,使用axios前置拦截器给请求追加一个请求头,后置拦截跳转到登录界面
前端拦截 :路由拦截器(拦截一些不需要访问后端的资源)

1.2.2技术准备

UUID, Springmvc拦截器-springboot
localStorage ,axios前/后置拦截器, 路由拦截器

1.2.3代码实现

后端拦截器

@Component
public class LoginInterceptor implements HandlerInterceptor {//判断是否有token,并在redis作用有。当然也可以做权限判断@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {String token = request.getHeader("token");if (!StringUtils.isEmpty(token)){System.out.println(token+"    enter...................");Object obj = redisTemplate.opsForValue().get(token);if (obj!=null){LoginInfo loginInfo = (LoginInfo) obj;//更新时间,让redis中数据不过期,类似于只要点页面session不过期             redisTemplate.opsForValue().set(token,loginInfo,30, TimeUnit.MINUTES);//@TODO 权限校验return true;}}//校验没有通过 返回一个前台能够识别错误 {"success":false,"message":'noLogin'}response.setCharacterEncoding("utf-8");response.setContentType("application/json; charset=utf-8");PrintWriter writer = response.getWriter();writer.write("{\"success\":false,\"message\":\"noLogin\"}");writer.flush();writer.close();return false;}
}
@Configuration
public class WebConfigurer implements WebMvcConfigurer {@Autowiredprivate LoginInterceptor loginInterceptor;// 这个方法用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效@Overridepublic void addInterceptors(InterceptorRegistry registry) {// addPathPatterns("/**") 表示拦截所有的请求,// excludePathPatterns("/login", "/register") 表示除了登陆与注册之外,因为登陆注册不需要登陆也可以访问registry.addInterceptor(loginInterceptor).addPathPatterns("/**").excludePathPatterns("/login", "/register","/assets/**","/js/**","nopermission");}}

具体登录

@Overridepublic AjaxResult loginAccount(LoginDto loginDto) {// 1 校验 nullString password = loginDto.getPassword();if (StringUtils.isEmpty(loginDto.getUsername())||StringUtils.isEmpty(password)||StringUtils.isEmpty(loginDto.getLoginType()))return AjaxResult.me().setSuccess(false).setMessage("请输入相关信息后再登录!");//2 从数据库查询 username logintype 用户是否存在,并且状态是否OKLoginInfo loginInfo= loginInfoMapper.loadByUserName(loginDto);if (loginInfo==null)return AjaxResult.me().setSuccess(false).setMessage("用户名或密码不正确!");if (loginInfo.getDisable()!=1)return AjaxResult.me().setSuccess(false).setMessage("用户已被禁用!请联系管理员!");//3 进行秘码比对String salt = loginInfo.getSalt();String secretPwd = MD5Utils.encrypByMd5(password + salt);if (!secretPwd.equals(loginInfo.getPassword()))return AjaxResult.me().setSuccess(false).setMessage("用户名或密码不正确!");//4 存放redis并返回token,其实为了前台不额外发一个请求可以把用户和token一起返回,但是密码要置空String token = UUID.randomUUID().toString();redisTemplate.opsForValue().set(token,loginInfo,30, TimeUnit.MINUTES);// 返回token,用户,所以用map返回Map<String,Object> result = new HashMap<>();result.put("token",token);loginInfo.setPassword(""); //但是密码要置空result.put("user",loginInfo);return AjaxResult.me().setResultObj(result);}

axios前/后置拦截

axios.interceptors.request.use(config=>{//携带token
let uToken =  localStorage.getItem("uToken");
if(uToken){config.headers['U-TOKEN']=uToken;
}
return config;
},error => {Promise.reject(error);
})
//2 使用axios后置拦截器,处理没有登录请求
axios.interceptors.response.use(result=>{console.log(result.data+"jjjjjjj");let data = result.data;if(!data.success && data.message==="noUser")location.href = "/login.html";return result;
},error => {Promise.reject(error);
})

1.2.4三方登录-微信登录的实现

微信登录的原理

(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI",同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)

所以首先需要微信注册,开发者认证,创建网站应用 ,获取到appid和appsecret,配置好回调域名
核心代码:

@Override
public AjaxResult loginWeixin(Map<String, String> params) {//1获取参数String code = params.get("code");String binderUrl = params.get("binderUrl");// 2 发起获取accesstoken// 2.1获取accesstoken请求urlString url = WxConstants.GET_ACCESSTOKEN_URL.replace("APPID",WxConstants.APPID).replace("SECRET",WxConstants.SECURITY).replace("CODE",code);//2.2 发请求String jsonStr = HttpClientUtils.httpGet(url);JSONObject jsonObject = JSONObject.parseObject(jsonStr);String accessToken = jsonObject.getString("access_token");String openid = jsonObject.getString("openid");System.out.println(accessToken);System.out.println(openid);//3 通过openid(微信号的唯一性标识)User user = userMapper.loadByOpenId(openid);if (user!=null){//3.1 已经绑定,做免密登录//4 存放redis并返回token,其实为了前台不额外发一个请求可以把用户和token一起返回,但是密码要置空LoginInfo loginInfo = loginInfoMapper.loadById(user.getLogininfo_id());String token = UUID.randomUUID().toString();redisTemplate.opsForValue().set(token,loginInfo,30, TimeUnit.MINUTES);// 返回token,用户,所以用map返回Map<String,Object> result = new HashMap<>();result.put("token",token);loginInfo.setPassword(""); //但是密码要置空result.put("user",loginInfo);return AjaxResult.me().setResultObj(result);}else{binderUrl=binderUrl+"?accessToken="+accessToken+"&openid="+openid;return AjaxResult.me().setSuccess(false).setMessage("binder").setResultObj(binderUrl);}
}

如果该微信没有对应账号,那么前台跳转到绑定页面,需要绑定/注册账号

@Override
public AjaxResult binder(LoginDto loginDto) {String accessToken = loginDto.getAccessToken();String openid = loginDto.getOpenid();//1 查询微信用户信息String url = WxConstants.GET_USER_URL.replace("ACCESS_TOKEN",accessToken).replace("OPENID",openid);String jsonStr = HttpClientUtils.httpGet(url);User user = null;LoginInfo loginInfo = loginInfoMapper.loadByUserName(loginDto);//2 判断用户是否存在,用户不存在创建用户同时创建logininfoif (loginInfo==null){user = wxUserJsonStr2User(jsonStr,loginDto);loginInfo = userService.addOfReturnLoginInfo(user);}else {//3 如果用户存在并且密码正确,获取用户String md5Pwd = MD5Utils.encrypByMd5(loginDto.getPassword() + loginInfo.getSalt());System.out.println(md5Pwd);System.out.println(loginInfo.getPassword());if (!loginInfo.getPassword().equals(md5Pwd))return AjaxResult.me().setSuccess(false).setMessage("用户名或密码不正确!");user = userMapper.loadByUsername(loginDto.getUsername());}//4 创建微信用户,并且和用户进行绑定WxUser wxUser = wxUserJsonStr2WxUser(jsonStr);wxUser.setUser_id(user.getId());wxUserMapper.save(wxUser);//5 免密登录String token = UUID.randomUUID().toString();Map<String,Object> result = new HashMap<>();result.put("token",token);loginInfo.setPassword(""); //但是密码要置空result.put("user",loginInfo);return AjaxResult.me().setResultObj(result);
}

1.3关注列表,历史记录…

基本的CRUD,这里就不一一详细说明

2.服务模块

2.1业务分析


平台提供基础的洗澡,美容,洁牙等基础服务,用户通过前台页面下单,平台把订单的推送给离用户最近的门店,由门店提供线下服务。
用户到店消费,由店员下单完成交易。
表设计:
服务表
服务详情表

2.2后端代码展示

CRUD基本操作

@RestController
@RequestMapping("/product")
public class ProductController {@Autowiredprivate IProductService service;@Autowiredprivate IProductDetailService detailService;/**增加或修改一条数据*/@PutMappingpublic AjaxResult save(@RequestBody Product product){try {Long id = product.getId();if (id == null){service.add(product);} else {service.update(product);}return AjaxResult.result();} catch (Exception e) {e.printStackTrace();return AjaxResult.result().setMessage("保存失败---"+e.getMessage());}}/**删除一条数据*/@DeleteMapping("/{id}")public AjaxResult delete(@PathVariable("id") Long id){try {service.delete(id);return AjaxResult.result();} catch (Exception e) {e.printStackTrace();return AjaxResult.result().setMessage("删除失败---"+e.getMessage());}}/**获取一条数据*/@GetMapping("/{id}")public Product getById(@PathVariable("id") Long id){return service.findById(id);}@GetMapping("/detail/{pid}")public ProductDetail getDetailByPid(@PathVariable("pid") Long pid){return detailService.getDetailByPid(pid);}/**获取所有数据*/@GetMappingpublic List<Product> getAll(){return service.findAll();}/**查询数据,axios get不支持传对象,所以用post*/@PostMappingpublic PageList<Product> list(@RequestBody ProductQuery query){return service.queryPage(query);}/**只展示上架的商品*/@PostMapping("/list")public PageList<Product> onSaleList(@RequestBody ProductQuery query){query.setState(1);return service.queryPage(query);}/**批量删除,axios delete不支持传对象,所以用patch*/@PatchMapping("/batch")public AjaxResult batchRemove(@RequestBody List<Long> ids){try {service.batchRemove(ids);return AjaxResult.result();} catch (Exception e) {e.printStackTrace();return AjaxResult.result().setMessage("批量删除失败"+e.getMessage());}}@PatchMapping("/onSale")public AjaxResult onSale(@RequestBody List<Long> ids){try {service.batchSale(ids,1);return AjaxResult.result();} catch (Exception e) {e.printStackTrace();return AjaxResult.result().setMessage("批量上架失败"+e.getMessage());}}@PatchMapping("/offSale")public AjaxResult offSale(@RequestBody List<Long> ids){try {service.batchSale(ids,0);return AjaxResult.result();} catch (Exception e) {e.printStackTrace();return AjaxResult.result().setMessage("批量下架失败"+e.getMessage());}}}

注意:product的增删改,需要与product_detail同步

@Override
public void save(Product product) {//添加时需要返回idproductMapper.save(product);ProductDetail detail = product.getDetail();detail.setProduct_id(product.getId());productDetailMapper.save(detail);
}@Override
public void update(Product product) {productMapper.update(product);//没有详情id 通过product_id修改productDetailProductDetail detail = product.getDetail();detail.setProduct_id(product.getId());productDetailMapper.updateByProductId(detail);
}

服务的上架下架(就是修改数据表中state的值)

@Override
public void batchSale(int flag, List<Long> ids) {if (flag==1){//修改上架状态及时间Map<String,Object> params = new HashMap<>();params.put("onsaleTime",new Date());params.put("ids",ids);productMapper.onsale(params);//update t_product set onsaleTime = xx and state = 1 where id in(1,2,3)}else{//修改下架状态及时间Map<String,Object> params = new HashMap<>();params.put("offsaleTime",new Date());params.put("ids",ids);productMapper.offSale(params);//update t_product set offsaleTime = xx and state = 0 where id in(1,2,3)}

三、项目技术

1)Springboot
2)FastDFS
3)Redis
4)短信消息
5)百度地图
6)微信三方登录
7)支付宝支付
8)加密技术
9)邮件技术
10)Quartz


总结

前后花了几个月,总算完成了这个项目,虽然参与的部分很少,但是收获却很多. 第一次真正接触前后端分离的项目,了解vue的基本语法,element组件,docker容器部署等,对我的职业无疑是很重要的经历, 感谢项目中的所有成员,争取再接再厉

springboot+ssm+vue前后端分离项目-宠物之家相关推荐

  1. SpringBoot+MyBatisPlus+Vue 前后端分离项目快速搭建【后端篇】【快速生成后端代码、封装结果集、增删改查、模糊查找】【毕设基础框架】

    前后端分离项目快速搭建[后端篇] 数据库准备 后端搭建 1.快速创建个SpringBoot项目 2.引入依赖 3.编写代码快速生成代码 4.运行代码生成器生成代码 5.编写application.pr ...

  2. SpringBoot+MyBatisPlus+Vue 前后端分离项目快速搭建【前端篇】【快速生成后端代码、封装结果集、增删改查、模糊查找】【毕设基础框架】

    前后端分离项目快速搭建[前端篇] 后端篇 前端篇 创建vue项目 安装所需工具 开始编码 1.在根目录下添加vue.config.js文件 2.编写main.js 3.编写App.vue 4.编写ax ...

  3. 基于SpringBoot+SpringCloud+Vue前后端分离项目实战 --开篇

    本文目录 前言 做项目的三大好处 强强联手(天狗组合) 专栏作者简介 专栏的优势 后端规划 1. SpringBoot 和 SpringCloud 的选择 2. Mybatis 和 MybatisPl ...

  4. 计算机毕业设计音乐论坛系统ssm+vue前后端分离项目

  5. 基于SSM+SpringBoot+MySQL+Vue前后端分离的博客论坛系统

    项目运行截图 系统首页 技术描述 开发工具: idea/eclipse 数据库: mysql Jar包仓库: Maven 前段框架: vue/ElementUI/echart 后端框架: spring ...

  6. 从0搭建一个Springboot+vue前后端分离项目(一)安装工具,创建项目

    从0搭建一个Springboot+vue前后端分离项目(二)使用idea进行页面搭建+页面搭建 参考学习vue官网文档 https://v3.cn.vuejs.org/guide/installati ...

  7. 阿里服务器部署springboot+vue前后端分离项目

    服务器部署springboot+vue前后端分离项目 最近刚刚在实习熟悉公司的业务,所有尝试着自己将项目部署到服务器上.本次部署的项目是Spring Boot+Vue前后端分离项目,后端使用的技术有M ...

  8. 【JAVA程序设计】基于SSM+VUE前后端分离的物流管理系统

    基于SSM+VUE前后端分离的物流管理系统 零.项目获取 一.项目简介 二.开发环境 三.项目技术 四.系统架构 五.运行截图 零.项目获取 获取方式(点击下载):是云猿实战 项目经过多人测试运行,可 ...

  9. 视频教程-SpringBoot+Security+Vue前后端分离开发权限管理系统-Java

    SpringBoot+Security+Vue前后端分离开发权限管理系统 10多年互联网一线实战经验,现就职于大型知名互联网企业,架构师, 有丰富实战经验和企业面试经验:曾就职于某上市培训机构数年,独 ...

  10. phython在file同时写入两个_喜大普奔,两个开源的 Spring Boot + Vue 前后端分离项目可以在线体验了

    折腾了一周的域名备案昨天终于搞定了. 松哥第一时间想到赶紧把微人事和 V 部落部署上去,我知道很多小伙伴已经等不及了. 1. 也曾经上过线 其实这两个项目当时刚做好的时候,我就把它们部署到服务器上了, ...

最新文章

  1. 【剑指offer-Java版】33把数组排成最小的数
  2. 先睹为快:Visual Studio 11测试版已于2.29在微软官方网站正式发布
  3. ocr识别技术-车牌识别一体机的核心关键
  4. 平正真诚——记红帆公司2011年秋季旅游·衡山
  5. 【电子书】C++编程开发30问
  6. cookie的简单学习
  7. 冒泡排序python代码_Python-排序-冒泡排序-优化
  8. Ubuntu系统备份工具大全(官方整理推荐)
  9. 前端学习(2214):认识react(1)
  10. JEECG 树列表操作总刷新列表,需要重新展开问题 【官方补丁,适用所有版本】
  11. Linux中errno的含义
  12. np.c_和np.r_
  13. 架构-LAMP特级学习(网站加速解决方案)
  14. C语言-排序中的快速排序(简称快排)
  15. 调用opensmile编译的DLL动态库API进行声音特征提取
  16. 游戏开发中的矩阵与变换
  17. 初识vue的使用和设计模式
  18. 解决桌面图标无法拖动的方法
  19. via自定义搜索引擎代码_Via浏览器定制进阶
  20. Memcache教程

热门文章

  1. 【重装系统Win10】U盘安装windows10正版操作系统 - 固体硬盘版(¥4.5-200)
  2. Android Studio之仿微信APP界面设计
  3. 躲避球av_躲避球HTML!
  4. CSDN调整图片大小、位置
  5. 【067】Color Hunter-用图片搜索配色方案
  6. 【微软内推】微软2023届校园招聘开始啦
  7. 微软2023届校招开始啦(文内有内推方法)
  8. Centos7 切换为163 网易yum
  9. 漫画 |《帝都程序猿十二时辰》
  10. 重庆大学科幻协会发展史