瑞吉外賣實戰項目全攻略——第五天

瑞吉外賣實戰項目全攻略——第五天

該系列將記錄一份完整的實戰項目的完成過程,該篇屬於第五天

案例來自B站黑馬程序員Java項目實戰《瑞吉外賣》,請結合課程資料閱讀以下內容

該篇我們將完成以下內容:

  • 新增套餐
  • 套餐信息分頁查詢
  • 批量停售/啟售
  • 刪除套餐
  • 修改套餐
  • 短訊發送
  • 手機驗證碼登錄

新增套餐

我們的功能開發通常分為三部分

需求分析

我們先打開F12,點開新增套餐,可以發現頁面直接發送了兩個請求

首先我們查看第一個請求:

這個請求是寫在CategoryController中用於查看套餐分類的請求,我們在前面已經完成了,它是為了展示套餐分類下拉框操作的:

然後還有第二個請求:

它是針對我們的菜品裏面的分類的獲取:

但是當我們點擊菜品中的響應菜品時,會跳出第三個請求,這個請求是我們需要完成的根據菜品分類id查找分類內的菜品:

另外還有一個未完成的操作當然是點擊保存後將數據傳遞到數據庫中:

然後我們查看一下我們這個操作需要的數據表Setmeal和SetmealDish

Setmeal是套餐表,用於存儲套餐的相關信息:

SetmealDish是套餐與菜品的關聯表,用於儲存兩者之間的關係:

最後注意我們在提交信息時的請求體中的數據是兩個數據表的集合

所以我們需要採用DTO的實體類來完成接收操作以及相關的業務開發操作

代碼實現

首先我們先來完成第一個操作,根據菜品分類id獲得相應菜品:

package com.qiuluo.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Dish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.DishServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/dish")
public class DishController {

    @Autowired
    private DishServiceImpl dishService;

    @Autowired
    private CategoryServiceImpl categoryService;

    /**
     * 根據id查詢菜品
     * @param dish
     * @return
     */
    @GetMapping("/list")
    public Result<List<Dish>> list(Dish dish){

        // 提取CategoryID
        Long id = dish.getCategoryId();

        // 判斷條件
        LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(id != null,Dish::getCategoryId,id);
        queryWrapper.eq(Dish::getStatus,1);
        queryWrapper.orderByAsc(Dish::getSort);

        List<Dish> list = dishService.list(queryWrapper);

        return Result.success(list);

    }
}

然後我們來完成比較複雜的提交套餐保存的功能:

  1. 準備工作
// 我們需要提前準備好一些接口以及實現類(形式類似,這裡不再贅述)

實體類SetmealDish
數據層SetmealDishMapper
業務層接口SetmealDishService
業務層SetmealDishServiceImpl 
服務層SetmealController
  1. 定義DTO實體類
package com.qiuluo.reggie.dto;

import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import lombok.Data;
import java.util.List;

@Data
public class SetmealDto extends Setmeal {

    private List<SetmealDish> setmealDishes;

    private String categoryName;
}
  1. 去業務層接口定義方法
package com.qiuluo.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.SetmealDto;

import java.util.List;

// 針對我們無法採用默認方法解決的功能,我們需要自己書寫方法
public interface SetmealService extends IService<Setmeal> {

    /**
     * 帶菜品關聯一同保存
     * @param setmealDto
     */
    public void saveWithDish(SetmealDto setmealDto);

}
  1. 業務層實現方法
package com.qiuluo.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qiuluo.reggie.common.CustomException;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.mapper.SetmealMapper;
import com.qiuluo.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper,Setmeal> implements SetmealService {

    @Autowired
    private SetmealDishServiceImpl setmealDishService;

    /**
     * 帶菜品一同保存
     * @param setmealDto
     */
    public void saveWithDish(SetmealDto setmealDto){
        // 保存套餐數據
        this.save(setmealDto);

        //保存套餐的菜品數據(注意:傳進的菜品關聯信息里沒有套餐的id,所以我們需要手動傳入)
        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();

        setmealDishes.stream().map((item)->{

            item.setSetmealId(setmealDto.getId());
            return item;

        }).collect(Collectors.toList());

        setmealDishService.saveBatch(setmealDishes);

    }

}
  1. 服務層完成功能
package com.qiuluo.reggie.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {

    @Autowired
    private SetmealServiceImpl setmealService;

    @Autowired
    private SetmealDishServiceImpl setmealDishService;

    @Autowired
    private CategoryServiceImpl categoryService;

    @PostMapping
    public Result<String> save(@RequestBody SetmealDto setmealDto){

        setmealService.saveWithDish(setmealDto);

        log.info("套餐新增成功");

        return Result.success("新創套餐成功");
    }

}

實際測試

我們需要測試兩點

  • 打開新創頁面時,點擊菜品可以看到菜品分類後的相關菜品
  • 點擊新增套餐,填寫數據後提交,數據庫中可以看到相關新添菜品

套餐信息分頁查詢

我們的功能開發通常分為三部分

需求分析

該功能與菜品管理的分頁查詢功能相似,我們不做贅述,簡單步驟如下

我們直接點擊套餐頁面,F12查看相關代碼即可:

因為返回數據中包含新的屬性categoryName,我們需要採用DTO來完成該操作

代碼實現

我們直接在SetmealController中完成該操作即可:

package com.qiuluo.reggie.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {

    @Autowired
    private SetmealServiceImpl setmealService;

    @Autowired
    private SetmealDishServiceImpl setmealDishService;

    @Autowired
    private CategoryServiceImpl categoryService;

    @GetMapping("page")
    public Result<Page> page(int page, int pageSize, String name){

        // 構造分頁器
        Page<Setmeal> pageInfo = new Page<>(page,pageSize);
        Page<SetmealDto> setmealDtoPage = new Page<>();

        // 構造條件
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(name != null,Setmeal::getName,name);
        queryWrapper.orderByDesc(Setmeal::getUpdateTime);

        // 查詢
        setmealService.page(pageInfo);

        // 賦值
        BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");

        List<Setmeal> records = pageInfo.getRecords();

        List<SetmealDto> list = records.stream().map((item) -> {

            SetmealDto setmealDto = new SetmealDto();

            BeanUtils.copyProperties(item,setmealDto);

            // 將CategoryName複製進去
            Long categoryId = item.getCategoryId();
            Category category = categoryService.getById(categoryId);

            if(category != null){
                String categoryName = category.getName();
                setmealDto.setCategoryName(categoryName);
            }

            return setmealDto;
        }).collect(Collectors.toList());

        setmealDtoPage.setRecords(list);

        // 返回結果
        return Result.success(setmealDtoPage);
    }

}

實際測試

在打開套餐分類頁面時,所有信息呈現即為功能開發成功

批量停售/啟售

我們的功能開發通常分為三部分

需求分析

該功能視頻中沒有講述,屬於課後簡單作業

我們的刪除功能中需要刪除已經停售的套餐業務,所以我們提前處理頁面的停售啟售操作

我們點擊單個停售啟售以及多個停售啟售可以觀察到url以及請求方法:

// 單個停售
請求 URL: //localhost:8080/setmeal/status/0?ids=1415580119015145474
請求方法: POST

// 多個停售
請求 URL: //localhost:8080/setmeal/status/0?ids=1415580119015145474,1583260610277715970
請求方法: POST

// 單個啟售
請求 URL: //localhost:8080/setmeal/status/1?ids=1415580119015145474
請求方法: POST

// 多個啟售
請求 URL: //localhost:8080/setmeal/status/1?ids=1415580119015145474,1583260610277715970
請求方法: POST

我們可以注意到我們的啟售,停售操作其實可以簡化為一個操作或兩個操作完成,這裡我們分為兩個操作完成

代碼實現

我們直接在SetmealController中完成該操作即可:

package com.qiuluo.reggie.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {

    @Autowired
    private SetmealServiceImpl setmealService;

    @Autowired
    private SetmealDishServiceImpl setmealDishService;

    @Autowired
    private CategoryServiceImpl categoryService;

    // 其實還可以整合成一個方法,url寫為/status,讀取後面的值設置為status,根據status對ids進行操作即可
    
    @PostMapping("/status/0")
    public Result<String> closeStatus(@RequestParam List<Long> ids){

        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
        queryWrapper.in(Setmeal::getId,ids);

        List<Setmeal> setmeals = setmealService.list(queryWrapper);

        for (Setmeal setmeal:setmeals
        ) {
            setmeal.setStatus(0);
            setmealService.updateById(setmeal);
        }

        return Result.success("修改成功");
    }

    @PostMapping("/status/1")
    public Result<String> openStatus(@RequestParam List<Long> ids){

        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper();
        queryWrapper.in(Setmeal::getId,ids);

        List<Setmeal> setmeals = setmealService.list(queryWrapper);

        for (Setmeal setmeal:setmeals
        ) {
            setmeal.setStatus(1);
            setmealService.updateById(setmeal);
        }

        return Result.success("修改成功");
    }

}

實際測試

回到套餐管理頁面,點擊啟售停售,操作成功即為功能開發成功

刪除套餐

我們的功能開發通常分為三部分

需求分析

我們刪除套餐的基本原則是當前套餐需要處於停售階段才可以刪除,所以我們在處理時需要先做判斷

我們需要完成單個刪除和多個刪除的操作,我們首先對兩個操作進行簡單分析

我們同樣F12查看url以及請求方式:

// 單個刪除
請求 URL: //localhost:8080/setmeal?ids=1583260610277715970
請求方法: DELETE

// 多個刪除
請求 URL: //localhost:8080/setmeal?ids=1583260610277715970,1583260610277715970
請求方法: DELETE

我們可以看到單個刪除操作和多個刪除操作可以直接歸為一個方法中實現

代碼實現

該操作比較複雜,我們分為幾個步驟:

  1. 業務層接口定義方法
package com.qiuluo.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.SetmealDto;

import java.util.List;

public interface SetmealService extends IService<Setmeal> {

    /**
     * 帶菜品關聯一同刪除
     * @param ids
     */
    public void removeWithDish(List<Long> ids);

}
  1. 業務層實現方法
package com.qiuluo.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qiuluo.reggie.common.CustomException;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.mapper.SetmealMapper;
import com.qiuluo.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper,Setmeal> implements SetmealService {

    @Autowired
    private SetmealDishServiceImpl setmealDishService;

    /**
     * 帶菜品關聯一同刪除
     * @param ids
     */
    public void removeWithDish(List<Long> ids){

        // 判斷是否是停售狀態,若不為停售不能刪除,拋出業務異常
        LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.in(Setmeal::getId,ids);
        queryWrapper.eq(Setmeal::getStatus,1);

        int count = this.count(queryWrapper);

        if (count > 0){
            throw new CustomException("刪除業務中有套餐處於啟售狀態,無法刪除");
        }

        // 可以刪除後執行刪除操作

        // 先刪除套餐
        this.removeByIds(ids);

        // 再刪除套餐關聯信息
        LambdaQueryWrapper<SetmealDish> lambdaQueryWrapper = new LambdaQueryWrapper();
        lambdaQueryWrapper.in(SetmealDish::getSetmealId,ids);

        setmealDishService.remove(lambdaQueryWrapper);

    }

}
  1. 服務層實現功能
package com.qiuluo.reggie.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {

    @Autowired
    private SetmealServiceImpl setmealService;

    @DeleteMapping
    public Result<String> delete(@RequestParam List<Long> ids){

        setmealService.removeWithDish(ids);

        return Result.success("刪除成功");
    }

}

實際測試

回到套餐管理頁面,點擊單個刪除多個刪除,刪除成功即為功能開發成功

修改套餐

我們的功能開發通常分為三部分

需求分析

該功能視頻沒有提及,屬於課後作業

首先我們打開F12點擊修改操作,查看出錯請求:

我們可以看到是GET請求,大概率是想要根據id獲得當前套餐的相關信息並返回給頁面

然後我們填寫信息後點擊提交,我們可以通過F12查看到相關url以及提交方式

該操作應該屬於更新操作

代碼實現

我們首先來完成頁面回顯操作:

package com.qiuluo.reggie.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {

    @Autowired
    private SetmealServiceImpl setmealService;

    @Autowired
    private SetmealDishServiceImpl setmealDishService;

    @Autowired
    private CategoryServiceImpl categoryService;

    @GetMapping("/{id}")
    public Result<SetmealDto> getById(@PathVariable Long id){

        // 我們需要把setmealDto返回回去,定義一個新的setmealDto用於保存數據
        SetmealDto setmealDto = new SetmealDto();

        // 將普通數據傳入

        Setmeal setmeal = setmealService.getById(id);

        BeanUtils.copyProperties(setmeal,setmealDto);

        // 將菜品信息傳遞進去

        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,id);

        List<SetmealDish> list = setmealDishService.list(queryWrapper);

        setmealDto.setSetmealDishes(list);

        // 返回setmealDto即可
        return Result.success(setmealDto);
    }
}

然後我們再來完成比較複雜的修改保存信息操作:

  1. 業務層接口定義
package com.qiuluo.reggie.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.dto.SetmealDto;

import java.util.List;

public interface SetmealService extends IService<Setmeal> {

    /**
     * 修改操作
     * @param setmealDto
     */
    public void updateWithDish(SetmealDto setmealDto);

}
  1. 業務層定義
package com.qiuluo.reggie.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.qiuluo.reggie.common.CustomException;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.mapper.SetmealMapper;
import com.qiuluo.reggie.service.SetmealService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;

import java.util.List;
import java.util.stream.Collectors;

@Service
@Slf4j
public class SetmealServiceImpl extends ServiceImpl<SetmealMapper,Setmeal> implements SetmealService {

    @Autowired
    private SetmealDishServiceImpl setmealDishService;

    /**
     * 修改操作
     * @param setmealDto
     */
    public void updateWithDish(SetmealDto setmealDto){

        // 首先修改套餐上的信息
        this.updateById(setmealDto);

        // 修改內部菜品操作(同樣先刪除再添加)

        // 刪除操作
        Long setmealId = setmealDto.getId();

        LambdaQueryWrapper<SetmealDish> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(SetmealDish::getSetmealId,setmealId);

        setmealDishService.remove(queryWrapper);

        // 新填操作

        List<SetmealDish> setmealDishes = setmealDto.getSetmealDishes();

        setmealDishService.saveBatch(setmealDishes);

    }

}
  1. 服務層實現功能
package com.qiuluo.reggie.controller;


import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.api.R;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.Category;
import com.qiuluo.reggie.domain.Setmeal;
import com.qiuluo.reggie.domain.SetmealDish;
import com.qiuluo.reggie.dto.DishDto;
import com.qiuluo.reggie.dto.SetmealDto;
import com.qiuluo.reggie.service.impl.CategoryServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealDishServiceImpl;
import com.qiuluo.reggie.service.impl.SetmealServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequestMapping("/setmeal")
public class SetmealController {

    @Autowired
    private SetmealServiceImpl setmealService;

    @PutMapping
    public Result<String> update(@RequestBody SetmealDto setmealDto){

        setmealService.updateById(setmealDto);

        return Result.success("修改成功");

    }
}

實際測試

我們需要注意兩處測試內容:

  • 點擊修改功能後,打開修改頁面,頁面出現相關信息
  • 點擊修改完成功能後,頁面信息內容成功修改即可

短訊發送

短訊發送章節比較特殊,我們在下面做簡單了解

短訊服務介紹

目前市場上有很多第三方提供的短訊服務,這些第三方短訊服務會和各個運營商(移動,聯通,電信)對接

我們只需要註冊稱為會員並按照提供的開發文檔進行調用就可以發送短訊

目前我們常見的提供短訊服務的公司:

  • 阿里雲
  • 華為雲
  • 騰訊雲
  • 京東
  • 夢網
  • 樂信

阿里雲短訊服務介紹

阿里雲短訊服務(Short Message Service)是廣大企業客戶快速觸達手機用戶所優選使用的通信能力

我們可以直接調用阿里雲的API或者群發助手就可以發送驗證碼,通知類信息和營銷類短訊等,速度快,安全穩定

接下來我們來簡單介紹阿里雲短訊申請的具體流程:

  1. 阿里雲賬號註冊/登錄

  1. 進入賬號中心進行實名認證等信息設置

  1. 進入控制台來到短訊服務頁面

  1. 設置短訊簽名:短訊是短訊發送者的署名,表示發送方的身份(手續繁雜,我們僅作了解)

  1. 設置模板:模板就是發送短訊的形式,其中${code}表示驗證碼佔位符(手續繁雜,但系統自動贈送一條模板)

  1. 設置AccessKey:使用子用戶表示有部分權限(在用戶頭像彈出的窗口中找到)

  1. 用戶界面新創用戶(選擇API調用訪問)

  1. 創建後會給出對應ID和密碼,請保存(AccessKey ID 和 AccessKey Secret)
# 不方便展示
  1. 為我們的用戶添加對應的SMS權限即可

到這裡阿里雲頁面操作基本結束

代碼開發部分內容展示

我們在前面也有提到:如果我們想用阿里雲進行短訊發送,查看官方相關文檔即可

這裡我們對代碼開發步驟進行簡單整合:

  1. 導入相關Maven坐標
        <!--阿里雲短訊服務-->
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-core</artifactId>
            <version>4.5.16</version>
        </dependency>
        <dependency>
            <groupId>com.aliyun</groupId>
            <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
            <version>2.1.0</version>
        </dependency>
  1. 調用API(資料中將API封裝為工具類)
package com.qiuluo.reggie.utils;

import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;

/**
 * 短訊發送工具類
 */
public class SMSUtils {

	/**
	 * 發送短訊
	 * @param signName 簽名
	 * @param templateCode 模板
	 * @param phoneNumbers 手機號
	 * @param param 參數
	 */
	public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
		// 下面兩個空需要我們手動填寫賬號AccessKey ID和密碼AccessKey Secret
		DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "", "");
		IAcsClient client = new DefaultAcsClient(profile);

		SendSmsRequest request = new SendSmsRequest();
		request.setSysRegionId("cn-hangzhou");
		request.setPhoneNumbers(phoneNumbers);
		request.setSignName(signName);
		request.setTemplateCode(templateCode);
		request.setTemplateParam("{\"code\":\""+param+"\"}");
		try {
			SendSmsResponse response = client.getAcsResponse(request);
			System.out.println("短訊發送成功");
		}catch (ClientException e) {
			e.printStackTrace();
		}
	}

}

此外,資料中還為我們提供了一個生成四位數隨機驗證碼的工具類:

package com.qiuluo.reggie.utils;

import java.util.Random;

/**
 * 隨機生成驗證碼工具類
 */
public class ValidateCodeUtils {
    /**
     * 隨機生成驗證碼
     * @param length 長度為4位或者6位
     * @return
     */
    public static Integer generateValidateCode(int length){
        Integer code =null;
        if(length == 4){
            code = new Random().nextInt(9999);//生成隨機數,最大為9999
            if(code < 1000){
                code = code + 1000;//保證隨機數為4位數字
            }
        }else if(length == 6){
            code = new Random().nextInt(999999);//生成隨機數,最大為999999
            if(code < 100000){
                code = code + 100000;//保證隨機數為6位數字
            }
        }else{
            throw new RuntimeException("只能生成4位或6位數字驗證碼");
        }
        return code;
    }

    /**
     * 隨機生成指定長度字符串驗證碼
     * @param length 長度
     * @return
     */
    public static String generateValidateCode4String(int length){
        Random rdm = new Random();
        String hash1 = Integer.toHexString(rdm.nextInt());
        String capstr = hash1.substring(0, length);
        return capstr;
    }
}

手機驗證碼登錄

我們的功能開發通常分為三部分

資料修改

在正式開始講解之前,我們將資料中的部分內容進行修改來滿足我們下面調試的需求:

  1. 修改front/page/login.html頁面(這裡在發送請求時僅將手機號發送回後端且自動生成驗證碼,我們需要將手機號和驗證碼均發送)
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <!-- 上述3個meta標籤*必須*放在最前面,任何其他內容都*必須*跟隨其後! -->
        <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0,user-scalable=no,minimal-ui">
        <title>菩提閣</title>
        <link rel="icon" href="./../images/favico.ico">
        <!--不同屏幕尺寸根字體設置-->
        <script src="./../js/base.js"></script>
        <!--element-ui的樣式-->
        <link rel="stylesheet" href="../../backend/plugins/element-ui/index.css" />
        <!--引入vant樣式-->
        <link rel="stylesheet" href="../styles/vant.min.css"/>
        <!-- 引入樣式  -->
        <link rel="stylesheet" href="../styles/index.css" />
        <!--本頁面內容的樣式-->
        <link rel="stylesheet" href="./../styles/login.css" />
      </head>
    <body>
        <div id="login" v-loading="loading">
            <div class="divHead">登錄</div>
            <div class="divContainer">
                <el-input placeholder=" 請輸入手機號碼" v-model="form.phone"  maxlength='20'/></el-input>
                <div class="divSplit"></div>
                <el-input placeholder=" 請輸入驗證碼" v-model="form.code"  maxlength='20'/></el-input>
                <span @click='getCode'>獲取驗證碼</span>
            </div>
            <div class="divMsg" v-if="msgFlag">手機號輸入不正確,請重新輸入</div>
            <el-button type="primary" :class="{btnSubmit:1===1,btnNoPhone:!form.phone,btnPhone:form.phone}" @click="btnLogin">登錄</el-button>
        </div>
        <!-- 開發環境版本,包含了有幫助的命令行警告 -->
        <script src="../../backend/plugins/vue/vue.js"></script>
        <!-- 引入組件庫 -->
        <script src="../../backend/plugins/element-ui/index.js"></script>
        <!-- 引入vant樣式 -->
        <script src="./../js/vant.min.js"></script>  
        <!-- 引入axios -->
        <script src="../../backend/plugins/axios/axios.min.js"></script>
        <script src="./../js/request.js"></script>
        <script src="./../api/login.js"></script>
    </body>
    <script>
        new Vue({
            el:"#login",
            data(){
                return {
                    form:{
                        phone:'',
                        code:''
                    },
                    msgFlag:false,
                    loading:false
                }
            },
            computed:{},
            created(){},
            mounted(){},
            methods:{
                getCode(){
                    this.form.code = ''
                    const regex = /^(13[0-9]{9})|(15[0-9]{9})|(17[0-9]{9})|(18[0-9]{9})|(19[0-9]{9})$/;
                    if (regex.test(this.form.phone)) {
                        this.msgFlag = false
                        // this.form.code = (Math.random()*1000000).toFixed(0)
                        const res = sendMsgApi({phone:this.form.phone})
                        sessionStorage.setItem("code",res)
                    }else{
                        this.msgFlag = true
                    }
                },
                async btnLogin(){
                    if(this.form.phone && this.form.code){
                        this.loading = true
                        const res = await loginApi({phone:this.form.phone,code:this.form.code})
                        this.loading = false
                        if(res.code === 1){
                            sessionStorage.setItem("userPhone",this.form.phone)
                            window.requestAnimationFrame(()=>{
                                window.location.href= '/front/index.html'
                            })                           
                        }else{
                            this.$notify({ type:'warning', message:res.msg});
                        }
                    }else{
                        this.$notify({ type:'warning', message:'請輸入手機號碼'});
                    }
                }
            }
        })
    </script>
</html>
  1. 修改front/api/login.js(缺少一個方法,實現驗證碼發送到後端的方法)
function loginApi(data) {
    return $axios({
      'url': '/user/login',
      'method': 'post',
      data
    })
  }

function loginoutApi() {
  return $axios({
    'url': '/user/loginout',
    'method': 'post',
  })
}

function sendMsgApi(data) {
    return $axios({
        'url': '/user/sendMsg',
        'method': 'post',
        data
    })
}  

需求分析

我們的用戶登錄通常採用手機號登錄,發送驗證碼,填寫驗證碼後登錄的流程操作

那麼手機號便是我們區分用戶的根本標識,我們的登錄信息中也以手機號為標識進行數據返回

首先我們簡單了解這次使用的數據表內容User:

我們點開手機端頁面開始簡單的信息整理(這裡注意:我們需要點開F12設置為手機端才可訪問頁面,這是H5的特性):

我們查看該頁面的兩個請求

第一個請求是發送驗證碼請求:

第二個請求是登錄請求:

至此我們分析暫時結束

代碼實現

接下來我們來完成功能的具體實現步驟:

  1. 準備工作
# 我們老規矩先來準備一些常見的內容

實體類User
數據層UserMapper
業務層接口UserService
業務層UserServiceImpl
服務層UserController
工具類SMSUtils,ValidateCodeUtils
  1. 攔截器設置修改
/*
在開始手機驗證碼的設置前,我們先來設置攔截器

如果你前面和我一起查看需求分析的請求信息,你會發現你無法實現sendMsg和login方法,因為這兩個方法被攔截下來了
所以我們需要先將這兩個方法放入到不必攔截的String[]裏面

另一方面,我們和後台設置一樣,要設置用戶登錄後才能進入頁面查看信息
所以我們可以重新判斷手機端用戶是否登錄,若登錄後再進行放行,否則攔截在登陸頁面

*/

package com.qiuluo.reggie.filter;

import com.alibaba.fastjson.JSON;
import com.qiuluo.reggie.common.BaseContext;
import com.qiuluo.reggie.common.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.AntPathMatcher;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 檢查用戶是否已經完成登錄
 */
@WebFilter(filterName = "loginCheckFilter",urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter{

    public static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        HttpServletResponse response = (HttpServletResponse) servletResponse;

        String requestURI = request.getRequestURI();

        log.info("攔截到請求:{}",requestURI);

        //定義不需要處理的請求路徑("/user/login","/user/sendMsg"是手機端登錄和短訊發送請求)         
        String[] urls = new String[]{
                "/employee/login",
                "/employee/logout",
                "/backend/**",
                "/front/**",
                "/common/**",
                "/user/login",
                "/user/sendMsg"
        };

        boolean check = check(urls, requestURI);

        if(check){
            log.info("本次請求{}不需要處理",requestURI);
            filterChain.doFilter(request,response);
            return;
        }

        //4-1、判斷後台登錄狀態,如果已登錄,則直接放行
        if(request.getSession().getAttribute("employee") != null){
            log.info("用戶已登錄,用戶id為:{}",request.getSession().getAttribute("employee"));

            log.info("登錄中...");
            log.info("線程id" + Thread.currentThread().getId());

            Long empId = (Long) request.getSession().getAttribute("employee");
            BaseContext.setCurrentId(empId);

            filterChain.doFilter(request,response);
            return;
        }

        //4-2、判斷移動端登錄狀態,如果已登錄,則直接放行(和上述內容完全一樣,修改Session中設置的名字即可)
        if(request.getSession().getAttribute("user") != null){
            log.info("用戶已登錄,用戶id為:{}",request.getSession().getAttribute("user"));

            log.info("登錄中...");
            log.info("線程id" + Thread.currentThread().getId());

            Long userId = (Long) request.getSession().getAttribute("user");
            BaseContext.setCurrentId(userId);

            filterChain.doFilter(request,response);
            return;
        }

        log.info("用戶未登錄");

        response.getWriter().write(JSON.toJSONString(Result.error("NOTLOGIN")));
        return;

    }

    /**
     * 路徑匹配,檢查本次請求是否需要放行
     * @param urls
     * @param requestURI
     * @return
     */
    public boolean check(String[] urls,String requestURI){
        for (String url : urls) {
            boolean match = PATH_MATCHER.match(url, requestURI);
            if(match){
                return true;
            }
        }
        return false;
    }
}
  1. 短訊發送實現
package com.qiuluo.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.User;
import com.qiuluo.reggie.service.UserService;
import com.qiuluo.reggie.utils.SMSUtils;
import com.qiuluo.reggie.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.jws.soap.SOAPBinding;
import javax.servlet.http.HttpSession;
import java.util.Map;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    // 前端發送phone和code,我們直接採用Map類型接收,採用get方法獲得值
    @PostMapping("/sendMsg")
    public Result<String> sendMsg(@RequestBody User user, HttpSession session){

        // 保存手機號
        String phone = user.getPhone();

        // 判斷手機號是否存在並設置內部邏輯
        if (phone != null){

            // 隨機生成四位密碼
            String code = ValidateCodeUtils.generateValidateCode(4).toString();

            // 因為無法申請signName簽名,我們直接在後台查看密碼
            log.info(code);

            // 我們採用阿里雲發送驗證碼
            // SMSUtils.sendMessage("簽名","模板",phone,code);

            // 將數據放在session中待比對
            session.setAttribute(phone,code);

            return Result.success("驗證碼發送成功");

        }

        return Result.success("驗證碼發送失敗");
    }

}
  1. 登錄功能實現
package com.qiuluo.reggie.controller;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.qiuluo.reggie.common.Result;
import com.qiuluo.reggie.domain.User;
import com.qiuluo.reggie.service.UserService;
import com.qiuluo.reggie.utils.SMSUtils;
import com.qiuluo.reggie.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.jws.soap.SOAPBinding;
import javax.servlet.http.HttpSession;
import java.util.Map;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/login")
    public Result<User> login(@RequestBody Map map, HttpSession session){
        log.info(map.toString());

        // 獲得手機號
        String phone = map.get("phone").toString();

        // 獲得驗證碼
        String code = map.get("code").toString();

        // 獲得Session中的驗證碼
        String codeInSession = session.getAttribute(phone).toString();

        // 進行驗證碼比對
        if (codeInSession != null && codeInSession.equals(code) ){
            
            // 登陸成功
            log.info("用戶登陸成功");

            // 判斷是否為新用戶,如果是自動註冊
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getPhone,phone);

            User user = userService.getOne(queryWrapper);

            if (user == null){

                user = new User();

                 user.setPhone(phone);
                 user.setStatus(1);
                 userService.save(user);
            }
            
            // 我們需要設置session里的user值為用戶id,因為我們的過濾器需要以此判斷用戶是否登錄
            session.setAttribute("user",user.getId());

            // 返回User信息,因為前端需要該數據來布置內部頁面
            return Result.success(user);
        }

        // 驗證碼比較失敗則登陸失敗
        return Result.error("登陸失敗");
    }
}

實際測試

這部分的測試步驟比較麻煩,我們來逐步測試:

  1. 來到APP界面,輸入手機號,點擊發送驗證碼

  1. 來到後端查看驗證碼

  1. 將驗證碼輸入並點擊登錄,登陸成功即可

結束語

該篇內容到這裡就結束了,希望能為你帶來幫助~

附錄

該文章屬於學習內容,具體參考B站黑馬程序員的Java項目實戰《瑞吉外賣》

這裡附上視頻鏈接:業務開發Day5-01-本章內容介紹_嗶哩嗶哩_bilibili