目录
Day 02 商品后台管理系统
1. 商品基本知识
商品详情页面:销售属性,讲解spu时会说,作用:给用户提供自己喜欢的商品
SKU:每种商品均对应有唯一的SKU编号。(具体到唯一商品)
SPU:一组可复用,易检索的标准化信息集合。 (手机)
分类:
basecategory1
basecategory2
basecategory3
平台属性:
baseAttrInfo: -- categoryId categoryLeve1
baseAttrValue
2. 回顾Mybatis
1. 如何配置1对n?多对1? 在实体类增加List属性,封装n的数据,
xml中 用 <collection> 配置
2. 两个重要的配置文件
映射文件:aseAttrInfoMapper.xml 配置方法的名称以及方法的实现
核心配置文件:mybatis-cfg.xml 配置的数据源以及读取那个配置映射文件
<mappers> <mapper resource=/com/atguigu/mapper/BaseAttrInfoMapper.xml /> <mapper resource=/com/atguigu/mapper/BaseAttrInfoMapper.xml /> <mapper package = "com.atguigu.mapper"/> </mappers>
mybatis 默认有个规定:
BaseAttrInfoMapper.java 文件与 BaseAttrInfoMapper.xml 通常在一起 (可以包扫描)BaseAttrInfoMapper.java 中定义接口,
BaseAttrInfoMapper.xml 是接口的实现类spring boot 整合mybatis-plus!
mybatis-plus:3. mybatis 在传递多个参数的时候,建议使用@Param()注解标识,用#{}填充占位符,
4. mybatis 的动态sql 标签
<if> : 进行条件的判断
<where>:自动添加where,处理 and、or 连接符
<trim>:在SQL语句前后添加前缀,后缀
<set>: 修改操作时出现的逗号问题
<choose> <when> <otherwise>:类似于switch,所有的条件中选择其一
<foreach>:循环遍历;可通过拼装SQL完成批量操作
5. 全注解开发可读性差,不容易维护,开发的时候建议写 xxx.xml
6. 有关于mybatis 的相关源码:底层源码用的cglib动态代理
UserInfoMapper userInfoMapper = sqlSession.getMapper(UserInfoMapper.java);7. Mybatis如何生成自增主键值
a. 实体类id上加入注解属性 @TableId(type = IdType.AUTO)
b. xml 在<insert>标签中使用 useGeneratedKeys 和 keyProperty 两个属性来获取自动生成的主键值
3. 添加平台属性接口 (多表查询)
思路:
平台属性对象 ---{平台属性值集合}1. 先获取页面传递的过来的数据,Json格式
2. 考虑在后台如何接收Json!
使用spring mvc 常用注解:
@RequestBody --- Json ---->javaObject
3 调用服务层方法保存数据!
数据库表:baseAttrInfo , baseAttrValue多表插入时使用事务 @Transactional(rollbackFor = Exception.class) //当发生异常直接回滚
4. debug:
a. 在控制器断点 有效代码!
如果根本不进入断点,说明mapping路径有问题!
b. 进入实现类
直接打断点!
F8 单行调试,也可以直接进入下一个断点
F7 进入方法内部,单步调试
如果出现错误,单独调试这一行代码即可!
JSON格式报错:Caused by: com.fasterxml.jackson.core.JsonParseException: Unexpected character ('}' (code 125)):@Service public class ManageServiceImpl implements ManageService { // select * from base_category1 where is deleted = 0 // Mybatis-Plus执行上述sql语句,mapper封装了单表的crud(泛型对应的表) @Autowired private BaseCategory1Mapper baseCategory1Mapper; @Autowired private BaseCategory2Mapper baseCategory2Mapper; @Autowired private BaseCategory3Mapper baseCategory3Mapper; @Autowired private BaseAttrInfoMapper baseAttrInfoMapper; @Autowired private BaseAttrValueMapper baseAttrValueMapper; //查询一级分类id数据 @Override public List<BaseCategory1> getCategory1() { return baseCategory1Mapper.selectList(null); } //根据以及分类id查询二级分类数据 @Override public List<BaseCategory2> getCategory2(Long category1Id) { return baseCategory2Mapper.selectList(new QueryWrapper<BaseCategory2>().eq("category1_id",category1Id)); } //根据二级分类id查询三级分类数据 @Override public List<BaseCategory3> getCategory3(Long category2Id) { return baseCategory3Mapper.selectList(new QueryWrapper<BaseCategory3>().eq("category2_id",category2Id)); } //select * from ... where category2_id = ? //根据分类id查询平台属性 @Override public List<BaseAttrInfo> getAttrInfoList(Long category1Id, Long category2Id, Long category3Id) { return baseAttrInfoMapper.selectAttrInfoList(category1Id, category2Id, category3Id); } //保存平台属性方法 @Override @Transactional(rollbackFor = Exception.class) //当发生异常直接回滚 public void saveAttrInfo(BaseAttrInfo baseAttrInfo) { // 1.插入数据库 base_attr_info baseAttrInfoMapper.insert(baseAttrInfo); // 2.从baseAttrInfo 获取平台属性值集合! List<BaseAttrValue> attrValueList = baseAttrInfo.getAttrValueList(); // 3.遍历循环,插入数据库 base_attr_value if(!CollectionUtils.isEmpty(attrValueList)){ attrValueList.forEach(baseAttrValue -> { // attr_id = base_attr_info.id ,界面传递时没有传递attr_id, // baseAttrInfo.getId() 为什么能获取主键值? 实体类 type = IdType.AUTO 能够自动获取自增主键 // 前提: 这个表必须先执行insert baseAttrValue.setAttrId(baseAttrInfo.getId()); baseAttrValueMapper.insert(baseAttrValue); }); } } }
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper SYSTEM "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <!--namespace 定义接口的全路径--> <mapper namespace="com.atguigu.gmall.product.mapper.BaseAttrInfoMapper"> <!-- resultMap: 表示返回自定义的映射结果集 - 将数据库中的字段与实体类的属性关联 id: 表示唯一标识 type: 这个返回结果集的数据类型 : 判断方法 集合的泛型 autoMapping: 表示自动映射,按照驼峰命名法自动封装 --> <resultMap id="baseAttrInfoMap" type="com.atguigu.gmall.model.product.BaseAttrInfo" autoMapping="true"> <!--id:主键 property:实体类的属性名 column:数据库字段名{包括字段起的别名} result:普通字段--> <id property="id" column="id"></id> <collection property="attrValueList" ofType="com.atguigu.gmall.model.product.BaseAttrValue"> <!--实体键的主键id可以重复,但是sql语句映射出来的别名不能重复 需要别名--> <id property="id" column="attr_value_id"></id> </collection> </resultMap> <!--哪个方法的实现 id: 表示方法名 resultMap: 表示返回自定义的映射结果集 通常配置1:n 获取其他较为复杂关系 resultType: 表示返回具体的数据类型 表示中返回单个实体类的时候,或者明确具体是什么类型 3.mybatis 在执行SQL时,;不要加,order by 会出错 4.#(占位符) and $(字符串拼接,注入问题) $在order by 排序字段时使用 5.mybatis 在传递多个参数时,建议使用@Param注解表示 6.mybatis 的动态sql标签 --> <select id="selectAttrInfoList" resultMap="baseAttrInfoMap"> select bai.id, bai.attr_name, bai.category_id, bai.category_level, bav.id attr_value_id, bav.value_name, bav.attr_id from base_attr_info bai join base_attr_value bav on bai.id = bav.attr_id <where> <trim prefix="(" suffix=")"> <if test="category1Id!=null and category1Id !=0"> (bai.category_id = #{category1Id} and bai.category_level = 1) </if> <if test="category2Id!=null and category2Id !=0"> or (bai.category_id = #{category2Id} and bai.category_level = 2) </if> <if test="category3Id!=null and category3Id !=0"> or (bai.category_id = #{category3Id} and bai.category_level = 3) </if> </trim> </where> and bai.is_deleted = 0 and bav.is_deleted = 0 order by bai.id,bav.id </select> </mapper>
Day 03 完成后台平台属性管理、SPU
数据持久化:
1. 修改平台属性
前端回显数据
baseAttrInfo --- 直接执行update 语句,因为这个id 可以确定!
baseAttrValue --- 不能执行update 语句,无法确定用户的具体操作策略:先逻辑删除 ,再新增
缺点:会导致一部分 id 号段无效!//保存-修改平台属性方法 @Override @Transactional(rollbackFor = Exception.class) //当发生异常直接回滚 public void saveAttrInfo(BaseAttrInfo baseAttrInfo) { //判断什么时候修改,什么时候新增 if(baseAttrInfo.getId()!=null){ //修改数据 base_attr_info baseAttrInfoMapper.updateById(baseAttrInfo); //修改平台属性值需要先删除数据,逻辑删除本质是修改 QueryWrapper<BaseAttrValue> wrapper = new QueryWrapper<>(); wrapper.eq("attr_id",baseAttrInfo.getId()); baseAttrValueMapper.delete(wrapper); }else { //新增 // 1.保存数据 base_attr_info baseAttrInfoMapper.insert(baseAttrInfo); } // 2.从baseAttrInfo 获取平台属性值集合,遍历循环,插入数据库 List<BaseAttrValue> attrValueList = baseAttrInfo.getAttrValueList(); if(!CollectionUtils.isEmpty(attrValueList)){ attrValueList.forEach(baseAttrValue -> { // attr_id = base_attr_info.id ,界面传递时没有传递attr_id, // baseAttrInfo.getId() 为什么能获取主键值? 实体类 type = IdType.AUTO 能够自动获取自增主键 // 前提: 这个表必须先执行insert baseAttrValue.setAttrId(baseAttrInfo.getId()); baseAttrValueMapper.insert(baseAttrValue); }); } }
2. gateway网关
service-product 与 后台vue 项目进行整合
后台无法获取数据,和端口号有关系
访问一级分类数据:
vue url: http://localhost/admin/product/getCategory1 默认80 没有数据:
controller :http://localhost:8206/admin/product/getCategory1 有数据:所以需要加入网关,用户发起数据访问的时候默认访问gateway : 80,由网关根据路径:找到 service-product 8206
server: port: 80 spring: redis: host: 192.168.86.90 port: 6379 database: 0 timeout: 1800000 password: cloud: gateway: discovery: #是否与服务发现组件进行结合,通过 serviceId(必须设置成大写) 转发到具体的服务实例。默认为false,设为true便开启通过服务中心的自动根据 serviceId 创建路由的功能。 locator: #路由访问方式:http://Gateway_HOST:Gateway_PORT/大写的serviceId/**,其中微服务应用名默认大写访问。 enabled: true routes: - id: service-product uri: lb://service-product predicates: - Path=/*/product/** # 路径匹配
3. 跨域问题
创建网关项目后,访问请求路径有数据了,但vue 项目依然访问失败
(Ctrl + f5 清空浏览器缓存)
跨域问题:请求的域名、端口、协议不同都会引发跨域问题 (http和https)为了防止跨站攻击,浏览器对于ajax请求的一种安全限制:只能是与当前页域名相同的路径,这能有效的阻止跨站攻击。
解决跨域问题:
1. jsonp(杰森):最早的解决方式, 只支持 GET 请求
2. nginx:反向代理,但是 在nginx.conf 配置不易理解,语义不清,不方便后期维护
3. springMVC提供@CrossOrigin 注解,只支持GET请求
4. cors:跨域资源共享 (原理:预检请求,预检响应)
预检请求: 客户端
OPTIONS /cors HTTP/1.1
Origin: http://localhost:1000
Access-Control-Request-Method: GET
Access-Control-Request-Headers: X-Custom-Header
User-Agent: Mozilla/5.0...预检响应: 服务器端设置的
Access-Control-Allow-Origin: http://localhost:1000
Access-Control-Allow-Credentials: true
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Max-Age: 1728000
4. Nacos 配置中心,持久化配置
3. 持久化配置,Nacos配置中心,本质是将配置写在数据库里
nacos 做配置中心,并且能够持久化,将配置中心写入Mysql,表名config_info
1. 依赖jar 包!
server-gateway ,service-product 解开注释的jar
2. 重新创建一个容器,并指定好 nacos 读取的数据库
先创建一个nacos 数据库,导入nacos.sql 文件!
docker run -d \
-e MODE=standalone \
-e PREFER_HOST_MODE=hostname \
-e SPRING_DATASOURCE_PLATFORM=mysql \
-e MYSQL_SERVICE_HOST=192.168.200.129 \
-e MYSQL_SERVICE_PORT=3306 \
-e MYSQL_SERVICE_USER=root \
-e MYSQL_SERVICE_PASSWORD=root \
-e MYSQL_SERVICE_DB_NAME=nacos \
-p 8848:8848 \
--name nacos \
--restart=always \
nacos/nacos-server:1.4.1
3. 在 nacos 中添加配置
抽象出一个公共的配置文件,编写对应的项目配置文件4. 修改配置文件指定读取nacos 的配置
5. spu相关业务介绍
制作SPU!
业务介绍:
SPU : 一组可复用,易检索的标准化信息集合,这个描述了产品的特性!SPU: 手机
华为手机,小米手机,锤子手机,vivo 手机
华为,小米,锤子,vivo: 品牌SPU 红旗--手机
销售属性:
颜色:红色,绿色
版本:256G + 1T 512G + 1.5TSPU相关表结构
商品和品牌什么关系?多对多;平时检索的时候,看到的是一对多
多对多关系如何建表?
三级分类Id 下有很多spu,spu 又对应很多品牌。我们可以通过三级分类Id和品牌id将两张表关联起来,建立多对多关系。
6. 根据三级分类Id 查询spu 列表
http://api.gmall.com/admin/product/1/10?category3Id=61
如何获取category3Id?
1.HttpServletRequest.getParameter("category3Id");
2.@RequestParam Long category3Id
3.象属传值 如果页面提交过来的参数与实体类的参数一致,自动注入mybatis-plus 提供了一个分页对象
page 对象,只封装了current,size,total等 基本属性
IPage 对象,封装了更多信息,可调用getRecords() 得到分页数据
@ApiOperation("根据三级分类id查询spu列表") @GetMapping("{page}/{size}") public Result getSpuInfoPage(@PathVariable Long page, @PathVariable Long size, SpuInfo spuInfo){ // mybatis-plus 提供了一个分页对象 // select * from sku_info where 如何获取category3_id = ? limit(?,?) Page<SpuInfo> spuInfoPage = new Page<>(page,size); IPage<SpuInfo> spuInfoPageList = manageService.getSpuInfoPage(spuInfoPage, spuInfo); return Result.ok(spuInfoPageList); }
BUG 可能自己都能解决,遇到的大多是环境问题
1. 先确定是不是IDEA问题;外部用text手写一段Java代码,放在工程中判断是否乱码。
2. 和公司磁盘加密有关,换个盼复就好了 (把每个文件都编译成二进制了)
Day 04 制作SKU
1. 回顾 (表结构、Redis、JVM)
相关表结构
三级分类:
baseCategory1;
baseCategory2;
baseCategory3;
平台属性:
baseAttrInfo;
baseAttrValue;
SPU:
spuInfo; 商品表
spuImage;spuPoster;
spuSaleAttr;
spuSaleAttrValue;
baseSaleAttr;
baseTradeMark;
baseCategoryTradeMark;redis 是什么?
redis 五种数据类型
项目中采用redis 做缓存的原因是
redis 优点:快 (什么是多路io复用)
项目中是如何应用的?
单节点:aof,rdb
集群:集群原理 16384/key crc32 算法:数值 ==》key value 存储在哪组 (集群为什么三主三从?)
主从复制:
可能出现的问题: 缓存击穿,缓存穿透,缓存雪崩
mysql redis 树同步问题?
本地锁:sync lock
分布式锁:zk、redis、mysql
类加载器:将Class加载入内存,双亲委派(类重复加载、安全),沙箱安全机制
本地方法栈:Native修饰的方法,需要操作硬件,Java无能为力
本地方法接口:
本地方法库:C++实现,.DLL
栈:实例对象的地址;本地变量,栈操作,栈数据(栈帧,先进后出)
程序计数器:记录程序运行的指针,可以忽略不计
方法区:类信息(元数据模板)、静态常量、Integer-128-127
堆:实例对象的数据;
新生区:伊甸区、幸存者一、幸存者二、轻GC,TO区总为空
老年区:重GC
jdk8 中 有什么更新?
去永久代变元空间:
元空间指的是本地的物理内存 RAM
2. 品牌CRUD \ 三级分类ID与品牌关系CRUD
Mybatis三接口:IService、ServiceImpl、BaseMapper
分类与品牌关系:
1. 根据三级分类Id 查询品牌数据2. 给三级分类Id 绑定品牌!
获取可选品牌列表3. 保存分类Id 与品牌的关系!
4. 删除分类Id 与品牌的关系!
3. MinIO 分布式文件存储系统
官方文档:MinIO | The MinIO Quickstart Guide
MinIO Quickstart Guide| Minio中文文档 (旧版本)
分布式文件存储系统
本质:将本地图片数据读取到服务器上,文件上传
java 基础 io 流
ssm 框架 springmvc 文件上传类 MultipartFile注意事项
1.文件的名称不能重复2.文件的后缀名
3.文件的字符集编码
4.文件的大小等
minio 开源的分布式文件存储系统,市场占用率高;
特点:性能高;可扩展;sdk;有界面;防止丢失数据;
存储机制:纠删码;在最高冗余度配置下,即使丢失1/2的磁盘也能恢复数据!
纠删码是一种恢复丢失和损坏数据的数学算法。
三种模式:
单主机单硬盘模式 (我们用的)
单主机多硬盘模式
多主机多硬盘分布式 (高可用集群版)<dependency> <groupId>io.minio</groupId> <artifactId>minio</artifactId> <version>8.2.0</version> </dependency>
minio: endpointUrl: http://IP:9000 accessKey: admin secreKey: admin123456 bucketName: gmall
1. 添加依赖
2. 添加配置文件 : 指定文件服务器的地址,用户名,密码,桶名称
3. 编写文件上传控制器
1. 创建客户端minioClient
2. 创建桶
3. 调用上传文件方法!测试的时候需要处理的细节
1.修改桶的权限 由private ---> public2.回显的时候要使用拼接的方式获取图片的全路径
3.Linux时间要与当前系统时间保持一致;解决方案:参考课件
@Api(description = "MinIO文件上传") @RestController @RequestMapping("admin/product") @RefreshScope //热部署读取到nacos配置文件的数据(配置文件改动直接热部署) public class FileUploadController { //获取文件上传对应的地址 @Value("${minio.endpointUrl}") public String endpointUrl; //用户名 @Value("${minio.accessKey}") public String accessKey; //密码 @Value("${minio.secreKey}") public String secreKey; //桶名 @Value("${minio.bucketName}") public String bucketName; // 文件上传控制器:MultipartFile @PostMapping("fileUpload") public Result fileUpload(MultipartFile file){ //声明一个遍历去接收url String url = ""; try { // Create a minioClient with the MinIO server playground, its access key and secret key. MinioClient minioClient = MinioClient.builder() .endpoint(endpointUrl) .credentials(accessKey, secreKey) .build(); // Make 'asiatrip' bucket if not exist. boolean found = minioClient.bucketExists(BucketExistsArgs.builder().bucket(bucketName).build()); if (!found) { // Make a new bucket called 'asiatrip'. minioClient.makeBucket(MakeBucketArgs.builder().bucket(bucketName).build()); } else { System.out.println("Bucket "+bucketName+" already exists."); } // Upload '/home/user/Photos/asiaphotos.zip' as object name 'asiaphotos-2015.zip' to bucket // 'asiatrip'. // /*minioClient.uploadObject( UploadObjectArgs.builder() .bucket("asiatrip") .object("asiaphotos-2015.zip") .filename("/home/user/Photos/asiaphotos.zip") .build());*/ //新的上传方法base_category_trademark String fileName = System.currentTimeMillis()+ UUID.randomUUID().toString(); // 后缀名:minio 可以用后缀名 xxx.png // file.getOriginalFilename() 截取 System.out.println("后缀名"+FilenameUtils.getExtension(file.getOriginalFilename())); minioClient.putObject( PutObjectArgs.builder().bucket(bucketName).object(fileName).stream( file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); // 上传之后的url; //为什么要拼接?因为url后面有很多全权限设置,默认情况下7天有效,超过七天图片访问不了! url = endpointUrl+"/"+bucketName+"/"+fileName; System.out.println("url = " + url); } catch (ErrorResponseException e) { e.printStackTrace(); } catch (InsufficientDataException e) { e.printStackTrace(); } catch (InternalException e) { e.printStackTrace(); } catch (InvalidKeyException e) { e.printStackTrace(); } catch (InvalidResponseException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } catch (ServerException e) { e.printStackTrace(); } catch (XmlParserException e) { e.printStackTrace(); } //返回数据 return Result.ok(url); } }
如果业务字段在表中没有怎么办?
自己组装Vo,或者使用别名
制作SPU:注意加入事物 @Transactional(rollbackFor = Exception.class)
4. SKU
SPU:一组可复用易检索的标准化信息集合
SKU:每种商品均对应唯一的编号
sku 图片 都应该是基于 SPU 选择的!
sku 应该有属于自己的url 地址!
SKU 相关表结构:
spuInfo:
id,spuName,category3Id,tmId,createTime,updateTime,isDelete
1, 红旗手机, 61 , 1skuInfo: 库存单元表
id,skuName,spuId,price,category3Id,defaultImage,isSale,createTime,updateTime,isDelete
1 ,红旗1 , 1 0/1
2 ,红旗2 , 1
skuImage: 库存单元图片表
id,imgName,skuId,imgUrl,createTime,updateTime,isDelete
1, xx1 1 http://
2, xx2 1
3, xx3 1sku 与 销售属性值Id 有关系么?
skuSaleAttrValue: sku 与 销售属性值Id 中间表 spuSaleAttrValue
id,skuId,spuSaleAttrId, spuSaleAttrValueId,createTime,updateTime,isDelete
1, 1 , 1, 1
2, 1 , 2, 1sku 与 平台属性值Id 有关系么?
skuAttrValue: sku 与 平台属性值Id 中间表 baseAttrValue
id,skuId,saleAttrId, saleAttrValueId,createTime,updateTime,isDelete
1, 1 , 1, 1
2, 1 , 2, 1商品数量:会单独有一个库存系统
skuId , skuNum
1, 1000
2, 10000