乐优商城(四)商品规格管理
文章目录
- 1. 商品规格
- 1.1 SPU 和 SKU
- 1.2 分析商品规格的关系
- 1.3 数据库设计
- 1.3.1 商品规格组表
- 1.3.2 商品规格参数表
- 2. 商品规格组
- 2.1 商品规格组前端
- 2.2 实现商品规格组查询
- 2.2.1 实体类
- 2.2.2 Mapper
- 2.2.3 Service
- 2.2.4 Controller
- 2.2.5 测试
- 3. 商品规格参数
- 3.1 商品规格参数前端
- 3.2 实现商品规格参数查询
- 3.2.1 Controller
- 3.2.2 Service
- 3.2.3 测试
- 4. 商品
- 4.1 分析商品的关系
- 4.2 数据库设计
- 4.2.1 SPU 表
- 4.2.2 SKU 表
- 4.3 商品前端
- 4.4 实现商品查询
- 4.4.1 实体类
- 4.4.2 Mapper
- 4.4.3 Controller
- 4.4.4 Service
- 4.4.5 测试
1. 商品规格
乐优商城是一个全品类的电商网站,因此商品的种类繁多,每一件商品,其属性又有差别。为了更准确描述商品及细分差别,抽象出两个概念:SPU 和 SKU
1.1 SPU 和 SKU
SPU:Standard Product Unit (标准产品单位) ,一组具有共同属性的商品集
SKU:Stock Keeping Unit(库存量单位),SPU 商品集因具体特性不同而细分的每个商品
上面的概念有些抽象,为便于理解下面有一张京东的 “小米 10” 商品页面图片:
在页面中的 “小米 10” 就是一个商品集,即 SPU
因为选择不同的颜色、版本而细分出不同的 “小米 10”,即 SKU。
比如:钛银色、8GB+256GB 是一个 SKU;冰蓝色、8GB+128GB 是一个 SKU
两者的作用:
- SPU 是一个抽象的商品集概念,是为了方便后台的管理。
- SKU 才是具体要销售的商品,每一个 SKU 的价格、库存可能会不一样,用户购买的是 SKU 而不是 SPU。
1.2 分析商品规格的关系
我们看看京东的 “小米 10” 商品的规格页面:
可以很容易分析出这里有两张表:规格组和规格参数。并且一个规格组对应着多个规格参数,一个规格参数对应着一个规格组。规格组和规格参数之间是一对多的关系。
并且一个分类对应着多个规格组,一个规格组对应着一个分类。分类和规格组之间是一对多的关系。
再来看看京东搜索 “手机” 后的过滤条件:
可以分析出:我们需要直接根据 “手机” 分类,得出 “品牌” 规格参数等。并且一个分类对应着多个规格参数,一个规格参数对应着一个分类。分类和规格参数之间是一对多的关系。
分类、规格组、规格参数之间的关系如下图所示:
1.3 数据库设计
1.3.1 商品规格组表
规格组表 tb_spec_group
CREATE TABLE `tb_spec_group` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`cid` bigint(20) NOT NULL COMMENT '商品分类id,一个分类下有多个规格组',`name` varchar(32) NOT NULL COMMENT '规格组的名称',PRIMARY KEY (`id`),KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8 COMMENT='规格参数的分组表,每个商品分类下有多个规格参数组';
1.3.2 商品规格参数表
规格参数表 tb_spec_param
CREATE TABLE `tb_spec_param` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',`cid` bigint(20) NOT NULL COMMENT '商品分类id',`group_id` bigint(20) NOT NULL,`name` varchar(256) NOT NULL COMMENT '参数名',`numeric` tinyint(1) NOT NULL COMMENT '是否是数字类型参数,true或false',`unit` varchar(256) DEFAULT '' COMMENT '数字类型参数的单位,非数字类型可以为空',`generic` tinyint(1) NOT NULL COMMENT '是否是sku通用属性,true或false',`searching` tinyint(1) NOT NULL COMMENT '是否用于搜索过滤,true或false',`segments` varchar(1024) DEFAULT '' COMMENT '数值类型参数,如果需要搜索,则添加分段间隔值,如CPU频率间隔:0.5-1.0',PRIMARY KEY (`id`),KEY `key_group` (`group_id`),KEY `key_category` (`cid`)
) ENGINE=InnoDB AUTO_INCREMENT=33 DEFAULT CHARSET=utf8 COMMENT='规格参数组下的参数名';
这里有几个字段比较特殊,下面给出具体的解释:
- numeric:用来判断规格参数是否是数字类型参数。如果是数字类型参数,还需要填写数字类型参数的单位。
- generic:用来判断规格参数是否是 SKU 通用属性。比如上面的 “小米 10” 的 “颜色” 和 “版本” 这两个规格参数就不是 SKU 通用属性,而是 SKU 特有属性,所以它们的值为 false。
- searching:用来判断规格参数是否用于搜索过滤。上面我们已经可以知道有些规格参数会作为搜索过滤的条件。
- segments:分段间隔值。如果一个字段既是数字类型参数,还能用于搜索过滤,那就可以给他分几个间隔值,比如电池容量:0-2000mAh、2000mAh-3000mAh、3000mAh-4000mAh。
2. 商品规格组
2.1 商品规格组前端
我们打开规格参数的页面,可以看到左侧展示了商品的分类
点击一个分类的最终分类,可以看到右侧提示 “该分类下暂无规格组或尚未选择分类”,由此可以得知右侧是用来展示规格组数据的,只是现在暂时没有数据。
我们找到前端请求规格组数据的代码:
由此可以得知:
- 请求方式:GET
- 请求路径:/spec/groups
- 请求参数:分类 id,这里用的是 Rest 风格的占位符
- 返回参数:规格组的集合
2.2 实现商品规格组查询
2.2.1 实体类
在 leyou-item-interface 项目中添加两个实体类
规格组 SpecGroup
@Table(name = "tb_spec_group")
public class SpecGroup {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private Long cid;private String name;@Transientprivate List<SpecParam> params;// getter、setter、toString 方法省略
}
注意:这里的 params 属性并不与数据库字段相对应,所以加上了 @Transient
规格参数 SpecParam
@Table(name = "tb_spec_param")
public class SpecParam {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private Long cid;private Long groupId;private String name;@Column(name = "`numeric`")private Boolean numeric;private String unit;private Boolean generic;private Boolean searching;private String segments;// getter、setter、toString 方法省略
}
注意:这里的 numeric 属性在 MySQL 中是一个关键字,所以使用 @Column 表示它是一个字段
2.2.2 Mapper
在 leyou-item-service 项目中添加两个 Mapper
规格组 SpecGroupMapper
public interface SpecGroupMapper extends Mapper<SpecGroup> {}
规格参数 SpecParamMapper
public interface SpecParamMapper extends Mapper<SpecParam> {}
2.2.3 Service
在 leyou-item-service 项目中添加 Service
@Service
public class SpecificationService {@Autowiredprivate SpecGroupMapper specGroupMapper;@Autowiredprivate SpecParamMapper specParamMapper;/*** 根据分类 id 查询分组** @param cid* @return*/public List<SpecGroup> querySpecGroupsByCid(Long cid) {SpecGroup specGroup = new SpecGroup();specGroup.setCid(cid);List<SpecGroup> specGroups = specGroupMapper.select(specGroup);return specGroups;}
}
2.2.4 Controller
在 leyou-item-service 项目中添加 Controller
@RestController
@RequestMapping("/spec")
public class SpecificationController {@Autowiredprivate SpecificationService specificationService;/*** 根据分类 id 查询分组** @param cid* @return*/@GetMapping("/groups/{cid}")public ResponseEntity<List<SpecGroup>> querySpecGroupsByCid(@PathVariable("cid") Long cid) {List<SpecGroup> specGroups = specificationService.querySpecGroupsByCid(cid);if (CollectionUtils.isEmpty(specGroups)) {return ResponseEntity.notFound().build();}return ResponseEntity.ok(specGroups);}
}
2.2.5 测试
成功加载规格组数据
3. 商品规格参数
3.1 商品规格参数前端
点击一个规格组 “主体”
可以看到规格组的表格切换到了规格参数的表格,只是暂时还没有数据
我们找到前端请求规格参数数据的代码:
由此可以得知:
- 请求方式:GET
- 请求路径:/spec/params
- 请求参数:规格组 id
- 返回参数:规格参数的集合
3.2 实现商品规格参数查询
3.2.1 Controller
在 SpecificationController 中添加方法
/*** 根据条件查询规格参数** @param gid* @return*/
@GetMapping("/params")
public ResponseEntity<List<SpecParam>> querySpecParams(@RequestParam("gid") Long gid) {List<SpecParam> params = specificationService.querySpecParams(gid);if (CollectionUtils.isEmpty(params)) {return ResponseEntity.notFound().build();}return ResponseEntity.ok(params);
}
3.2.2 Service
在 SpecificationService 中添加方法
/*** 根据条件查询规格参数* @param gid* @return*/
public List<SpecParam> querySpecParams(Long gid) {SpecParam specParam = new SpecParam();specParam.setGroupId(gid);List<SpecParam> params = specParamMapper.select(specParam);return params;
}
3.2.3 测试
成功加载规格参数数据
4. 商品
前面我们已经介绍了 SPU 和 SKU 的概念,了解了 SPU 是一个商品集,而 SKU 才是具体要销售的商品。所以商品必不可少的两张表就是 SPU 和 SKU,下面我们分析一下 SPU、SKU 和其他表之间的关系。
4.1 分析商品的关系
还是用上面举过的例子,“小米 10” 就是一个 SPU,它只对应 “小米” 这一个品牌,但小米品牌有多个 SPU,如:小米 9、小米 8 等。品牌和 SPU 之间是一对多的关系。
而 “小米 10” 是一部手机 ,它只对应手机这一个分类,而手机分类却可以对应多个 SPU。分类和 SPU 之间是一对多的关系。
前面已经讲过了,一个 SPU 可以有多个 SKU,而一个 SKU 只能有一个 SPU。SPU 和 SKU 之间是一对多的关系。
商品的关系如下图所示:
4.2 数据库设计
4.2.1 SPU 表
SPU 表 tb_spu
CREATE TABLE `tb_spu` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'spu id',`title` varchar(128) NOT NULL DEFAULT '' COMMENT '标题',`sub_title` varchar(256) DEFAULT '' COMMENT '子标题',`cid1` bigint(20) NOT NULL COMMENT '1级类目id',`cid2` bigint(20) NOT NULL COMMENT '2级类目id',`cid3` bigint(20) NOT NULL COMMENT '3级类目id',`brand_id` bigint(20) NOT NULL COMMENT '商品所属品牌id',`saleable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否上架,0下架,1上架',`valid` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0已删除,1有效',`create_time` datetime DEFAULT NULL COMMENT '添加时间',`last_update_time` datetime DEFAULT NULL COMMENT '最后修改时间',PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=195 DEFAULT CHARSET=utf8 COMMENT='spu表,该表描述的是一个抽象性的商品,比如 iphone8';
这张表似乎少了一些字段,比如商品描述,售后信息等,但这些数据都比较大,为了不影响查询效率我们做了表的垂直拆分,将 SPU 的详情放到了另一张表 tb_spu_detail
CREATE TABLE `tb_spu_detail` (`spu_id` bigint(20) NOT NULL,`description` text COMMENT '商品描述信息',`generic_spec` varchar(2048) NOT NULL DEFAULT '' COMMENT '通用规格参数数据',`special_spec` varchar(1024) NOT NULL COMMENT '特有规格参数及可选值信息,json格式',`packing_list` varchar(1024) DEFAULT '' COMMENT '包装清单',`after_service` varchar(1024) DEFAULT '' COMMENT '售后服务',PRIMARY KEY (`spu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
这里有几个字段比较特殊,下面给出具体的解释:
generic_spec
用来保存通用规格参数信息的值,这里为了方便查询,使用了 JSON 格式。
其中都是键值对:
- key:对应的规格参数的 spec_param 的 id
- value:对应规格参数的值
special_spec
用来保存特有规格参数及可选值,也就是 SKU 的特有属性。
其中都是键值对:
- key:对应的规格参数的 spec_param 的 id
- value:对应规格参数的数组,因为 SKU 特有属性可能有多个
4.2.2 SKU 表
SKU 表 tb_sku
CREATE TABLE `tb_sku` (`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'sku id',`spu_id` bigint(20) NOT NULL COMMENT 'spu id',`title` varchar(256) NOT NULL COMMENT '商品标题',`images` varchar(1024) DEFAULT '' COMMENT '商品的图片,多个图片以‘,’分割',`price` bigint(15) NOT NULL DEFAULT '0' COMMENT '销售价格,单位为分',`indexes` varchar(32) DEFAULT '' COMMENT '特有规格属性在spu属性模板中的对应下标组合',`own_spec` varchar(1024) DEFAULT '' COMMENT 'sku的特有规格参数键值对,json格式,反序列化时请使用linkedHashMap,保证有序',`enable` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否有效,0无效,1有效',`create_time` datetime NOT NULL COMMENT '添加时间',`last_update_time` datetime NOT NULL COMMENT '最后修改时间',PRIMARY KEY (`id`),KEY `key_spu_id` (`spu_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=27359021729 DEFAULT CHARSET=utf8 COMMENT='sku表,该表表示具体的商品实体,如黑色的 64g的iphone 8';
这里有几个字段比较特殊,下面给出具体的解释:
indexes
tb_spu_detail 表的 special_spec 字段是用来保存 SKU 特有属性的,而 indexes 字段就是这些特有属性的下标组合。这个设计在商品详情页会特别有用,当用户点击选中一个特有属性,你就能根据角标快速定位到 SKU。
比如 special_spec 字段如下:
indexes 字段:
- 0_0_0:表示白色、3GB、16GB
- 1_0_0:表示金色、3GB、16GB
- 2_0_0:表示玫瑰金、3GB、16GB
own_spec
用来保存 SKU 特有属性的键值对,使用了 JSON 格式,比如:
SKU 还应该有一个库存字段,但 SKU 表中的其他字段读的频率较高,而库存字段写的频率比较高,因此做了表的垂直拆分,使读写不会互相干扰。
库存表 tb_stock
CREATE TABLE `tb_stock` (`sku_id` bigint(20) NOT NULL COMMENT '库存对应的商品sku id',`seckill_stock` int(9) DEFAULT '0' COMMENT '可秒杀库存',`seckill_total` int(9) DEFAULT '0' COMMENT '秒杀总数量',`stock` int(9) NOT NULL COMMENT '库存数量',PRIMARY KEY (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='库存表,代表库存,秒杀库存等信息';
4.3 商品前端
点击商品列表,可以看到这是一个可以实现分页、查询的表单,和之前做过的品牌的查询很相似。
我们找到前端请求商品数据的代码:
由此可以得知:
- 请求方式:GET
- 请求路径:spu/page
- 请求参数:
- key:搜索条件,String
- saleable:上下架,boolean(全部为 null,上架为 true,下架为 false)
- page:当前页,int
- rows:每页大小,int
- 返回参数:规格组的集合
- total:总条数
- items:当前页数据
4.4 实现商品查询
4.4.1 实体类
在 leyou-item-interface 中添加实体类:
SPU
@Table(name = "tb_spu")
public class Spu {@Id@GeneratedValue(strategy = GenerationType.IDENTITY)private Long id;private Long brandId;private Long cid1;// 1级类目private Long cid2;// 2级类目private Long cid3;// 3级类目private String title;// 标题private String subTitle;// 子标题private Boolean saleable;// 是否上架private Boolean valid;// 是否有效,逻辑删除用private Date createTime;// 创建时间private Date lastUpdateTime;// 最后修改时间// getter、setter、toString 方法省略
}
SPU 详情
@Table(name="tb_spu_detail")
public class SpuDetail {@Idprivate Long spuId;// 对应的SPU的idprivate String description;// 商品描述private String specialSpec;// 商品特殊规格的名称及可选值模板private String genericSpec;// 商品的全局规格属性private String packingList;// 包装清单private String afterService;// 售后服务// getter、setter、toString 方法省略
}
此时,我们发现一个问题,商品页面中的商品分类和品牌的应该是字符串
SPU 表的商品分类和品牌却只是 id,所以在实体类中还需要有两个属性,用来封装商品分类的 name 和品牌分类的 name。我们肯定不能直接修改 Spu 实体类,但可以拓展一个实体类 SpuBo,Bo 即 Business Object
public class SpuBo extends Spu{private String cname;private String bname;// getter、setter、toString 方法省略
}
4.4.2 Mapper
在 leyou-item-service 项目中添加两个 Mapper
Spu
public interface SpuMapper extends Mapper<Spu> {}
Spu 详情
public interface SpuDetail extends Mapper<SpuDetail> {}
4.4.3 Controller
在 leyou-item-service 项目中添加 Controller
@RestController
@RequestMapping("/spu")
public class SpuController {@Autowiredprivate SpuService spuService;/*** 根据查询条件分页查询商品信息* @param key 搜索条件* @param saleable 上下架* @param page 当前页* @param rows 每页大小* @return*/@GetMapping("/page")public ResponseEntity<PageResult<SpuBo>> querySpuByPage(@RequestParam(name = "key", required = false) String key,@RequestParam(name = "saleable", required = false) Boolean saleable,@RequestParam(name = "page", defaultValue = "1") Integer page,@RequestParam(name = "rows", defaultValue = "5") Integer rows) {PageResult<SpuBo> pageResult = spuService.querySpuByPage(key, saleable, page, rows);if (CollectionUtils.isEmpty(pageResult.getItems())) {ResponseEntity.notFound().build();}return ResponseEntity.ok(pageResult);}
}
4.4.4 Service
在 leyou-item-service 项目中添加 Service
@Service
public class SpuService {@Autowiredprivate SpuMapper spuMapper;@Autowiredprivate BrandMapper brandMapper;@Autowiredprivate CategoryService categoryService;/*** 根据查询条件分页查询商品信息** @param key 搜索条件* @param saleable 上下架* @param page 当前页* @param rows 每页大小* @return*/public PageResult<SpuBo> querySpuByPage(String key, Boolean saleable, Integer page, Integer rows) {// 初始化 example 对象Example example = new Example(Spu.class);Example.Criteria criteria = example.createCriteria();// 添加搜索条件if (StringUtils.isNotBlank(key)) {criteria.andLike("title", "%" + key + "%");}// 添加上下架if (saleable != null) {criteria.andEqualTo("saleable", saleable);}// 添加分页PageHelper.startPage(page, rows);// 执行查询,获取 Spu 集合List<Spu> spus = spuMapper.selectByExample(example);// 将 Spu 集合包装成 pageInfoPageInfo<Spu> spuPageInfo = new PageInfo<>(spus);// 将 Spu 集合转化为 SpuBo 集合ArrayList<SpuBo> spuBos = new ArrayList<>();for (Spu spu : spus) {SpuBo spuBo = new SpuBo();// 复制共同的属性到 SpuBo 对象中BeanUtils.copyProperties(spu, spuBo);// 查询分类名称,并添加到 SpuBo 对象中List<String> names = categoryService.queryNamesByIds(Arrays.asList(spu.getCid1(), spu.getCid2(), spu.getCid3()));spuBo.setCname(StringUtils.join(names,"/"));// 查询品牌名称,并添加到 SpuBo 对象中Brand brand = brandMapper.selectByPrimaryKey(spu.getBrandId());spuBo.setBname(brand.getName());// 添加 SpuBo 到 SpuBo 集合spuBos.add(spuBo);}// 返回 PageResult<SpuBo>return new PageResult<SpuBo>(spuPageInfo.getTotal(), spuBos);}
}
在 CategoryService 添加方法
/*** 查询分类名称* @param ids* @return*/
public List<String> queryNamesByIds(List<Long> ids) {ArrayList<String> names = new ArrayList<>();for (Long id : ids) {Category category = categoryMapper.selectByPrimaryKey(id);names.add(category.getName());}return names;
}
4.4.5 测试
乐优商城(四)商品规格管理相关推荐
- 乐优商城day13(商品详情页,rabbitMQ安装)
所有代码发布在 [https://github.com/hades0525/leyou] Day13(rabbitmq) 2019年2月13日 14:45 使用thymeleaf thymeleaf基 ...
- 乐优商城学习笔记五-商品规格管理
0.学习目标 了解商品规格数据结构设计思路 实现商品规格查询 了解SPU和SKU数据结构设计思路 实现商品查询 了解商品新增的页面实现 独立编写商品新增后台功能 1.商品规格数据结构 乐优商城是一个全 ...
- 【javaWeb微服务架构项目——乐优商城day05】——商品规格参数管理(增、删、改,查已完成),SPU和SKU数据结构,商品查询
乐优商城day05 0.学习目标 1.商品规格数据结构 1.1.SPU和SKU 1.2.数据库设计分析 1.2.1.思考并发现问题 1.2.2.分析规格参数 1.2.3.SKU的特有属性 1.2.4. ...
- 乐优商城(05)--商品管理
乐优商城(05)–商品管理 一.导入图片资源 现在商品表中虽然有数据,但是所有的图片信息都是无法访问的,因此需要把图片导入到服务器中: 将images.zip文件上传至/leyou/static目录: ...
- 乐优商城之项目搭建(四)
文章目录 (一)项目分类 (二)电商行业 (三)专业术语 (四)项目介绍 (五)技术选型 (六)开发环境 (七)搭建后台环境:父工程 (八)搭建后台环境:eureka (九)搭建后台环境:zuul ( ...
- 商城-商品规格管理-商品规格数据结构
商城-商品规格管理-商品规格数据结构 1.商品规格数据结构 1.1.SPU和SKU 1.2.数据库设计分析 1.2.1.思考并发现问题 1.2.2.分析规格参数 1.2.3.SKU的特有属性 1.2. ...
- 【javaWeb微服务架构项目——乐优商城day03】——(搭建后台管理前端,Vuetify框架,使用域名访问本地项目,实现商品分类查询,cors解决跨域,品牌的查询)
乐优商城day03 0.学习目标 1.搭建后台管理前端 1.1.导入已有资源 1.2.安装依赖 1.3.运行一下看看 1.4.目录结构 1.5.调用关系 2.Vuetify框架 2.1.为什么要学习U ...
- 乐优商城笔记六:商品详情页
使用模板引擎 Thymeleaf + nginx 完成商品详情页静态化 完成乐优商城商品详情页 搭建商品详情页微服务 创建子工程 GroupId:com.leyou.service ArtifactI ...
- 乐优商城 Day 09(thymeleaf,Rabbitmq,商品详情页,非教程)
乐优商城学习Day09: 注意:此次代码都是在第八天的基础上 第八天的链接如下: https://blog.csdn.net/zcylxzyh/article/details/100859210 此次 ...
最新文章
- Photoshop 混色模式学习
- 算法竞赛入门经典读书笔记(二)7.1简单枚举
- 【正一专栏】梅西、内马尔分开明天会更好
- 用神经网络分类随机数与无理数
- 局部特征(2)——Harris角点
- winform在表格中输入一个完整的时间字段_B端交互组件之表格篇
- css margin和border,Margin、Border、Padding属性的区别和联系
- 留言板小程序开发笔记3
- sumk-db的主要方法及注解
- 搬家请搬家公司好还是请朋友帮忙好?
- 源码多多- Discuz x2.5 版块的常用设置方法和技巧
- mac osx终端命令大全
- win10taskkill强行结束进程_win10系统强制结束进程命令的方案
- css字体加粗_CSS字体
- 心はちょっと疲れた、誰も片隅に休んで……作者の宝物は
- 完美解决:Java微信语音amr格式转mp3格式,兼容Linux/Mac/Windows,支持Maven
- 基金定投:100%抄到底的方法
- Android 原生分享文件到微信
- SpringBoot 和 Vue 前后端分离教程(附源码)
- linux同时开启两个端口,linux下squid开多个端口的办法