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

前言

宠物乐园这个项目是为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)完成注册
springboot+ssm+vue前后端分离项目-宠物之家_第1张图片手机发送短信验证码流程
springboot+ssm+vue前后端分离项目-宠物之家_第2张图片

1.1.2 表的设计

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

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 后端

短信发送:springboot+ssm+vue前后端分离项目-宠物之家_第5张图片

 @Override
    public 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();

完成注册:

 @Override
    public 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作用有。当然也可以做权限判断
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public 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 {

    @Autowired
    private LoginInterceptor loginInterceptor;
    
    // 这个方法用来注册拦截器,我们自己写好的拦截器需要通过这里添加注册才能生效
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // addPathPatterns("/**") 表示拦截所有的请求,
        // excludePathPatterns("/login", "/register") 表示除了登陆与注册之外,因为登陆注册不需要登陆也可以访问
       registry.addInterceptor(loginInterceptor).addPathPatterns("/**").
              excludePathPatterns("/login", "/register","/assets/**","/js/**","nopermission");
    }

}

具体登录

@Override
    public AjaxResult loginAccount(LoginDto loginDto) {
        // 1 校验 null
        String password = loginDto.getPassword();
        if (StringUtils.isEmpty(loginDto.getUsername())||
                StringUtils.isEmpty(password)||
                StringUtils.isEmpty(loginDto.getLoginType()))
            return AjaxResult.me().setSuccess(false).setMessage("请输入相关信息后再登录!");

        //2 从数据库查询 username logintype 用户是否存在,并且状态是否OK
        LoginInfo 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)springboot+ssm+vue前后端分离项目-宠物之家_第6张图片

所以首先需要微信注册,开发者认证,创建网站应用 ,获取到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请求url
    String 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 判断用户是否存在,用户不存在创建用户同时创建logininfo
    if (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业务分析


平台提供基础的洗澡,美容,洁牙等基础服务,用户通过前台页面下单,平台把订单的推送给离用户最近的门店,由门店提供线下服务。
用户到店消费,由店员下单完成交易。
表设计:
服务表
springboot+ssm+vue前后端分离项目-宠物之家_第7张图片服务详情表
springboot+ssm+vue前后端分离项目-宠物之家_第8张图片

2.2后端代码展示

CRUD基本操作

@RestController
@RequestMapping("/product")
public class ProductController {
    @Autowired
    private IProductService service;
    @Autowired
    private IProductDetailService detailService;
    /**增加或修改一条数据*/
    @PutMapping
    public 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);
    }
    /**获取所有数据*/
    @GetMapping
    public List<Product> getAll(){
        return service.findAll();
    }
    /**查询数据,axios get不支持传对象,所以用post*/
    @PostMapping
    public 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) {
    //添加时需要返回id
    productMapper.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修改productDetail
    ProductDetail 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容器部署等,对我的职业无疑是很重要的经历, 感谢项目中的所有成员,争取再接再厉

你可能感兴趣的:(spring,boot,vue.js,docker,mysql,java)