基于 Vue + Java 的刷题优惠券项目设计方案

在当今数字化学习与营销相结合的趋势下,刷题优惠券项目能够有效吸引用户参与刷题学习,提升用户的活跃度与参与度。本项目采用前后端分离架构,前端基于 Vue3 + Echarts 构建用户界面,后端运用 redis + mysql + SpringBoot + Redisson 实现业务逻辑与数据存储,重点实现日常券和秒杀券的相关流程。

一、项目概述

刷题优惠券项目旨在为刷题平台用户提供优惠激励,促进用户更多地参与刷题活动。通过发放日常优惠券和定时秒杀优惠券,增加用户的刷题动力和平台的吸引力。用户可在前端界面查看优惠券信息、领取优惠券,并在刷题消费时使用优惠券享受折扣或其他优惠。

二、前端设计与实现

(一)技术选型与环境搭建

  • 前端采用 Vue3 核心框架,借助 Vue CLI 快速搭建项目结构。安装相关依赖,如 Echarts 用于数据可视化展示(若有优惠券相关数据统计展示需求),Axios 用于与后端进行数据交互。

(二)页面布局与组件设计

  • 优惠券展示组件:设计一个组件用于展示各类优惠券信息,包括日常券和秒杀券。以列表形式呈现优惠券的名称、优惠金额或折扣、有效期、领取状态等信息。对于秒杀券,突出显示倒计时信息,以吸引用户关注。
<template>
  <div class="coupon-list">
    <div v-for="coupon in coupons" :key="coupon.id" class="coupon-item">
      <div class="coupon-header">
        <h3>{{ coupon.name }}</h3>
        <span v-if="coupon.isSeckill">{{ coupon.discount }}</span>
        <span v-else>{{ coupon.amount }} 元优惠</span>
      </div>
      <div class="coupon-body">
        <p>有效期:{{ coupon.startDate }}{{ coupon.endDate }}</p>
        <p v-if="coupon.isSeckill">秒杀倒计时:{{ coupon.countdown }}</p>
        <p v-if="coupon.isReceived">已领取</p>
        <button v-else @click="receiveCoupon(coupon)" :disabled="coupon.isDisabled">领取优惠券</button>
      </div>
    </div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  data() {
    return {
      coupons: [],
    };
  },
  mounted() {
    // 页面加载时获取优惠券列表
    this.getCoupons();
  },
  methods: {
    getCoupons() {
      axios
      .get('/api/coupons')
      .then((response) => {
          this.coupons = response.data;
          // 对于秒杀券,初始化倒计时
          this.initSeckillCountdown();
        })
      .catch((error) => {
          console.error('获取优惠券列表失败', error);
        });
    },
    receiveCoupon(coupon) {
      axios
      .post('/api/receiveCoupon', { couponId: coupon.id })
      .then((response) => {
          if (response.data.success) {
            coupon.isReceived = true;
            coupon.isDisabled = true;
            // 领取成功提示
          } else {
            // 领取失败提示
          }
        })
      .catch((error) => {
          console.error('领取优惠券失败', error);
        });
    },
    initSeckillCountdown() {
      // 遍历秒杀券,设置倒计时更新函数
      this.coupons.forEach((coupon) => {
        if (coupon.isSeckill) {
          const endTime = new Date(coupon.endDate).getTime();
          const interval = setInterval(() => {
            const now = new Date().getTime();
            const timeLeft = endTime - now;
            if (timeLeft <= 0) {
              clearInterval(interval);
              coupon.countdown = '已结束';
            } else {
              const seconds = Math.floor(timeLeft / 1000);
              const minutes = Math.floor(seconds / 60);
              const hours = Math.floor(minutes / 60);
              coupon.countdown = `${hours}:${minutes % 60}:${seconds % 60}`;
            }
          }, 1000);
        }
      });
    },
  },
};
</script>

<style scoped>
.coupon-list {
  width: 400px;
  margin: 0 auto;
}
.coupon-item {
  border: 1px solid #ddd;
  margin-bottom: 10px;
  padding: 10px;
}
.coupon-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
}
.coupon-body {
  margin-top: 10px;
}
</style>
  • 使用优惠券组件:在刷题支付或相关消费场景中,提供一个组件用于用户选择使用优惠券。该组件展示用户已领取的可用优惠券列表,用户可选择一张优惠券进行使用,并显示使用该优惠券后的应付金额等信息。
<template>
  <div class="use-coupon">
    <h3>选择优惠券</h3>
    <div v-for="coupon in availableCoupons" :key="coupon.id" class="coupon-option">
      <input type="radio" :id="coupon.id" :value="coupon" v-model="selectedCoupon">
      <label :for="coupon.id">{{ coupon.name }} - {{ coupon.amount }} 元优惠</label>
    </div>
    <p>应付金额:{{ totalAmount - (selectedCoupon? selectedCoupon.amount : 0) }}</p>
    <button @click="confirmUseCoupon">确认使用</button>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  props: {
    totalAmount: {
      type: Number,
      required: true,
    },
  },
  data() {
    return {
      availableCoupons: [],
      selectedCoupon: null,
    };
  },
  mounted() {
    // 获取可用优惠券列表
    this.getAvailableCoupons();
  },
  methods: {
    getAvailableCoupons() {
      axios
      .get('/api/availableCoupons')
      .then((response) => {
          this.availableCoupons = response.data;
        })
      .catch((error) => {
          console.error('获取可用优惠券列表失败', error);
        });
    },
    confirmUseCoupon() {
      if (this.selectedCoupon) {
        axios
        .post('/api/useCoupon', { couponId: this.selectedCoupon.id })
        .then((response) => {
            if (response.data.success) {
              // 使用优惠券成功,进行后续支付或业务逻辑处理
            } else {
              // 使用优惠券失败提示
            }
          })
        .catch((error) => {
            console.error('使用优惠券失败', error);
          });
      } else {
        // 未选择优惠券提示
      }
    },
  },
};
</script>

<style scoped>
.use-coupon {
  width: 300px;
  margin: 0 auto;
}
.coupon-option {
  margin-bottom: 10px;
}
</style>

三、后端设计与实现

(一)技术选型与架构设计

  • 后端采用 Spring Boot 框架搭建项目基础,整合 Redis 用于缓存优惠券信息、用户领取记录等数据,提高数据读取速度和系统性能。引入 Redisson 框架,借助其强大的分布式锁等功能来处理秒杀券的并发问题,确保秒杀活动的公平性与稳定性。使用 MySQL 数据库存储用户信息、优惠券信息、领取记录、使用记录等持久化数据。采用分层架构设计,包括表现层(Controller)、业务逻辑层(Service)、数据访问层(DAO)和数据持久层(数据库)。

(二)数据库设计

  • 用户表(user)
    • id:用户 ID,主键,自增长。
    • username:用户名。
    • password:密码。
CREATE TABLE user (
  id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL,
  password VARCHAR(255) NOT NULL
);
  • 优惠券表(coupon)
    • id:优惠券 ID,主键,自增长。
    • name:优惠券名称。
    • type:优惠券类型(1 - 日常券,2 - 秒杀券)。
    • amount:优惠金额(对于日常券)。
    • discount:折扣(对于秒杀券)。
    • start_date:有效期开始时间。
    • end_date:有效期结束时间。
    • total_quantity:总数量(对于秒杀券)。
    • remaining_quantity:剩余数量。
CREATE TABLE coupon (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  type INT NOT NULL,
  amount DECIMAL(10, 2),
  discount DECIMAL(3, 2),
  start_date TIMESTAMP NOT NULL,
  end_date TIMESTAMP NOT NULL,
  total_quantity INT,
  remaining_quantity INT
);
  • 优惠券领取表(coupon_receive)
    • id:领取记录 ID,主键,自增长。
    • user_id:用户 ID,外键关联 user 表。
    • coupon_id:优惠券 ID,外键关联 coupon 表。
    • receive_time:领取时间。
CREATE TABLE coupon_receive (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  coupon_id INT NOT NULL,
  receive_time TIMESTAMP NOT NULL,
  FOREIGN KEY (user_id) REFERENCES user(id),
  FOREIGN KEY (coupon_id) REFERENCES coupon(id)
);
  • 优惠券使用表(coupon_use)
    • id:使用记录 ID,主键,自增长。
    • user_id:用户 ID,外键关联 user 表。
    • coupon_id:优惠券 ID,外键关联 coupon 表。
    • use_time:使用时间。
CREATE TABLE coupon_use (
  id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  coupon_id INT NOT NULL,
  use_time TIMESTAMP NOT NULL,
  FOREIGN KEY (user_id) REFERENCES user(id),
  FOREIGN KEY (coupon_id) REFERENCES coupon(id)
);

(三)Redis 缓存策略

  • 将优惠券的基本信息缓存到 Redis 中,以提高查询速度。设置合理的缓存过期时间,例如根据优惠券的有效期动态设置,确保缓存数据的时效性。当优惠券信息在数据库中发生更新时,同步更新 Redis 缓存。对于用户领取优惠券的记录,也可以缓存到 Redis 中,方便快速判断用户是否已领取某张优惠券。
import redis.clients.jedis.Jedis;

public class RedisCacheService {
    private static final String REDIS_KEY_PREFIX = "coupon:";
    private Jedis jedis;

    public RedisCacheService() {
        // 初始化 Jedis 连接
        this.jedis = new Jedis("localhost", 6379);
    }

    public void cacheCoupon(Coupon coupon) {
        // 缓存优惠券信息,键为 coupon:couponId,值为 JSON 序列化的优惠券对象
        String key = REDIS_KEY_PREFIX + coupon.getId();
        // 将优惠券对象转换为 JSON 字符串(这里假设使用了 JSON 序列化工具)
        String value = JSONSerializer.serialize(coupon);
        // 根据优惠券有效期设置缓存过期时间
        long expirationSeconds = (coupon.getEndDate().getTime() - System.currentTimeMillis()) / 1000;
        jedis.setex(key, expirationSeconds, value);
    }

    public Coupon getCachedCoupon(int couponId) {
        // 获取缓存的优惠券信息
        String key = REDIS_KEY_PREFIX + couponId;
        String value = jedis.get(key);
        if (value!= null) {
            // 将 JSON 字符串反序列化为优惠券对象(这里假设使用了 JSON 反序列化工具)
            return JSONDeserializer.deserialize(value, Coupon.class);
        }
        return null;
    }

    public void cacheCouponReceiveRecord(int userId, int couponId) {
        // 缓存用户领取优惠券记录,键为 coupon:receive:userId:couponId,值为 1 表示已领取
        String key = REDIS_KEY_PREFIX + "receive:" + userId + ":" + couponId;
        jedis.set(key, "1");
    }

    public boolean isCouponReceived(int userId, int couponId) {
        // 判断用户是否已领取优惠券
        String key = REDIS_KEY_PREFIX + "receive:" + userId + ":" + couponId;
        return "1".equals(jedis.get(key));
    }

    public void close() {
        // 关闭 Jedis 连接
        jedis.close();
    }
}

(四)Redisson 分布式锁处理秒杀券

  • 在处理秒杀券的领取时,使用 Redisson 的分布式锁来控制并发。当用户请求领取秒杀券时,首先尝试获取对应秒杀券的分布式锁。只有获取到锁的用户才能进行领取操作的后续流程,包括检查剩余数量、更新数据库和缓存等。领取完成后释放锁,确保其他用户能够继续竞争领取。
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;

import java.util.concurrent.TimeUnit;

public class SeckillCouponService {
    private static final String LOCK_PREFIX = "seckill:coupon:";
    private RedissonClient redisson;

    public SeckillCouponService() {
        // 初始化 Redisson 客户端
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        redisson = Redisson.create(config);
    }

    public boolean tryReceiveSeckillCoupon(int userId, int couponId) {
        // 获取秒杀券的分布式锁
        RLock lock = redisson.getLock(LOCK_PREFIX + couponId);
        try {
            // 尝试加锁,设置锁的超时时间为 10 秒
            boolean locked = lock.tryLock(10, TimeUnit.SECONDS);
            if (locked) {
                try {
                    // 检查秒杀券剩余数量
                    Coupon coupon = couponService.getCouponById(couponId);
                    if (coupon.getRemainingQuantity() > 0) {
                        // 领取秒杀券逻辑,更新数据库和缓存
                        couponService.receiveSeckillCoupon(userId, couponId);
                        return true;
                    }
                } finally {
                    // 释放锁
                    lock.unlock();
                }
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        return false;
    }

    public void close() {
        // 关闭 Redisson 客户端
        redisson.shutdown();
    }
}

(五)接口设计与实现

  • 获取优惠券列表接口
    • 接收前端请求,从 Redis 缓存中获取优惠券列表信息。如果缓存中不存在或已过期,则从数据库中查询优惠券信息,缓存到 Redis 中并返回给前端。
import java.util.List;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CouponController {
    private final CouponService couponService;

    public CouponController(CouponService couponService) {
        this.couponService = couponService;
    }

    @GetMapping("/api/coupons")
    public List<Coupon> getCoupons() {
        return couponService.getCoupons();
    }
}
  • 领取优惠券接口
    • 对于日常券,检查用户是否已领取,若未领取则更新数据库和缓存,记录领取信息。对于秒杀券,调用 Redisson 分布式锁处理的领取方法,根据返回结果告知前端领取成功或失败。
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CouponReceiveController {
    private final CouponService couponService;
    private final SeckillCouponService seckillCouponService;

    public CouponReceiveController(CouponService couponService, SeckillCouponService seckillCouponService) {
        this.couponService = couponService;
        this.seckillCouponService = seckillCouponService;
    }

    @PostMapping("/api/receiveCoupon")
    public ReceiveCouponResponse receiveCoupon(@RequestBody ReceiveCouponRequest request) {
        int userId = request.getUserId();
        int couponId = request.getCouponId();
        Coupon coupon = couponService.getCouponById(couponId);
        if (coupon.getType() == 1) { // 日常券
            if (!couponService.isCouponReceived(userId, couponId)) {
                couponService.receiveCoupon(userId, couponId);
                return new ReceiveCouponResponse(true);
            } else {
                return new ReceiveCouponResponse(false);
            }
        } else if (coupon.getType() == 2) { // 秒杀券
            boolean success = seckillCouponService.tryReceiveSeckillCoupon(userId,
12-02 09:16