文章目录
1. 手机验证码功能实现
1.1 业务流程:
(1)业务端输入手机号,点击发送
(2)后台接收请求,验证手机号,生成验证码。
这里要注意验证码是在后台生成好的。我原来一直以为手机里的验证码是运营商生成的,原来是错误的。
(3)将生成验证码并通过阿里云接口向指定的手机号发送。
(4)手机接收到验证码后,填写到前端,登录
(5)后台根据前端填写的验证码和手机号和后端去匹配,如果一致就通过,否则,返回错误信息。
1.2 依赖包
<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.3 工具类
阿里短信的工具类,最后一个param就是传入后台生成的验证吗。
package com.guigutool.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){
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();
}
}
}
1.4 登录案例实现
(1) 点击发送验证码按钮,后端的处理
@Autowired
private RedisTemplate redisTemplate;
@PostMapping("/sendMsg")
public R<String> sendMsg(@RequestBody User user, HttpSession session){
//获取手机号
String phone = user.getPhone();
if(StringUtils.isNotEmpty(phone)){
//生成随机的4位验证吗
String code = ValidateCodeUtils.generateValidateCode(4).toString();
log.info("code:{}", code);
//调用阿里云提供的短信API完成发送短信
SMSUtils.sendMessage("Reggie","",phone,code);
//需要将生成的验证码保存到Session
//session.setAttribute(phone,code);
//将生成的验证码保存到Redis中,并且设置有效期为5分钟
redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
return R.success("验证码发送成功");
}
return R.error("验证码发送失败");
}
(2) 填写验证码后,登录处理
/**
* 移动端用户登录
* @param map
* @param session
* @return
*/
@PostMapping("/login")
public R<User> login(@RequestBody Map map, HttpSession session){
String phone = map.get("phone").toString();
String code = map.get("code").toString();
// Object codeInSession = session.getAttribute(phone);
//从redis中获取缓存的验证码
Object codeInSession = redisTemplate.opsForValue().get(phone);
if(Objects.nonNull(codeInSession) && codeInSession.equals(code)){
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getPhone,phone);
User user = userService.getOne(queryWrapper);
if(Objects.isNull(user)){
user = new User();
user.setPhone(phone);
user.setStatus(1);
userService.save(user);
}
//登录成功
session.setAttribute(SystemConstant.MOBILE_USER_ID,user.getId());
//如果用户登录成功,则删除Redis中缓存的验证码
redisTemplate.delete(phone);
return R.success(user);
}
return R.error("登录失败");
}
2. 缓存
2.1 Cache缓存
SpringCache是一个基于注解的缓存功能,只要简单的加一个注解,就能实现缓存功能。
SpringCache也提供了一层抽象接口,底层可以切换不同的cache实现类,接口名称是CacheManager。
CacheManager是Spring提供的各种缓存技术抽象接口。
以下是一些一些不同的实现类:
CacheManager | 描述 |
---|---|
EhCacheCacheManager | 使用EhCache作为缓存技术 |
GuavaCacheManager | 使用Google的GuavaCache作为缓存技术 |
RedisCacheManager | 使用Redis作为缓存技术 |
2.2 SpringCache缓存使用
(1)默认配置
只要加入spring-boot-starter-web,就会存在SpringCache的配置。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>compile</scope>
</dependency>
默认有以下4个实现类,所以可以不做任何改动。
(2) 在启动应用主程序添加@EnableCaching注解
(3) 注入
//Ctrl+Alt+鼠标左点击,可查看该接口的实现类
@Autowired
private CacheManager cacheManager;
(4)注解说明
注解 | 说明 | 用途 |
---|---|---|
@EnableCaching | 开启缓存注解功能 | 全部配置,在启动app类上注解 |
@Cacheable | 在方法执行前spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;若没有数据,调用方法并将方法返回值放到缓存中 | 查询操作 |
@CachePut | 将方法的返回值放到缓存中 | 保存或者修改操作 |
@CacheEvict | 将一条或者多条记录从缓存中删除 | 删除操作 |
(5)使用案例
定义缓存的名称
private final String cacheName = "userCache";
保存操作
/**
* cachePut:将方法返回值放入缓存
* value: 缓存的名称,每个缓存名称下可以有多个key
* key:缓存的key, 这个取自于方法的返回值
* 例如:这里的key使用的是返回的user对象的id
*/
@CachePut(value=cacheName, key = "#result.id")
@PostMapping
public User save(User user){
userService.save(user);
return user;
}
删除操作
/**
* 这里的key使用的是传入的参数
* @param id
*/
@CacheEvict(value = cacheName, key="#id")
@DeleteMapping("/{id}")
public void delete(@PathVariable Long id){
userService.removeById(id);
}
修改操作
// @CacheEvict(value = cacheName, key="#p0.id") (1)根据接收的参数设置的key
// @CacheEvict(value = cacheName, key="#user.id") (2)根据接收的参数设置的key
// @CacheEvict(value = cacheName, key="#root.args[0].id") (3)根据接收的参数设置的key
// @CacheEvict(value = cacheName, key="#result.id") (4)根据返回的值设置的key
@CacheEvict(value = cacheName, key="#result.id")
@PutMapping
public User update(User user){
userService.updateById(user);
return user;
}
查询操作
condition 满足条件时,才缓存
/**
* condition: 条件,满足条件时,才缓存数据
* unless: 满足条件,不缓存 #注意这里会发生缓存穿透,就是当用户查询的数据不存在时,会一直调用sql,如果有太多的请求请数据库发送sql语句,也会加重数据库的负载
*/
@Cacheable(value = cacheName,key="#id", condition = "#result != null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id){
User user = userService.getById(id);
return user;
}
unless: 满足条件时,不缓存
/**
* condition: 条件,满足条件时,才缓存数据
* unless: 满足条件,不缓存 #注意这里会发生缓存穿透,就是当用户查询的数据不存在时,会一直调用sql,如果有太多的请求请数据库发送sql语句,也会加重数据库的负载
*/
@Cacheable(value = cacheName,key="#id", unless = "#result == null")
@GetMapping("/{id}")
public User getById(@PathVariable Long id){
User user = userService.getById(id);
return user;
}
@Cacheable(value = cacheName, key = "#user.id+':'+#user.name")
@GetMapping("/list")
public List<User> list(User user){
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Objects.nonNull(user.getName()),User::getName, user.getName());
List<User> userList = userService.list(wrapper);
return userList;
}
2.3 SpringRedis缓存
添加依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
yml添加:
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
cache:
redis:
time-to-live: 1800000 #设置缓存有效期
从SpringCache缓存切换到Redis缓存,只需要修改配置即可。
3. 接口文档Swagger
3.1 增强版本
使用增强功能knife4j
(1) Pom配置
<!-- 增强swagger-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>
(2) WebMvc配置
/**
* @Author: KingWang
* @Date: 2023/3/7
* @Desc:
**/
@Slf4j
@Configuration
@EnableSwagger2
@EnableKnife4j
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/");
//swagger文档需要添加下面2行
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
@Bean
public Docket createRestApi(){
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.guigutool.reggie.controller"))
.paths(PathSelectors.any())
.build();
}
public ApiInfo apiInfo(){
return new ApiInfoBuilder()
.title("瑞吉外卖")
.version("1.0")
.description("瑞吉外卖接口文档")
.build();
}
}
(3) 过滤中放行静态资源访问权限
LoginCheckFilter中的doFilter方法,如果不放行,则需要登录后才可以查看doc.html。
@Override
protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("拦截到请求:{}", request.getRequestURI());
//1. 获取本次请求的URI
String requestURI = request.getRequestURI();
//2.定义不需要处理的请求路径
String[] urls = new String[]{
"/employee/login",
"/employee/logout",
"/backend/**",
"/front/**",
"/common/**",
"/user/sendMsg", //移动端发送短信
"/user/login", //移动端登录
"/doc.html", //生成swagger api文件,这里不放行,在登录后,也可以查看
"/webjars/**",
"/swagger-resources",
"/v2/api-docs"
};
//其他内容省略...
}
(4) 接口文档查看
地址:http://localhost:8080/doc.html#/home
3.2 常用Swagger注解
注解 | 说明 |
---|---|
@Api | 用于请求的类上,例如Controller,表示对类的说明 |
@ApiModel | 用在类上,通常是实体类,表示一个返回响应数据的信息 |
@ApiModelProperty | 用在属性上,描述响应类的属性 |
@ApiOperation | 用在请求的方法上,说明方法的用途,作用 |
@ApiImplicitParams | 用在请求的方法上,表示一组参数说明 |
@ApiImplicitParam | 用在@ApiImplicitParams注解中,指定一个请求参数的各个方面 |
3.3 Swagger注解实例
(1)实体类注解
/**
* 套餐
*/
@Data
@ApiModel("套餐")
public class Setmeal implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty("主键Id")
private Long id;
@ApiModelProperty("分类Id")
private Long categoryId;
@ApiModelProperty("套餐名称")
private String name;
@ApiModelProperty("套餐价格")
private BigDecimal price;
@ApiModelProperty("状态 0:停用 1:启用")
private Integer status;
@ApiModelProperty("编码")
private String code;
@ApiModelProperty("描述信息")
private String description;
@ApiModelProperty("图片")
private String image;
@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;
//是否删除
private Integer isDeleted;
}
(2)Controller注解
/**
* @Author: KingWang
* @Date: 2023/3/14
* @Desc:
**/
@RequestMapping("/setmeal")
@Slf4j
@RestController
@Api(tags="套餐相关接口")
public class SetmealController {
private final String cacheName = "setmealCache";
@Autowired
private SetmealDishService setmealDishService;
@Autowired
private SetmealService setmealService;
@Autowired
private CategoryService categoryService;
@Autowired
private RedisTemplate redisTemplate;
/**
* 新增套餐
* @param setmealDto
* @return
*/
@ApiOperation(value = "新增套餐")
@PostMapping
@CacheEvict(value = cacheName, allEntries = true)
public R<String> save(@RequestBody SetmealDto setmealDto){
setmealService.saveWithDish(setmealDto);
Set keys = redisTemplate.keys("setmeal:*");
redisTemplate.delete(keys);
return R.success("新增套餐成功");
}
@ApiOperation(value = "套餐分页查询")
@GetMapping("/page")
public R<Page> page(String name, int page,int pageSize){
Page<Setmeal> pageInfo = new Page(page,pageSize);
LambdaQueryWrapper<Setmeal> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.like(null != name, Setmeal::getName,name);
setmealService.page(pageInfo, queryWrapper);
Page<SetmealDto> setmealDtoPage = new Page<>(page,pageSize);
BeanUtils.copyProperties(pageInfo,setmealDtoPage,"records");
List<SetmealDto> setmealDtoList = pageInfo.getRecords().stream().map(x-> {
SetmealDto setmealDto = new SetmealDto();
BeanUtils.copyProperties(x,setmealDto);
Category category = categoryService.getById(x.getCategoryId());
setmealDto.setCategoryName(category.getName());
return setmealDto;
}).collect(Collectors.toList());
setmealDtoPage.setRecords(setmealDtoList);
return R.success(setmealDtoPage);
}
/**
* 删除套餐
* @param ids
* @return
*
* allEntries=true 清理指定key下的所有数据
*/
@ApiOperation(value = "删除套餐")
@DeleteMapping
@CacheEvict(value = cacheName, allEntries = true)
public R<String> delete(@RequestParam List<String> ids){
log.info("delete ids: {}",ids);
setmealService.removeWithDish(ids);
Set keys = redisTemplate.keys("setmeal:*");
redisTemplate.delete(keys);
return R.success("套餐删除成功");
}
@ApiOperation(value = "套餐列表查询")
@Cacheable(value = cacheName,key = "#setmeal.categoryId+':'+ #setmeal.status")
@GetMapping("/list")
public R<List<Setmeal>> list(Setmeal setmeal){
List<Setmeal> setmealList = null;
String key = "setmeal:" + setmeal.getCategoryId();
setmealList = (List<Setmeal>)redisTemplate.opsForValue().get(key);
if(Objects.nonNull(setmealList)){
return R.success(setmealList);
}
LambdaQueryWrapper<Setmeal> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Objects.nonNull(setmeal.getId()),Setmeal::getId,setmeal.getId());
wrapper.eq(Objects.nonNull(setmeal.getCategoryId()),Setmeal::getCategoryId,setmeal.getCategoryId());
wrapper.eq(Objects.nonNull(setmeal.getStatus()),Setmeal::getStatus,setmeal.getStatus());
wrapper.orderByDesc(Setmeal::getUpdateTime);
setmealList = setmealService.list(wrapper);
redisTemplate.opsForValue().set(key, setmealList);
return R.success(setmealList);
}
}
(3)工具类R注解
@Data
@ApiModel("返回结果")
public class R<T> implements Serializable {
@ApiModelProperty("编码")
private Integer code; //编码:1成功,0和其它数字为失败
@ApiModelProperty("返回信息")
private String msg; //返回的信息
@ApiModelProperty("数据")
private T data; //数据
@ApiModelProperty("动态数据")
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;
}
}
文档展示:
4. 前后端分离部署
4.1 nginx部署前端项目
nginx配置:
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
root html/dist;
index index.html;
}
location ^~ /api/ {
rewrite ^/api/(.*)$ /$1 break;
proxy_pass http://192.168.80.250:8080;
}
#error_page 404 /404.html;
从前端访问80页面后,代理访问到后端http://192.168.80.250:8080的地址请求
4.2 shell脚本一键部署后端项目
后端一键部署流程,git拉取 -> 编译,打包 -> 启动
脚本名称命名为bootStart.sh.
注意不能带reggie这个名称字样,否则下面判断进程是否存活时,会找到脚本自己。
#!/bin/sh
echo =================================
echo $(date "+%Y-%m-%d %H:%M:%S") 自动化部署脚本启动
echo =================================
echo $(date "+%Y-%m-%d %H:%M:%S") 停止原来运行中的工程
APP_NAME=reggie
tpid=`ps -ef|grep ${APP_NAME}|grep -v grep|grep -v $$|awk '{print $2}'`
if [[ ${tpid} != "" ]]; then
echo '当前进程号:' $tpid
echo 'Stop Process...'
kill -15 $tpid
fi
sleep 3
tpid=`ps -ef|grep ${APP_NAME}|grep -v grep|grep -v $$|awk '{print $2}'`
if [[ ${tpid} != "" ]]; then
echo '当前进程号:' $tpid '任然存在'
echo 'Kill Process!'
kill -9 $tpid
else
echo 'Stop Success!'
fi
echo $(date "+%Y-%m-%d %H:%M:%S") 准备从Git仓库拉取最新代码
cd /opt/local/reggie
echo $(date "+%Y-%m-%d %H:%M:%S") 开始从Git仓库拉取最新代码
git pull
echo currTime 代码拉取完成
echo $(date "+%Y-%m-%d %H:%M:%S") 开始打包
output=`mvn clean package -Dmaven.test.skip=true`
cd target
echo $(date "+%Y-%m-%d %H:%M:%S") 启动项目
nohup java -jar reggie-1.0-SNAPSHOT.jar &> reggie.log &
echo $(date "+%Y-%m-%d %H:%M:%S") 项目启动完成
启动示例:
5. Git常用操作
5.1 Git本地与远程建立连接
(1) 项目根目录添加.gitignore文件
gitignore文件可以指定在提交到远程仓库时,不需要提交的文件。
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
.idea
/test/
/target/
/target/
/target/classes/
(2)本地创建git仓库
(3)提交文件
添加gitignore文件
git add .gitignore
git commit
(4) 在远程gitee创建一个项目地址
(5)推送到远程仓库
点开Define remote,输入在gitee创建的库的地址,
5.2 分支
分支创建
5.3 合并
将develop分支开发好的内容合并到当前master分支上