目录
架构的细分
使用实体类来接收配置文件中的值
webMvcConfig类:
jwt令牌
管理端的拦截器:
JwtProperties:
JwtTokenAdminInterceptor :
对密码加密操作
Redis:
分页查询
整体思想
为什么动态 SQL 推荐传实体类?
多表操作
Service层:
Mapper:
动态sql——update:
删除或者增加多条数据
增加多条数据:
Service 层:
Mapper 层
删除多条数据:
Service 层调用:
Mapper层:
总结:
什么时候需要外键约束?
什么时候需要用到多个表的数据关联查询?
需要获取相关联表的数据:
公共字段的处理:
微信登录后端开发:
整体的思路:
如何获取openid?
HttpClientUtil:
使用注解自动获取前端的值
1. @RequestParam
适用场景:
数据传递方式:
示例:
请求类型:
2. @RequestBody
适用场景:
数据传递方式:
示例:
请求类型:
3. @PathVariable
适用场景:
数据传递方式:
示例:
请求类型:
总结:
在更新的时候,如何进行覆盖?
如何完成两个 List 集合的转换?
转换逻辑步骤
架构的细分
首先从前单一的pojo类细分成了pojo,dto和vo,约定的就是dto接收前端的数据,vo返回给前端数据,pojo直接处理数据库的操作。(DTO用于数据传输,VO则专注于前端展示,POJO直接操作数据库)
使用实体类来接收配置文件中的值
在开发中,通常使用实体类来接收配置文件中的值,这种做法在Spring Boot等框架中较为常见。具体步骤如下:
使用实体类接收配置文件值:
配置文件引用:
- 在
application.yml
文件中,通过${sky.alioss.endpoint}
等形式引用了application-dev.yml
中的sky.alioss
配置项。这种引用方式有助于集中管理各环境的具体配置信息。
alioss:
endpoint: ${sky.alioss.endpoint}
access-key-id: ${sky.alioss.access-key-id}
access-key-secret: ${sky.alioss.access-key-secret}
bucket-name: ${sky.alioss.bucket-name}
配置属性绑定到实体类:
AliOssProperties
类使用了@ConfigurationProperties
注解,并指定了prefix = "sky.alioss"
,可以直接将配置文件中的sky.alioss
属性绑定到类中的字段。这种方式使得配置参数的管理更加结构化。- 使用了
@Component
注解,将AliOssProperties
加入 Spring 容器,确保可以在项目中通过依赖注入来使用该类。@Component @ConfigurationProperties(prefix = "sky.alioss") @Data public class AliOssProperties { private String endpoint; private String accessKeyId; private String accessKeySecret; private String bucketName; }
在业务逻辑中引用配置类:
- 通过依赖注入的方式,在
JwtTokenAdminInterceptor
中使用JwtProperties
实体类(类似于AliOssProperties
)来获取配置的 JWT 信息。 - 例如,
jwtProperties.getAdminTokenName()
和jwtProperties.getAdminSecretKey()
引用了JwtProperties
中的配置项,这些配置可以通过application.yml
或application-dev.yml
中的值自动注入,简化了配置管理并提高了代码的可读性。
总结:
- 使用实体类接收配置文件值,通过
@ConfigurationProperties
注解实现配置属性的绑定,使得配置和代码逻辑解耦,配置更易管理。 - 通过
${}
方式引用其他配置文件的属性,增强了配置的灵活性,特别适合多环境配置管理。
webMvcConfig类:
package com.sky.config;
import com.sky.interceptor.JwtTokenAdminInterceptor;
import com.sky.interceptor.JwtTokenUserInterceptor;
import com.sky.json.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import java.util.List;
/**
* 配置类,注册web层相关组件
*/
@Configuration
@Slf4j
@EnableSwagger2
public class WebMvcConfiguration extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Autowired
private JwtTokenUserInterceptor jwtTokenUserInterceptor;
/**
* 注册自定义拦截器
*
* @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");
}
/**
* 通过knife4j生成接口文档
* @return
*/
@Bean
public Docket docket1() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("管理端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
.paths(PathSelectors.any())
.build();
return docket;
}
@Bean
public Docket docket2() {
ApiInfo apiInfo = new ApiInfoBuilder()
.title("苍穹外卖项目接口文档")
.version("2.0")
.description("苍穹外卖项目接口文档")
.build();
Docket docket = new Docket(DocumentationType.SWAGGER_2)
.groupName("用户端接口")
.apiInfo(apiInfo)
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
.paths(PathSelectors.any())
.build();
return docket;
}
/**
* 设置静态资源映射
* @param registry
*/
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
}
/**
* 扩展nvc框架的消息转换器
* @param converters
*/
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters){
// 创建一个消息转换器对象,把java对象转为json字符串
MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,可以将Java对象转为json字符串
converter.setObjectMapper(new JacksonObjectMapper());
//将我们自己的转换器放入spring Mvc框架的容器中
converters.add(0,converter);
}
}
webMvcConfig类里面配置的内容:
-
拦截器配置 (
addInterceptors
):- 用于注册自定义的拦截器,拦截请求并执行特定的逻辑,比如鉴权、日志记录等。
- 在
addInterceptors
方法中,通过registry.addInterceptor(ResourceHandlerRegistry registry
)
将自定义拦截器加入到 Spring MVC 的拦截链中。 - 注意!!!只有需要对所有请求都要进行身份验证的时候才需要注入到配置类,其他轻量级的就只需要Component注解。
-
Knife4j 配置:
- 配置 Swagger 接口文档生成工具 Knife4j,通常会涉及设置
Docket
Bean,用于扫描控制器和接口,生成 API 文档。
- 配置 Swagger 接口文档生成工具 Knife4j,通常会涉及设置
-
静态资源映射:
- 配置 Spring MVC 处理静态资源文件的方式,通常是映射 JavaScript、CSS、图片等静态文件。
-
消息转换器配置:
- 通过扩展 Spring MVC 的
HttpMessageConverters
,可以自定义消息转换器,以满足特定的 JSON 序列化、反序列化需求。
- 通过扩展 Spring MVC 的
jwt令牌
登录成功后,可以使用jwtutil生成jwt令牌
JWTUtil:
package com.sky.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;
public class JwtUtil {
/**
* 生成jwt
* 使用Hs256算法, 私匙使用固定秘钥
*
* @param secretKey jwt秘钥
* @param ttlMillis jwt过期时间(毫秒)
* @param claims 设置的信息
* @return
*/
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();
// 当你调用 compact() 方法时,它会将 JWT 的三部分(头部、载荷和签名)进行编码,
// 并将它们合并成一个字符串。这个字符串是最终的 JWT。
}
/**
* 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;
}
}
如何使用JWTUtil呢?首先 我们知道的是createJWT的代码需要三个参数,
jwt秘钥,jwt过期时间(毫秒),以及设置的信息(设置的信息是一个map,通常map的key是字符串(Id)Value是其id值)其实就是JWT 载荷
//登录成功后,生成jwt令牌
Map<String, Object> claims = new HashMap<>();
claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
String token = JwtUtil.createJWT(
jwtProperties.getAdminSecretKey(),
jwtProperties.getAdminTtl(),
claims);
登录成功后,我们可以将生成的 JWT 令牌存储在另一个实体类中,并将其返回给前端。这样,在前端请求其他需要认证的接口时,可以通过在请求头中携带 JWT 令牌,拦截器就能够通过 preHandle
方法来对该令牌进行校验。通常的做法如下:
- 从请求中获取 JWT 令牌,通常是在请求头中,如
Authorization
字段中提取令牌。 - 解析 JWT 令牌,调用
JwtUtil
等工具类进行解析。 - 校验令牌的合法性,比如检查是否有效、是否过期、签名是否正确等。
- 设置用户信息,通过
BaseContext
将用户信息设置到当前线程中,以便后续的业务逻辑中使用。 - 拦截请求,如果令牌无效或校验失败,则返回错误信息,拒绝请求;否则,放行请求,继续执行控制器方法。
管理端的拦截器:
同样使用使用实体类来接收配置文件中的值。这个方法在第2点提过。
JwtProperties:
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@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;
}
JwtTokenAdminInterceptor :
package com.sky.interceptor;
import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 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 {
//判断当前拦截到的是Controller的方法还是其他资源
if (!(handler instanceof HandlerMethod)) {
//当前拦截到的不是动态方法,直接放行
return true;
}
//1、从请求头中获取令牌
String token = request.getHeader(jwtProperties.getAdminTokenName());
System.out.println(token);
//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;
}
}
}
对密码加密操作
注册的时候,需要对密码进行加密。
employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
PasswordConstant.DEFAULT_PASSWORD.是默认密码123456
Redis:
可以使用Redis来存储个别字段,例如需要被客户端和用户端同时查看到的店铺的状态。
推荐存储到Redis的情况:
-
用户会话信息: 使用 Redis 存储用户的登录状态、权限信息等。这些数据通常被多个客户端访问,Redis 提供了高效的读写性能。
-
热点数据: 如果某些字段(例如商品价格、库存数量等)频繁被访问,可以将其缓存到 Redis 中,避免每次都查询数据库。
-
共享字段: 对于需要同时由客户端和用户端访问的字段,Redis 作为缓存可以保证数据的一致性和快速访问。例如,某些公共配置或实时更新的数据,可以存放在 Redis 中,客户端和服务器都能轻松读取。
-
缓存过期机制: Redis 提供了缓存过期的功能,当字段数据发生变化时,可以自动使缓存失效并重新加载。
分页查询
分页查询:使用pageHelper,如果存在可选的参数需要根据参数来进行分页查询的时候,特需要动态sql(如果这个参数的值不为null,那么就在sql语句添加条件限制)
CategoryPageQueryDTO:
package com.sky.dto;
import lombok.Data;
import java.io.Serializable;
@Data
public class CategoryPageQueryDTO implements Serializable {
//页码
private int page;
//每页记录数
private int pageSize;
//分类名称
private String name;
//分类类型 1菜品分类 2套餐分类
private Integer type;
}
controller:
/**
*分类分页查询
* @param categoryPageQueryDTO
* @return
*/
@GetMapping("/page")
@ApiOperation("分类分页查询")
public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
PageResult pageResult = categoryService.page(categoryPageQueryDTO);
System.out.println("-----------------------------------------------");
System.out.println(categoryPageQueryDTO.getType());
return Result.success(pageResult);
}
Service:
/**
* 分类分页查询
* @param categoryPageQueryDTO
* @return
*/
@Override
public PageResult page(CategoryPageQueryDTO categoryPageQueryDTO) {
PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
// page-> PageResult
long total = page.getTotal();
List<Category> result = page.getResult();
return new PageResult(total,result);
}
categoryPageQueryDTO里面封装了pageSize和Page(第几个页面)
sql映射文件:
<select id="pageQuery" resultType="com.sky.entity.Category">
select * from category
<where>
<if test="type != null"> and type = #{type} </if>
<if test="name != null and name != ''" >and name like concat('%',#{name},'%')</if>
</where>
order by sort asc,create_time desc
</select>
注意,第二个到后一个if标签中需要包含and字段
整体思想
对于某个单一字段的更新,我们可以先创建一个对象,然后直接调用动态的更新sql语句,参数为带有更新字段的对象,这样不需要重复写单个字段的更新的sql语句
创建对象 :
Dish dish = Dish.builder()
.id(id)
.status(status)
.build();
dishMapper.update(dish);
动态更新语句
<update id="update" useGeneratedKeys="true" keyProperty="id">
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>
</set>
<where>
id = #{id}
</where>
</update>
我们可以在单独改变status的时候调用此接口,也可以在修改菜品的时候调用。注意在这个sql动态语句中,每一个if标签里面最后都有一个,(逗号)
但是一定要注意的是!!!!如果xml里面写的是动态sql的话,确保未使用的字段为 null
或空字符串(也就是要使用实体类,保证字段为空)
为什么动态 SQL 推荐传实体类?
动态 SQL 的逻辑依赖传入参数的字段值:
- 动态 SQL 通过
<if>
标签判断参数是否为null
或满足某种条件来拼接 SQL。 - 如果只传递一个字段,而其他字段的默认值不为
null
,它们仍会参与条件拼接,从而生成错误的 SQL。
我就出现过这个错误,我调用xml的动态sql,但是我的参数只有id,这个时候的动态sql:
==> Preparing: select * from orders WHERE number like concat('%',?,'%') and phone like concat('%',?,'%') and user_id = ? and status = ? and order_time >= ? and order_time <= ? and id = ?
==> Parameters: 7(Long), 7(Long), 7(Long), 7(Long), 7(Long), 7(Long), 7(Long)
<== Total: 0
很明显,其他的字段都被附上了7,这个时候就错了。
多表操作
对于涉及到多个表的操作:
就需要使用注解@Transactional
@Transactional
注解用于管理事务,确保在多个数据库操作过程中要么全部成功,要么全部失败,从而保持数据的一致性和完整性。
场景描述:
- 在 Table1 中插入数据后,获取生成的 ID(假设这个 ID 是自增的)。
- 使用这个 ID 来插入 Table2,作为外键或关联字段。
解决方法:
- 在 Mapper XML 配置中,使用
useGeneratedKeys
和keyProperty
来获取自增 ID。 - 在 Service 层中,通过获取生成的 ID,再执行插入第二个表的操作。
具体实例如下:
新增菜品,首先我们知道菜品中有口味,口味又是一个单独的数据表,所以我们需要开启事务,保证数据的唯一性 ,同时需要注意的是,在插入了菜品后,我们需要得到对应菜品的id值来关联给Flavor_dish的dish_id值。所以我们需要用到useGeneratedKeys="true" keyProperty="id",这样在Service层中使用getId才能获得相应的属性
Service层:
/**
* 新增菜品和口味,
* 涉及多张表的数据的一致性,需要加上注解-事务处理的注解
*
* @param dishDTO
*/
@Override
// 注意需要在启动类上面添加@EnableTransactionManagement
// 开启注解方式的事务管理
@Transactional
public void add(DishDTO dishDTO) {
Dish dish = new Dish();
BeanUtils.copyProperties(dishDTO, dish);
// 像菜品表插入一条数据
dishMapper.add(dish);
// 获取insert语句生成的主键值
Long id = dish.getId();
List<DishFlavor> flavors = dishDTO.getFlavors();
if (flavors != null && flavors.size() > 0) {
// 遍历口味,赋id值
flavors.forEach(dishFlavor -> {
dishFlavor.setDishId(id);
});
// 向口味表插入n条数据
disFlavorMapper.addBatch(flavors);
}
}
Mapper:
<insert id="add" useGeneratedKeys="true" keyProperty="id">
insert into dish(name, category_id, price, image, description, create_time, update_time, create_user, update_user,status)
values (#{name},#{categoryId},#{price},#{image},#{description},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})
</insert>
动态sql——update:
<update id="update">
update setmeal
<set>
<if test="categoryId != null">category_id = #{categoryId},</if>
<if test="description != null">description = #{description},</if>
</set>
where id = #{id}
</update>
注意啊!!每一个if标签文本的最后都需要逗号!!本人老是喜欢错
删除或者增加多条数据
向一个表中增加或者删除多条数据
增加多条数据:
如果 Service 层的参数是 List
集合,并需要传递给 Mapper
层进行批量操作,可以使用 MyBatis
的批量插入或更新功能。
Service 层:
public void batchInsert(List<Entity> entities) {
mapper.batchInsert(entities);
}
Mapper 层
<insert id="batchInsert" parameterType="java.util.List">
INSERT INTO 表名 (列1, 列2, 列3)
VALUES
<foreach collection="list" item="item" separator=",">
(#{item.列1}, #{item.列2}, #{item.列3})
</foreach>
</insert>
删除多条数据:
Service 层调用:
void deleteIds(List<Long> ids);
Mapper层:
使用 IN
语法
<delete id="deleteIds">
delete from dish where id in
<foreach collection="ids" item="id" open="(" close=")" separator=",">
#{id}
</foreach>
</delete>
总结:
在开发中,尽可能使用 对应类的 List
集合 作为参数,并配合 动态 SQL 来处理批量增删改查(CRUD)操作,能够显著减少 SQL 代码的重复性,提高代码的简洁性和可维护性。 其实也就是整体思想和删除或者增加多条数据的体现。
注意:我在写多条增加语句的时候,写成了:
<insert id="add">
insert into setmeal_dish(setmeal_id, dish_id, name, price, copies)
values
<foreach collection="setmealDishes" item="ds" separator="," open="(" close=")">
setmeal_id = #{ds.setmealId},dish_id=#{ds.dishId},name=#{ds.name},price = #{ds.price},copies = #{ds.copies}
</foreach>
</insert>
但实际上,括号要放在里面,每一次都需要括号!!!!
什么时候需要外键约束?
- 多个表的数据关联查询:需要查询多个表中的数据。
- 需要获取相关联表的数据:例如,查询菜品的同时也需要返回菜品所属的分类。
怎么说呢?举个例子吧:
套餐和菜品的关系:
假设你有一个 setmeal_dish
表,它记录了套餐(setmeal
)和菜品(dish
)之间的关系。比如,一个套餐可以包含多个菜品,一个菜品也可以出现在多个套餐中。这个表通常会有两个外键:
setmeal_id
(指向 setmeal
表的外键)
dish_id
(指向 dish
表的外键)
-
外键约束的作用:
- 外键约束可以确保
setmeal_dish
表中的setmeal_id
和dish_id
必须分别在setmeal
和dish
表中存在。如果你尝试插入一个不存在的套餐ID或菜品ID,数据库会拒绝操作,从而避免了无效数据的插入。 - 同时,外键约束还可以保证数据一致性。例如,如果删除了一个套餐(
setmeal
表中的某个记录),可以通过外键约束配置,确保所有相关的套餐-菜品关系(即setmeal_dish
表中的记录)也被删除或更新,从而避免孤立的数据。
- 外键约束可以确保
什么时候需要用到多个表的数据关联查询?
需要获取相关联表的数据:
在开发中,我们需要通过套餐的id值,得到套餐里面包含的菜品 的具体信息。这个时候我们肯定需要查询这个关系表,那么我们就不能单一的写成:
<select id="getDishByCondition" resultMap="SetmealDishMap">
select * from setmeal_dish
<where>
<if test="name != null">and name = #{name}</if>
<if test="price != null">and price = #{price}</if>
<if test="setmealId != null">and setmeal_id = #{setmealId}</if>
</where>
</select>
因为如果这样写的话,我们只能获得菜品的id值,不能获得dish表当中具体的菜品信息。所以就应该使用多个表的关联查询:
<select id="getDishByCondition" resultType="com.sky.vo.DishItemVO">
select sd.name, sd.copies, d.image, d.description
from setmeal_dish sd
left join dish d on sd.dish_id = d.id
<where>
<if test="name != null">and sd.name = #{name}</if>
<if test="price != null">and sd.price = #{price}</if>
<if test="setmealId != null">and sd.setmeal_id = #{setmealId}</if>
</where>
</select>
这样的话,既能获取到 setmeal_dish 的字段还能获取到对应的dish的详细信息。
总之:在查看开发文档时,注意检查响应数据是否包含了来自多个表的字段。如果响应数据中确实有多个表的字段,那么就可以考虑使用多表关联查询来获取这些数据。
公共字段的处理:
对于公共字段的处理:
需要注意的是:
当 AutoFillAspect
切面代码执行时,遇到 List
类型参数,会试图直接对 List
调用 setCreateTime
方法。然而,List
本身是集合接口,没有这个方法,因此会导致 NoSuchMethodException
。因此,在多条记录同时增加的时候我们就需要注意不能加上@AutoFill的注解
void add(List<SetmealDish> setmealDishes);
微信登录后端开发:
微信登录的后端开发代码:
整体的思路:
-
前端:获取
code
-
前端通过微信登录接口获取到授权码
code
。微信登录流程是通过微信SDK来完成的,具体步骤如下:- 前端调用微信的
wx.login()
接口,获取code
。 code
代表用户的临时凭证,微信后台用它来换取用户的openid
和session_key
。
- 前端调用微信的
-
- 后端Controller接收到
openid
后调用Service层,Service调用mapper.getByOpenid(openid)
查询数据库,判断该用户是否已注册。 - 调用controller,controller调用Service层
- Service层通过调用微信服务接口获取openid(使用自己封装的HttpClientUtil来发送请求获得Response响应的字符串)
-
后端Service层接收到前端传递的
code
后,调用微信的API来换取openid
和session_key
。后端通过向微信的服务器发送请求,获取用户的openid
。 -
微信的接口 URL:
https://api.weixin.qq.com/sns/jscode2session
-
请求参数:
appid
: 微信小程序的appID
secret
: 微信小程序的appSecret
js_code
: 前端传递的code
grant_type
: 固定为authorization_code
- 然后调用Mapper层根据
openid
查询用户信息,如果用户已注册,controller层生成JWT token后封装成vo对象返回给前端;如果未注册,则返回相调用接口userMapper.add(user);。
-
-
- 前端根据后端返回的用户信息和token来处理登录状态,进行页面跳转等操作
接下来是细节的介绍:
主要是Service层的impl:
如何获取openid?
private String getOpenId(String code) {
// 调用微信接口服务,获取当前微信用户的openid
Map<String, String> map = new HashMap<>();
map.put("appid", ...); // 填入小程序的AppID
map.put("secret", ...); // 填入小程序的AppSecret
map.put("js_code", code); // 前端传递的code
map.put("grant_type", "authorization_code"); // 固定参数,代表授权模式
// 发送HTTP请求到微信API接口获取用户信息
String response = HttpClientUtil.doget("https://api.weixin.qq.com/sns/jscode2session", map);
// 解析返回的JSON,获取openid
JSONObject jsonObject = JSON.parseObject(response);
return jsonObject.getString("openid"); // 返回openid
}
其他的ServiceImpl:
@Service
@Slf4j
public class UserServiceImpl implements UserService {
// 微信服务接口地址
public static final String EX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";
@Autowired
private WeChatProperties weChatProperties;
@Autowired
UserMapper userMapper;
/**
* 微信登录
* @param loginDTO
* @return
*/
@Override
public User wxlogin(UserLoginDTO loginDTO) {
String openid = getOpenId(loginDTO.getCode());
System.out.println(openid);
// 判断openid是否为空,如果为空表示登录失败,就抛出异常
if (openid == null) {
throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
}
// TODo 这里只能获取到openid,由于个人开发的权限问题其他的获取不到
// 当前用户的openid是否存在于数据库(为新用户)
User user = userMapper.getByOpenid(openid);
if(user == null ){
user = User.builder()
.openid(openid)
.createTime(LocalDateTime.now())
.build();
System.out.println("==========================");
System.out.println(user);
// 如果是新用户,自动完成注册
userMapper.add(user);
}
// 返回用户对象
return user;
}
一个小细节:
由于返回的值需要用户的id值,所以在增加用户的mapper是xml文件形式,useGeneratedKeys="true"
和keyProperty="id"
,可以让数据库生成的自增id
自动填充到User
对象的id
字段中,这样返回的时候user才能有id值
由于小程序端也需要保证用户是登录的状态,所以我们也需要设置一个拦截器,根据用户端的拦截器改一改方法中的一些参数的值就可以了。
同样的在webMvcconfig配置类里面注入该拦截器,将该拦截器加入到拦截链条里面即可。
HttpClientUtil:
package com.sky.utils;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
/**
* Http工具类
*/
public class HttpClientUtil {
static final int TIMEOUT_MSEC = 5 * 1000;
/**
* 发送GET方式请求
* @param url
* @param paramMap
* @return
*/
public static String doGet(String url,Map<String,String> paramMap){
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
String result = "";
CloseableHttpResponse response = null;
try{
URIBuilder builder = new URIBuilder(url);
if(paramMap != null){
for (String key : paramMap.keySet()) {
builder.addParameter(key,paramMap.get(key));
}
}
URI uri = builder.build();
//创建GET请求
HttpGet httpGet = new HttpGet(uri);
//发送请求
response = httpClient.execute(httpGet);
//判断响应状态
if(response.getStatusLine().getStatusCode() == 200){
result = EntityUtils.toString(response.getEntity(),"UTF-8");
}
}catch (Exception e){
e.printStackTrace();
}finally {
try {
response.close();
httpClient.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
// 创建参数列表
if (paramMap != null) {
List<NameValuePair> paramList = new ArrayList();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
}
// 模拟表单
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
/**
* 发送POST方式请求
* @param url
* @param paramMap
* @return
* @throws IOException
*/
public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
// 创建Httpclient对象
CloseableHttpClient httpClient = HttpClients.createDefault();
CloseableHttpResponse response = null;
String resultString = "";
try {
// 创建Http Post请求
HttpPost httpPost = new HttpPost(url);
if (paramMap != null) {
//构造json格式数据
JSONObject jsonObject = new JSONObject();
for (Map.Entry<String, String> param : paramMap.entrySet()) {
jsonObject.put(param.getKey(),param.getValue());
}
StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
//设置请求编码
entity.setContentEncoding("utf-8");
//设置数据类型
entity.setContentType("application/json");
httpPost.setEntity(entity);
}
httpPost.setConfig(builderRequestConfig());
// 执行http请求
response = httpClient.execute(httpPost);
resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
} catch (Exception e) {
throw e;
} finally {
try {
response.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return resultString;
}
private static RequestConfig builderRequestConfig() {
return RequestConfig.custom()
.setConnectTimeout(TIMEOUT_MSEC)
.setConnectionRequestTimeout(TIMEOUT_MSEC)
.setSocketTimeout(TIMEOUT_MSEC).build();
}
}
使用注解自动获取前端的值
1. @RequestParam
@RequestParam
注解用于从 查询参数 中获取数据,通常用于 GET 请求 或带有查询参数的 POST 请求。前端发送的数据以 URL 的查询字符串形式传递。
适用场景:
- 用于接收 URL 中的查询参数(query parameters)。
- 常见于表单提交或 URL 中的查询字符串。
数据传递方式:
- 前端通过 查询字符串 发送数据,形式如
?name=value&age=30
。
示例:
前端请求:
GET /user?name=JohnDoe&age=30
如果请求中的参数是一个普通类型(例如 Long
、Integer
、String
等),并且该参数名称与方法中的参数名称相同,则通常可以通过 @RequestParam
来自动映射。如果没有显式地指定 @RequestParam
,Spring 会默认从请求参数中提取与方法参数同名的值。(因此,只要参数名和方法中的参数名称相同,那么就不需要显示写注解了)
但是,如果请求中的参数是一个集合(例如 List
),那么你需要使用 @RequestParam
来指定参数名,因为 Spring 无法根据名称自动推断列表类型。
/**
* 菜品的批量删除
* @param ids
* @return
*/
@DeleteMapping
@ApiOperation("菜品的批量删除")
public Result delete(@RequestParam List<Long> ids){
log.info("ids的值为:{}",ids);
dishService.delete(ids);
return Result.success();
}
请求类型:
- 主要用于 query string 类型的数据。
2. @RequestBody
@RequestBody
注解用于从 请求体(body)中获取数据,通常与 POST、PUT 或 PATCH 请求一起使用。它常用于接收 JSON 或 XML 数据。
适用场景:
- 用于处理 请求体 中的复杂数据,尤其是 JSON 数据。
- 适合用于 API 接口中,接收客户端发送的对象。
数据传递方式:
- 前端通过 HTTP 请求的 请求体 发送数据,通常是 JSON 格式。
示例:
前端请求:
POST /user
Content-Type: application/json
{
"name": "JohnDoe",
"age": 30
}
注意:
@RequestBody
注解通常与 对象类型 参数结合使用,Spring 会自动将 JSON 转换成相应的 Java 对象。- 需要确保请求头的
Content-Type
为application/json
或其他合适的媒体类型。
请求类型:
- 适用于 JSON 或 XML 格式的数据,通常是 POST 或 PUT 请求。
3. @PathVariable
@PathVariable
注解用于从 路径参数 中获取数据,通常用于 RESTful 风格的 URL 中的动态部分。这些动态值是 URL 中的占位符。
适用场景:
- 用于从 URL 中的路径中获取参数,常用于 REST API 的路径设计。
- 适合处理资源的唯一标识符,如 ID。
数据传递方式:
- 前端通过 URL 的路径 传递参数,通常用于资源标识符,如用户 ID、商品 ID 等。
示例:
前端请求:
GET /user/123
注意:
@PathVariable
注解用于 URL 模板中的路径变量部分。- 路径参数和方法参数之间的映射是通过变量名称来自动匹配的。
请求类型:
- 用于 路径参数 的数据,通常是 RESTful URL 中的一部分。
总结:
-
@RequestParam
:- 用于获取 查询参数(query parameters)。
- 常见于
GET
请求,数据以查询字符串形式传递。
-
@RequestBody
:- 用于获取 请求体 中的数据,通常是 JSON 或 XML。
- 常见于
POST
、PUT
请求。
-
@PathVariable
:- 用于获取 路径参数,通常是 URL 的动态部分(如 ID)。
- 常见于 RESTful API。
在更新的时候,如何进行覆盖?
就比如说苍穹外卖吧,比如说在修改菜品的时候还需要修改口味,这个时候我刚开始的时候想的是修改菜品+修改口味,我有一个想法就是通过菜品的id值,查数据库找到口味的id值(list集合),然后进行修改。但其实还有一个更加简便的方法就是直接先根据菜品id直接删除最后在添加。
其实这两种没有本质区别,知识第二种逻辑更加简单罢了。
如何完成两个 List
集合的转换?
转换逻辑步骤
-
使用
stream()
转换流:orderDetailList.stream()
将订单详情列表orderDetailList
转换为流,便于对其中的每个元素进行操作。
-
使用
map()
映射操作:map
是流操作的一部分,作用是将流中的每个元素进行映射转换。- 通过映射操作,将
OrderDetail
对象转换为ShoppingCart
对象。
-
复制属性:
BeanUtils.copyProperties(x, shoppingCart, "id")
复制两个对象之间的相同属性,忽略id
。- 忽略字段
"id"
是因为你可能希望使用其他逻辑生成新的id
,或者保留购物车对象的id
不变。
-
添加其他属性:
- 设置
userId
和createTime
等特定的属性,这些属性在订单详情中可能不存在,需要手动赋值。
- 设置
-
收集结果:
collect(Collectors.toList())
将流操作的结果收集为一个新的List
集合。
/**
* 再来一单
*
* @param id
*/
public void repetition(Long id) {
// 查询当前用户id
Long userId = BaseContext.getCurrentId();
// 根据订单id查询当前订单详情
List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);
// 将订单详情对象转换为购物车对象
List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {
ShoppingCart shoppingCart = new ShoppingCart();
// 将原订单详情里面的菜品信息重新复制到购物车对象中
BeanUtils.copyProperties(x, shoppingCart, "id");
shoppingCart.setUserId(userId);
shoppingCart.setCreateTime(LocalDateTime.now());
return shoppingCart;
}).collect(Collectors.toList());
// 将购物车对象批量添加到数据库
shoppingCartMapper.insertBatch(shoppingCartList);
}