YYGH-15-活动模块

活动模块

项目构思

最近在浏览博客的时候发现一个比较好的文章,计划自己实现一个活动模块,这里我不只是想要实现一个秒杀活动,还有做任务得奖励,奖励就是暂时是体检门诊,后期会添加其他活动奖励。

微服务 Spring Boot 整合Redis 实现优惠卷秒杀 一人一单_Bug 终结者的博客-CSDN博客

订单部分

1.先建立一个act模块,在数据库中简历yygh_act数据库,创建表coupon_user(存放活动券),字段id,coupon_id,user_id,start_time,end_time,state(0未使用,1已使用,2已过期),create_time,update_time,is_deleted,创建表coupon_admin(管理活动券库存)id,coupon_id,stock,details,start_time,end_time,create_time,update_time,is_deleted

2.做了一个区分,活动页面和订单页面是分开的也就是说,活动体检卷都可以用

3.管理员在后台管理系统添加一个对应活动券库存的增删改查管理页面

4.task模块建立一个任务,每一个小时执行一次,检查购物卷是否过期

秒杀部分

1.这里我们用Redis做全局唯一ID,写一个全局ID生成器,这里每一个订单会生成一个ID

2.在用户端完成一个秒杀页面实现下单功能,用户下单完成后会在coupon_user生成一个state为0的记录

任务部分

1.这里客户端有一个界面,显示签到,当前账号如果签到大于3天可以获得一张优惠券,当3天的时候清零

2.创建一个表sign_task,字段有id,coupon_id,user_id,last_time,number,receive_number,receive_last_time,create_time,update_time,is_deleted

3.这里我们设定每人每个月只能领取一次奖励,同时签到必须是连续的3天

数据库

根据上文完成了数据库的设计

image-20221001131616859

image-20221001131630725

image-20221001131657598

后端

持久层

SignTask:签到

@Data
@ApiModel(description = "SignTask")
@TableName("sign_task")
public class SignTask extends BaseEntity implements Serializable {


    /**
     * 优惠券id
     */
    @ApiModelProperty(value = "购物券id")
    @TableField("coupon_id")
    private Long couponId;

    /**
     * 用户id
     */
    @ApiModelProperty(value = "用户id")
    @TableField("user_id")
    private Long userId;

    /**
     * 最后签到时间
     */
    @ApiModelProperty(value = "最后签到时间")
    @TableField("last_time")
    private Date lastTime;

    /**
     * 签到次数
     */
    @ApiModelProperty(value = "签到次数")
    @TableField("number")
    private Integer number;

    /**
     * 收到优惠券次数
     */
    @ApiModelProperty(value = "收到优惠券次数")
    @TableField("receive_number")
    private Integer receiveNumber;

    /**
     * 收到优惠券最后时间
     */
    @ApiModelProperty(value = "收到优惠券次数")
    @TableField("receive_last_time")
    private Date receiveLastTime;

}

CouponUser:购物券用户

    /**
     * 优惠券id
     */
    @ApiModelProperty(value = "购物券id")
    @TableField("coupon_id")
    private Long couponId;

    /**
     * 用户id
     */
    @ApiModelProperty(value = "用户id")
    @TableField("user_id")
    private Long userId;

    /**
     * 优惠券开始时间
     */
    @ApiModelProperty(value = "优惠券开始时间")
    @TableField("start_time")
    private Date startTime;

    /**
     * 优惠券结束时间
     */
    @ApiModelProperty(value = "优惠券结束时间")
    @TableField("end_time")
    private Date endTime;

    /**
     * 0未使用,1已使用,2已过期
     */
    @ApiModelProperty(value = "标记")
    @TableField("state")
    private Integer state;

CouponAdmin:购物券管理

@Data
@ApiModel(description = "CouponAdmin")
@TableName("coupon_admin")
public class CouponAdmin extends BaseEntity implements Serializable {


    /**
     * 购物券id
     */
    @ApiModelProperty(value = "购物券id")
    @TableField("coupon_id")
    private Long couponId;

    /**
     * 库存
     */
    @ApiModelProperty(value = "库存")
    @TableField("inventory")
    private Long inventory;

    /**
     * 详情
     */
    @ApiModelProperty(value = "详情")
    @TableField("details")
    private String details;

    /**
     * 优惠券开始时间
     */
    @ApiModelProperty(value = "优惠券开始时间")
    @TableField("start_time")
    private Date startTime;

    /**
     * 优惠券结束时间
     */
    @ApiModelProperty(value = "优惠券结束时间")
    @TableField("end_time")
    private Date endTime;

}

完成接口

完成对应三个表与mybatis的映射

image-20221001131923358

下面开始写业务代码

秒杀功能

先来完成关于优惠券管理和优惠券使用的几个功能

这里面最重要的功能就是秒杀业务,这里面我想的是每名用户只能秒杀一次。

首先通过Redis实现全局唯一ID

@Component
public class RedisIdWorker {
    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

其次通过@Transactional完成事务的控制

@Service
public class CouponUserServiceImpl extends ServiceImpl<CouponUserMapper, CouponUser> implements CouponUserService {

    @Autowired
    private CouponAdminService couponAdminService;

    @Autowired
    private RedisIdWorker redisIdWorker;

    @Override
    public Result seckillVoucher(CouponUserVo couponUserVo, HttpServletRequest request) {
        //1. 查询优惠卷
        CouponAdmin couponAdmin = couponAdminService.getById(couponUserVo.getCouponId());
        //2. 判断秒杀是否开始 开始时间大于当前时间表示未开始抢购
        if (couponAdmin.getStartTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始!");
        }
        //3. 判断秒杀是否结束
        if (couponAdmin.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀已经结束!");
        }
        //4. 判断库存是否充足
        if (couponAdmin.getInventory() < 1) {
            return Result.fail("库存不足!");
        }
        String token = request.getHeader("token");
        Long userId = JwtHelper.getUserId(token);
        //5. 查询订单
        //5.1 查询订单
        int count = query().eq("user_id", userId).eq("coupon_id", couponUserVo.getCouponId()).count();
        //5.2 判断并返回
        if (count > 0) {
            return Result.fail("用户已经购买过!");
        }

        //生成订单
        return createOrder(couponUserVo, couponAdmin, userId);
    }

    @Transactional
    public Result createOrder(CouponUserVo couponUserVo, CouponAdmin couponAdmin, Long userId) {
        //6. 扣减库存
        /**
         * .eq("voucher_id", voucherId).update().gt("stock",0)
         */
        boolean success = couponAdminService.update().setSql("inventory = inventory -1").eq("id", couponUserVo.getCouponId()).gt("inventory",0).update();
        if (!success) {
            return Result.fail("库存不足!");
        }

        //7. 创建订单
        CouponUser voucherOrder = new CouponUser();
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        voucherOrder.setUserId(userId);
        voucherOrder.setCouponId(couponAdmin.getId());
        voucherOrder.setState(0);
        BeanUtils.copyProperties(couponAdmin, voucherOrder);
        save(voucherOrder);
        //8. 返回订单id
        return Result.ok(orderId);
    }
}

在持久层使用乐观锁,防止超卖

 	@ApiModelProperty(value = "乐观锁")
    @TableField("voucher_id")
    @Version
    private Integer voucherId;

由于秒杀业务,是典型的高并发应用场景,这里考虑使用Sentinel来对这个接口进行

流量控制,熔断降级,负载保护

1.在pom.xml中加入下面依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>

2.下载控制台

https://github.com/alibaba/Sentinel/releases

3.修改act模块控制台配置

# sentinel配置,指定服务使用端口
spring.cloud.sentinel.transport.dashboard=localhost:8081

这里我们通过cmd启动起来sentinel,中间还遇到了很多小插曲

java -Dserver.port=8333 -Dcsp.sentinel.dashboard.server=localhost:8333 -Dproject.name=sentinel-dashboard  -jar sentinel-dashboard-1.8.5.jar

这是启动命令

image-20221002163934809

image-20221002171007530

spring.cloud.sentinel.transport.port= 8719
spring.cloud.sentinel.transport.dashboard= 127.0.0.1:8333

配置

image-20221002171851599

在配置文件添加以下配置:

spring.cloud.sentinel.filter.url-patterns= /**

重新启动即可在 sentinel-dashboard 中的簇点链路中展示所有 url,路由生效

image-20221002174515970

多访问几次生效之后配置流控,熔断和服务降级

流控

image-20221002174620047

熔断

image-20221002175040090

服务降级

@Override
@Transactional
@SentinelResource(
        value = "message",
        blockHandler = "blockHandler",//指定发生BlockException时进入的方法
        fallback = "fallback"//指定发生Throwable时进入的方法
)
public Result seckillVoucher(CouponUserVo couponUserVo, HttpServletRequest request) {
    //1. 查询优惠卷
    CouponAdmin couponAdmin = couponAdminService.getById(couponUserVo.getCouponId());
    //2. 判断秒杀是否开始 开始时间大于当前时间表示未开始抢购
    if (couponAdmin.getStartTime().isAfter(LocalDateTime.now())) {
        return Result.fail("秒杀尚未开始!");
    }
    //3. 判断秒杀是否结束
    if (couponAdmin.getEndTime().isBefore(LocalDateTime.now())) {
        return Result.fail("秒杀已经结束!");
    }
    //4. 判断库存是否充足
    if (couponAdmin.getInventory() < 1) {
        return Result.fail("库存不足!");
    }
    String token = request.getHeader("token");
    Long userId = JwtHelper.getUserId(token);
    //5. 查询订单
    //5.1 查询订单
    int count = query().eq("user_id", userId).eq("coupon_id", couponUserVo.getCouponId()).count();
    //5.2 判断并返回
    if (count > 0) {
        return Result.fail("用户已经购买过!");
    }

    //6. 扣减库存
    /**
     * .eq("voucher_id", voucherId).update().gt("stock",0)
     */
    boolean success = couponAdminService.update().setSql("inventory = inventory -1").eq("id", couponUserVo.getCouponId()).gt("inventory",0).update();
    if (!success) {
        return Result.fail("库存不足!");
    }

    //7. 创建订单
    CouponUser voucherOrder = new CouponUser();
    long orderId = redisIdWorker.nextId("order");
    log.info(String.valueOf(orderId));
    BeanUtils.copyProperties(couponAdmin, voucherOrder);
    voucherOrder.setId(orderId);
    voucherOrder.setUserId(userId);
    voucherOrder.setCouponId(couponAdmin.getId());
    voucherOrder.setState(0);
    int i = 0/0;
    save(voucherOrder);
    //8. 返回订单id
    return Result.ok(orderId);

}

//BlockException时进入的方法
public Result blockHandler(BlockException ex) {
    log.error("{}", ex);
    return Result.fail("接口被限流或者降级了...");
}
//Throwable时进入的方法
public Result fallback(Throwable throwable) {
    log.error("{}", throwable);
    return Result.fail("接口发生异常了...");
}

这样就完成了秒杀服务的核心功能

其他的服务都是围绕CouponAdmin,CouponUser的增删改查

@RestController
@RequestMapping("api/cat")
public class CouponUserController {

    @Autowired
    private CouponUserService couponUserService;

    @PostMapping("/seckill")
    public Result seckillVoucher(@RequestBody CouponUserVo couponUserVo, HttpServletRequest request) {
        return couponUserService.seckillVoucher(couponUserVo, request);
    }

    @PostMapping("/useCoupon")
    public Result useCoupon(HttpServletRequest request, @RequestBody CouponUserVo couponUserVo) {
        Boolean aBoolean = couponUserService.useCoupon(request, couponUserVo);
        if (aBoolean) {
            return Result.ok();
        } else {
            return Result.fail();
        }
    }

    //根据用户id展示
    @GetMapping("/getList")
    public Result getListByUser(HttpServletRequest request) {
        List<CouponUser> couponUserList = couponUserService.getListByUser(request);
        if (couponUserList.size() > 0) {
            return Result.ok(couponUserList);
        }else {
            return Result.fail("无可用优惠券");
        }
    }
}
@RestController
@RequestMapping("admin/cat")
public class CouponAdminController {

    @Autowired
    private CouponAdminService couponAdminService;

    @PostMapping("/add")
    public Result add(@RequestBody CouponAdminVo couponAdminVo) {
        return Result.ok(couponAdminService.saveCouponAdmin(couponAdminVo));
    }

    @DeleteMapping("/{couponAdminId}")
    public Result delete(@PathVariable Long couponAdminId) {
        QueryWrapper<CouponAdmin> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("id",couponAdminId);
        return Result.ok(couponAdminService.remove(queryWrapper));
    }

    @PostMapping("/update")
    public Result update(@RequestBody CouponAdminVo couponAdminVo) {;
        return Result.ok(couponAdminService.update(couponAdminVo));
    }

    @GetMapping("/list")
    public Result list(){
        return Result.ok(couponAdminService.list());
    }

}
任务功能

接口:SignIn(签到)

此时我意识道之前数据库设计的不是很合理他需要一个发布任务的数据库。我们的管理员可用创建任务。创建sign_task_admin数据库,主要字段(id,coupon_id,details,inventory,start_time,end_time),当我们创建一个任务的时候同时会在coupon_admin中创建一个数据

数据库设计好了

image-20221002200723299

建立了对于的mapper和service。

@RestController
@RequestMapping("admin/act")
public class SignTaskAdminController {

    @Autowired
    private SignTaskAdminService signTaskAdminService;

    @PostMapping("/createTask")
    public Result createTask(@RequestBody SignTaskAdminVo signTaskAdminVo){
        signTaskAdminService.createTask(signTaskAdminVo);
        return Result.ok();
    }
}
@Service
public class SignTaskAdminServiceImpl extends ServiceImpl<SignTaskAdminMapper, SignTaskAdmin> implements SignTaskAdminService {

    @Autowired
    private CouponAdminService couponAdminService;

    //创建任务
    @Override
    public Boolean createTask(SignTaskAdminVo signTaskAdminVo) {
        Long couponAdminId = couponAdminService.createTask(signTaskAdminVo);
        SignTaskAdmin signTaskAdmin = new SignTaskAdmin();
        signTaskAdmin.setCouponId(couponAdminId);
        signTaskAdmin.setIsDeleted(0);
        signTaskAdmin.setCreateTime(new Date());
        signTaskAdmin.setUpdateTime(new Date());
        BeanUtils.copyProperties(signTaskAdminVo, signTaskAdmin);
        int insert = baseMapper.insert(signTaskAdmin);
        return insert > 0;
    }
}

添加更新方法

之前写了签到任务管理

下面是任务功能的核心签到功能

签到任务请求进来之后会有几种情况

  • 第一次签到

    1.查询所有合格的签到任务

    2.取最后一个任务,让其订单库存减一

    3.创建第一个签到任务

  • 非第一次签到

    1.判断最后签到时间,如果不是上一天,那么签到次数清零,更新最后签到时间

    2.如果是最后签到时间,判断签到次数,如果大于等于3次,次数清零,完成次数加一,更新收到购物券的次数

    3.否则次数加一

@Service
public class SignTaskServiceImpl extends ServiceImpl<SignTaskMapper, SignTask> implements SignTaskService {

    @Autowired
    private SignTaskAdminService signTaskAdminService;

    @Autowired
    private CouponAdminService couponAdminService;

    @Autowired
    private CouponUserService couponUserService;

    @Override
    public Result signTask(HttpServletRequest request) {
        String token = request.getHeader("token");
        Long userId = JwtHelper.getUserId(token);
        QueryWrapper<SignTask> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("user_id", userId);
        SignTask signTask = baseMapper.selectOne(queryWrapper);
        //第一次签到
        if (ObjectUtils.isEmpty(signTask)) {
            return first(userId);
        }
        //非第一次
        else {
            //0.检查之前有没有领取过
            if (signTask.getReceiveNumber() != 0) {
                return Result.fail("已经领取过了");
            }

            //1.判断最后一次签到是不是昨天
            Date lastTime = signTask.getLastTime();
            //每月更新
            LocalDateTime lastLocalTime = lastTime.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
            int dayOfYearLast = lastLocalTime.getDayOfMonth();
            LocalDateTime now = LocalDateTime.now();
            int dayOfYearNow = now.getDayOfMonth();
            if (dayOfYearNow - dayOfYearLast == 1) {
                //是昨天
                int i = signTask.getNumber() + 1;
                if (i == 3) {
                    //发放购物券
                    signTask.setNumber(0);
                    signTask.setReceiveNumber(1);
                    signTask.setReceiveLastTime(new Date());
                    signTask.setLastTime(new Date());
                    signTask.setUpdateTime(new Date());
                    baseMapper.updateById(signTask);
                    //通知user发放购物券
                    couponUserService.saveSign(signTask);
                    return Result.ok("发放成功");
                } else {
                    //签到成功
                    signTask.setLastTime(new Date());
                    signTask.setReceiveNumber(signTask.getNumber() + 1);
                    signTask.setUpdateTime(new Date());
                    baseMapper.updateById(signTask);
                    return Result.ok("签到成功");
                }
            } else {
                //不是昨天
                signTask.setNumber(1);
                signTask.setUpdateTime(new Date());
                signTask.setLastTime(new Date());
                baseMapper.updateById(signTask);
                return Result.ok("签到成功");
            }
        }
    }

    //第一次签到
    private Result first(Long userId) {
        //1.查询所有合格的签到任务
        List<SignTaskAdmin> signTaskAdminList = signTaskAdminService.list();
        //2.获取最后一个结束时间最后的任务
        List<SignTaskAdmin> collect = signTaskAdminList.stream().filter(i -> i.getStartTime().before(new Date())).filter(i -> i.getEndTime().after(new Date()))
                .sorted((x, y) -> (int) (x.getEndTime().getTime() - y.getEndTime().getTime())).collect(Collectors.toList());
        SignTaskAdmin signTaskAdmin = collect.get(0);
        //3.更新订单,订单库存减一
        signTaskAdmin.setInventory(signTaskAdmin.getInventory() - 1);
        signTaskAdminService.updateById(signTaskAdmin);
        CouponAdmin couponAdmin = couponAdminService.getById(signTaskAdmin.getCouponId());
        couponAdmin.setInventory(couponAdmin.getInventory() - 1);
        QueryWrapper<CouponAdmin> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq("id", couponAdmin.getId());
        couponAdminService.update(couponAdmin, queryWrapper);
        //4.创建签到任务
        SignTask signTask1 = new SignTask();
        signTask1.setCouponId(signTaskAdmin.getCouponId());
        signTask1.setUserId(userId);
        signTask1.setLastTime(new Date());
        signTask1.setNumber(1);
        signTask1.setIsDeleted(0);
        signTask1.setReceiveNumber(0);
        signTask1.setCreateTime(new Date());
        signTask1.setUpdateTime(new Date());
        baseMapper.insert(signTask1);
        return Result.ok("签到成功");
    }
}

根据上面的步骤完成了代码的编写,进行测试

前端

image-20221006103842967

image-20221006103856715

image-20221006110459311

image-20221006110512594

这样我们的后台管理系统就做好了

<template>
  <div class="app-container">
    购物券设置列表

    <el-table :data="list" stripe style="width: 100%">
      <el-table-column type="index" width="50" label="序号"/>
      <el-table-column prop="id" label="任务id"/>
      <el-table-column prop="couponId" label="购物券id"/>
      <el-table-column prop="inventory" label="库存"/>
      <el-table-column prop="details" label="详情"/>
      <el-table-column type="date" prop="startTime" label="优惠券开始时间"/>
      <el-table-column type="date" prop="endTime" label="优惠券结束时间"/>

      <el-table-column label="操作" align="center">
        <template slot-scope="scope">
          <router-link :to="'/act/sign/'+scope.row.id">
            <el-button type="primary" size="mini" icon="el-icon-edit"/>
          </router-link>
        </template>
      </el-table-column>
      <el-pagination
        :current-page="page"
        :page-size="limit"
        :total="total"
        style="padding: 30px 0; text-align: center"
        layout="total, prev, pager, next, jumper"
        @current-change="getList"
      />
    </el-table>
  </div>
</template>

<script>
// 引入接口定义的js文件
import act from '@/api/act'

export default {
  data() {
    return {
      current: 1, // 当前页
      limit: 3, // 一个页显示的记录数
      serchObj: {}, // 条件封装对象
      list: [], // 每页数据集合
      total: 0,
      multipleSelection: [] // 批量选择中选择的记录列表
    }
  },
  created() {
    this.getList()
  },
  methods: {
    lockHostSet(id, status) {
      act.lockHospSet(id, status).then((response) => {
        this.getList()
      })
    },

    handleSelectionChange(selection) {
      this.multipleSelection = selection
    },
    getList(page = 1) {
      this.current = page
      act.getList()
        .then((Response) => {
          this.list = Response.data
        })
        .catch((error) => {
          console.log(error)
        })
    }
  }
}
</script>
<template>
  <div class="app-container">购物券设置添加
    <el-form label-width="120px">
      <el-form-item label="库存">
        <el-input v-model="CouponAdmin.inventory"/>
      </el-form-item>

      <el-form-item label="详情">
        <el-input v-model="CouponAdmin.details"/>
      </el-form-item>

      <el-form-item label="优惠券开始时间">
        <el-input v-model="CouponAdmin.startTime" type="date"/>
      </el-form-item>
      <el-form-item label="优惠券结束时间">
        <el-input v-model="CouponAdmin.endTime" type="date"/>
      </el-form-item>

      <el-form-item>
        <el-button type="primary" @click="saveOrUpdate">保存</el-button>
      </el-form-item>
    </el-form>

  </div>
</template>
<script>
import act from '@/api/act'

export default {
  data() {
    return {
      CouponAdmin: {}
    }
  },
  created() { // 页面渲染之前执行
    // 获取路由id值
    // 调用接口得到医院设置信息
    if (this.$route.params && this.$route.params.id) {
      const id = this.$route.params.id
      this.getHostSet(id)
    } else {
      // 表单数据清空
      this.CouponAdmin = {}
    }
  },
  methods: {
    // 根据id查询
    getHostSet(id) {
      act.getCoupon(id)
        .then(response => {
          this.CouponAdmin = response.data
        })
    },
    // 添加
    save() {
      act.createTask(this.CouponAdmin)
        .then(response => {
          // 提示
          this.$message({
            type: 'success',
            message: '添加成功!'
          })
          // 跳转列表页面,使用路由跳转方式实现
          this.$router.push({ path: '/act/coupon' })
        })
    },
    // 修改
    update() {
      act.updateTask(this.CouponAdmin)
        .then(response => {
          // 提示
          this.$message({
            type: 'success',
            message: '修改成功!'
          })
          // 跳转列表页面,使用路由跳转方式实现
          this.$router.push({ path: '/act/coupon' })
        })
    },
    saveOrUpdate() {
      // 判断添加还是修改
      if (!this.CouponAdmin.id) { // 没有id,做添加
        this.save()
      } else { // 修改
        this.update()
      }
    }
  }
}
</script>

这里用到了虚拟路由

{
  path: '/act',
  component: Layout,
  redirect: '/act',
  name: 'BasesInfo',
  meta: { title: '活动中心', icon: 'table' },
  alwaysShow: true,
  children: [
    {
      path: 'coupon',
      name: '购物券管理',
      component: () => import('@/views/act/coupon/list'),
      meta: { title: '购物券管理' }
    },
    {
      path: 'add',
      name: '购物券添加',
      component: () => import('@/views/act/coupon/add'),
      meta: { title: '购物券添加' }
    },
    {
      path: 'task',
      name: '任务管理',
      component: () => import('@/views/act/sign/list'),
      meta: { title: '任务管理' }
    },
    {
      path: 'signAdd',
      name: '签到添加',
      component: () => import('@/views/act/sign/add'),
      meta: { title: '签到添加' }
    },
    {
      path: 'userList',
      name: '活动订单列表',
      component: () => import('@/views/act/user/list'),
      meta: { title: '活动订单列表' }
    },
    {
      path: 'edit/:id',
      name: 'EduTeacherEdit',
      component: () => import('@/views/act/coupon/add'),
      meta: { title: '编辑', noCache: true },
      hidden: true
    },
    {
      path: 'sign/:id',
      name: 'EduTeacherEdit',
      component: () => import('@/views/act/sign/add'),
      meta: { title: '编辑', noCache: true },
      hidden: true
    }
  ]
},

接下来就是用户端

image-20221006111801943

image-20221006111750101

在这里找一个按钮样式

image-20221006111858964

image-20221006130352520

image-20221006130406737

页面完成

<template>
  <!-- header -->
  <div class="nav-container page-component">
    <!--左侧导航 #start -->
    <div class="nav left-nav">
      <div class="nav-item selected">
        <span class="v-link clickable dark" onclick="javascript:window.location='/act'">活动中心 </span>
      </div>
      <div class="nav-item">
        <span class="v-link selected dark" onclick="javascript:window.location='/actorder'">购物券列表 </span>
      </div>
    </div>
    <!-- 左侧导航 #end -->
    <!-- 右侧内容 #start -->
    <div class="page-container">
      <el-button type="success" @click="signTask()">签到</el-button>
      <el-table :data="list" stripe style="width: 100%">
        <el-table-column prop="id" label="购物券id"/>
        <el-table-column prop="inventory" label="库存"/>
        <el-table-column prop="details" label="详情"/>
        <el-table-column type="date" prop="startTime" label="优惠券开始时间"/>
        <el-table-column type="date" prop="endTime" label="优惠券结束时间"/>

        <el-table-column label="操作" align="center">
          <template slot-scope="scope">
            <el-button type="danger" @click="seckill(scope.row.id)">秒杀</el-button>
          </template>
        </el-table-column>
      </el-table>

    </div>
    <!-- 右侧内容 #end -->
    <!-- 登录弹出框 -->
  </div>
  <!-- footer -->
</template>

<script>
import '~/assets/css/hospital_personal.css'
import '~/assets/css/hospital.css'
import '~/assets/css/personal.css'
import actApi from '@/api/act'

const defaultForm = {
  name: '',
  certificatesType: '',
  certificatesNo: '',
  certificatesUrl: ''
}


export default {
  data() {
    return {
      certificatesTypeList: [],
      userInfo: {
        param: {}
      },
      submitBnt: '提交',
      list: [],
      couponId: ''
    }
  },
  created() {
    this.getList()

  },
  methods: {
    seckill(id) {
      actApi.seckill(id).then(response => {
        this.$message.success("签到成功")
        window.location.reload()
      }).catch(e => {
        this.submitBnt = '提交'
      })
    },
    signTask() {
      actApi.signTask().then(response => {
        this.$message.success("签到成功")
        window.location.reload()
      }).catch(e => {
        this.submitBnt = '提交'
      })
    },
    getList() {

      actApi.getList()
        .then((Response) => {
          this.list = Response.data.records
        })
        .catch((error) => {
          console.log(error)
        })
    }
  }
}
</script>
<style>
.header-wrapper .title {
  font-size: 16px;
  margin-top: 0;
}

.content-wrapper {
  margin-left: 0;
}

.patient-card .el-card__header .detail {
  font-size: 14px;
}

.page-container .title {
  letter-spacing: 1px;
  font-weight: 700;
  color: #333;
  font-size: 16px;
  margin-top: 0;
  margin-bottom: 20px;
}

.page-container .tips {
  width: 100%;
  padding-left: 0;
}

.page-container .form-wrapper {
  padding-left: 92px;
  width: 580px;
}

.form-normal {
  height: 40px;
}

.bottom-wrapper {
  width: 100%;
  padding: 0;
  margin-top: 0;
}
</style>
<template>
  <!-- header -->
  <div class="nav-container page-component">
    <!--左侧导航 #start -->
    <div class="nav left-nav">
      <div class="nav-item ">
        <span class="v-link clickable dark" onclick="javascript:window.location='/act'">活动中心 </span>
      </div>
      <div class="nav-item selected">
        <span class="v-link selected dark" onclick="javascript:window.location='/actorder'">购物券列表 </span>
      </div>
    </div>
    <!-- 左侧导航 #end -->
    <!-- 右侧内容 #start -->
    <div class="page-container">
      <el-table :data="list" stripe style="width: 100%">
        <el-table-column prop="id" label="id"/>
        <el-table-column prop="couponId" label="购物券id"/>
<!--        <el-table-column prop="userId" label="用户id"/>-->
        <el-table-column type="date" prop="startTime" label="优惠券开始时间"/>
        <el-table-column type="date" prop="endTime" label="优惠券结束时间"/>
        <el-table-column label="状态" width="80">
          <template slot-scope="scope">{{ scope.row.state === 0 ? "可用" : "不可用" }}</template>
        </el-table-column>
        <el-table-column label="操作" align="center">
          <template slot-scope="scope">
            <el-button type="success" @click="use(scope.row.id)">使用</el-button>
          </template>
        </el-table-column>
      </el-table>

    </div>
    <!-- 右侧内容 #end -->
    <!-- 登录弹出框 -->
  </div>
  <!-- footer -->
</template>

<script>
import '~/assets/css/hospital_personal.css'
import '~/assets/css/hospital.css'
import '~/assets/css/personal.css'
import actApi from '@/api/act'

const defaultForm = {
  name: '',
  certificatesType: '',
  certificatesNo: '',
  certificatesUrl: ''
}


export default {
  data() {
    return {
      certificatesTypeList: [],
      userInfo: {
        param: {}
      },
      submitBnt: '提交',
      list: [],
      couponId: ''
    }
  },
  created() {
    this.getList()

  },
  methods: {
    use(id) {
      actApi.use(id).then(response => {
        this.$message.success("签到成功")
        window.location.reload()
      }).catch(e => {
        this.submitBnt = '提交'
      })
    },
    getList() {

      actApi.getListByUser()
        .then((Response) => {
          this.list = Response.data
        })
        .catch((error) => {
          console.log(error)
        })
    }
  }
}
</script>
<style>
.header-wrapper .title {
  font-size: 16px;
  margin-top: 0;
}

.content-wrapper {
  margin-left: 0;
}

.patient-card .el-card__header .detail {
  font-size: 14px;
}

.page-container .title {
  letter-spacing: 1px;
  font-weight: 700;
  color: #333;
  font-size: 16px;
  margin-top: 0;
  margin-bottom: 20px;
}

.page-container .tips {
  width: 100%;
  padding-left: 0;
}

.page-container .form-wrapper {
  padding-left: 92px;
  width: 580px;
}

.form-normal {
  height: 40px;
}

.bottom-wrapper {
  width: 100%;
  padding: 0;
  margin-top: 0;
}
</style>

BUG

事务未生效

image-20221002173413214

image-20221002173422749

image-20221002173429502

其实库存是100发现事务效果未生效,推测应该是

@Transactional

查阅网上资料发现

同一个类中的方法直接内部调用,会导致事务失效

image-20221002174159150

代码改成这个

@Override
@Transactional
public Result seckillVoucher(CouponUserVo couponUserVo, HttpServletRequest request) {
    //1. 查询优惠卷
    CouponAdmin couponAdmin = couponAdminService.getById(couponUserVo.getCouponId());
    //2. 判断秒杀是否开始 开始时间大于当前时间表示未开始抢购
    if (couponAdmin.getStartTime().isAfter(LocalDateTime.now())) {
        return Result.fail("秒杀尚未开始!");
    }
    //3. 判断秒杀是否结束
    if (couponAdmin.getEndTime().isBefore(LocalDateTime.now())) {
        return Result.fail("秒杀已经结束!");
    }
    //4. 判断库存是否充足
    if (couponAdmin.getInventory() < 1) {
        return Result.fail("库存不足!");
    }
    String token = request.getHeader("token");
    Long userId = JwtHelper.getUserId(token);
    //5. 查询订单
    //5.1 查询订单
    int count = query().eq("user_id", userId).eq("coupon_id", couponUserVo.getCouponId()).count();
    //5.2 判断并返回
    if (count > 0) {
        return Result.fail("用户已经购买过!");
    }

    //6. 扣减库存
    /**
     * .eq("voucher_id", voucherId).update().gt("stock",0)
     */
    boolean success = couponAdminService.update().setSql("inventory = inventory -1").eq("id", couponUserVo.getCouponId()).gt("inventory",0).update();
    if (!success) {
        return Result.fail("库存不足!");
    }

    //7. 创建订单
    CouponUser voucherOrder = new CouponUser();
    long orderId = redisIdWorker.nextId("order");
    log.info(String.valueOf(orderId));
    BeanUtils.copyProperties(couponAdmin, voucherOrder);
    voucherOrder.setId(orderId);
    voucherOrder.setUserId(userId);
    voucherOrder.setCouponId(couponAdmin.getId());
    voucherOrder.setState(0);
    int i = 0/0;
    save(voucherOrder);
    //8. 返回订单id
    return Result.ok(orderId);

}

这样再次测试就生效了

image-20221002174340346

image-20221002174317666

更新签到管理时mybatis出现异常

nested exception is org.apache.ibatis.binding.BindingException: Parameter ‘MP_OPTLOCK_VERSION_ORIGINAL’ not found. Available parameters are [param1, et]

记录一下排错过程

couponAdminService.updateById(couponAdmin);

这里我采用了update方法,后面加上了一个mapper,这个bug的原因考虑到可能是Entity类继承baseEntity,id位置不是第一个

前端渲染数据将后端数据四舍五入

vue和nuxt的整合项目报错【Vue warn】: The client-side rendered virtual DOM tree is…

使用购物券业务一直有问题,发现这个问题,将购物券id设置成16位的解决

image-20221007163137730

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值