苍穹外卖复习
jwt的登录验证
1.JwtProperties(jwt的基本配置项):
@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {
/**
* 管理端员工生成jwt令牌相关配置
*/
private String adminSecretKey;
private long adminTtl;
private String adminTokenName;
/**
* 用户端微信用户生成jwt令牌相关配置
*/
private String userSecretKey;
private long userTtl;
private String userTokenName;
}
在配置文件中设置好
sky:
jwt:
# 设置jwt签名加密时使用的秘钥
admin-secret-key: itcast
# 设置jwt过期时间
admin-ttl: 7200000
# 设置前端传递过来的令牌名称
admin-token-name: token
user-secret-key: itheima
user-ttl: 7200000
user-token-name: authentication
2.JwtTokenInterceptor(jwt的拦截器)
/**
* jwt令牌校验的拦截器
*/
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtProperties jwtProperties;
/**
* 校验jwt
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("当前线程的id"+ Thread.currentThread().getId());
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
//2、校验令牌
try {
log.info("jwt校验:{}", token);
Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
log.info("当前员工id:", empId);
BaseContext.setCurrentId(empId);
//3、通过,放行
return true;
} catch (Exception ex) {
//4、不通过,响应401状态码
response.setStatus(401);
return false;
}
}
}
JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
这行代码的作用是验证并解析JWT令牌:
JwtUtil.parseJWT():调用JWT工具类的解析方法,该方法会验证令牌的签名有效性并解码内容 。jwtProperties.getAdminSecretKey():获取用于验证签名的密钥,这个密钥必须与生成令牌时使用的密钥一致 。token:待解析的JWT令牌字符串,通常从HTTP请求头中获取。Claims claims:解析成功后返回的声明对象,包含了JWT负载(Payload)中的所有数据 。Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());- 这行代码从解析后的声明中提取具体的用户标识信息:
claims.get(JwtClaimsConstant.EMP_ID):从Claims对象中获取键为EMP_ID的声明值,这通常是在用户登录生成JWT时存入的员工ID 。.toString():将获取到的对象转换为字符串,确保类型安全。Long.valueOf():把字符串转换为Long类型的员工ID,便于后续在业务中使用。
其中的parseJWT是token解密:
/**
* Token解密
*
* @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个
* @param token 加密后的token
* @return
*/
public static Claims parseJWT(String secretKey, String token) {
// 得到DefaultJwtParser
Claims claims = Jwts.parser()
// 设置签名的秘钥
.setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8))
// 设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
生成jwt:
public static String createJWT(String secretKey, long ttlMillis, Map<String, Object> claims) {
// 指定签名的时候使用的签名算法,也就是header那部分
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
// 设置jwt的body
JwtBuilder builder = Jwts.builder()
// 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
// 设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8))
// 设置过期时间
.setExpiration(exp);
return builder.compact();
}
3.在代码中使用jwt:
@PostMapping("/login")
@ApiOperation(value = "员工登录")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
log.info("员工登录:{}", employeeLoginDTO);
Employee employee = employeeService.login(employeeLoginDTO);
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
.id(employee.getId())
.userName(employee.getUsername())
.name(employee.getName())
.token(token)
.build();
return Result.success(employeeLoginVO);
}
在webConfiguration中注册自定义拦截器:
/**
* 注册自定义拦截器
*
* @param registry
*/
protected void addInterceptors(InterceptorRegistry registry) {
log.info("开始注册自定义拦截器...");
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**")
.excludePathPatterns("/admin/employee/login");
registry.addInterceptor(jwtTokenUserInterceptor)
.addPathPatterns("/user/**")
.excludePathPatterns("/user/user/login")
.excludePathPatterns("/user/shop/status");
}
新增菜品
在新增菜品中用到了集合遍历,下面这三种遍历效果一样
传统 for 循环
for (DishFlavor flavor : flavors) { flavor.setDishId(dishId); }
语法冗长,但逻辑清晰
Stream API
flavors.stream().peek(flavor -> flavor.setDishId(dishId)).collect(...)
适合链式操作,但需注意副作用(如修改原集合)
peek的作用
对流中的每个 flavor对象执行 setDishId(dishId)操作,为每个口味对象设置关联的菜品 ID。
关键点:peek是中间操作,不会终止流,需配合 collect等终止操作触发执行。
链式操作流程
•
流生成:flavors.stream()将集合转为流。
•
副作用操作:peek修改元素属性(设置 dishId)。
•
终止操作:collect(...)收集结果(如转 List或 Map)。
方法引用
flavors.forEach(flavor -> flavor.setDishId(dishId));
与 Lambda 类似,但更简洁
在新增菜品的时候的xml语句
1.插入数据,并且设置自增键
核心机制
- 数据库自动生成主键 当数据库表的主键字段设置为 自增(AUTO_INCREMENT) 时(如 MySQL 的
id INT AUTO_INCREMENT),执行INSERT语句时数据库会自动生成唯一的主键值。 无需手动插入:你不需要在 SQL 中显式为id字段赋值(如id=#{id}),数据库会自动填充。 - MyBatis 回填主键到实体类
useGeneratedKeys="true":启用 MyBatis 的 主键回填功能,通过 JDBC 的getGeneratedKeys()方法获取数据库生成的主键。keyProperty="id":将获取到的主键值赋给 Java 对象的id属性(需与实体类属性名一致)。
<!-- useGeneratedKeys:true 表示获取主键值
keyProperty="id" 表示将主键值赋给id属性-->
insert into dish
(status, name, category_id, price, image, description, create_time, update_time, create_user,update_user) values
(#{status}, #{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime},#{createUser}, #{updateUser})
2:批量插入数据
collection="flavors"
功能:指定要迭代的集合来源
item="dishFlavor"
功能:定义集合中每个元素的别名。
separator=","
功能:指定元素之间的分隔符。
insert into dish_flavor(dish_id, name, value) values
(#{dishFlavor.dishId},#{dishFlavor.name},#{dishFlavor.value})
菜品的分页查询
除了DTO和VO外,分页查询还需要的是一个PageResult来获取一个总的记录数和当前页的数据集合:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable {
private long total; //总记录数
private List records; //当前页数据集合
}
@GetMapping("/page")
@ApiOperation("菜品分页查询")
public Result<PageResult>page (DishPageQueryDTO dishPageQueryDTO) {
log.info("菜品分页查询");
PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
return Result.success(pageResult) ;
}
实际实现分页查询:
PageHelper.startPage:在分页查询中使用到了PageHelper.startPage来设置分页参数并启动分页拦截器。
分页参数即当前页码和当前要返回多少数据,这是用前端给我们的DTO中获取的
Page:这是内置的类型,用于分页,含有方法来帮助获得总记录数和数据集合。
public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
PageHelper.startPage(dishPageQueryDTO.getPage(),dishPageQueryDTO.getPageSize());
Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
return new PageResult(page.getTotal(),page.getResult());
}
查询的xml语句:
作用:
- 查询
dish表所有字段(d.*)和category表的name字段(别名categoryName)。 - 左外连接:即使
dish表的category_id在category表中无匹配记录,仍会返回dish数据(categoryName为NULL)。
<select id="pageQuery" resultType="com.sky.vo.DishVO">
select d.* , c.name as categoryName from dish d left outer join category c on d.category_id = c.id
<where>
<if test="name != null">
and d.name like concat('%',#{name},'%')
</if>
<if test="categoryId != null">
and d.category_id = #{categoryId}
</if>
<if test="status != null">
and d.status = #{status}
</if>
</where>
order by d.create_time desc
</select>
删除菜品
测功能较为简单
单个删除:
@Delete("delete from dish where id = #{id}")
批量删除:
open和close是在最后的结果上加上左括号和右括号
<delete id="deleteByDishIds">
delete from dish_flavor where dish_id in
<foreach collection="dishIds" item="dishids" separator="," open="(" close=")">
#{dishids}
</foreach>
</delete>
修改菜品
正常的修改菜品
<update id="update">
update dish
<set>
<if test="name != null">name = #{name}, </if>
<if test="categoryId != null">category_id = #{categoryId}, </if>
<if test="price != null">price = #{price}, </if>
<if test="image != null">image = #{image}, </if>
<if test="description != null">description = #{description}, </if>
<if test="status != null">status = #{status}, </if>
<if test="updateTime != null">update_time = #{updateTime}, </if>
<if test="updateUser != null">update_user = #{updateUser}, </if>
</set>
where id = #{id}
</update>
在修改对应的口味的时候,执行删除和插入口味的操作
Redis
在java中使用redis
1.导入Spring Data Redis 的maven坐标
2.配置Redis数据源
3.编写配置类,创建RedisTemplate对象
4.通过RedisTemplate对象操作Redis
利用RedisTemplate对象模板来进行编写redis:
@Configuration
public class RedisConfiguration {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 设置Key的序列化器
template.setKeySerializer(new StringRedisSerializer());
// 设置Value的序列化器为JSON序列化器
template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
// 设置Hash Key的序列化器
template.setHashKeySerializer(new StringRedisSerializer());
// 设置Hash Value的序列化器
template.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return template;
}
}
为什么要设置序列化器?
答:
RedisTemplate默认使用 JDK 序列化,这会导致存储在 Redis 里的 key 和 value 是二进制的、不可读的格式(类似 \xac\xed\x00\x05t\x00\x03city)。
通过设置 KeySerializer为 StringRedisSerializer,确保了所有 Redis 的 key 都会以可读的字符串形式存储(例如,直接就是 "city"),这在命令行调试或可视化工具中查看时非常清晰。
操作几种类型的数据:
ValueOperations valueOperations = redisTemplate.opsForValue();
HashOperations hashOperations = redisTemplate.opsForHash();
ListOperations listOperations = redisTemplate.opsForList();
SetOperations setOperations = redisTemplate.opsForSet();
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
/**
* 操作字符串类型数据
*/
@Test
public void testString() {
//set
redisTemplate.opsForValue().set("city", "BEIJING");
//get
String city = (String) redisTemplate.opsForValue().get("city");
System.out.println(city);
//setex
redisTemplate.opsForValue().set("province", "shanxi", 2, TimeUnit.MINUTES);
//setnx
redisTemplate.opsForValue().setIfAbsent("look", "1");
redisTemplate.opsForValue().setIfAbsent("look", "2");
}
/**
* 操作哈希类型的数据
*/
@Test
public void testHash() {
//hset het hdel hkeys hvals
HashOperations hashOperations = redisTemplate.opsForHash();
hashOperations.put("100", "name", "tom");
hashOperations.put("100", "age", "20");
String name = (String) hashOperations.get("100", "age");
System.out.println(name);
Set keys = hashOperations.keys("100");
System.out.println(keys);
List values = hashOperations.values("100");
System.out.println(values);
hashOperations.delete("100", "age");
}
/**
* 操作列表类型数据
*/
//Lpush Lrange rpop Llen
@Test
public void testList() {
ListOperations listOperations = redisTemplate.opsForList();
listOperations.leftPushAll("mylist", "a", "b", "c");
listOperations.leftPush("mylist", "d");
List mylist = listOperations.range("mylist", 0, -1);
System.out.println(mylist);
listOperations.rightPop("mylist");
Long size = listOperations.size("mylist");
System.out.println(size);
}
@Test
public void testSet() {
//sadd smebers scard sinter sunion srem
SetOperations setOperations = redisTemplate.opsForSet();
setOperations.add("set1", "a", "b", "c", "d");
setOperations.add("set2", "b", "c", "d", "e");
Set members = setOperations.members("set1");
System.out.println(members);
Long size = setOperations.size("set1");
System.out.println(size);
Set intersection = setOperations.intersect("set1", "set2");
System.out.println(intersection);
Set union = setOperations.union("set1", "set2");
System.out.println(union);
setOperations.remove("set1", "a", "b");
}
/**
* 操作有序集合
*/
@Test
public void testZSet() {
//zadd zrange zincrby zrem
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
zSetOperations.add("zset1", "a", 10);
zSetOperations.add("zset1", "b", 20);
zSetOperations.add("zset1", "c", 30);
Set zset1 = zSetOperations.range("zset1", 0, -1);
System.out.println(zset1);
zSetOperations.incrementScore("zset1", "c", 1);
zSetOperations.remove("zset1", "a");
}
/**
* 通用命令操作
*/
//keys exists type del
@Test
public void testCommon() {
Set keys = redisTemplate.keys("*");
System.out.println(keys);
Boolean name = redisTemplate.hasKey("name");
Boolean age = redisTemplate.hasKey("set1");
for (Object key : keys) {
DataType type = redisTemplate.type(key);
System.out.println(type.name());
}
redisTemplate.delete("mylist");
}
HttpClient
作用:1.发送http请求
2.接受响应数据
代码解释:
CloseableHttpClient httpClient = HttpClients.createDefault();
创建HTTP客户端实例。这是发送请求的基础,类似于打开一个浏览器
HttpClients.createDefault()会创建一个具有默认配置的HTTP客户端,该客户端支持连接复用(连接池),能有效提升性能
HttpGet httpGet = new HttpGet("http://localhost:8080/...");
构建GET请求对象。指定了请求的URL(这里是一个本地服务的地址)和HTTP方法(GET)
HttpGet对象封装了请求的所有信息,包括URL、请求头、参数等。你可以通过 setHeader方法为其添加请求头
CloseableHttpResponse response = httpClient.execute(httpGet);
发送请求并获取响应。这是执行网络调用的核心步骤
httpClient.execute(...)方法会同步地发送请求,并返回一个 CloseableHttpResponse对象,该对象包含了服务器返回的全部信息
int statusCode = response.getStatusLine().getStatusCode();
从响应中获取HTTP状态码(如200表示成功,404表示未找到等)
状态码是HTTP协议规定的一部分,位于响应行的第一行,通过 StatusLine对象获取
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
从响应中提取响应体内容。响应体是服务器返回的实际数据(如JSON、HTML等)
HttpEntity代表了HTTP消息的实体内容(如请求体或响应体)。EntityUtils.toString(...)是一个工具方法,将实体内容流转换为字符串
response.close();
httpClient.close();
关闭连接,释放资源。这非常重要,可以防止资源泄漏
GET方式请求:
@Test
public void testGet() throws IOException {
//创建httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");
//发送请求并接收响应结果
CloseableHttpResponse response = httpClient.execute(httpGet);
//获取服务端返回的状态码
int statusCode = response.getStatusLine().getStatusCode();
System.out.println(statusCode);
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("服务端返回的数据是"+body);
//关闭资源
response.close();
httpClient.close();
}
1. 接收原始数据
CloseableHttpResponse
代表完整的HTTP响应,其内部通过 socket 连接接收来自服务器的原始字节流。这是最底层的数据形式。
优点:完整保留了服务器发送的原始信息。
缺点:对开发者不友好,无法直接读取和操作。
2. 协议层封装
HttpEntity entity = response.getEntity();
HttpEntity是一个封装对象,它包裹了原始的字节流 (InputStream),并提供了访问响应体(内容字节)、内容类型 (Content-Type) 和内容长度 (Content-Length) 等元数据的方法。它将杂乱的字节流包装成一个结构化的对象。
优点:提供了一个标准的接口来访问响应内容和元数据,屏蔽了底层字节流的复杂性。
缺点:内容本身仍是字节,需要进一步处理。
3. 应用层解码
String body = EntityUtils.toString(entity);
这是最关键的一步!EntityUtils.toString()方法会做两件事:
- 从
HttpEntity中读取字节流。
2. 根据元数据(如Content-Type头中的charset=UTF-8)或默认设置,将字节流解码成字符串。这一步将机器理解的字节转换成了人类和程序容易处理的文本。
post方式请求:
@Test
public void testPost() throws IOException, JSONException {
//创建httpClient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
//创建请求对象
HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");
jsonObject.put("password","123456");
StringEntity stringEntity = new StringEntity(jsonObject.toString());
//指定请求编码方式
stringEntity.setContentEncoding("UTF-8");
stringEntity.setContentType("application/json");
httpPost.setEntity(stringEntity);
//发送请求
CloseableHttpResponse response = httpClient.execute(httpPost);
//解析返回结果
int statusCode = response.getStatusLine().getStatusCode();
System.out.println("响应码为:"+statusCode);
HttpEntity entity = response.getEntity();
String body = EntityUtils.toString(entity);
System.out.println("响应数据为"+body);
//关闭资源
response.close();
httpClient.close();
}
代码解析:
CloseableHttpClient httpClient = HttpClients.createDefault();
创建HTTP客户端实例。这是发送所有请求的基础,类似于打开一个浏览器
HttpPost httpPost = new HttpPost("http://localhost...");
创建POST请求对象,并设定请求的目标URL(这里是一个本地登录接口)
JSONObject jsonObject = new JSONObject();
jsonObject.put("username","admin");...
构建JSON格式的请求参数。这里将一个包含用户名(admin)和密码(123456)的Java对象转换为JSON字符串,作为请求体发送
StringEntity stringEntity = new StringEntity(jsonObject.toString());
stringEntity.setContentEncoding("UTF-8");
stringEntity.setContentType("application/json");
httpPost.setEntity(stringEntity);
设置请求体。将JSON字符串包装成StringEntity,并明确指定其内容编码为UTF-8,内容类型为application/json,然后将其放入POST请求中
CloseableHttpResponse response = httpClient.execute(httpPost);
执行请求。客户端将携带参数的请求发送至服务器,并获取一个包含所有返回信息的响应对象
int statusCode = response.getStatusLine().getStatusCode();
String body = EntityUtils.toString(entity);
解析响应。从响应中提取HTTP状态码(如200成功)和响应体(服务器返回的实际数据,如登录结果)
response.close();
httpClient.close();
关闭连接,释放资源。这是一个好习惯,可以防止资源泄漏
(为什么将其包装成StringEnity?
答:因为HTTP请求体(Body)传输的是字节流(byte stream),而不是Java对象。直接将JSON对象(如JSONObject)放进去是不可能的,必须有一个“转换器”将它变成符合HTTP协议标准的格式。StringEntity就是这个关键的转换器。
下面这个表格清晰地对比了两种方式的本质区别:
| 特性 | 直接放入JSON对象 (不可行) | 使用StringEntity (标准做法) |
|---|---|---|
| HTTP协议要求 | 请求体必须是字节流 | 将字符串转换为字节流,符合协议要求 |
| 数据格式 | Java内存中的对象,无法直接传输 | 字符串(String),是字节流的直接来源 |
| 内容类型(Content-Type) | 无法自动设置,服务器无法识别数据格式 | 可明确设置(如application/json),告诉服务器如何解析 |
| 字符编码 | 无法控制,可能导致乱码 | 可指定编码(如UTF-8),确保字符正确传输 |
)
微信登录
配置微信登陆的设置项:
配置为微信用户生成jwt令牌时使用的配置项:
DTO:
VO:
Controller:
@RestController
@RequestMapping("/user/user")
@Slf4j
@Api(tags = "C端用户接口")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private JwtProperties jwtProperties;
/**
* 微信登陆
* @param userLoginDTO
* @return
*/
@PostMapping("/login")
@ApiOperation("微信登录")
public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
log.info("微信用户登录:{}",userLoginDTO.getCode());
//微信登录
User user = userService.wxlogin(userLoginDTO);
//微信用户生成jwt令牌
Map<String,Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.USER_ID,user.getId());
String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);
UserLoginVO userLoginVO = UserLoginVO.builder()
.id(user.getId())
.openid(user.getOpenid())
.token(token)
.build();
return Result.success(userLoginVO);
}
}
Service:
@Service
@Slf4j
public class UserServiceImpl implements UserService {
public static final String WX_LOGIN ="https://api.weixin.qq.com/sns/jscode2session";
@Autowired
private WeChatProperties weChatProperties;
@Autowired
private UserMapper userMapper;
@Autowired
private UserController userController;
/**
* 微信登录
* @param userLoginDTO
* @return
*/
@Override
public User wxlogin(UserLoginDTO userLoginDTO) {
// Map<String, String> map = new HashMap();
//
// map.put("appid",weChatProperties.getAppid());
// map.put("secret",weChatProperties.getSecret());
// map.put("js_code",userLoginDTO.getCode());
// map.put("grant_type","authorization_code");
// //调用微信接口服务,获取用户的openid
// String json = HttpClientUtil.doGet(WX_LOGIN,map);
//
//
// JSONObject jsonObject = JSON.parseObject(json);
// String openid = jsonObject.getString("openid");
String openid =getOpenid(userLoginDTO.getCode());
//判断openid是否为空?抛出异常:合法
if (openid ==null){
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}
//判断微信用户是否为外卖系统的新用户,是的话进行注册并保存openid
User user = userMapper.getByOpenid(openid);
if (user == null){
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
userMapper.insert(user);
}
return user;
}
private String getOpenid(String code) {
Map<String, String> map = new HashMap();
map.put("appid",weChatProperties.getAppid());
map.put("secret",weChatProperties.getSecret());
map.put("js_code",code);
map.put("grant_type","authorization_code");
//调用微信接口服务,获取用户的openid
String json = HttpClientUtil.doGet(WX_LOGIN,map);
JSONObject jsonObject = JSON.parseObject(json);
String openid = jsonObject.getString("openid");
return openid;
}
}
还有jwt的拦截器编写和注册拦截器...(和jwt登录验证一起统一学习)
缓存菜品
例如:在查询菜品的时候开始是否缓存?
/**
* 根据分类id查询菜品
*
* @param categoryId * @return
*/
@GetMapping("/list")
@ApiOperation("根据分类id查询菜品")
public Result<List<DishVO>> list(Long categoryId) {
//构造redis中的key,规则:dish_分类id
String key = "dish_"+categoryId;
//查询redis中是否存在菜品数据
List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
if (list != null && list.size()>0) {
//如果存在,直接返回,无需查询数据库
return Result.success(list);
}
Dish dish = new Dish();
dish.setCategoryId(categoryId);
dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品
//如果不存在,查询数据库,将查询到的数据放入redis中
list = dishService.listWithFlavor(dish);
redisTemplate.opsForValue().set(key,list);
return Result.success(list);
}
删除缓存操作(像在进行对菜品修改的时候,对菜品删除的时候,它的缓存数据就需要删除):
private void claeanCache(String pattern){
Set keys = redisTemplate.keys(pattern);
redisTemplate.delete(keys);
}
利用注解来进行缓存:
@Cacheable
- 作用:适用于查询方法。遵循“先查缓存,没有再执行方法”的流程。
- 用法:标注在方法上。主要指定两个属性:
value/cacheNames:缓存名称(必填),可指定多个,如@Cacheable("users")或@Cacheable({"users", "admins"})。key:缓存键(可选)。支持 SpEL 表达式,默认为方法参数组合。如@Cacheable(value="user", key="#id")。condition:条件缓存(可选)。满足条件才缓存,如@Cacheable(..., condition="#id > 10")。
@CachePut
- 作用:适用于更新/新增方法。总是会执行方法,并将方法返回的结果更新到缓存中。
- 用法:通常用于更新数据库后同步更新缓存。必须和
@Cacheable使用相同的value和key,以确保更新的是同一份数据。 - 注意:与
@Cacheable最大区别在于,它从不跳过方法执行。
@CacheEvict
- 作用:适用于删除方法。用于从缓存中移除数据。
- 用法:标注在方法上。关键属性:
value/cacheNames:指定要清除的缓存名称。key:指定要清除的缓存键。allEntries:是否清空整个缓存区域(可选,默认为false)。若为true,则忽略key,清除value下的所有缓存。beforeInvocation:清除操作在方法调用前还是调用后执行(可选,默认为false即方法调用后执行)。设置为true可避免方法执行异常导致缓存未清除。
微信支付:
Spring Task定时任务编写:
@Scheduled(cron = "0 0 1 * * ?")//每天凌晨一点触发一次
/**
* 处理一直处于派送中的订单
*/
@Scheduled(cron = "0 0 1 * * ?")//每天凌晨一点触发一次
public void processDeliveryOrder(){
log.info("定时处理处于派送中的订单:{}", LocalDateTime.now());
List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS,LocalDateTime.now().plusMinutes(-60));
for (Orders orders : ordersList) {
orders.setStatus(Orders.COMPLETED);
orderMapper.update(orders);
}
}
}
利用此注解进行定时任务触发。
Websocket:
实现步骤:
直接使用websocket.html页面作为WebSocket客户端
导入WebSocket的maven坐标
导入WebSocket服务端组件WebSocketServer,用于和客户端通信
导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
导入定时任务类WebSocketTask,定时向客户端推送数据
WebSocketServer:
/**
* WebSocket服务
*/
@Component
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {
//存放会话对象
private static Map<String, Session> sessionMap = new HashMap();
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
System.out.println("客户端:" + sid + "建立连接");
sessionMap.put(sid, session);
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, @PathParam("sid") String sid) {
System.out.println("收到来自客户端:" + sid + "的信息:" + message);
}
/**
* 连接关闭调用的方法
*
* @param sid
*/
@OnClose
public void onClose(@PathParam("sid") String sid) {
System.out.println("连接断开:" + sid);
sessionMap.remove(sid);
}
/**
* 群发
*
* @param message
*/
public void sendToAllClient(String message) {
Collection<Session> sessions = sessionMap.values();
for (Session session : sessions) {
try {
//服务器向客户端发送消息
session.getBasicRemote().sendText(message);
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
APch@ServerEndpoint("/ws/{sid}")
将该类声明为一个WebSocket服务端点,客户端通过 ws://地址:端口/ws/客户端唯一标识连接
@Component
表明这是一个Spring管理的Bean,通常需要配合ServerEndpointExporter配置才能生效
sessionMap
一个静态的Map,核心设计,用于全局保存所有已连接的客户端会话(Session)及其标识(sid)
@OnOpen
标注客户端连接成功时调用的方法,用于初始化操作
@OnMessage
标注收到客户端消息时调用的方法,用于处理业务逻辑
@OnClose
标注连接关闭时调用的方法,用于清理资源
@PathParam("sid")
用于从连接路径中提取变量sid的值,作为客户端的唯一标识
sendToAllClient
一个自定义的群发方法,用于向所有连接的客户端推送消息
代码片段
角色与作用
通俗理解
session
代表一次独立的 WebSocket 连接
好比是和一个朋友建立的专属电话线路
.getBasicRemote()
获取一个同步的消息发送对象
选择用普通电话模式交流(说完一句等对方回应)
.sendText(message)
同步地发送文本消息
在电话里说出一句话,并等待对方听到
WebSocketConfiguration
/**
* WebSocket配置类,用于注册WebSocket的Bean
*/
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
测试任务:
WebSocketTask
@Component
public class WebSocketTask {
@Autowired
private WebSocketServer webSocketServer;
/**
* 通过WebSocket每隔5秒向客户端发送消息
*/
@Scheduled(cron = "0/5 * * * * ?")
public void sendMessageToClient() {
webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
}
}
Apache ECharts
/**
* 订单统计
* @param begin
* @param end
* @return
*/
@GetMapping("/ordersStatistics")
@ApiOperation("订单数据统计")
public Result<OrderReportVO> ordersStatistics(@DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate begin,
@DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate end)
{
log.info("订单数据统计:{},{}",begin,end);
return Result.success(reportService.getOrderStatistics(begin,end));
}
@Override
public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
List<LocalDate> dateList = new ArrayList<>();
dateList.add(begin);
while(!begin.equals(end)){
begin = begin.plusDays(1);
dateList.add(begin);
}
List<Integer> orderCountList = new ArrayList<>();
List<Integer> ValidOrderCountList = new ArrayList<>();
Integer TotalOrderCount = 0;
Integer ValidOrderCount = 0;
for (LocalDate date : dateList) {
LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
Map map = new HashMap();
map.put("begin", beginTime);
map.put("end", endTime);
map.put("status", Orders.COMPLETED);
Integer totalOrder = userMapper.countOrderByMap(map);
TotalOrderCount = TotalOrderCount + totalOrder;
orderCountList.add(totalOrder);
Integer validOrder = userMapper.countOrderValidByMap(map);
ValidOrderCount = ValidOrderCount + validOrder;
ValidOrderCountList.add(validOrder);
}
Double OrderCompletionRate = ValidOrderCount.doubleValue()/TotalOrderCount.doubleValue();
return OrderReportVO.builder()
.dateList(StringUtils.join(dateList, ","))
.orderCountList(StringUtils.join(orderCountList, ","))
.validOrderCountList(StringUtils.join(ValidOrderCountList, ","))
.totalOrderCount(TotalOrderCount)
.validOrderCount(ValidOrderCount)
.orderCompletionRate(OrderCompletionRate)
.build();
}
<select id="countOrderByMap" resultType="java.lang.Integer">
select count(id) from orders
<where>
<if test="begin != null">
and order_time >#{begin}
</if>
<if test="end != null">
and order_time <#{end}
</if>
</where>
</select>
Apache POI:
@Override
public void exportBusinessData(HttpServletResponse response) {
//1.查询数据库,获取营业数据————查询销量30天的运营数据
LocalDate dateBegin = LocalDate.now().minusDays(30);
LocalDate dateEnd = LocalDate.now().minusDays(1);
LocalDateTime beginTime = LocalDateTime.of(dateBegin, LocalTime.MIN);
LocalDateTime endTime = LocalDateTime.of(dateEnd, LocalTime.MAX);
BusinessDataVO businessDataVO = workspaceService.getBusinessData(beginTime, endTime);
//2.通过poi将数据写入excel文件
InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
try {
//基于模板文件创建一个心的excel文件
XSSFWorkbook excel = new XSSFWorkbook(in);
XSSFSheet sheet = excel.getSheet("Sheet1");
//填充数据--时间
sheet.getRow(1).getCell(1).setCellValue("时间:"+dateBegin+"至:"+dateEnd);
sheet.getRow(3).getCell(2).setCellValue(businessDataVO.getTurnover());
sheet.getRow(3).getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());
sheet.getRow(3).getCell(6).setCellValue(businessDataVO.getNewUsers());
//获得第五行
XSSFRow row = sheet.getRow(4);
row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());
row.getCell(4).setCellValue(businessDataVO.getUnitPrice());
//填充明细数据
for (int i = 0; i < 30; i++) {
LocalDate date = dateBegin.plusDays(i);
//查询某一天的营业数据
BusinessDataVO businessData = workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
//获得行
row = sheet.getRow(7 + i);
//获得单元格
row.getCell(1).setCellValue(date.toString());
row.getCell(2).setCellValue(businessData.getTurnover());
row.getCell(3).setCellValue(businessData.getValidOrderCount());
row.getCell(4).setCellValue(businessData.getOrderCompletionRate());
row.getCell(5).setCellValue(businessData.getUnitPrice());
row.getCell(6).setCellValue(businessData.getNewUsers());
}
//3.通过输出流将excel文件下载到客户端浏览器
ServletOutputStream out = response.getOutputStream();
excel.write(out);
//关闭资源
out.close();
excel.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
浙公网安备 33010602011771号