外界发起请求先到的位置是商品服务,商品服务调用类别服务,类别服务查询到数据后返回商品服务,商品服务再进行数据库的查询,封装结果后返回给前端。前端传过来的是categoryName:[数组]
数组里边装的是类别名称。类别名称到商品服务后要调用对应的类别服务。类别服务:/category/hots,数组就没办法路径传参了,用请求体传类别的集合,这个集合装的是类别的名称,通过类别集合到类别服务要进行数据库的查询,之后返回封装。之前的是返回封装的类别对象,但是封装的类别对象也会转成LinkedHashMap。我们希望返回R,但R的data中装类别的id,也就是类别名称对应的类别集合。到商品服务就:1、判断类别查询效果,2、根据类别的id进行商品的查询(只查询热门商品,并且7条),最后结果封装返回就可以。
开发步骤:
1、编写param(编写一个接集合的,上次的是单个值)
2、先完成类别的一套业务(根据类别的集合,怎么查到对应的值)
3、定义feign客户端(在原来的里边加方法就可以了),商品服务通过客户端调用类别服务
4、编写商品的一套业务
1、
package com.qqhru.param;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
import java.util.List;
/**
* 热门商品参数接收对象
*/
@Data
public class ProductHotParam {
@NotEmpty//集合不能为空,集合是NotEmpty
private List<String> categoryName;
}
2、
/**
* 热门类别id查询
* @return
*/
@PostMapping("/hots")
public R hots(@RequestBody @Validated ProductHotParam productHotParam, BindingResult result){
if (result.hasErrors()){
return R.fail("类别集合查询失败!");
}
return categoryService.hotsCategory(productHotParam);
}
/**
* 根据传入的热门类别名称集合,返回类别对应的id集合
* @param productHotParam
* @return
*/
R hotsCategory(ProductHotParam productHotParam);
3、
/**
* 根据传入的热门类别名称集合,返回类别对应的id集合
* @param productHotParam
* @return
*/
@Override
public R hotsCategory(ProductHotParam productHotParam) {
//1、封装查询类别名称
QueryWrapper<Category> categoryQueryWrapper = new QueryWrapper<>();
//eq是单个值,in是集合
categoryQueryWrapper.in("category_name",productHotParam.getCategoryName());//哪一列:category_name 值:productHotParam.getCategoryName()
//如果不指定列,默认查询表中的和类别名称有关的所有列
categoryQueryWrapper.select("category_id");//查询指定类别(查询指定列)
//查询数据库
List<Object> ids = categoryMapper.selectObjs(categoryQueryWrapper);//类别id的集合
/*将类别集合封装返回*/
R ok = R.ok("类别集合查询成功", ids);
log.info("CategoryServiceImpl.hotsCategory业务结束,结果:{}",ok);
return ok;
}
/**
* 类别的调用接口
*/
@FeignClient("category-service")//客户端调用的服务名称
public interface CategoryClient {
//根据类别集合查询类别id的方法
/*调用类别里边根据类别集合查询类别id的方法*/
/*注意:写全路径,根据路径配上服务去注册中心找服务的对应地址,最终向服务发起请求*/
@PostMapping("/category/hots")
R hots(@RequestBody ProductPromoParam productPromoParam);
}
@PostMapping("/hots")
public R hots(@RequestBody @Validated ProductPromoParam productPromoParam,BindingResult result){
if (result.hasErrors()){
return R.fail("数据查询失败!");
}
return productService.hots(productPromoParam);
}
/**
* 多类别热门商品查询,根据类别名称集合!最多查询7条
* 1、调用类别服务
* 2、类别集合id查询商品
* 3、结果集封装即可
* @param productPromoParam 类别名称集合
* @return
*/
R hots(ProductPromoParam productPromoParam);
/**
* 多类别热门商品查询,根据类别名称集合!最多查询7条
* 1、调用类别服务
* 2、类别集合id查询商品
* 3、结果集封装即可
* @param productPromoParam 类别名称集合
* @return
*/
@Override
public R hots(ProductPromoParam productPromoParam) {
R r = categoryClient.hots(productPromoParam);
if (r.getCode().equals(R.FAIL_CODE)){
log.info("ProductServiceImpl.hots业务结束,结果:{}",r.getMsg());
return r;
}
//
List<Object> ids = (List<Object>) r.getData();
//进行商品数据查询
QueryWrapper<Product> productQueryWrapper = new QueryWrapper<>();
productQueryWrapper.in("category_id",ids);//商品表中的category_id等于ids
productQueryWrapper.orderByDesc("product_sales");//热门商品要倒叙,根据商品价格倒叙
//封装分页参数
IPage<Product> page = new Page<>();
page = productMapper.selectPage(page,productQueryWrapper);
List<Product> records = page.getRecords();
R ok = R.ok("多类别热门商品查询成功");
return ok;
}
@GetMapping("/list")
public R list(){
return categoryService.list();
}
/**
* 查询类别数据,进行返回
* @return
*/
R list();
@GetMapping("/category/list")
R list();
@PostMapping("/category/list")
public R clist(){
return productService.clist();
}
/**
* 查询类别商品集合
* @return
*/
R clist();
/**
* 查询类别商品集合
* @return
*/
@Override
public R clist() {
R r = categoryClient.list();// R ok = R.ok("类别集合全部数据查询成功!", categoryList);
log.info("ProductServiceImpl.clist业务结束,结果:{}",r);
return r;
}
/**
* 类别实体类
* 如果有天改了category,需要在ProductService修改获取category_id的key
*/
@Data
@TableName("category")
public class Category {
@JsonProperty("category_id")//json格式化,显示在前端
@TableId(type = IdType.AUTO)
private Integer categoryId;
@JsonProperty("category_name")
private String categoryName;
}
1、定义接收参数的param
2、controller、service、mapper
1、
/*单类别(指定类别)商品展示*/
@Data
public class ProductIdsParam extends PageParam{
//可以为空的集合,但是不能传空的值
@NotNull
private List<Integer> categoryID;
private int currentPage = 1;
private int pageSize = 15;//默认值
}
2、
@PostMapping("/bycategory")
public R byCategory(@RequestBody @Validated ProductIdsParam productIdsParam,BindingResult result){
if (result.hasErrors()){
return R.fail("类别商品查询失败!");
}
return productService.byCategory(productIdsParam);
}
3、
/**
* 通用性的业务
* 如果传入了类别型的id,根据id查询并且分页
* 如果没有传入类别的id,查询全部!
* @param productIdsParam
* @return
*/
R byCategory(ProductIdsParam productIdsParam);
4、
@Override
public R byCategory(ProductIdsParam productIdsParam) {
List<Integer> categoryID = productIdsParam.getCategoryID();
/*封装参数*/
QueryWrapper<Product> queryWrapper = new QueryWrapper<>();
if (!categoryID.isEmpty()) {
queryWrapper.in("category_id",categoryID);
}
/*分页*/
IPage<Product> page = new Page<>(productIdsParam.getCurrentPage(),productIdsParam.getPageSize());
/*查询:在当页显示的商品。如果queryWrapper为空,就查询全部*/
page = productMapper.selectPage(page,queryWrapper);
/*结果集封装时候封装一个对应的total*/
R ok = R.ok("查询成功", page.getRecords(), page.getTotal());
log.info("ProductServiceImpl.byCategory业务结束,结果:{}",ok);
return ok;
}
5、
@PostMapping("/all")
public R all(@RequestBody @Validated ProductIdsParam productIdsParam,BindingResult result){
if (result.hasErrors()){
return R.fail("类别商品查询失败!");
}
return productService.byCategory(productIdsParam);
}
/*商品id参数接收*/
import lombok.Data;
import javax.validation.constraints.NotNull;
@Data
public class ProductIdParam {
@NotNull
private Integer productID;
}
@PostMapping("/detail")
public R detail(@RequestBody @Validated ProductIdParam productIdParam, BindingResult result){
if (result.hasErrors()){
return R.fail("商品详情查询失败!");
}
return productService.detail(productIdParam.getProductID());
}
/**
* 根据商品id查询商品详情信息
* @param productID
* @return
*/
R detail(Integer productID);
@Override
public R detail(Integer productID) {
Product product = productMapper.selectById(productID);
/*查到数据封装*/
R ok = R.ok(product);
log.info("ProductServiceImpl.detail业务结束,结果:{}",ok);
return ok;
}
商品图片返回的是一个新的表,还要声明一个对应的实体类,实体类在声明的时候需不需要json格式化
/**
* 商品图片实体类
*/
@TableName("product_picture")
public class Picture {
@TableId(type = IdType.AUTO)
private Integer id;
@TableField("product_id")//数据库表中字段,不声明也没事,会自动改成驼峰式,显示声明效率会更高
@JsonProperty("product_id")//前台测试结果中显示成的JSON格式
private Integer productId;
@TableField("product_picture")
@JsonProperty("product_picture")
private String productPicture;
private String intro;
}
@PostMapping("/pictures")
public R pictures(@RequestBody @Validated ProductIdParam productIdParam, BindingResult result){
if (result.hasErrors()){
return R.fail("商品图片详情查询失败!");
}
return productService.pictures(productIdParam.getProductID());
}
/**
* 查询商品对应的图片详情集合
* @param productID
* @return
*/
R pictures(Integer productID);
//现在不能用productMapper了,因为返回结果已经指定是product_pictures表了
public interface PictureMapper extends BaseMapper<Picture> {
}
/**
* 查询商品对应的图片详情集合
* @param productID
* @return
*/
@Override
public R pictures(Integer productID) {
QueryWrapper<Picture> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("product_id",productID);
List<Picture> pictureList = pictureMapper.selectList(queryWrapper);
R ok = R.ok(pictureList);
log.info("ProductServiceImpl.pictures业务结束,结果:{}",ok);
return ok;
}
每一次点击(商品)都要进行mysql
的数据库的查询,每一次查询都会消耗一定的性能,而缓存是把它存储到运行内存中,用更高效的数据库给缓存起来,最后在查询的时候就不用走mysql
了,提升查询的性能和提高用户体验度。
缓存本身利用的是redis缓存,所以要连接配置redis的地址和激活缓存配置
如何把配置类应用到每个服务中?把配置类放到通用模块,哪个模块想要使用配置类,只需要声明配置类去继承它。配置文件也放到通用模块,只要下边任何服务需要,只需要激活它就可以了。
/*缓存配置类,被其他子类继承*/
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import java.time.Duration;
/**
* description: 缓存配置类 放置配置的方法,子模板继承和复用
*/
public class CacheConfiguration {
//配置缓存manager
@Bean
@Primary //同类型,多个bean,默认生效! 默认缓存时间1小时! 可以选择!
public RedisCacheManager cacheManagerHour(RedisConnectionFactory redisConnectionFactory){
RedisCacheConfiguration instanceConfig = instanceConfig(1 * 3600L);//缓存时间1小时
//构建缓存对象
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(instanceConfig)
.transactionAware()
.build();
}
//缓存一天配置
@Bean
public RedisCacheManager cacheManagerDay(RedisConnectionFactory redisConnectionFactory){
RedisCacheConfiguration instanceConfig = instanceConfig(24 * 3600L);//缓存时间1小时
//构建缓存对象
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(instanceConfig)
.transactionAware()
.build();
}
/**
* 实例化具体的缓存配置!
* 设置缓存方式JSON
* 设置缓存时间 单位秒
* @param ttl
* @return
*/
private RedisCacheConfiguration instanceConfig(Long ttl){
//设置jackson序列化工具
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer
= new Jackson2JsonRedisSerializer<Object>(Object.class);
//常见jackson的对象映射器,并设置一些基本属性
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(MapperFeature.USE_ANNOTATIONS,false);
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(ttl)) //设置缓存时间
.disableCachingNullValues()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
}
}
spring:
cache:
type: redis
redis:
host: 192.168.23.140
port: 6379
jedis: # 设置Redis连接池
pool:
max-wait: 2000ms
min-idle: 2
max-idle: 8
max-active: 10
/*商品模块配置类:哪个想要哪个继承CacheConfiguration就可以了*/
@Configuration
public class ProductConfiguration extends CacheConfiguration {
}
激活缓存:
profiles:
active: cache
让缓存生效需要在启动类上加@EnableCaching
//开启缓存,让缓存生效
怎么看一个接口适不适合缓存?要看接口的使用率以及接口的更新率。
商品搜索是不宜添加缓存的,因为每次搜索的关键字(key)是不一样的,很少两次搜索相同的数据。缓存时候有key和事件
单类别热门商品缓存时间不宜过长,因为它会受销售的影响,所以设置为1小时
缓存一般在业务层
什么时候创建param?只有param和实体类完全不符或者局部属性的时候就要声明param,但这次不要求数据库store_collect的collect表中时间和id不是前端传的,前端就穿两个东西,刚好和实体类的属性相等,所以不用创建param,所以直接使用实体类接值就可以了。第二收藏数据用mybatisplus
进行插入,用BaseMapper
的insert
方法,insert方法里边要的是数据库对应的实体类,所以不创建param。
/*收藏实体类*/
@Data
@TableName("collect")
public class Collect implements Serializable {
public static final Long serialVersionUID = 1L;
@TableId(type = IdType.AUTO)
private Integer id;
@JsonProperty("user_id")
@TableField("user_id")//对应数据库的属性名
private Integer userId;
@TableId("product_id")
@JsonProperty("product_id")
private Integer productId;
@JsonProperty("collect_time")
@TableField("collect_time")
private Long collectTime;
}
@RestController
@RequestMapping("/collect")
public class CollectionController {
@Autowired
private CollectService collectService;
@PostMapping("/save")
public R save(@RequestBody Collect collect){
return collectService.save(collect);
}
}
public interface CollectService {
/**
* 收藏添加的方法
* @param collect
* @return 001 004
*/
R save(Collect collect);
}
/*收藏实现类*/
@Service
@Slf4j
public class CollectServiceImpl implements CollectService {
@Autowired
private CollectMapper collectMapper;
/**
* 收藏添加的方法
* @param collect
* @return 001 004
*/
@Override
public R save(Collect collect) {
//1.先判断(查询)数据是否存在,
QueryWrapper<Collect> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id",collect.getUserId());
queryWrapper.eq("product_id",collect.getProductId());
//查询有几条
Long count = collectMapper.selectCount(queryWrapper);
if (count > 0){//有
//说明已经添加了,不能再添加
return R.fail("该商品已经添加收藏,请到我的收藏查看");
}
//2.如果不存在,则添加,在添加之前补充时间值,因为前端不传时间
collect.setCollectTime(System.currentTimeMillis());
int rows = collectMapper.insert(collect);//影响行数
log.info("CollectServiceImpl.save业务结束,结果:{}",rows);
return R.ok("添加收藏成功");
}
}
/*数据库接口*/
public interface CollectMapper extends BaseMapper<Collect> {
}
根据user_id去收藏表中去查出用户对应有哪些商品,结果返回的是商品的详情,还需要把当前用户对应的商品集合,最终完成信息的查询和返回。
1、在收藏服务中根据user_id去查询对应的product_id集合
2、调用商品服务,传入id集合,完成数据查询
3、结果返回即可
收藏服务要调用商品服务,要传入商品id集合。商品服务中根据传入的商品id集合进行数据库的查询,返回数据库信息封装的R
创建vo就是用来返回结果的,实体类需要实现Serializable接口。
ProductCollectParam可以复用,里边装的是product的id集合
/**
* mq序列化方式,选择json!
* @return
*/
@Bean
public MessageConverter messageConverter(){
return new Jackson2JsonMessageConverter();
}
导入依赖
<dependency>
<groupId>com.aliyun.ossgroupId>
<artifactId>aliyun-sdk-ossartifactId>
<version>3.14.1version>
dependency>
<dependency>
<groupId>joda-timegroupId>
<artifactId>joda-timeartifactId>
<version>2.10.14version>
dependency>
<dependency>
<groupId>commons-iogroupId>
<artifactId>commons-ioartifactId>
<version>2.4version>
dependency>
dependencies>
声明配置
#OSS配置
aliyun:
oss:
file:
# 控制台 - oss - 点击对应桶 - 概览 - 地域节点
endpoint: 你的地域节点
keyid: 你的accesskey
keysecret: 你的accesssecret
bucketname: 你的桶名
导入工具类
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.GetObjectRequest;
import com.aliyun.oss.model.ObjectMetadata;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.util.Date;
/**
* projectName: b2c-cloud-store
*
* @author: hwf
* description:
*/
@Component
@Data
@ConfigurationProperties(prefix = "aliyun.oss.file")
public class AliyunOSSUtils {
//基本属性 读取配置文件
private String endPoint;
private String keyId;
private String keySecret;
private String bucketName;
/**
* byte数组格式上传文件并返回上传后的URL地址
* @param objectName 完整文件名, 例如abc/efg/123.jpg
* @param content 文件内容, byte数组格式
* @param contentType 文件类型 image/png image/jpeg
* @param hours 过期时间 单位小时
* @Author hwf
*/
public String uploadImage(String objectName,
byte[] content,String contentType,int hours) throws Exception {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endPoint, keyId, keySecret);
// 创建上传文件的元信息,可以通过文件元信息设置HTTP header(设置了才能通过返回的链接直接访问)。
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentType(contentType);
// 文件上传
ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(content), objectMetadata);
// 设置URL过期时间为hours小时。
Date expiration = new Date(System.currentTimeMillis() + 3600 * 1000 * 300 * hours);
//返回url地址
String url = ossClient.generatePresignedUrl(bucketName, objectName, expiration).toString();
//关闭OSSClient。
ossClient.shutdown();
return url;
}
/**
* 下载文件到本地
* @param objectName 完整文件名, 例如abc/efg/123.jpg
* @param localFile 下载到本地文件目录
* @Author hwf
*/
public void downFile(String objectName,
String localFile) throws Exception {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endPoint, keyId, keySecret);
// 下载OSS文件到本地文件。如果指定的本地文件存在会覆盖,不存在则新建。
ossClient.getObject(new GetObjectRequest(bucketName, objectName), new File(localFile));
// 关闭OSSClient。
ossClient.shutdown();
}
/**
* 删除文件
* @param objectName 完整文件名, 例如abc/efg/123.jpg
* @Author hwf
*/
public void deleteFile(String objectName) {
// 创建OSSClient实例。
OSS ossClient = new OSSClientBuilder().build(endPoint, keyId, keySecret);
// 删除文件。如需删除文件夹,请将ObjectName设置为对应的文件夹名称。如果文件夹非空,则需要将文件夹下的所有object删除后才能删除该文件夹。
ossClient.deleteObject(bucketName, objectName);
// 关闭OSSClient。
ossClient.shutdown();
}
}
实战代码:
@PostMapping("upload")
public Object upload(MultipartFile img) throws Exception {
String filename = img.getOriginalFilename();
String contentType = img.getContentType();
long millis = System.currentTimeMillis();
filename = millis + filename; //防止重复
String url = aliyunOSSUtils.uploadImage(filename, img.getBytes(), contentType, 1000);
System.out.println("url = " + url);
return R.ok("上传成功",url);
}