乐优商城(填坑)——秒杀商品添加

一、需求

后台商品管理中,将商品添加到可秒杀商品列表

选中商品将其设置为可秒杀。

选择具体的参与秒杀的商品规格,然后设置相关参数,点击保存即可。

二、后端接口修改

原来的添加秒杀商品接口在leyou-secskill微服务中,现在将其移动到leyou-item中,代码如下:

2.1 Controller

/**
     * 添加秒杀商品
     * @param seckillParameters
     * @return
     * @throws ParseException
     */
    @PostMapping("/seckill/add")
    public ResponseEntity<Boolean> addSeckillGoods(@RequestBody List<SeckillParameter> seckillParameters) throws ParseException {
        if (seckillParameters != null && seckillParameters.size() > 0){
            for (SeckillParameter seckillParameter : seckillParameters){
                this.goodsService.addSeckillGoods(seckillParameter);
            }
        }else {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
        }
        return ResponseEntity.ok().build();
    }

2.2 Service

/**
     * 添加秒杀商品
     * @param seckillParameter
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void addSeckillGoods(SeckillParameter seckillParameter) throws ParseException {

        SimpleDateFormat sdf =  new SimpleDateFormat( "yyyy-MM-dd HH:mm" );
        //1.根据spu_id查询商品
        Long id = seckillParameter.getId();
        Sku sku = this.querySkuById(id);
        //2.插入到秒杀商品表中
        SeckillGoods seckillGoods = new SeckillGoods();
        seckillGoods.setEnable(true);
        seckillGoods.setStartTime(sdf.parse(seckillParameter.getStartTime().trim()));
        seckillGoods.setEndTime(sdf.parse(seckillParameter.getEndTime().trim()));
        seckillGoods.setImage(sku.getImages());
        seckillGoods.setSkuId(sku.getId());
        seckillGoods.setStock(seckillParameter.getCount());
        seckillGoods.setTitle(sku.getTitle());
        seckillGoods.setSeckillPrice(sku.getPrice()*seckillParameter.getDiscount());
        this.seckillMapper.insert(seckillGoods);
        //3.更新对应的库存信息,tb_stock
        Stock stock = stockMapper.selectByPrimaryKey(sku.getId());
        System.out.println(stock);
        if (stock != null) {
            stock.setSeckillStock(stock.getSeckillStock() != null ? stock.getSeckillStock() + seckillParameter.getCount() : seckillParameter.getCount());
            stock.setSeckillTotal(stock.getSeckillTotal() != null ? stock.getSeckillTotal() + seckillParameter.getCount() : seckillParameter.getCount());
            stock.setStock(stock.getStock() - seckillParameter.getCount());
            this.stockMapper.updateByPrimaryKeySelective(stock);
        }else {
            LOGGER.info("更新库存失败!");
        }

        //4.更新redis中的秒杀库存
        updateSeckillStock();
    }



    /**
     * 更新秒杀商品数量
     * @throws Exception
     */
    public void updateSeckillStock(){
        //1.查询可以秒杀的商品
        List<SeckillGoods> seckillGoods = this.querySeckillGoods();
        if (seckillGoods == null || seckillGoods.size() == 0){
            return;
        }
        BoundHashOperations<String,Object,Object> hashOperations = this.stringRedisTemplate.boundHashOps(KEY_PREFIX);
        if (hashOperations.hasKey(KEY_PREFIX)){
            hashOperations.delete(KEY_PREFIX);
        }
        seckillGoods.forEach(goods -> hashOperations.put(goods.getSkuId().toString(),goods.getStock().toString()));
    }

service的具体实现和前边没有什么大的改变,就是多了一步更新redis中秒杀库存的操作,原来是放在leyou-secskill中Controller初始化时进行的。上面代码中所包含的辅助类直接copy到leyou-item中就可以了。

三、前端页面

新增一个对话框,用来显示可秒杀的商品:

点击保存的时候将数据进行封装,然后发到后台即可。

dataTable中新增秒杀按钮:

点击事件:(主要作用就是数据传递)

对话框代码:

<template>
  <v-form  ref="SeckillForm">
      <v-container grid-list-md>
        <v-layout column wrap>
          <v-flex xs12 lg6>
            <v-layout row>
              <v-text-field
                readonly
                label="商品编号"
                prepend-icon="label"
                style="width: 50px"
                v-model="goods_message.goodsId"
              ></v-text-field>
              <v-text-field
                readonly
                label="商品名称"
                prepend-icon="label"
                style="width: 100px"
                v-model="goods_message.goodsTitle"
              ></v-text-field>
              <v-text-field
                readonly
                label="商品分类"
                prepend-icon="label"
                style="width: 100px"
                v-model="goods_message.goodsCname"
              ></v-text-field>
              <v-text-field
                readonly
                label="品牌"
                prepend-icon="label"
                style="width: 100px"
                v-model="goods_message.goodsBname"
              ></v-text-field>
            </v-layout>
            <v-layout row>
              <v-layout row>
                <v-menu
                  ref="menu1"
                  :close-on-content-click="false"
                  v-model="menu1"
                  :nudge-right="40"
                  lazy
                  transition="scale-transition"
                  offset-y
                  full-width
                  max-width="290px"
                  min-width="290px"
                >
                  <v-text-field
                    slot="activator"
                    v-model="dateFormatted"
                    label="开始日期"
                    prepend-icon="event"
                    persistent-hint
                    readonly
                    @blur="date = parseDate(dateFormatted)"
                  ></v-text-field>
                  <v-date-picker locale="zh-cn" v-model="date" no-title @input="menu1 = false"></v-date-picker>
                </v-menu>
                <v-dialog
                  ref="dialog1"
                  v-model="modal1"
                  :return-value.sync="time1"
                  persistent
                  lazy
                  full-width
                  width="290px"
                >
                  <v-text-field
                    slot="activator"
                    v-model="time1"
                    label="开始时间"
                    prepend-icon="access_time"
                    readonly
                  ></v-text-field>
                  <v-time-picker
                    v-if="modal1"
                    v-model="time1"
                    full-width
                  >
                    <v-spacer></v-spacer>
                    <v-btn flat color="primary" @click="modal1 = false">Cancel</v-btn>
                    <v-btn flat color="primary" @click="$refs.dialog1.save(time1)">OK</v-btn>
                  </v-time-picker>
                </v-dialog>
              </v-layout>
              <v-layout row>
                <v-menu
                  :close-on-content-click="false"
                  v-model="menu2"
                  :nudge-right="40"
                  lazy
                  transition="scale-transition"
                  offset-y
                  full-width
                  max-width="390px"
                  min-width="390px"
                >
                  <v-text-field
                    slot="activator"
                    v-model="dateFormatted2"
                    label="结束日期"
                    prepend-icon="event"
                    persistent-hint
                    readonly
                    @blur="date2 = parseDate(dateFormatted2)"
                  ></v-text-field>
                  <v-date-picker locale="zh-cn" v-model="date2" no-title @input="menu2 = false"></v-date-picker>
                </v-menu>
                <v-dialog
                  ref="dialog2"
                  v-model="modal2"
                  :return-value.sync="time2"
                  persistent
                  lazy
                  full-width
                  width="290px"
                >
                  <v-text-field
                    slot="activator"
                    v-model="time2"
                    label="结束时间"
                    prepend-icon="access_time"
                    readonly
                  ></v-text-field>
                  <v-time-picker
                    v-if="modal2"
                    v-model="time2"
                    full-width
                  >
                    <v-spacer></v-spacer>
                    <v-btn flat color="primary" @click="modal2 = false">Cancel</v-btn>
                    <v-btn flat color="primary" @click="$refs.dialog2.save(time2)">OK</v-btn>
                  </v-time-picker>
                </v-dialog>
              </v-layout>
            </v-layout>
          </v-flex>
          <v-flex xs12 lg6>
            <v-data-table
              :headers="headers"
              :items="sku"
              class="elevation-1"
              hide-actions
            >
              <template v-if="props.item.enable === false" slot="items" slot-scope="props">
                <td class="text-xs-center" style="width: 150px">{{ props.item.spec }}</td>
                <td class="text-xs-center" >{{ props.item.price }}</td>
                <td class="text-xs-center">{{ props.item.stock }}</td>
                <td class="text-xs-center">
                  <v-select
                  :items="items"
                  item-text="dis"
                  item-value="value"
                  v-model="props.item.discount"
                ></v-select></td>
                <td class="text-xs-center">
                  <v-text-field
                    label="数量"
                    v-model="props.item.seckill_count"
                  ></v-text-field>
                </td>
                <td class="text-xs-center">
                  <v-checkbox color="primary"  v-model="props.item.enable"/>
                </td>
              </template>
            </v-data-table>
          </v-flex>
        </v-layout>
      </v-container>
    <v-layout class="pt-3" >
      <v-spacer></v-spacer>
      <v-btn color="blue darken-1" flat @click="clear">清空</v-btn>
      <v-btn color="blue darken-1" flat @click="submit">保存</v-btn>
    </v-layout>
  </v-form>
</template>

<script>
  export default {
    name: "MySeckillForm",
    data: vm => ({
      headers: [
        { text: '规格属性', value: 'spec', align:"center", sortable:false},
        { text: '价格', value: 'price' , align:"center", sortable:false},
        { text: '现有库存', value: 'stock' , align:"center", sortable:false},
        { text: '折扣', align:"center", sortable:false},
        { text: '秒杀数量' , align:"center", sortable:false},
        { text: '是否秒杀' , align:"center", sortable:false}
      ],
      goods_count:'',
      date: new Date().toISOString().substr(0, 10),
      date2: new Date().toISOString().substr(0, 10),
      dateFormatted: vm.formatDate(new Date().toISOString().substr(0, 10)),
      dateFormatted2: vm.formatDate(new Date().toISOString().substr(0, 10)),
      menu1: false,
      menu2: false,
      time1: null,
      modal1: false,
      time2: null,
      modal2: false,
      items: [
        {dis:'一折',value:0.1},
        {dis:'二折',value:0.2},
        {dis:'三折',value:0.3},
        {dis:'四折',value:0.4},
        {dis:'五折',value:0.5},
        {dis:'六折',value:0.6},
        {dis:'七折',value:0.7},
        {dis:'八折',value:0.8},
      ],
      goods_message:{}, //秒杀商品信息
      skus:[],
      sku_temp:[],
    }),
    props:{
      seckill_goods_message:{type:Object},
    },
    computed:{
      sku:{
        // getter
        get: function () {
          let temp = [];
          this.skus.forEach(sku => {
            const ownSpec = JSON.parse(sku.ownSpec);
            let str = "";
            for (let key in ownSpec){
              str += (" " + ownSpec[key])
            }
            temp.push({
              id:sku.id,
              spec:str,
              price:this.$format(sku.price),
              stock:sku.stock,
              enable:false,
            })
          });
          return temp;
        },
        // setter
        set: function (newValue) {

        }
      }
    },
    watch: {
      seckill_goods_message:{
        deep:true,
        handler(val){
          console.log(val);
          if(val){
            this.goods_message = Object.deepCopy(val);
            this.loadData(this.goods_message.goodsId);
          }else{
            this.clear();
          }
        }
      },
      date (val) {
        this.dateFormatted = this.formatDate(this.date)
      },
      date2 (val) {
        this.dateFormatted2 = this.formatDate(this.date2)
        if (this.date2 < this.date){
          this.$message.confirm("结束日期必须大于开始日期!").then(() => {
            this.date2 = null;
          }).catch(() => {
            this.date2 = null;
          });
        }
      },
    },
    created(){
      let temp = [];
      this.skus.forEach(sku => {
        const ownSpec = JSON.parse(sku.ownSpec);
        let str = "";
        for (let key in ownSpec){
          str += (" " + ownSpec[key])
        }
        temp.push({
          id:sku.id,
          spec:str,
          price:this.$format(sku.price),
          stock:sku.stock,
          enable:false,
        })
      });
      this.sku_temp = temp;
      this.clear();
    },
    methods: {
      loadData(id){
        //查询spu下的所有sku
        this.$http.get("/item/goods/sku/list/" + id).then((resp) => {
          this.skus = resp.data;
        }).catch();
      },
      formatDate (date) {
        if (!date) return null;

        const [year, month, day] = date.split('-');
        return `${year}年${month}月${day}日`
      },
      parseDate (date) {
        if (!date) return null;

        const [month, day, year] = date.split('/');
        return `${year}-${month.padStart(2, '0')}-${day.padStart(2, '0')}`
      },
      clear(){
        this.goods_count = null;
        this.date = null;
        this.date2 = null;
        this.time1 = null;
        this.time2 = null;
        this.discount = null;
      },
      submit(){
        const startTime = this.date +" " + this.time1;
        const endTime = this.date2 +" " + this.time2;
        let result = [];
        this.sku.forEach(temp => {
          if (temp.enable){
            result.push({
              startTime:startTime,
              endTime:endTime,
              id:temp.id,
              count:parseInt(temp.seckill_count),
              discount:temp.discount
            });
          }
        });
        this.verify().then(() => {
          this.$http({
            url:"/item/goods/seckill/add",
            method:"post",
            headers : {
              'Content-Type' : 'application/json;charset=utf-8'
            },
            dataType:"json",
            data:JSON.stringify(result)
          }).then(() =>{
            //添加成功
            setTimeout(() =>{
              this.$emit('seckill_close');
              this.$message.success("保存成功!");
              this.clear();
            },2000);

          }).catch(() => {
            this.$message.error("添加失败!");
          });
        }).catch(() => {
          this.$router.push("/login");
        });
      }
    }
  }
</script>

<style scoped>

</style>

主页面中调用:

 

 

 

完整代码:https://github.com/lyj8330328

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
自己做的乐优商城的XMIND文件,学习分享下。乐优商城 搭建父工程 pom.xml 添加依赖 springCloud mybatis启动器 通用Mapper启动器 mysql驱动 分页助手启动器 FastDFS客户端 其他配置 构建设置 环境设置 EurekaServer注册中心 添加的依赖 启动类 application.yml 创建Zuul网关 依赖 启动类 application.yml 创建商品微服务 ly-item-interface:主要是对外暴露的API接口及相关实体类 ly-item-service:所有业务逻辑及内部使用接口 创建父工程ly-item ly-item-interface ly-item-service 依赖 启动器 application.yml 添加商品微服务的路由规则 通用工具模块Common utils CookieUtils IdWorker JsonUtils NumberUtils 依赖 通用异常处理 测试结构 pojo service @Service web @RestController @RequestMapping @Autowired @PostMapping 引入Common依赖 Common advice 拦截异常、 CommonExceptionHandler ResponseEntity @ControllerAdvice @ExceptionHandler enums 异常的枚举 、ExceptionEnum exception 自定义异常、LyException 接口RuntimeException @Getter @NoArgsConstructor @AllArgsConstructor vo 异常结果处理对象、ExceptionResult @Data 构造方法ExceptionResult ly-item-service CategoryQuery 分类查询 实体类 @Table(name="tb_category") 声明此对象映射到数据库的数据表,通过它可以为实体指定表(talbe) @Data 注解在类上, 为类提供读写属性, 此外还提供了 equals()、hashCode()、toString() 方法 @Id & @GeneratedValue(strategy= GenerationType.IDENTITY) 自动增长,适用于支持自增字段的数据库 mapper Mapper IdListMapper 根据id操作数据库 @RequestMapping("category") Controller @RestController @Controller 处理HTTP请求 @ResponseBody 返回 json 数据 @GetMapping("list") ResponseEntity @ResponseBody可以直接返回Json结果 不仅可以返回json结果,还可以定义返回的HttpHeaders和HttpStatus service @Service 自动注册到Spring容器,不需要再在applicationContext.xml文件定义bean了 @Autowired 它可以对类成员变量、方法及构造函数进行标注,完成自动装配的工作。 select select * from category c where c.pid = #{pid} CollectionUtils.isnotblank 判断集合是否为空 测试 可以利用url直接查询数据库,能否访问得到数据 报错 启动类 没有扫描到 @MapperScan("com.leyou.item.mapper") ,目录结构关系 访问网页报错 CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. 跨域问题 浏览器对于ajax请求的一种安全限制:一个页面发起的ajax请求,只能是于当前页同域名的路径,这能有效的阻止跨站攻击。因此:跨域问题 是针对ajax的一种限制。 解决跨域问题的方案 CORS 规范化的跨域请求解决方案,安全可靠 什么是cors 它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。 原理 简单请求 当浏览器发现发现的ajax请求是简单请求时,会在请求头中携带一个字段:Origin 如果服务器允许跨域,需要在返回的响应头中携带下面信息 Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*,代表任意 Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true 实现非常简单 gateway网关中编写一个配置类 GlobalCorsConfig 添加CORS配置信息 允许的域,不要写*,否则cookie就无法使用了 是否发送Cookie信息 允许的请求方式 允许的头信息 有效时长 添加映射路径,我们拦截一切请求 返回新的CorsFilter 提交方式 GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源 BUG 分类不能打开,当添加后却能打开。 修改一天的BUG 最后发现是实体类里属性大小写的问题引起。 注意 Bule_bird 就必须写成 BlueBird Brand 查询 实体类 PageResult 响应结果 分页结果一般至少需要两个数据 总条数 total 当前页数据 items 有些还需要总页数 总页数 totalPage Controller @RequestParam(value = "page",defaultValue = "1") Integer page GET和POST请求传的参数会自动转换赋值到@RequestParam 所注解的变量上 defaultValue 默认值 required 默认值为true , 当为false时 这个注解可以不传这个参数 null || .size()==0 ResponseEntity(HttpStatus.NOT_FOUND) 返回404没找到 ResponseEntity.ok 返回ok状态 service 开始分页 通用分页拦截器 PageHelper.startPage(page,row); 过滤 Example查询 Example example = new Example(Brand.class); mybatis的逆向工程中会生成实例及实例对应的example,example用于添加条件,相当where后面的部分 xxxExample example = new xxxExample(); Criteria criteria = new Example().createCriteria(); StringUtils.isNotBlank isNotBlank(str) 等价于 str != null && str.length > 0 && str.trim().length > 0 str.trim() 去掉字符串头尾的空格 测试 报错500 com.mysql.jdbc.exceptions.jdbc4.MySQLSyntaxErrorException: Unknown column 'idASC' in 'order clause' 错误:(desc ? "DESC" : "ASC"); 正确:(desc ? " DESC" : " ASC"); 字符串空格问题 新增 Controller (Brand brand,@RequestParam("cids") List cids) ResponseEntity 无返回值 new ResponseEntity(HttpStatus.CREATED); 201成功 service @Transactional 自动纳入 Spring 的事务管理 使用默认配置,抛出异常之后,事务会自动回滚,数据不会插入到数据库。 setId(null) insert(brand) 新增中间表 mapper @Insert (#{cid},#{bid}) @Param 表示给参数命名,名称就是括号中的内容 name 命名为aa,然后sql语句....where s_name= #{aa} 中就可以根据aa得到参数值 修改 回显 Controller @PathVariable("bid") 通过 @PathVariable 可以将 URL 中占位符参数绑定到控制器处理方法的入参中:URL 中的 {xxx} 占位符可以通过@PathVariable(“xxx“) 绑定到操作方法的入参中。 select * from tb_category where id in (select category_id from tb_category_brand where brand_id = #{bid}) 测试 报错500 空指针异常 调用Service时候 忘记@Autowired 保存 VO视图对象 @NoArgsConstructor 生成一个无参数的构造方法 @AllArgsConstructor 会生成一个包含所有变量 Controller @PutMapping 添加信息,倾向于用@PostMapping,如果是更新信息,倾向于用@PutMapping。两者差别不是很明显 return ResponseEntity.ok().build(); 无返回值 service 根据id修改 先删除后新增 删除(前端有问题,待完善) spec Group 品牌分类id查询 实体类 @Transient 指定该属性或字段不是永久的。 它用于注释实体类,映射超类或可嵌入类的属性或字段。 @Column(name = "'numeric'") 用来标识实体类中属性与数据表中字段的对应关系 name 定义了被标注字段在数据库表中所对应字段的名称; mapper service Controller 测试 报错500 实体类@table路径写错 新增 Controller @RequestBody 常用其来处理application/json类型 子主题 2 将请求体中的JSON字符串绑定到相应的bean上 修改 Controller @PutMapping service updateByPrimaryKey 删除 Controller @DeleteMapping @PathVariable Param 规格组id查询规格 url:params?gid=14 @GetMapping("params") Controller @RequestParam 新增 @PostMapping("param") @RequestBody ResponseEntity.status(HttpStatus.CREATED).build(); 修改 @RequestBody 删除 @PathVariable 分支主题 3 遇到的问题 pom.xml 文件未下载完整,删掉后重新下载 能存在重复文件,IDEA不能确定文件路径,需要搜索删掉多余的 Param 删除 小问题:数据库删除后页面没有立即显示 Brand 删除(前端有问题,待完善)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值