文章目录
- 基于 Vue + Java 的刷题优惠券项目设计方案
- 一、项目概述
- 二、前端设计与实现
- (一)技术选型与环境搭建
- (二)页面布局与组件设计
- 三、后端设计与实现
- (一)技术选型与架构设计
- (二)数据库设计
- (三)Redis 缓存策略
- (四)Redisson 分布式锁处理秒杀券
- (五)接口设计与实现
基于 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,