目录
第三步:登录Mysql数据库,给用户添加以下权限 (总在 identified by处报错)
一、软件开发整体介绍
软件开发流程
角色分工
软件环境
reggie项目介绍
技术选型
功能架构
角色
二、通用步骤
1、建库建表
2、建maven
3、改pom
4、建yaml
5、建主启动类
静态资源的映射
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
log.info("开始静态资源映射");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
}
6、业务逻辑类
1. entity 实体类
@Data
public class Employee implements Serializable {
2. mapper
@Mapper
public interface EmployeeMapper extends BaseMapper<Employee> {
}
3. service
public interface EmployeeService extends IService<Employee> {
}
4. serviceimpl
@Service
public class EmployeeServiceImpl extends ServiceImpl<EmployeeMapper, Employee> implements EmployeeService {
}
5. controller
@Slf4j
@RestController
@RequestMapping("/employee")
public class EmployeeController {
@Autowired
private EmployeeServiceImpl employeeService;
}
6. 通用结果类,服务端响应的所有结果最终都会包装成此种类型返回给前端页面
@Data
public class R<T> {
private Integer code; //编码:1成功,0和其它数字为失败
private String msg; //错误信息
private T data; //数据
private Map map = new HashMap(); //动态数据
public static <T> R<T> success(T object) {
R<T> r = new R<T>();
r.data = object;
r.code = 1;
return r;
}
public static <T> R<T> error(String msg) {
R r = new R();
r.msg = msg;
r.code = 0;
return r;
}
public R<T> add(String key, Object value) {
this.map.put(key, value);
return this;
}
}
三、员工功能
1. login
//1、将页面提交的密码进行md5加密处理
String password = employee.getPassword();
password = DigestUtils.md5DigestAsHex(password.getBytes());
//.......
//6、登录成功,将用户id存入Session并返回成功结果
request.getSession().setAttribute("employee", emp.getId());
return R.success(emp);
2. logout
//员工退出
@PostMapping("/logout")
public R<String> logout(HttpServletRequest request){
//清理Session中保存的当前员工登录的id
request.getSession().removeAttribute("employee");
return R.success("退出成功");
}
3. 过滤器
/**
* @ClassName: LoginCheckFilter
* @Description: 检查用户是否已经完成登录
* @author: 名字
* @date: 2022/5/10 15:57
*/
@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;
// 1、获取本次请求的URI
String requestURI = request.getRequestURI();
log.info("拦截到请求:{}",requestURI);
// 定义不需要处理的请求路径
String[] urls=new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**"
};
// 2、判断本次请求是否需要处理
boolean check = check(urls, requestURI);
// 3、如果不需要处理,则直接放行
if(check){
log.info("本次请求{}不需要处理",requestURI);
filterChain.doFilter(request,response);
return;
}
// 4、判断登录状态,如果已登录,则直接放行
if(request.getSession().getAttribute("employee")!=null){
log.info("用户已登录,用户id为:{}",request.getSession().getAttribute("employee"));
filterChain.doFilter(request,response);
return;
}
log.info("用户未登录");
// 5、如果未登录则返回未登录结果,通过输出流向客户端页面响应数据
response.getWriter().write(JSON.toJSONString(R.error("NOTLOGIN")));
return;
}
//路径匹配,检查本次请求是否需要放行
public boolean check(String[] urls,String requestURI){
for (String url : urls) {
boolean match = PATH_MATCHER.match(url, requestURI);
if(match==true){
return true;
}
}
return false;
}
}
4.新增员工
5.全局捕获异常
@Slf4j
//对哪些标注了的注解进行处理
@ControllerAdvice(annotations={RestController.class,Controller.class})
//返回前台数据
@ResponseBody
public class GlobalExceptionHandler {
//对哪些异常进行处理
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> exceptionHandler(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
//split 0开头
String msg=split[2]+"已存在";
return R.error(msg);
}
return R.error("未知错误");
}
}
6.设置MP分页插件 ,员工分页信息查询
@Configuration
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor(){
MybatisPlusInterceptor mybatisPlusInterceptor = new MybatisPlusInterceptor();
mybatisPlusInterceptor.addInnerInterceptor(new PaginationInnerInterceptor());
return mybatisPlusInterceptor;
}
}
7.启用、禁用员工账户(根据id查询到具体用户,更改status)
分页查询时服务端响应给页面的数据中id的值为19位数字,类型为long,页面中js处理long型数字只能精确到前16位,所以最终通过ajax请求提交给服务端的时候id就改变了
解决:我们可以在服务端给页面响应json数据时进行处理,将long型数据统一转为String字符串。
如下:
8.对象转换器JacksonobjectMapper
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为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);
}
}
配置类中扩展SpringMVC的消息转换器
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
//创建消息转换器
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java转换为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
super.extendMessageConverters(converters);
}
9.编辑员工信息
10.通用代码开发流程
在开发代码之前需要梳理一下操作过程和对应的程序的执行流程:
1、页面发送ajax请求,将参数携带到服务端
2、服务端Controller接收页面提交的数据并调用Service更新数据
3、Service调用Mapper操作数据库
4、最后将得到的数据通过 return R.success(T); 传送给前台页面的 data 用于展示 。
11. 通用公共字段自动填充
在实体类的属性上加入@TableField注解,指定自动填充的策略
@TableField(fill = FieldFill.INSERT)//插入时填充字段
private LocalDateTime createTime;
@TableField(fill = FieldFill.INSERT_UPDATE)//插入和更新时填充字段
private LocalDateTime updateTime;
@TableField(fill = FieldFill.INSERT)
private Long createUser;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Long updateUser;
按照框架要求编写元数据对象处理器,在此类中统一为公共字段赋值,此类需要实现MetaObjectHandler接口
@Component
@Slf4j
public class MyMetaObjectHandler implements MetaObjectHandler {
//插入时自动填充
@Override
public void insertFill(MetaObject metaObject) {
log.info("公共字段自动填充【insert】。。。");
log.info(metaObject.toString());
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
metaObject.setValue("createUser",BaseContext.getCurrentId());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
//更新时自动填充
@Override
public void updateFill(MetaObject metaObject) {
log.info("公共字段自动填充【update】。。。");
log.info(metaObject.toString());
metaObject.setValue("updateTime",LocalDateTime.now());
metaObject.setValue("updateUser",BaseContext.getCurrentId());
}
}
客户端发送的每次http请求,对应的在服务端都会分配一个新的线程来处理,都属于相同的一个线程
ThreadLocal并不是一个Thread,而是Thread的局部变量。当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。
/**
* 基于ThreadLocal封装的工具类,用于保存和获取当前登录用户的id
*/
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();
}
}
在过滤器filter中调用BaseContext来设置当前登录用户的id
if (request.getSession().getAttribute("employee") != null) {
log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("employee"));
Long empId= (Long) request.getSession().getAttribute("employee");
BaseContext.setCurrentId(empId);
filterChain.doFilter(request, response);
return;
}
四、 分类功能
1.实体类Category
2.mapper
3.service
4.serviceimpl
5.controller
6.新增分类
7.page分页数据的展示
@GetMapping("/page")
public R<Page> page(int page, int pageSize) {
//构造分页构造器
Page<Category> pageInfo=new Page<>(page,pageSize);
//构造条件构造器
LambdaQueryWrapper<Category> queryWrapper=new LambdaQueryWrapper<>();
//添加排序条件,根据sort进行排序
queryWrapper.orderByAsc(Category::getSort);
//进行分页查询
categoryService.page(pageInfo,queryWrapper);
return R.success(pageInfo);
}
8. 删除分类 :根据id删除分类的功能,但是并没有检查删除的分类是否关联了菜品或者套餐
CategoryServicelmpl实现remove方法
@Service
public class CategoryServicelmpl extends ServiceImpl<CategoryMapper, Category> implements CategoryService {
@Autowired
private DishService dishService;
@Autowired
private SetmealService setmealService;
@Override
public void remove(Long id) {
LambdaQueryWrapper<Dish> dishLambdaQueryWrapper=new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
dishLambdaQueryWrapper.eq(Dish::getCategoryId,id);
int count1 = dishService.count(dishLambdaQueryWrapper);
//查询当前分类是否关联菜品,如果已经关联,抛出业务异常
if(count1>0){
//已经关联菜品,抛出业务异常
throw new CustomException("已经关联菜品,不能删除");
}
//查询当前分类是否关联了套餐,如果已经关联,抛出业务异常
LambdaQueryWrapper<Setmeal> setmealLambdaQueryWrapper=new LambdaQueryWrapper<>();
//添加查询条件,根据分类id进行查询
setmealLambdaQueryWrapper.eq(Setmeal::getCategoryId,id);
int count2 = setmealService.count(setmealLambdaQueryWrapper);
if(count2>0){
//已经关联套餐,抛出业务异常
throw new CustomException("已经关联套餐,不能删除");
}
//正常删除分类
super.removeById(id);
}
}
9.自定义异常customException
public class CustomException extends RuntimeException{
public CustomException(String message){
super(message);
}
}
在全局异常处理器GlobalExceptionHandler添加
//进行异常处理方法
@ExceptionHandler(CustomException.class)
public R<String> exceptionHandler(CustomException ex){
log.error(ex.getMessage());
return R.error(ex.getMessage());
}
10.修改分类
五、菜品功能
1. 文件上传下载
文件上传时,对页面的form表单有如下要求:
- method="post" 采用post方式提交数据
- enctype="multipart/form-data" 采用multipart格式上传文件
- type="file" 使用input的file控件上传
- 通过浏览器进行文件下载,通常有两种表现形式:
- 以附件形式下载,弹出保存对话框,将文件保存到指定磁盘目录
- 直接在浏览器中打开
通过浏览器进行文件下载,本质上就是服务端将文件以流的形式写回浏览器的过程
upload
@Slf4j
@RestController
@RequestMapping("/common")
public class CommonController {
@Value("${reggie.path}")
private String basePath;
//文件上传
@PostMapping("/upload")
public R<String> upload(MultipartFile file){
//file 是一个临时文件,需要转存到指定位置,否则请求完成后临时文件会删除
//log.info("file:{}",file.toString());
//原始文件名
String originalFilename = file.getOriginalFilename();
String suffix = originalFilename.substring(originalFilename.lastIndexOf("."));
//使用UUID随机生成文件名,防止因为文件名相同造成文件覆盖
String fileName = UUID.randomUUID().toString()+suffix;
//创建一个目录对象
File dir = new File(basePath);
//判断当前目录是否存在
if(!dir.exists()){
//目录不存在
dir.mkdirs();
}
try {
//将临时文件转存到指定位置
file.transferTo(new File(basePath+fileName));
} catch (IOException e) {
e.printStackTrace();
}
return R.success(fileName);
}
}
download
//文件下载
@GetMapping("/download")
public void download(String name, HttpServletResponse response){
try {
//输入流,通过输入流读取文件内容
FileInputStream fileInputStream=new FileInputStream(new File(basePath+name));
//输出流,通过输出流将文件写回浏览器,在浏览器中展示图片
ServletOutputStream outputStream = response.getOutputStream();
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();
}
}
2. 新增菜品
新增页面录入的菜品信息插入到dish表,如果添加了口味做法,还需要向dish_flavor表插入数据。
DTO,全称为Data Transfer object,即数据传输对象,一般用于展示层与服务层之间的数据传输。
@Data
public class DishDto extends Dish {
private List<DishFlavor> flavors = new ArrayList<>();
private String categoryName;
private Integer copies;
}
@Service
public class DishServiceImpl extends ServiceImpl<DishMapper, Dish> implements DishService {
@Autowired
private DishFlavorService dishFlavorService;
@Override
@Transactional
public void saveWithFlavor(DishDto dishDto) {
//保存菜品基本信息到菜品表dish
this.save(dishDto);
Long dishid = dishDto.getId();
//菜品口味
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> {
item.setDishId(dishid);
return item;
}).collect(Collectors.toList());
//dishFlavorService.saveBatch(dishDto.getFlavors());
//保存菜品口味到菜品数据表dish_flavor
dishFlavorService.saveBatch(flavors);
}
}
涉及多表操作,在启动类上开启事务支持添加@EnableTransactionManagement
注解
3.菜品的分页
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String name) {
//构造分页构造器
Page<Dish> pageInfo = new Page<>(page, pageSize);
Page<DishDto> dishDtoPage = new Page<>();
//构造条件构造器
LambdaQueryWrapper<Dish> queryWrapper = new LambdaQueryWrapper<>();
//添加过滤条件
queryWrapper.like(!StringUtils.isEmpty(name), Dish::getName, name);
//添加排序条件
queryWrapper.orderByDesc(Dish::getUpdateTime);
//进行分页查询
dishService.page(pageInfo, queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,dishDtoPage,"records");
List<Dish> records = pageInfo.getRecords();
List<DishDto> list=records.stream().map((item)->{
DishDto dishDto=new DishDto();
BeanUtils.copyProperties(item,dishDto);
Long categoryId = item.getCategoryId();
//根据id查分类对象
Category category = categoryService.getById(categoryId);
if(category!=null){
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
return dishDto;
}).collect(Collectors.toList());
dishDtoPage.setRecords(list);
return R.success(dishDtoPage);
}
4.菜品的修改
//菜品的回显
@Override
@Transactional
public DishDto getByIdWithFlavor(Long id) {
//查询菜品基本信息
Dish dish = this.getById(id);
DishDto dishDto=new DishDto();
BeanUtils.copyProperties(dish,dishDto);
//查询菜品口味信息
LambdaQueryWrapper<DishFlavor> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId,dish.getId());
List<DishFlavor> list = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(list);
return dishDto;
}
@Override
public void updateWithFlavor(DishDto dishDto) {
//更新dish表基本信息
this.updateById(dishDto);
//更新dish_flavor表信息delete操作
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId, dishDto.getId());
dishFlavorService.remove(queryWrapper);
//更新dish_flavor表信息insert操作
List<DishFlavor> flavors = dishDto.getFlavors();
flavors = flavors.stream().map((item) -> {
item.setDishId(dishDto.getId());
return item;
}).collect(Collectors.toList());
dishFlavorService.saveBatch(flavors);
}
5.菜品的删除
//停售起售菜品
@PostMapping("/status/{status}")
public R<String> sale(@PathVariable int status,
String[] ids){
for(String id: ids){
Dish dish = dishService.getById(id);
dish.setStatus(status);
dishService.updateById(dish);
}
return R.success("修改成功");
}
//删除菜品
@DeleteMapping
public R<String> delete(String[] ids){
for (String id:ids) {
dishService.removeById(id);
}
return R.success("删除成功");
}
六、套餐管理功能
1.新增套餐
加入DTO 数据传输对象
同新增分类类似 ----> 5.2
2.分页查询同理 ----> 5.3
3.套餐的删除及批量删除
观察删除单个套餐和批量删除套餐的请求信息可以发现,两种请求的地址和请求方式都是相同的,不同的则是传递的id个数
@DeleteMapping
public R<String> delete(String[] ids){
int index=0;
for(String id:ids) {
Setmeal setmeal = setmealService.getById(id);
if(setmeal.getStatus()!=1){
setmealService.removeById(id);
}else {
index++;
}
}
if (index>0&&index==ids.length){
return R.error("选中的套餐均为启售状态,不能删除");
}else {
return R.success("删除成功");
}
}
@PostMapping("/status/{status}")
public R<String> sale(@PathVariable int status,String[] ids){
for (String id:ids){
Setmeal setmeal = setmealService.getById(id);
setmeal.setStatus(status);
setmealService.updateById(setmeal);
}
return R.success("修改成功");
}
4.修改套餐 同理 -----> 5.4
Failed to convert value of type ‘java.lang.String’ to required type ‘java.lang.Long’; nested exception is java.lang.NumberFormatException: For input string: “XXX”]
原因并不在于类型转换出错,真正的报错原因在于http请求类型设置错误
七、手机验证码登录
1. 阿里云短信服务 阿里云官网:
https://www.aliyun.com/
2. 阿里云短信服务-设置短信签名
短信签名是短信发送者的署名,表示发送方的身份。
3. 短信模板包含短信发送内容、场景、变量信息。
阿里云短信服务-设置AccessKey
4. 导入依赖
<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>1.1.0</version>
</dependency>
5. 调用aip
public class SMSUtils {
/**
* 发送短信
* @param signName 签名
* @param templateCode 模板
* @param phoneNumbers 手机号
* @param param 参数
*/
public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
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();
}
}
}
6. 过滤器放行
// 4-2、判断登录状态,如果已登录,则直接放行
if (request.getSession().getAttribute("user") != null) {
log.info("用户已登录,用户id为:{}", request.getSession().getAttribute("user"));
Long userId= (Long) request.getSession().getAttribute("user");
BaseContext.setCurrentId(userId);
filterChain.doFilter(request, response);
return;
}
7. UserController处理post请求(发送验证码的请求)
8. 在UserController编写login或regiest处理post请求
八、用户地址簿
1. 导入功能代码 pojo,mapper,service,serviceImpl,controller
2. 新增地址
3. 展示地址列表
4. 修改默认地址
5. 根据id回显地址
6. 查询指定用户的全部地址
九、首页菜品展示
1. DishController中的list
@GetMapping("/list")
public R<List<DishDto>> list(Dish dish) {
//构造查询条件
LambdaQueryWrapper<Dish> lambdaQueryWrapper = new LambdaQueryWrapper<>();
//添加条件,查询状态为1的(起售状态)
lambdaQueryWrapper.eq(Dish::getStatus, 1);
lambdaQueryWrapper.eq(dish.getCategoryId() != null, Dish::getCategoryId, dish.getCategoryId());
//条件排序条件
lambdaQueryWrapper.orderByAsc(Dish::getSort).orderByDesc(Dish::getUpdateTime);
List<Dish> list = dishService.list(lambdaQueryWrapper);
List<DishDto> dishDtoList = list.stream().map((item) -> {
DishDto dishDto = new DishDto();
BeanUtils.copyProperties(item, dishDto);
Long categoryId = item.getCategoryId();
//根据id查分类对象
Category category = categoryService.getById(categoryId);
if (category != null) {
String categoryName = category.getName();
dishDto.setCategoryName(categoryName);
}
//当前菜品id
Long dishId = item.getId();
LambdaQueryWrapper<DishFlavor> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(DishFlavor::getDishId, dishId);
//SQL: select* from dishflavor where dish_id=?;
List<DishFlavor> dishFlavorlist = dishFlavorService.list(queryWrapper);
dishDto.setFlavors(dishFlavorlist);
return dishDto;
}).collect(Collectors.toList());
return R.success(dishDtoList);
}
2. SetmealController里添加list方
@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal){
LambdaQueryWrapper<Setmeal> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(setmeal.getCategoryId()!=null,Setmeal::getCategoryId,setmeal.getCategoryId());
queryWrapper.eq(setmeal.getStatus()!=null,Setmeal::getStatus,setmeal.getStatus());
queryWrapper.orderByDesc(Setmeal::getUpdateTime);
List<Setmeal> list = setmealService.list(queryWrapper);
return R.success(list);
}
十、购物车
@PostMapping("/add")
public R<ShoppingCart> add(@RequestBody ShoppingCart shoppingCart) {
log.info("购物车数据:{}", shoppingCart);
//设置用户id,指定当前是哪个用户的购物车数据
Long currentId = BaseContext.getCurrentId();
shoppingCart.setUserId(currentId);
//查询当前菜品或者套餐是否已经在购物车当中
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId, currentId);
if (dishId != null) {
//添加到购物车的为菜品
queryWrapper.eq(ShoppingCart::getDishId, dishId);
} else {
//添加到购物车的为套餐
queryWrapper.eq(ShoppingCart::getSetmealId, shoppingCart.getSetmealId());
}
//SQL:select *from shopping_cart where user_id=? and dish_id/setmeal_id =?
ShoppingCart cartServiceone = shoppingcartService.getOne(queryWrapper);
if(cartServiceone!=null) {
//如果已经存在,则在原来的基础上加一
Integer number = cartServiceone.getNumber();
cartServiceone.setNumber(number+1);
shoppingcartService.updateById(cartServiceone);
}else {
//如果不存在,则添加到购物车中,默认为一
shoppingCart.setNumber(1);
shoppingCart.setCreateTime(LocalDateTime.now());
shoppingcartService.save(shoppingCart);
cartServiceone=shoppingCart;
}
return R.success(cartServiceone);
}
购物车的展示
@GetMapping("/list")
public R<List<ShoppingCart>> list(){
log.info("查看购物车");
LambdaQueryWrapper<ShoppingCart> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
queryWrapper.orderByDesc(ShoppingCart::getCreateTime);
List<ShoppingCart> list = shoppingcartService.list(queryWrapper);
return R.success(list);
}
菜品的减少
@PostMapping("/sub")
public R<ShoppingCart> sub(@RequestBody ShoppingCart shoppingCart){
Long setmealId = shoppingCart.getSetmealId();
Long dishId = shoppingCart.getDishId();
LambdaQueryWrapper<ShoppingCart> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,BaseContext.getCurrentId());
if (setmealId!=null){
queryWrapper.eq(ShoppingCart::getSetmealId,setmealId);
}else {
queryWrapper.eq(ShoppingCart::getDishId,dishId);
}
ShoppingCart one = shoppingcartService.getOne(queryWrapper);
Integer number = one.getNumber();
if(number==1){
shoppingcartService.remove(queryWrapper);
}else {
one.setNumber(number-1);
shoppingcartService.updateById(one);
}
return R.success(one);
}
十一、订单
1、在购物车中点击 【去结算】 按钮,页面跳转到订单确认页面
2、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的默认地址
3、在订单确认页面,发送ajax请求,请求服务端获取当前登录用户的购物车数据
4、在订单确认页面点击 【去支付】 按钮,发送ajax请求,请求服务端完成下单操作
开发用户下单功能,其实就是在服务端编写代码去处理前端页面发送的请求即可。
用户的下单
@Service
public class OrderServiceImpl extends ServiceImpl<OrderMapper, Orders> implements OrderService {
@Autowired
private ShoppingcartService shoppingcartService;
@Autowired
private UserService userService;
@Autowired
private AddressBookService addressBookService;
@Autowired
private OrderDetailService orderDetailService;
@Override
@Transactional
public void submit(Orders orders) {
//获取当前用户id
Long currentId = BaseContext.getCurrentId();
//查询当前用户的购物车数据
LambdaQueryWrapper<ShoppingCart> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(ShoppingCart::getUserId,currentId);
List<ShoppingCart> list = shoppingcartService.list(queryWrapper);
if (list==null||list.size()==0){
throw new CustomException("购物车为空,不能下单");
}
//查询用户数据
User user = userService.getById(currentId);
//查询地址数据
Long addressBookId = orders.getAddressBookId();
AddressBook addressBook = addressBookService.getById(addressBookId);
if(addressBook==null){
throw new CustomException("地址有误,不能下单");
}
long orderId = IdWorker.getId();//订单号
AtomicInteger amount=new AtomicInteger(0);
List<OrderDetail> orderDetails=list.stream().map((item)->{
OrderDetail orderDetail = new OrderDetail();
orderDetail.setOrderId(orderId);
orderDetail.setNumber(item.getNumber());
orderDetail.setDishFlavor(item.getDishFlavor());
orderDetail.setDishId(item.getDishId());
orderDetail.setSetmealId(item.getSetmealId());
orderDetail.setName(item.getName());
orderDetail.setImage(item.getImage());
orderDetail.setAmount(item.getAmount());
amount.addAndGet(item.getAmount().multiply(new BigDecimal(item.getNumber())).intValue());
return orderDetail;
}).collect(Collectors.toList());
//向订单表中插入一条数据
orders.setNumber(String.valueOf(orderId));
orders.setId(orderId);
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
orders.setAmount(new BigDecimal(amount.get()));//计算总金额
orders.setUserId(currentId);
orders.setUserName(user.getName());
orders.setConsignee(addressBook.getConsignee());
orders.setPhone(addressBook.getPhone());
orders.setAddress((addressBook.getProvinceName()==null?"":addressBook.getProvinceName())
+(addressBook.getCityName()==null?"":addressBook.getCityName())
+(addressBook.getDistrictName()==null?"":addressBook.getDistrictName())
+(addressBook.getDetail()==null?"":addressBook.getDetail()));
this.save(orders);
//向订单明细表中插入多条数据
orderDetailService.saveBatch(orderDetails);
//清空购物车数据
shoppingcartService.remove(queryWrapper);
}
}
//用户下单
@PostMapping("/submit")
public R<String> submit(@RequestBody Orders orders){
log.info("订单数据:{}",orders);
orderService.submit(orders);
return R.success("下单成功");
}
十二、功能的补充
用户登出
在UserController添加loginout方法
//用户登出
@PostMapping("/loginout")
public R<String> loginout(HttpServletRequest request){
//清理Session中保存的当前用户登录的id
request.getSession().removeAttribute("user");
return R.success("退出成功");
}
订单管理
//订单管理
@Transactional
@GetMapping("/userPage")
public R<Page> userPage(int page,int pageSize){
//构造分页构造器
Page<Orders> pageInfo = new Page<>(page, pageSize);
Page<OrdersDto> ordersDtoPage = new Page<>();
//构造条件构造器
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
//添加排序条件
queryWrapper.orderByDesc(Orders::getOrderTime);
//进行分页查询
orderService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,ordersDtoPage,"records");
List<Orders> records=pageInfo.getRecords();
List<OrdersDto> list = records.stream().map((item) -> {
OrdersDto ordersDto = new OrdersDto();
BeanUtils.copyProperties(item, ordersDto);
Long Id = item.getId();
//根据id查分类对象
Orders orders = orderService.getById(Id);
String number = orders.getNumber();
LambdaQueryWrapper<OrderDetail> lambdaQueryWrapper=new LambdaQueryWrapper<>();
lambdaQueryWrapper.eq(OrderDetail::getOrderId,number);
List<OrderDetail> orderDetailList = orderDetailService.list(lambdaQueryWrapper);
int num=0;
for(OrderDetail l:orderDetailList){
num+=l.getNumber().intValue();
}
ordersDto.setSumNum(num);
return ordersDto;
}).collect(Collectors.toList());
ordersDtoPage.setRecords(list);
return R.success(ordersDtoPage);
}
再来一单
//再来一单
@Transactional
@PostMapping("/again")
public R<String> again(@RequestBody Orders order1){
//取得orderId
Long id = order1.getId();
Orders orders = orderService.getById(id);
//设置订单号码
long orderId = IdWorker.getId();
orders.setId(orderId);
//设置订单号码
String number = String.valueOf(IdWorker.getId());
orders.setNumber(number);
//设置下单时间
orders.setOrderTime(LocalDateTime.now());
orders.setCheckoutTime(LocalDateTime.now());
orders.setStatus(2);
//向订单表中插入一条数据
orderService.save(orders);
//修改订单明细表
LambdaQueryWrapper<OrderDetail> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(OrderDetail::getOrderId,id);
List<OrderDetail> list = orderDetailService.list(queryWrapper);
list.stream().map((item)->{
//订单明细表id
long detailId = IdWorker.getId();
//设置订单号码
item.setOrderId(orderId);
item.setId(detailId);
return item;
}).collect(Collectors.toList());
//向订单明细表中插入多条数据
orderDetailService.saveBatch(list);
return R.success("再来一单");
}
管理端订单明细
@GetMapping("/page")
public R<Page> page(int page, int pageSize, String number,String beginTime,String endTime){
//构造分页构造器
Page<Orders> pageInfo = new Page<>(page, pageSize);
Page<OrdersDto> ordersDtoPage=new Page<>();
//构造条件构造器
LambdaQueryWrapper<Orders> queryWrapper = new LambdaQueryWrapper<>();
//根据number进行模糊查询
queryWrapper.like(!StringUtils.isEmpty(number),Orders::getNumber,number);
//根据Datetime进行时间范围查询
// log.info("开始时间:{}",beginTime);
// log.info("结束时间:{}",endTime);
if(beginTime!=null&&endTime!=null){
queryWrapper.ge(Orders::getOrderTime,beginTime);
queryWrapper.le(Orders::getOrderTime,endTime);
}
//添加排序条件
queryWrapper.orderByDesc(Orders::getOrderTime);
//进行分页查询
orderService.page(pageInfo,queryWrapper);
//对象拷贝
BeanUtils.copyProperties(pageInfo,ordersDtoPage,"records");
List<Orders> records=pageInfo.getRecords();
List<OrdersDto> list=records.stream().map((item)->{
OrdersDto ordersDto=new OrdersDto();
BeanUtils.copyProperties(item,ordersDto);
String name="用户"+item.getUserId();
ordersDto.setUserName(name);
return ordersDto;
}).collect(Collectors.toList());
ordersDtoPage.setRecords(list);
return R.success(ordersDtoPage);
}
外卖订单派送
@PutMapping
public R<String> send(@RequestBody Orders orders){
Long id = orders.getId();
Integer status = orders.getStatus();
LambdaQueryWrapper<Orders> queryWrapper=new LambdaQueryWrapper<>();
queryWrapper.eq(Orders::getId,id);
Orders one = orderService.getOne(queryWrapper);
one.setStatus(status);
orderService.updateById(one);
return R.success("派送成功");
}
十三、功能优化一 缓存优化
导依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
改yaml
spring
redis:
host: 150.158.175.115
port: 6379
password: xxx
database: 0
添加配置类 ( 将redis存储的key值不使用默认的jdk序列化器,便于我们对key值的查找,value值默认设置的时候使用的是jdk序列化器,但是我们取回value数据时会自动反序列化,因此不用修改 )
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
RedisTemplate<Object,Object> redisTemplate = new RedisTemplate<>();
//默认的Key序列化器为: JdkSerializationRedisSerializer
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory( connectionFactory) ;
return redisTemplate;
}
}
缓存短信验证码
在服务端UserController中注入RedisTemplate对象,用于操作Redis
@Autowired
private RedisTemplate redisTemplate;
在服务端UserController的sendMsg方法中,将随机生成的验证码缓存到Redis中,并设置有效期为5分钟
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
//从redis中获取保存的验证码
Object codeInSession =redisTemplate.opsForValue().get(phone);
//如果用户登录成功则删除Redis中缓存的验证码
redisTemplate.delete(phone);
缓存菜品数据
1、DishController的list方法,先从Redis中获取菜品数据,如果有则直接返回,无需查询数据库;
如果没有则查询数据库,并将查询到的菜品数据放入Redis
List<DishDto> dishDtoList=null;
//动态构造Key
String key="dish_"+dish.getCategoryId()+"_"+dish.getStatus();
//先从redis中获取缓存数据
dishDtoList= (List<DishDto>) redisTemplate.opsForValue().get(key);
if(dishDtoList!=null){
//如果存在,则直接返回,无需查询数据库
return R.success(dishDtoList);
}
...
...
...
//如果不存在,则查询数据库,并且将查询到的菜品数据添加到缓存中
redisTemplate.opsForValue().set(key,dishDtoList,60, TimeUnit.MINUTES);
2、DishController的save和update方法,加入清理缓存的逻辑
//清理所有菜品缓存数据
//Set keys = redisTemplate.keys("dish_*");
//redisTemplate.delete(keys);
//清理某个分类下面的菜品缓存数据
String key="dish_"+dishDto.getCategoryId()+"_"+dishDto.getStatus();
redisTemplate.delete(key);
Spring cache是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能。
spring Cache 常用注解
具体例子
//list
* @Cacheable 在方法执行前,spring先查询缓存中是否存有数据,
* 如果有数据,则直接返回缓存中的数据,如果无,则调用方法,然后将数据保存在缓存中
* @CachePut 将方法的返回值缓存在缓存中
*
* value 缓存的名称 key 缓存的索引 key可以有以下3种:
* 1、#categoryId 传入的参数 2、#root.args[0] 传入的第一个参数
* 3、#result.data.get(0).categoryId 返回值
*
* condition及unless 进行缓存成立的条件
// @CachePut(value = "setmealCache",key = "#result.data.get(0).categoryId+'_'+#result.data.get(0).status",unless = "#result.data==null")
// @CachePut(value = "setmealCache",key = "#root.args[0]+'_'+#root.args[1]",unless = "#result.data==null")
@Cacheable(value = "setmealCache",key="#categoryId+'_'+#status",condition = "#result.data!=null")
@GetMapping("/list")
public R<List<Setmeal>> list(Long categoryId,Integer status){}
//save、update、delete
* @CacheEvict 将一条或多条数据从缓存中删除
* value 缓存的名称
* allEntries 是否全部删除 默认为 false
@CacheEvict(value = "setmealCache",allEntries = true)
@PostMapping
public R<String> save(@RequestBody SetmealDto setmealDto){}
1、导入Spring Cache (使用redis缓存技术)
<!-- spring Cache 和redis 整合-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2、改 yaml
spring:
cache:
redis:
time-to-live: 1800000#设置缓存有效期
3、在启动类上加入@EnableCaching注解,开启缓存注解功能
4、在Controller的list方法上加入@Cacheable 注解进行缓存操作
5、在Controller的save、update、delete方法上加入@CacheEvict注解,进行删除缓存操作
套餐数据的缓存 (使用spring Cache 和 redis 的整合)
步骤同上 1 —- 5
注意:要让R实现Serializable接口(序列化),注解才能生效
十四、功能优化二 读写分离 优化
1、Mysql主从复制
- master将改变记录到二进制日志( binary log)
- slave将master的binary log拷贝到它的中继日志(relay log)
- slave重做中继日志中的事件,将改变应用到自己的数据库中
配置-主库Master
第一步:修改Mysq1数据库的配置文件/etc/my.cnf
[mysqld]
log-bin=mysql-bin #[必须]启用二进制日志
server-id=101 #[必须]服务器唯一ID
第二步:重启mysql服务
systemctl restart mysqld
第三步:登录Mysql数据库,给用户添加以下权限 (总在 identified by处报错)
GRANT REPLICATION SLAVE ON *.* to 'root'@'%' identified by 'Root@123456' with grant option;
修改权限,但一直报错:
ERROR 1064 (42000): You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'identified by 'Root@123456' with grant option' at line 1
MySQL8的权限修改语句已经改成:
GRANT REPLICATION SLAVE ON *.* TO 'root'@'%' WITH GRANT OPTION;
无需再后面添加
identified by 'Root@123456'
常用于建立复制时所需要用到的用户权限,也就是slave必须被master授权具有该权限的用户,才能通过该用户复制
第四步:登录Mysql数据库,show master status;
记录下结果中File和Position的值
注:上面SQL的作用是查看Master的状态,执行完此SQL后不要再执行任何操作
配置-从库Slave
第一步:修改Mysq1数据库的配置文件/etc/my.cnf
[mysqld]
server-id=102 #[必须]服务器唯一ID
第二步:重启Mysql服务systemctl restart mysqld
第三步:登录Mysql数据库
stop slave;
change master to
master_host='192.168.188.100',master_user='xiaoming',master_password='Root@123456',master_log_file='mysql-bin.000003',master_log_pos=441;
start slave;
//注意主从服务器的server-id 要唯一,不能重复
//否则 slave I/O runing 总为 No
第四步:登录Mysql数据库,查看从数据库的状态 show slave status;
2、读写分离
使用Sharding-JDBC可以在程序中轻松的实现数据库读写分离。
主库update ,从库 select
1.引入依赖
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.0.0-RC1</version>
</dependency>
2.在配置文件中配置读写分离规则
spring:
shardingsphere:
datasource:
names:
master,slave
# 主数据源
master:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url:jdbc:mysql://150.158.175.115:3306/reggieserverTimezone=Asia/Shanghai&
useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull
&useSSL=false&allowPublicKeyRetrieval=true
username: xxx
password: xxx
# 从数据源
slave:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://192.168.101.1:3306/reggie?serverTimezone=Asia/Shanghai
&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull
&useSSL=false&allowPublicKeyRetrieval=true
username: xxx
password: xxx
masterslave:
# 读写分离配置
load-balance-algorithm-type: round_robin #轮询
# 最终的数据源名称
name: dataSource
# 主库数据源名称
master-data-source-name: master
# 从库数据源名称列表,多个逗号分隔
slave-data-source-names: slave
props:
sql:
show: true #开启SQL显示,默认false
#允许bean定义覆盖配置项
main:
allow-bean-definition-overriding: true
十五、功能优化三 前后端分离开发
开发流程
接口(API接口) 就是一个http的请求地址,主要就是去定义:请求路径、请求方式、请求参数、响应数据等内容
Yapi
介绍
YApi是高效、易用、功能强大的api管理平台,旨在为开发、产品、测试人员提供更优雅的接口管理服务。可以帮助开发者轻松创建、发布、维护 API,YApi还为用户提供了优秀的交互体验,开发人员只需利用平台提供的接口数据写入工具以及简单的点击操作就可以实现接口的管理。
YApi让接口开发更简单高效,让接口的管理更具可读性、可维护性,让团队协作更合理。
源码地址: GitHub - YMFE/yapi: YApi 是一个可本地部署的、打通前后端及QA的、可视化的接口管理平台
要使用YApi,需要自己进行部署。
使用
使用YApi可以执行下面操作
- 添加项目
- 添加分类
- 添加接口
- 编辑接口
- 查看接口
Swagger
介绍
使用Swagger你只需要按照它的规范去定义接口及接口相关的信息,再通过Swagger衍生出来的一系列项目和工具,就可以做到生成各种格式的接口文档,以及在线接口调试页面等等。
官网:API Documentation & Design Tools for Teams | Swagger
knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案。
使用方式
操作步骤:
1、导入knife4j的maven坐标
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.2</version>
</dependency>
2、导入knife4j相关配置类
WebMvcConfig
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Bean
public Docket createRestApi() {
//文档类型
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.ka.reggie.controller"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}
3、设置静态资源,否则接口文档页面无法访问(addResourceHandlers方法)
registry.addResourceHandler("doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
4、在LoginCheckFilter中设置不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/sendMsg",
"/user/login",
"/doc.html",
"/webjars/**",
"/swagger-resources",
"/v2/api-docs"
};
常用注解
注解 | 说明 |
---|---|
@Api | 用在请求的类上,例如Controller,表示对类的说明 |
@ApiModel | 用在类上,通常是实体类,表示一个返回响应数据的信息 |
@ApiModelProperty | 用在属性上,描述响应类的属性 |
@ApiOperation | 用在请求的方法上,说明方法的用途、作用 |
@ApilmplicitParams | 用在请求的方法上,表示一组参数说明 |
ApilmplicitParam | 用在@ApilmplicitParams注解中,指定一个请求参数的各个方面 |
项目部署
部署架构
部署环境说明
服务器:
-
192.168.138.100(服务器A)
Nginx:部署前端项目、配置反向代理
Mysql:主从复制结构中的主库
Redis:缓存中间件
-
192.168.138.101(服务器B)
jdk:运行Java项目
git:版本控制工具
maven:项目构建工具
jar: Spring Boot项目打成jar包基于内置Tomcat运行
Mysql:主从复制结构中的从库
部署前端项目
第一步:在服务器A中安装Nginx,将课程资料中的dist目录上传到Nginx的html目录下
第二步:修改Nginx配置文件nginx.conf
server{
listen 80;
server_name localhost;
location /{
root html/dist;
index index.html;
}
location ^~ /api/{
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://192.168.188.101:8080;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html{
root html;
}
}
部署后端项目
第一步∶在服务器B中安装jdk、git、maven、MySQL,使用git clone命令将git远程仓库的代码克隆下来
第二步:将资料中提供的reggieStart.sh文件上传到服务器B,通过chmod命令设置执行权限
第三步:执行reggieStart.sh脚本文件,自动部署项目
注意:将本地的img图片全部上传到部署的服务器上,并将yaml的img路径改为部署服务器上的img目录的路径