目標(biāo)很明確,快速掌握最最基礎(chǔ)的SpringBoot + MyBatis-Plus怎么用,兩天趕著把項目做了一大半,但過程里缺乏一些思考和總結(jié),現(xiàn)在來復(fù)盤一下。僅列出覺得有價值的部分。
還是很適合作為上手項目,業(yè)務(wù)邏輯確實比較簡單,主要是要掌握一整套流程,以及涉及到多個表的連接查詢操作、一個表的分頁查詢應(yīng)該如何處理,以及文件的上傳下載、手機短信發(fā)送驗證碼知識。
但這樣的項目,如果不主動思考,能得到的東西就很少了,因為它開發(fā)的流程已經(jīng)給了一個答案,雖然未必是標(biāo)準(zhǔn)答案,但是直接照著抄、不考慮應(yīng)該怎么實現(xiàn),可能除了查表更熟練以外能收獲的技能不多。不過查表更熟練也算小提升吧。
以及覺得如果有個ER圖 / 接口說明的話,會清晰很多,不用這樣對著前端分析傳過來什么,應(yīng)該傳回去什么。
MyBatis Plus確實方便了很多,這個項目從頭到尾沒寫過<if> <foreach> <set> <where>
,方便得讓人不安,牛的。
自己一個人git還是缺少鍛煉,體會不到那種pull
下來發(fā)現(xiàn)有沖突,需要merge的絕望。
下一步速成redis和微服務(wù),主要還是學(xué)學(xué)各種中間件怎么使。然后找個能拿得出手的項目。
零、MyBatisPlus
極大簡化CRUD代碼。
- 基本上是傻瓜式操作,因為幾乎不用記對應(yīng)的SQL查詢要怎么寫,戳一個
.
就能得到一波hint和提示補全。 - 提供分頁插件。
- 提供全局?jǐn)r截規(guī)則,設(shè)置
@TableField
及對應(yīng)的MetaObjectHandler
就可以對字段進(jìn)行填充。
一、管理端登錄
1.0 統(tǒng)一的返回結(jié)果Result類
還是有必要的,之前寫前端的時候很需要這個code和msg讓我知道這個接口我是調(diào)成功了還是失敗了,調(diào)失敗了的話問題在哪。
@Data
public class Result<T> {
/**
* code - 編碼:1成功,0和其它數(shù)字為失敗
* msg - 錯誤信息
* data - 數(shù)據(jù)
* map - 動態(tài)數(shù)據(jù)
*/
private Integer code;
private String msg;
private T data;
private Map map = new HashMap();
public static <T> Result<T> success(T object) {
Result<T> result = new Result<T>();
result.data = object;
result.code = 1;
return result;
}
public static <T> Result<T> error(String msg) {
Result result = new Result();
result.msg = msg;
result.code = 0;
return result;
}
public Result<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
1.1 admin/login
說明:這一部分是好久好久好久以前寫的,改了改前端和接口,但邏輯是一樣的。
客戶端請求(TODO: 前端裸傳密碼還是有一點怪怪……有時間了解一下現(xiàn)實世界的實現(xiàn)):
POST
/admin/login
參數(shù):
{
"name": "扣扣",
"password": "koukou123456"
}
管理員實體:
@Data
public class Admin {
private Long adminId;
private String password;
private String phoneNumber;
private String name;
}
邏輯:
- 將參數(shù)password進(jìn)行MD5加密
import org.springframework.util.DigestUtils;
password = DigestUtils.md5DigestAsHex(password.getBytes());
-
判斷數(shù)據(jù)庫中是否存在該對象,與數(shù)據(jù)庫中取到的密碼是否一致
-
登錄成功時,將管理員id存入當(dāng)前session,作為本次會話的一個屬性。
request.getSession().setAttribute("admin", adm.getAdminId());
AdminController
代碼:
/**
* 密碼md5加密 + 根據(jù)name查詢數(shù)據(jù)庫 + 比對密碼
* @param request 該參數(shù)為了將該admin對象的id存入當(dāng)前session中
* @param admin 封裝好的Admin Bean參數(shù)
* @return
*/
@PostMapping("/login")
public Result<Admin> login(HttpServletRequest request, @RequestBody Admin admin) {
// 1 將頁面提交的密碼進(jìn)行md5加密處理
String password = admin.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
// 2 根據(jù)頁面提交的用戶名username查詢數(shù)據(jù)庫
LambdaQueryWrapper<Admin> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(Admin::getName, admin.getName());
Admin adm = adminService.getOne(queryWrapper);
// 3、無結(jié)果返回登陸失敗
if (adm == null) {
return Result.error("用戶名錯誤,登錄失敗");
}
// 4、比對密碼
if (!adm.getPassword().equals(password)) {
return Result.error("密碼錯誤,登錄失敗");
}
// 5、登錄成功,將管理員id存入Session并返回登錄成功結(jié)果
request.getSession().setAttribute("admin", adm.getAdminId());
return Result.success(adm);
}
1.2 admin/logout
把當(dāng)前管理員的id移出session
:
@PostMapping("/logout")
public Result<String> login(HttpServletRequest request) {
request.getSession().removeAttribute("admin");
return Result.success("退出成功");
}
1.3 Filter
Servelet中的Filter接口。需要加入@WebFilter
注解聲明攔截路徑,并在啟動類加入@ServletComponentScan
注解,使得這個Filter可以被Scan到。
一些頁面 / 接口需要在訪問前判斷當(dāng)前是否為登錄狀態(tài),所以設(shè)置這個Filter。
核心邏輯為判斷當(dāng)前訪問的Url以及從Session中取出id。
/**
* 檢查是否登錄
*/
@WebFilter(urlPatterns = "/*")
@Slf4j
public class LoginCheckFilter implements Filter {
/**
* 路徑匹配器,用于檢查該路徑是否需要攔截
*/
private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher();
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void destroy() {
Filter.super.destroy();
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
}
/**
* 判斷requestUrl是否在urls中
*/
public boolean canPass(String[] urls, String requestURI) {
for (String url: urls) {
if (PATH_MATCHER.match(url, requestURI)) {
return true;
}
}
return false;
}
}
核心為doFilter
方法,邏輯如下:
-
定義可放行請求路徑集合,判斷
request
的Url是否在集合中,如果在集合中,可以直接放行; -
嘗試從session中得到login時存入的屬性(可能是管理員login,也可能是用戶login)
req.getSession().getAttribute("admin");
-
如果返回值不為空,說明已經(jīng)登錄,可以放行
-
否則需要
response
拒絕請求:Result<String> error = Result.error("對不起,您尚未登錄,請先進(jìn)行登錄操作!"); resp.getWriter().write(JSONObject.toJSONString(error)); return;
完整代碼如下:
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;
// 可放行集合
String[] canPassUrls = {
"/admin/login",
"/admin/logout",
// 靜態(tài)資源路徑就不處理了
"/backend/**",
"/front/**",
// 一些其他請求,發(fā)送短信、移動端登錄
"/common/**",
"/user/sendMsg",
"/user/login"
};
// 1、得到URI
String requestURI = req.getRequestURI();
log.info("攔截到請求: {}", requestURI);
// 2、得到登錄狀態(tài)
Object adminLoginId = req.getSession().getAttribute("admin");
Object userLoginId = req.getSession().getAttribute("user");
// 3、如果未登錄且是不可訪問頁面,拒絕請求
if (!canPass(canPassUrls, requestURI) && adminLoginId == null && userLoginId == null) {
Result<String> error = Result.error("對不起,您尚未登錄,請先進(jìn)行登錄操作!");
resp.getWriter().write(JSONObject.toJSONString(error));
return;
}
if (adminLoginId != null) {
BaseContext.setCurrentId((Long)adminLoginId);
}
if (userLoginId != null) {
BaseContext.setCurrentId((Long)userLoginId);
}
filterChain.doFilter(servletRequest, servletResponse);
}
1.4 自定義消息轉(zhuǎn)換器
這部分只是意會了,讓我自己寫可能還是不會。
long轉(zhuǎn)為js會精度丟失,那么我們就對數(shù)據(jù)進(jìn)行轉(zhuǎn)型,響應(yīng)json時進(jìn)行處理,將long轉(zhuǎn)為字符串。
并且轉(zhuǎn)換時間格式。
還是有點AOP的。
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;
/**
* 對象映射器:基于jackson將Java對象轉(zhuǎn)為json,或者將json轉(zhuǎn)為Java對象
* 將JSON解析為Java對象的過程稱為 [從JSON反序列化Java對象]
* 從Java對象生成JSON的過程稱為 [序列化Java對象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";
public JacksonObjectMapper() {
super();
//收到未知屬性時不報異常
this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);
//反序列化時,屬性不存在的兼容處理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));
//注冊功能模塊 例如,可以添加自定義序列化器和反序列化器
this.registerModule(simpleModule);
}
}
WebMVCConfig
中需要進(jìn)行相依ing的設(shè)置。
import com.beautysalon.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
@Slf4j
@Configuration
public class WebMVCConfig extends WebMvcConfigurationSupport {
/**
* 設(shè)置靜態(tài)資源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("擴展消息轉(zhuǎn)換器");
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
messageConverter.setObjectMapper(new JacksonObjectMapper());
converters.add(0, messageConverter);
}
}
二、員工管理
2.1 新增員工-字段填充
可以統(tǒng)一處理的變量可以使用注解@TableField
,然后再定義一個Handler
實現(xiàn)填充方法。
@Slf4j
@Data
public class Employee {
private Long id;
private String name;
private String username;
private String password;
private String phone;
private String sex;
private String idNumberReal;
private Integer status;
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createByAdmin;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateByAdmin;
}
實現(xiàn)MetaObjectHandler
接口和insertFill
、upDateFill
方法。
可以使用hasSetter
判斷是否具有某個屬性。
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
@Slf4j
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自動填充[insert]");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
if (metaObject.hasSetter("createByAdmin")) {
metaObject.setValue("createByAdmin", BaseContext.getCurrentId());
metaObject.setValue("updateByAdmin", BaseContext.getCurrentId());
}
if (metaObject.hasSetter("createUser")) {
metaObject.setValue("createUser", BaseContext.getCurrentId());
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自動填充[update]");
metaObject.setValue("updateTime", LocalDateTime.now());
if (metaObject.hasSetter("updateByAdmin")) {
metaObject.setValue("updateByAdmin", BaseContext.getCurrentId());
}
if (metaObject.hasSetter("updateUser")) {
metaObject.setValue("updateUser", BaseContext.getCurrentId());
}
}
}
BaseContext
如下,在login時設(shè)置了BaseContext相關(guān)屬性,需要填充時再get,因為是靜態(tài)方法,所以不需要注入:
/**
* 基于ThreadLocal封裝工具類
*/
@Component
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
}
2.2 全局異常捕獲
使用@ControllerAdvice
和@ExceptionHandler
注解,@ExceptionHandler
指明了捕獲什么樣的異常。
/**
* 全局異常處理
*/
@ControllerAdvice(annotations = {RestController.class, Controller.class})
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
* 異常處理方法
* @return
*/
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public Result<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return Result.error(msg);
}
return Result.error("未知錯誤");
}
/**
* 異常處理方法
* @return
*/
@ExceptionHandler(CustomException.class)
public Result<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return Result.error(ex.getMessage());
}
}
2.3 員工信息分頁查詢
需要配置MyBatis提供的分頁插件攔截器:
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
/**
* 分頁插件
* @return
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
使用MyBatis-Plus
的Page
進(jìn)行分頁:
@GetMapping("/page")
public Result<Page<Employee>> page(@RequestParam Integer page,
@RequestParam Integer pageSize,
@RequestParam(required = false) String name) {
log.info("員工分頁信息查詢:{}, {}", page, pageSize);
// 配置分頁構(gòu)造器
Page<Employee> pageInfo = new Page<>(page, pageSize);
// 條件構(gòu)造器
LambdaQueryWrapper<Employee> queryWrapper = new LambdaQueryWrapper<>();
// 添加過濾條件,如果name不為空,加入name=#{name}條件
queryWrapper.like(!StringUtils.isEmpty(name), Employee::getName, name);
// 添加排序條件
queryWrapper.orderByDesc(Employee::getUpdateTime);
// 執(zhí)行查詢
employeeService.page(pageInfo, queryWrapper);
return Result.success(pageInfo);
}
三、分類管理
3.1 分類的刪除
刪除前需要先去dish
表和setmeal
表查看有無菜品。操作涉及到3個表:
- dish表是否有元素categoryId為當(dāng)前分類
- setmeal表是否有元素categoryId為當(dāng)前分類
- category表刪除該分類
四、菜品管理
4.1 文件的上傳與下載
上傳:保存到本地指定位置
下載:作為Response吐給瀏覽器顯示
1 上傳
在屬性的yml文件中定義相關(guān)路徑位置:
koukou:
path: E:\\leetcode\\project_pre\\BeautySalon\\src\\main\\resources\\front\\upload\\
使用${}
指定圖片保存路徑
@Value("${koukou.path}")
private String basePath;
@PostMapping("/upload")
public Result<String> upload(MultipartFile file) {
// 提取文件相關(guān)信息
String filename = file.getOriginalFilename();
int index = filename.lastIndexOf('.');
String ext = filename.substring(index);
// UUID賦予新名稱
String newName = UUID.randomUUID().toString();
String path = basePath + newName + ext;
log.info(path);
// 保存文件
try {
file.transferTo(new File(path));
}
catch (IOException e) {
e.printStackTrace();
}
return Result.success(newName + ext);
}
2 下載
/**
* 讓本地的圖片在瀏覽器上顯示,寫入Response的輸出流
* @param name
* @param response
*/
@GetMapping("/download")
public void download(String name, HttpServletResponse response) {
try {
// 輸入流
FileInputStream fileInputStream = new FileInputStream(new File(basePath + name));
// 輸出流
ServletOutputStream outputStream = response.getOutputStream();
// 設(shè)置response的content類型
response.setContentType("image/jpeg");
int len = 0;
byte[] bytes = new byte[1024];
while ((len = fileInputStream.read(bytes)) != -1) {
outputStream.write(bytes, 0, len);
outputStream.flush();
}
outputStream.close();
fileInputStream.close();
}
catch (Exception e) {
e.printStackTrace();
}
}
4.2 新增菜品
@Transactional(rollbackFor = Exception.class)
開啟事務(wù),并在啟動類上加上@EnableTransactionManagement
.
設(shè)計到三個表:
- 菜品的分類:因為前端在新增菜品時,需要選擇菜品分類,因此需要返回菜品的所有可能分類取值。
- dish表,表示菜品
- dish_flavor表,表示菜品的口味,由于是一對多關(guān)系,該表存儲了dish的主碼id
1、查詢所有可能的菜品分類,使用一個category來接收參數(shù),解釋是這樣以后需求增加時(比如按其它屬性search)不必重構(gòu)這個方法:
@GetMapping("/list")
public Result<List<Category>> list(Category category) {
log.info("根據(jù)條件查詢分類數(shù)據(jù)");
LambdaQueryWrapper<Category> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(category.getType() != null, Category::getType, category.getType());
queryWrapper.orderByAsc(Category::getSort).orderByDesc(Category::getUpdateTime);
List<Category> list = categoryService.list(queryWrapper);
return Result.success(list);
}
2、這個add請求由于攜帶了額外的信息,用一個DishDTO
接?。?/p>
@PostMapping
public Result<String> save(@RequestBody DishDto dishDto) {
dishService.saveWithFlavor(dishDto);
return Result.success("成功保存菜品");
}
DishDto
繼承了Dish
類,包含Dish的所有屬性,但增加了flavors
的擴展。
categoryName
我覺得是想說明怎么實現(xiàn)兩個表的連接,把categoryId
轉(zhuǎn)為categoryName
。
/**
* DTO:Data Transfer Object,用于傳輸數(shù)據(jù), 對dish的擴展
*/
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
- 先將
dishDto
存入Dish表 - 然后設(shè)置每個
Flavor
的dishId,并存進(jìn)Flavor表。
4.3 修改菜品
修改菜品的邏輯比較類似,但首先需要先把這個菜品的信息查詢出來,放進(jìn)DishDto
里傳給前端,前端顯示這個菜品。
使用到了BeanUtils.copyProperties
進(jìn)行兩個對象間的復(fù)制。
然后前端進(jìn)行修改,然后再傳回后端,后端進(jìn)行修改。類似地,先update這個dish,然后再update這個菜品對應(yīng)的口味。
4.4 菜品信息分頁查詢
類似地,需要查找菜品及其對應(yīng)的口味,并將categoryId
轉(zhuǎn)為name,同樣用到了BeanUtils
進(jìn)行Page之間的復(fù)制。
- 查找滿足條件的分頁數(shù)據(jù)
Page<Dish>
,賦值給Page<DishDto>
- 查找所有
dish
的口味和種類,賦值給DishDto
,加入列表。
@GetMapping("/page")
public Result<Page> page(int page, int pageSize, String name) {
// 先把分頁數(shù)據(jù)查出來
Page<Dish> pageInfo = new Page<>(page, pageSize);
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(name != null, Dish::getName, name);
queryWrapper.orderByDesc(Dish::getUpdateTime);
dishService.page(pageInfo, queryWrapper);
// 和另一個flavor表綜合
Page<DishDto> dishDtoPage = new Page<>();
// 把查詢出來的數(shù)據(jù)拷貝到新對象
BeanUtils.copyProperties(pageInfo, dishDtoPage, "records");
// 處理records
List<Dish> dishes = pageInfo.getRecords();
List<DishDto> dishDtos = new ArrayList<>();
for (Dish dish: dishes) {
DishDto dishDto = new DishDto();
// 把dish拷貝到新對象
BeanUtils.copyProperties(dish, dishDto);
Long categoryId = dish.getCategoryId();
String categoryName = categoryService.getById(categoryId).getName();
dishDto.setCategoryName(categoryName);
dishDtos.add(dishDto);
}
// 賦值
dishDtoPage.setRecords(dishDtos);
return Result.success(dishDtoPage);
}
五、套餐管理
5.1 添加套餐
和新增菜品的邏輯很類似,涉及到setmeal和setmealdish兩張表,setmeal保存套餐信息,setmealdish記錄菜品與套餐間的關(guān)系。
- 先保存套餐信息
- 然后設(shè)置 套餐菜品關(guān)系 的套餐id,存入表
5.2 批量刪除套餐
需要先批量刪除setmeal套餐表,然后用.in
判斷菜品套餐關(guān)系表,刪除SetmealDish表中含該套餐id的項。
5.3 套餐信息分頁查詢
與菜品信息分頁查詢類似:
- Setmeal和SetmealDto之間的BeanUtils.copyProperties
- 以及兩個Page之間的BeanUtils.copyProperties
@GetMapping("/page")
public Result<Page> page(int page, int pageSize, String name) {
Page<Setmeal> pageInfo = new Page<>(page, pageSize);
// 需要返回的數(shù)據(jù)類型
Page<SetmealDto> dtoPage = new Page<>();
// 先把這一頁的信息查出來
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(name != null, Setmeal::getName, name);
setmealService.page(pageInfo, queryWrapper);
List<Setmeal> setmeals = pageInfo.getRecords();
List<SetmealDto> setmealDtos = new ArrayList<>();
BeanUtils.copyProperties(pageInfo, dtoPage, "records");
// 將id轉(zhuǎn)換為name
for (Setmeal setmeal: setmeals) {
String categoryName = categoryService.getById(setmeal.getCategoryId()).getName();
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(setmeal, setmealDto);
setmealDto.setCategoryName(categoryName);
setmealDtos.add(setmealDto);
}
dtoPage.setRecords(setmealDtos);
return Result.success(dtoPage);
}
六、用戶相關(guān)
6.1 發(fā)送驗證碼
生成4位驗證碼:
public class ValidateCodeUtils {
public static String generateValidateCode4String(int length){
Random rdm = new Random();
String hash1 = Integer.toHexString(rdm.nextInt());
String capstr = hash1.substring(0, length);
return capstr;
}
}
發(fā)送短信,即調(diào)用API發(fā)請求的過程:
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;
/**
* 短信發(fā)送工具類
*/
public class SMSUtils {
private static final String SIGN_NAME = "小扣外賣";
private static final String TEMPLATE_CODE = "SM1";
/**
* 發(fā)送短信
* @param phoneNumbers 手機號
* @param param 參數(shù)
*/
public static void sendMessage(String phoneNumbers, String param){
DefaultProfile profile = DefaultProfile.getProfile(
"cn-hangzhou",
"key",
"private key"
);
IAcsClient client = new DefaultAcsClient(profile);
SendSmsRequest request = new SendSmsRequest();
request.setSysRegionId("cn-hangzhou");
request.setPhoneNumbers(phoneNumbers);
request.setSignName(SIGN_NAME);
request.setTemplateCode(TEMPLATE_CODE);
request.setTemplateParam("{\"code\":\"" + param + "\"}");
try {
SendSmsResponse response = client.getAcsResponse(request);
System.out.println("短信發(fā)送成功");
}catch (ClientException e) {
e.printStackTrace();
}
}
}
controller
需要調(diào)用工具類發(fā)送短信,并將驗證碼存入Session:
@PostMapping("/sendMsg")
public Result<String> sendMsg(@RequestBody User user, HttpSession session) {
String code = ValidateCodeUtils.generateValidateCode4String(4);
SMSUtils.sendMessage(user.getPhone(), code);
log.info("發(fā)送驗證碼:{}", code);
// 將驗證碼保存到Session
session.setAttribute(user.getPhone(), code);
return Result.success("短信發(fā)送成功,驗證碼為" + code);
}
6.2 登錄
- 將用戶發(fā)來的驗證碼,與session中存起來的驗證碼進(jìn)行比較
- 不同,登錄失敗
- 相同,用戶表中是否有該user,如果是新用戶,加入user表里
- 將id存入session,以便
CheckLoginFilter
能夠取到 - 如果仔細(xì)觀察你會發(fā)現(xiàn)
userService.save(user)
以后用戶自動擁有了一個id。
- 將id存入session,以便
@PostMapping("/login")
public Result<User> login(@RequestBody Map<String, String> map, HttpSession session) {
// 獲取手機號、驗證碼進(jìn)行比對
String phone = map.get("phone");
String code = map.get("code");
String sessionCode = (String) session.getAttribute(phone);
// 比對成功,登錄成功
if (code != null && code.equals(sessionCode)) {
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone, phone);
User user = userService.getOne(queryWrapper);
// 如果當(dāng)前用戶是新用戶,加入user表中
if (user == null) {
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
session.setAttribute("user", user.getId());
log.info("用戶登錄成功,{}", user.getId());
return Result.success(user);
}
// 比對失敗
return Result.error("登錄失敗");
}
七、購物車
7.1 添加菜品和套餐
購物車表:
- 判斷是菜品還是套餐
- 每個用戶對應(yīng)一個購物車id,查看該用戶的購物車中是否存在該item
- 存在,count + 1,更新;不存在,count=1,寫入。
@PostMapping("/add")
public Result<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart){
log.info("購物車數(shù)據(jù):{}",shoppingCart);
// 先設(shè)置相應(yīng)屬性,然后看看這道菜購物車?yán)镉袥]有,如果沒有,加入表;如果有,number+1
shoppingCart.setUserId(BaseContext.getCurrentId());
// 查看當(dāng)前菜品 或 套餐是否在購物車中
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper();
queryWrapper.eq(ShoppingCart::getUserId, shoppingCart.getUserId());
if (shoppingCart.getDishId() != null) {
queryWrapper.eq(ShoppingCart::getDishId, shoppingCart.getDishId());
}
if (shoppingCart.getSetmealId() != null) {
queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
ShoppingCart target = shoppingCartService.getOne(queryWrapper);
if (target != null) {
// 在購物車?yán)?,?shù)量加一
target.setNumber(target.getNumber() + 1);
shoppingCartService.updateById(target);
}
else {
shoppingCart.setNumber(1);
shoppingCartService.save(shoppingCart);
target = shoppingCart;
}
return Result.success(target);
}
文件配置
通過配置這里設(shè)置了端口,發(fā)送response的編碼,mybatis plus的名字映射方式,全局id的生成方式,文件上傳路徑。
application.yml
文章來源:http://www.zghlxwxcb.cn/news/detail-501057.html
server:
port: 629
servlet:
encoding:
force: true
charset: UTF-8
spring:
application:
#應(yīng)用的名稱,
name:
datasource:
druid:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/beautysalon?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
username: root
password:
mybatis-plus:
configuration:
#在映射實體或者屬性時,將數(shù)據(jù)庫中表名和字段名中的下劃線去掉,按照駝峰命名法映射
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
# 全局id的生成方式
id-type: ASSIGN_ID
koukou:
path: E:\\leetcode\\project_pre\\BeautySalon\\src\\main\\resources\\front\\upload\\
pom.xml
文章來源地址http://www.zghlxwxcb.cn/news/detail-501057.html
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.20</version>
</dependency>
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.23</version>
</dependency>
<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>
</dependencies>
到了這里,關(guān)于【瑞吉外賣】適合速成SpringBoot和MyBatis的作業(yè)項目的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!