文章目录

    • 10.1查询 仓库维护 列表(模糊查询)wms_ware_info
    • 10.2 查询 商品库存 列表 wms_ware_sku
    • 10.3 查询新增 采购需求 wms_purchase_detail
    • 10.4 采购单维护栏
      • 1、查询采购单(自己添加)
      • 2、查询未领取的 采购单(分配采购单)wms_purchase
      • 3、合并 采购需求 到采购单
    • 10.5 领取采购单
    • 10.6 完成采购
  • 商品服务 - Spu管理
    • 11.1 获取spu规格
    • 11.2 修改商品规格

仓储服务对应于gulimall-ware服务,首先需要将该服务注册进nacos注册中心

① 配置nacos的注册中心地址:(application.yml),增加一些配置

server:port: 11000
spring:datasource:username: rootpassword: rooturl: jdbc:mysql://192.168.146.129:3306/gulimall_wms?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverapplication:name: gulimall-warecloud:nacos:discovery:server-addr: localhost:8848jackson:date-format: yyyy-MM-dd HH:mm:ss
mybatis-plus:mapper-locations: classpath:/mapper/**/*.xmlglobal-config:db-config:id-type: autologic-delete-value: 1logic-not-delete-value: 0
logging:level:com.atguigu: debug

②配置bootstrap.properties文件

spring.application.name=gulimall-ware
spring.cloud.nacos.config.server-addr=localhost:8848
spring.cloud.nacos.config.namespace=e64cac44-a064-4d89-9608-3f98dbab2232
spring.cloud.nacos.config.extension-configs[0].data-id=gulimall-ware.yml
spring.cloud.nacos.config.extension-configs[0].group=DEFAULT_GROUP
spring.cloud.nacos.config.extension-configs[0].refresh=true

③gulimall-ware主启动类:开启服务注册与发现,事务

package com.atguigu.gulimall.ware;
@EnableTransactionManagement
@MapperScan("com.atguigu.gulimall.ware.dao")
@EnableDiscoveryClient
@SpringBootApplication
public class GulimallWareApplication {public static void main(String[] args) {SpringApplication.run(GulimallWareApplication.class, args);}
}

④gulimall-gateway中配置路由网关

- id: ware_routeuri: lb://gulimall-warepredicates:- Path=/api/ware/**filters:- RewritePath=/api/(?<segment>.*),/$\{segment}

10.1查询 仓库维护 列表(模糊查询)wms_ware_info

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/mZgdqOWe

需要根据条件进行模糊检索:

①gulimall-ware : WareInfoController

@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){PageUtils page = wareInfoService.queryPage(params);return R.ok().put("page", page);
}

②gulimall-ware : WareInfoServiceImpl

@Override
public PageUtils queryPage(Map<String, Object> params) {QueryWrapper<WareInfoEntity> queryWrapper = new QueryWrapper<>();String key = (String) params.get("key");if(!StringUtils.isEmpty(key)){queryWrapper.and((wrapper)->{wrapper.eq("id",key)//前端界面只有一个白框,关键字key (参数名).or().like("name",key).or().like("address",key).or().like("areacode",key);});}IPage<WareInfoEntity> page = this.page(new Query<WareInfoEntity>().getPage(params),queryWrapper);return new PageUtils(page);
}

③测试:模糊查询

10.2 查询 商品库存 列表 wms_ware_sku

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/hwXrEXBZ

①gulimall-ware : WareSkuController

@RequestMapping("/list")
public R list(@RequestParam Map<String, Object> params){PageUtils page = wareSkuService.queryPage(params);return R.ok().put("page", page);
}

②gulimall-ware : WareSkuServiceImpl

@Override
public PageUtils queryPage(Map<String, Object> params) {QueryWrapper<WareSkuEntity> queryWrapper = new QueryWrapper<>();String skuId = (String) params.get("skuId");if(!StringUtils.isEmpty(skuId)){queryWrapper.eq("sku_id",skuId);//前端界面有sku_id选项,不再是关键字key (参数名)}String wareId = (String) params.get("wareId");//前端界面有ware_id选项,if(!StringUtils.isEmpty(wareId)){queryWrapper.eq("ware_id",wareId);}IPage<WareSkuEntity> page = this.page(new Query<WareSkuEntity>().getPage(params),queryWrapper);return new PageUtils(page);
}

③测试查询

10.3 查询新增 采购需求 wms_purchase_detail

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/Ss4zsV7R

①gulimall-ware : PurchaseDetailController

@RequestMapping("/list")//@RequiresPermissions("ware:purchasedetail:list")public R list(@RequestParam Map<String, Object> params){PageUtils page = purchaseDetailService.queryPage(params);return R.ok().put("page", page);}

②gulimall-ware : PurchaseDetailServiceImpl

@Override
public PageUtils queryPage(Map<String, Object> params) {//   key: '华为',//检索关键字//   status: 0,//状态//   wareId: 1,//仓库idQueryWrapper<PurchaseDetailEntity> queryWrapper = new QueryWrapper<>();String key = (String) params.get("key");if(!StringUtils.isEmpty(key)){queryWrapper.and((wrapper)->{wrapper.eq("purchase_id",key).or().eq("sku_id",key);});}String status = (String) params.get("status");if(!StringUtils.isEmpty(status)){queryWrapper.eq("status", status);}String wareId = (String) params.get("wareId");if(!StringUtils.isEmpty(wareId)){queryWrapper.eq("ware_id",wareId);}IPage<PurchaseDetailEntity> page = this.page(new Query<PurchaseDetailEntity>().getPage(params),queryWrapper);return new PageUtils(page);
}

③测试

10.4 采购单维护栏

主要就是两个栏目 采购单和采购需求

1、查询采购单(自己添加)

连接:ware/purchase/list

①gulimall-ware : PurchaseController

@RequestMapping("/list")//@RequiresPermissions("ware:purchase:list")public R list(@RequestParam Map<String, Object> params){PageUtils page = purchaseService.queryPage(params);return R.ok().put("page", page);}

②gulimall-ware : PurchaseServiceImpl

@Overridepublic PageUtils queryPage(Map<String, Object> params) {QueryWrapper<PurchaseEntity> queryWrapper = new QueryWrapper<>();String key = (String) params.get("key");if(!StringUtils.isEmpty(key)){queryWrapper.and((wrapper)->{wrapper.eq("id",key).or().like("assignee_name",key);});}String status = (String) params.get("status");if(!StringUtils.isEmpty(status)){queryWrapper.eq("status", status);}IPage<PurchaseEntity> page = this.page(new Query<PurchaseEntity>().getPage(params),queryWrapper);return new PageUtils(page);}

③前端添加状态status,在purchase.vue中添加**status: this.dataForm.status,**因为原先没有这个语句,所以后端写了判断status的语句查询也不行,所以加上前端这句,params就会携带数据到后端处理,当再次返回页面就能模糊查询status相关的。

// 获取数据列表
params: this.$http.adornParams({page: this.pageIndex,limit: this.pageSize,status: this.dataForm.status,key: this.dataForm.key})

④测试

2、查询未领取的 采购单(分配采购单)wms_purchase

:https://easydoc.xyz/s/78237135/ZUqEdvA4/hI12DNrH

①gulimall-ware : PurchaseController

@RequestMapping("/unreceive/list")//查询未领取采购的列表
public R unreceivelist(@RequestParam Map<String, Object> params){PageUtils page = purchaseService.queryPageUnreceivedPurchase(params);return R.ok().put("page", page);
}

②gulimall-ware : PurchaseServiceImpl

@Override
public PageUtils queryPageUnreceivedPurchase(Map<String, Object> params) {IPage<PurchaseEntity> page = this.page(new Query<PurchaseEntity>().getPage(params),new QueryWrapper<PurchaseEntity>()//新建和已分配状态的采购需求.eq("status",0).or().eq("status",1));return new PageUtils(page);
}

③采购单(新增和分配)测试

首选新增采购单,接着添加一个管理员,其次点击分配采购人员

3、合并 采购需求 到采购单

:https://easydoc.xyz/s/78237135/ZUqEdvA4/cUlv9QvK

将下面的两个采购需求合并为一个采购单:


①gulimall-ware : PurchaseController

@PostMapping("/merge")
public R merge(@RequestBody MergeVo mergeVo){purchaseService.mergePurchase(mergeVo);return R.ok();
}

②gulimall-ware : PurchaseServiceImpl

@Transactional
@Override
public void mergePurchase(MergeVo mergeVo) {Long purchaseId = mergeVo.getPurchaseId();//如果没有默认的采购单(没有选择采购单),还需要新建采购单if(purchaseId==null){PurchaseEntity purchaseEntity = new PurchaseEntity();//采购单的状态为新建状态purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.CREATED.getCode());purchaseEntity.setCreateTime(new Date());purchaseEntity.setUpdateTime(new Date());this.save(purchaseEntity);//得到采购单idpurchaseId = purchaseEntity.getId();}else{//如果选择采购单,则执行下面步骤List<Long> items = mergeVo.getItems();Long finalPurchaseId = purchaseId;List<PurchaseDetailEntity> collect = items.stream().map((item) -> {//新建采购需求实体类PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();purchaseDetailEntity.setId(item);purchaseDetailEntity.setPurchaseId(finalPurchaseId);purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode());return purchaseDetailEntity;}).collect(Collectors.toList());purchaseDetailService.updateBatchById(collect);//在合并完采购单后,希望新的采购单时间显示是正确的PurchaseEntity purchaseEntity = new PurchaseEntity();purchaseEntity.setCreateTime(new Date());purchaseEntity.setUpdateTime(new Date());purchaseEntity.setId(purchaseId);this.updateById(purchaseEntity);}
}

③测试结果:

10.5 领取采购单

  • 当领取采购单之后,采购需求的采购状态应该变为正在采购
  • 采购员领取采购单之后,采购单状态应该变为已领取,并且采购单不能再分配其他人采购:

领取采购单前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/vXMBBgw1

①gulimall-ware : PurchaseController

/***领取采购单* */@PostMapping("/received")public R received(@RequestBody List<Long> ids){purchaseService.received(ids);return R.ok();}

②gulimall-ware : PurchaseServiceImpl

/***领取采购单* @param ids 采购单id* */@Overridepublic void received(List<Long> ids) {//1、确认当前采购单是新建或者已分配状态List<PurchaseEntity> collect = ids.stream().map(id -> {PurchaseEntity byId = this.getById(id);return byId;}).filter(item -> {//过滤:当采购单的状态是新建和已分配时,就返回trueif (item.getStatus() == WareConstant.PurchaseStatusEnum.CREATED.getCode() ||item.getStatus() == WareConstant.PurchaseStatusEnum.ASSIGNED.getCode()) {return true;}return false;}).map(item->{//改变采购单的状态为 已领取item.setStatus(WareConstant.PurchaseStatusEnum.RECEIVE.getCode());item.setUpdateTime(new Date());return item;}).collect(Collectors.toList());//2、更新采购单this.updateBatchById(collect);//3、改变采购项(采购需求)的状态collect.forEach((item)->{//entities:采购需求的集合List<PurchaseDetailEntity> entities = purchaseDetailService.listDetailByPurchaseId(item.getId());List<PurchaseDetailEntity> detailEntities = entities.stream().map(entity -> {PurchaseDetailEntity entity1 = new PurchaseDetailEntity();entity1.setId(entity.getId());//将采购状态改为正在采购entity1.setStatus(WareConstant.PurchaseDetailStatusEnum.BUYING.getCode());return entity1;}).collect(Collectors.toList());//更新采购需求purchaseDetailService.updateBatchById(detailEntities);});}

上面调用的方法PurchaseDetailServiceImpl中

@Override
public List<PurchaseDetailEntity> listDetailByPurchaseId(Long id) {List<PurchaseDetailEntity> purchaseId= this.list(new QueryWrapper<PurchaseDetailEntity>().eq("purchase_id", id));return purchaseId;
}

③测试:模仿采购人员的第三方APP

采购需求--------正在采购

采购单-------已领取

此时思考一个问题:当采购单的状态不再是新建和已分配时,还能再合并 采购需求到采购单吗

修改原先的mergePurchase方法,进行状态判断

//TODO 确认采购单状态是0,1才可以合并PurchaseEntity purchaseEntity1 = purchaseDao.selectById(purchaseId);if (purchaseEntity1.getStatus()==WareConstant.PurchaseStatusEnum.CREATED.getCode()||purchaseEntity1.getStatus()==WareConstant.PurchaseStatusEnum.ASSIGNED.getCode()) {List<Long> items = mergeVo.getItems();Long finalPurchaseId = purchaseId;List<PurchaseDetailEntity> collect = items.stream().map((item) -> {//新建采购需求实体类PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();purchaseDetailEntity.setId(item);purchaseDetailEntity.setPurchaseId(finalPurchaseId);purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.ASSIGNED.getCode());return purchaseDetailEntity;}).collect(Collectors.toList());purchaseDetailService.updateBatchById(collect);

10.6 完成采购

前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/cTQHGXbK

在gulimall-ware里创建vo并添加2个文件

PurchaseDoneVo、PurchaseItemDoneVo

①gulimall-ware:PurchaseController

/*** 完成采购* */@PostMapping("/done")public R finish(@RequestBody PurchaseDoneVo purchaseDoneVo){purchaseService.done(purchaseDoneVo);return R.ok();}

②gulimall-ware:PurchaseServiceImpl

@Transactional@Overridepublic void done(PurchaseDoneVo purchaseDoneVo) {//1、改变采购单每一个采购项(采购需求)的状态Boolean flag = true;List<PurchaseItemDoneVo> items = purchaseDoneVo.getItems();List<PurchaseDetailEntity> list = new ArrayList<>();for(PurchaseItemDoneVo item:items){PurchaseDetailEntity purchaseDetailEntity = new PurchaseDetailEntity();if(item.getStatus()==WareConstant.PurchaseDetailStatusEnum.HASERROR.getCode()){flag=false;purchaseDetailEntity.setStatus(item.getStatus());}else{purchaseDetailEntity.setStatus(WareConstant.PurchaseDetailStatusEnum.FINISH.getCode());//将成功采购的进行入库PurchaseDetailEntity entity = purchaseDetailService.getById(item.getItemId());//这个方法是入库wms_ware_sku,进入WareSkuServiceImplwareSkuService.addStock(entity.getSkuId(),entity.getWareId(),entity.getSkuNum());}purchaseDetailEntity.setId(item.getItemId());list.add(purchaseDetailEntity);}//批量更新采购项的状态purchaseDetailService.updateBatchById(list);//2、改变采购单状态PurchaseEntity purchaseEntity = new PurchaseEntity();purchaseEntity.setId(purchaseDoneVo.getId());if(flag==true){purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.FINISH.getCode());}else{purchaseEntity.setStatus(WareConstant.PurchaseStatusEnum.HASERROR.getCode());}purchaseEntity.setUpdateTime(new Date());this.updateById(purchaseEntity);}

③gulimall–ware:WareSkuServiceImpl

@Overridepublic void addStock(Long skuId, Long wareId, Integer skuNum) {//1、判断如果还没有这个库存记录新增List<WareSkuEntity> entities = wareSkuDao.selectList(new QueryWrapper<WareSkuEntity>().eq("sku_id", skuId).eq("ware_id", wareId));if(entities == null || entities.size() == 0){WareSkuEntity skuEntity = new WareSkuEntity();skuEntity.setSkuId(skuId);skuEntity.setStock(skuNum);skuEntity.setWareId(wareId);skuEntity.setStockLocked(0);//TODO 远程查询sku的名字,如果失败,整个事务无需回滚//1、自己catch异常//TODO 还可以用什么办法让异常出现以后不回滚?高级try {//远程调用gulimall-productR info = productFeignService.info(skuId);Map<String,Object> data = (Map<String, Object>) info.get("skuInfo");if(info.getCode() == 0){skuEntity.setSkuName((String) data.get("skuName"));}}catch (Exception e){}wareSkuDao.insert(skuEntity);}else{//进入数据库wareSkuDao.addStock(skuId,wareId,skuNum);}}

④WareSkuDao

@Mapper
public interface WareSkuDao extends BaseMapper<WareSkuEntity> {void addStock(@Param("skuId") Long skuId, @Param("wareId") Long wareId, @Param("skuNum") Integer skuNum);
}

⑤mapper:WareSkuDao

<update id="addStock">UPDATE `wms_ware_sku` SET stock=stock+#{skuNum} WHERE sku_id=#{skuId} AND ware_id=#{wareId}</update>

⑥远程调用gulimall-product

Feign接口

@FeignClient("gulimall-product")
public interface ProductFeignService {/***      /product/skuinfo/info/{skuId}***   1)、让所有请求过网关;*          1、@FeignClient("gulimall-gateway"):给gulimall-gateway所在的机器发请求*          2、/api/product/skuinfo/info/{skuId}*   2)、直接让后台指定服务处理*          1、@FeignClient("gulimall-product")*          2、/product/skuinfo/info/{skuId}** @return*/@RequestMapping("/product/skuinfo/info/{skuId}")public R info(@PathVariable("skuId") Long skuId);
}

ware主启动类添加**@EnableFeignClients注解**

⑦测试



商品服务 - Spu管理

11.1 获取spu规格


前端接口:https://easydoc.xyz/s/78237135/ZUqEdvA4/GhhJhkg7

①gulimall-product:AttrController

/*** 获取spu规格* */@GetMapping("/base/listforspu/{spuId}")public R baseAttrlistforspu(@PathVariable("spuId") Long spuId){List<ProductAttrValueEntity> entities = productAttrValueService.baseAttrlistforspu(spuId);return R.ok().put("data",entities);}

②gulimall-product:ProductAttrValueServiceImpl

@Override
public List<ProductAttrValueEntity> baseAttrlistforspu(Long spuId) {List<ProductAttrValueEntity> entities= this.baseMapper.selectList(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id", spuId));return entities;
}

③测试

发现报400的错误

原因是sys_menu表少了一行数据

解决办法:数据库中gulimall-admin中的sys_menu表中添加一行数据:


重启前端项目和后端项目后,再次点击spu管理中的规格回显:

11.2 修改商品规格

前端:https://easydoc.xyz/s/78237135/ZUqEdvA4/GhnJ0L85

①gulimall-product:AttrController

@PostMapping("/update/{spuId}")
public R updateSpuAttr(@PathVariable("spuId") Long spuId,@RequestBody List<ProductAttrValueEntity> entities){productAttrValueService.updateSpuAttr(spuId,entities);return R.ok();
}

②gulimall-product:ProductAttrValueServiceImpl

@Transactional
@Override
public void updateSpuAttr(Long spuId, List<ProductAttrValueEntity> entities) {//1、删除这个spuId之前对应的所有属性this.baseMapper.delete(new QueryWrapper<ProductAttrValueEntity>().eq("spu_id",spuId));List<ProductAttrValueEntity> collect = entities.stream().map((item) -> {item.setSpuId(spuId);return item;}).collect(Collectors.toList());this.saveBatch(collect);
}

谷粒商城基础篇------仓储服务(gulimall-ware) - 仓库管理相关推荐

  1. 谷粒商城基础篇(保姆级总结)

    谷粒商城基础篇 文章目录 谷粒商城基础篇 项目相关基础 知识介绍 微服务架构图和项目描述 **微服务划分图** Vrgrant systemctl命令 配置环境 Docker自启动命令 下载mysql ...

  2. 【谷粒商城基础篇】仓储服务:仓库维护

    谷粒商城笔记合集 分布式基础篇 分布式高级篇 高可用集群篇 ===简介&环境搭建=== 项目简介与分布式概念(第一.二章) 基础环境搭建(第三章) ===整合SpringCloud=== 整合 ...

  3. 【谷粒商城基础篇】基础环境搭建

    谷粒商城笔记合集 分布式基础篇 分布式高级篇 高可用集群篇 ===简介&环境搭建=== 项目简介与分布式概念(第一.二章) 基础环境搭建(第三章) ===整合SpringCloud=== 整合 ...

  4. 谷粒商城基础篇——Day01

    01.分布式基础&项目环境搭建 一.项目简介 1. 项目背景 1.1 电商模式 市面上有 5 种常见的电商模式 B2B.B2C.C2B.C2C.O2O 1) B2B 模式 B2B(Busine ...

  5. 谷粒商城-基础篇(详细流程梳理+代码)

    文章目录 前言 一.项目环境搭建 1.1.安装virtualbox以及vagrant 1.2.Docker安装MySQL与Redis 1.3.前后端开发工具统一配置 1.4.Git工具安装与配置 1. ...

  6. 《谷粒商城基础篇》分布式基础环境搭建

    前沿:思考一个问题,为啥要做笔记? 为了知识更有条理,为了自己学过之后下次遇到立刻可以想起来,即使想不起,也可以通过自己的笔记快速定位~ 毕竟互联网的知识迭代速度非常之快 笔记更是知识输入的一条路径, ...

  7. 谷粒商城-基础篇-环境搭建(P1-P44)

    文章目录 一.项目简介 二.分布式基础概念 1.微服务 2.集群&分布式&节点 3.远程调用 4.负载均衡 5.服务注册/发现&注册中心 6.配置中心 7.服务熔断&服 ...

  8. 【笔记/后端】谷粒商城基础篇

    目录 一.环境配置 1 Docker 1.1 Docker是什么? 1.2 安装&启动 1.2.1 阿里云镜像加速 1.3 安装MySQL 1.4 安装Redis 2 开发环境 2.1 Mav ...

  9. 谷粒商城基础篇-1.分布式基础概念架构图与功能模块图

    一.分布式基础概念 1.微服务: 把一个单独的应用程序开发我i一套小服务,每个小服务运行在自己的进程中,并使用轻量级通信,如http API.这些服务围绕业务能力搭建,并通过完全自动化部署机制独立部署 ...

最新文章

  1. VS Code 安装插件、自定义模板、自定义配置参数、自定义主题、配置参数说明、常用的扩展插件
  2. 【怎样写代码】确保对象的唯一性 -- 单例模式(六):扩展案例
  3. XHProf安装使用笔记
  4. 应用Mongoose开发MongoDB(2)模型(models)
  5. 何时该用无服务器,何时该用Kubernetes?
  6. JSON.NET 简单的使用
  7. django+nginx+uwsgi项目部署文档整理
  8. 智能运维 devops_Coffee Shop DevOps:如何使用反馈循环变得更智能
  9. 电脑端一些快捷开源创建平台
  10. flask post json_使用Flask构建web项目的代码架构以及技术栈模板(一)
  11. 敏捷开发中XP与SCRUM的区别
  12. IT书籍汇总下载(python_c++_java_android_网络安全)等-持续更新
  13. malloc(): corrupted top size
  14. Linux系统在Xshell6布置定时任务
  15. 协同办公市场暴增背后:融云通信能力是需求重点
  16. Python爬虫:js的btoa和atob和pythonBase64编码解码比对分析
  17. Matlab中Savitzky-Golay filtering(最小二乘平滑滤波)函数sgolayfilt的使用方法
  18. MT7921:WIFI、AP、BT基础知识
  19. 支持向量机——线性可分支持向量机
  20. 【Android】Chromium架构简介

热门文章

  1. Cadence Allegro(4):M3铜柱定位孔,并添加至工程
  2. 计算机专业英语缩略词考试,【优质】计算机专业英语缩略词
  3. 什么是SQL执行计划
  4. 手机系统计算机怎么解决办法,手机与电脑怎么连接【解决教程】
  5. CentOS 7 安装 Oracle 11.2.0.4
  6. 回顾阿里巴巴Java开发手册中分层领域模型规约之DO,DTO,BO,AO,VO,POJO
  7. 根据日期判断星期几(使用基姆拉尔森计算公式)
  8. javascript实现输出打印九九乘法表、水仙花数、
  9. 【技术认证介绍】华为认证介绍
  10. 视效剧情口碑双爆棚!Netflix 现象级剧集《怪奇物语》第四季神级视效专访大揭秘!