目录
- 一、简介
- 二、参数列表
- 三、接口规范
- 四、controller层
-
- [4.1 convertWinningRecordsResult:service层与controller层返回结果转换方法](#4.1 convertWinningRecordsResult:service层与controller层返回结果转换方法)
- [4.2 WinningRecordsParam :传入参数](#4.2 WinningRecordsParam :传入参数)
- [4.3 WinningRecordsResult :controller层返回结果](#4.3 WinningRecordsResult :controller层返回结果)
- 五、service层
-
- [5.1 定义接口](#5.1 定义接口)
- [5.2 实现接口](#5.2 实现接口)
- [5.3 WinningRecordsDTO :service返回结果](#5.3 WinningRecordsDTO :service返回结果)
- 六、dao层
- 七、前端

一、简介
时序图:

二、参数列表
| 参数名 | 描述 | 类型 | 默认值 | 条件 |
|---|---|---|---|---|
| activityId | 活动id | Long | 必须 | |
| prizeId | 奖品id | Long | 非必须 |
三、接口规范
java
[请求] /winning-records/show POST
{
"activityId":23
}
[响应]
{
"code": 200,
"data": [
{
"winnerId": 15,
"winnerName": "张三",
"prizeName": "华为⼿机",
"prizeTier": "⼀等奖",
"winningTime": "2024-05-21T11:55:10.000+00:00"
},
{
"winnerId": 21,
"winnerName": "李四",
"prizeName": "华为⼿机",
"prizeTier": "⼀等奖",
"winningTime": "2024-05-21T11:55:10.000+00:00"
}
],
"msg": ""
}
四、controller层
com/yj/lottery_system/controller 包下 DrawPrizeController.java 类:
java
@RequestMapping("/winning-records/show")
public CommonResult<List<WinningRecordsResult>> showWinningRecords(@RequestBody@Valid WinningRecordsParam param){
//打印日志
log.info("showWinningRecords WinningRecordsParam : {}", JacksonUtil.writeValueAsString(param));
//调用service
List<WinningRecordsDTO> winningRecordsDTOList = drawPrizeService.getRecords(param);
return CommonResult.success(convertWinningRecordsResult(winningRecordsDTOList));
}
4.1 convertWinningRecordsResult:service层与controller层返回结果转换方法
com/yj/lottery_system/controller 包下 DrawPrizeController.java 类:
java
/**
* showWinningRecords 返回结果转换方法
* @param winningRecordsDTOList
* @return
*/
private List<WinningRecordsResult> convertWinningRecordsResult(List<WinningRecordsDTO> winningRecordsDTOList) {
if(CollectionUtils.isEmpty(winningRecordsDTOList)) {
return Arrays.asList();
}
List<WinningRecordsResult> winningRecordsResultList = winningRecordsDTOList.stream()
.map(winningRecordsDTO -> {
WinningRecordsResult winningRecordsResult = new WinningRecordsResult();
winningRecordsResult.setWinnerId(winningRecordsDTO.getWinnerId());
winningRecordsResult.setWinnerName(winningRecordsDTO.getWinnerName());
winningRecordsResult.setPrizeName(winningRecordsDTO.getPrizeName());
winningRecordsResult.setPrizeTier(winningRecordsDTO.getPrizeTier().getMessage());
winningRecordsResult.setWinningTime(winningRecordsDTO.getWinningTime());
return winningRecordsResult;
}).collect(Collectors.toList());
return winningRecordsResultList;
}
4.2 WinningRecordsParam :传入参数
com.yj.lottery_system.controller.param 包下
java
package com.yj.lottery_system.controller.param;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.io.Serializable;
@Data
public class WinningRecordsParam implements Serializable {
//活动id
@NotNull(message = "活动id不能为空")
private Long activityId;
//奖品id
private Long prizeId;
}
4.3 WinningRecordsResult :controller层返回结果
com.yj.lottery_system.controller.result 包下:
java
package com.yj.lottery_system.controller.result;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class WinningRecordsResult implements Serializable {
//中奖者id
private Long winnerId;
//中奖者姓名
private String winnerName;
//奖品名
private String prizeName;
//奖品等级
private String prizeTier;
//获奖时间
private Date winningTime;
}
五、service层
5.1 定义接口
com/yj/lottery_system/service 包下 IDrawPrizeService.java 类:
java
/**
* 获取中奖记录
* @param param
* @return
*/
List<WinningRecordsDTO> getRecords(WinningRecordsParam param);
5.2 实现接口
com/yj/lottery_system/service/impl 包下:DrawPrizeServiceImpl.java 类
java
/**
* 获取中奖记录
* @param param
* @return
*/
@Override
public List<WinningRecordsDTO> getRecords(WinningRecordsParam param) {
//查询Redis
String key = null == param.getPrizeId()
? String.valueOf(param.getActivityId())
: param.getActivityId()+"_"+param.getPrizeId();
List<WinningRecodesDO> winningRecords = getWinningRecords(key);
if(!CollectionUtils.isEmpty(winningRecords)) {
return convertToWinningRecordsDTOList(winningRecords);
}
//没查到查库
winningRecords = winningRecordMapper.selectByActivityIdOrPrizeId(param.getActivityId(), param.getPrizeId());
if(CollectionUtils.isEmpty(winningRecords)) {
log.info("查询中奖记录为空 param: {}",
JacksonUtil.writeValueAsString(param));
return Arrays.asList();
}
//存放记录进Redis
cacheWinningRecords(key,winningRecords,WINNING_RECORDS_TIMEOUT);
return convertToWinningRecordsDTOList(winningRecords);
}
private List<WinningRecordsDTO> convertToWinningRecordsDTOList(List<WinningRecodesDO> winningRecords) {
if(CollectionUtils.isEmpty(winningRecords)) {
return Arrays.asList();
}
List<WinningRecordsDTO> winningRecordsDTOList = winningRecords.stream()
.map(winningRecodesDO -> {
WinningRecordsDTO winningRecordsDTO = new WinningRecordsDTO();
winningRecordsDTO.setWinnerId(winningRecodesDO.getWinnerId());
winningRecordsDTO.setWinnerName(winningRecodesDO.getWinnerName());
winningRecordsDTO.setPrizeName(winningRecodesDO.getPrizeName());
winningRecordsDTO.setPrizeTier(ActivityPrizeTiresEnum.forName(winningRecodesDO.getPrizeTier()));
winningRecordsDTO.setWinningTime(winningRecodesDO.getWinningTime());
return winningRecordsDTO;
}).collect(Collectors.toList());
return winningRecordsDTOList;
}
5.3 WinningRecordsDTO :service返回结果
com.yj.lottery_system.service.dto 包下:
java
package com.yj.lottery_system.service.dto;
import com.yj.lottery_system.service.enums.ActivityPrizeTiresEnum;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class WinningRecordsDTO implements Serializable {
//中奖者id
private Long winnerId;
//中奖者姓名
private String winnerName;
//奖品名
private String prizeName;
//奖品等级
private ActivityPrizeTiresEnum prizeTier;
//获奖时间
private Date winningTime;
}
六、dao层
com/yj/lottery_system/dao/mapper 包下:WinningRecordMapper.java类
java
/**
* 根据活动id 或 奖品id 查询中奖记录
* @param activityId
* @param prizeId
*/
@Select("<script>" +
"select * from winning_record " +
"where activity_id = #{activityId} " +
"<if test=\"prizeId!=null\">" +
"and prize_id = #{prizeId}" +
"</if>" +
"</script>")
List<WinningRecodesDO> selectByActivityIdOrPrizeId(@Param("activityId")Long activityId, @Param("prizeId")Long prizeId);
七、前端
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<title>抽奖页面</title>
<style type="text/css">
* {
margin: 0;
padding: 0;
}
html, body {
width: 100%;
height: 100%;
}
body {
text-align: center;
background: url("./pic/bg.png") no-repeat;
overflow: hidden;
background-size: 100% 100%;
font-weight: bold;
color: #D40000;
}
#container {
min-width: 1000px;
min-height: 700px;
}
#title {
font-size: 100px;
margin-top: 80px;
}
#disc {
font-size: 40px;
margin: 10px 0;
}
#image {
margin-top: 20px;
max-height: 280px;
border: 1px solid #E23540FF;
border-radius: 20px;
}
#list {
margin: 0 auto;
max-width: 800px;
}
#list span {
display: inline-block;
width: 160px;
font-size: 36px;
margin-top: 8px;
}
.records-item {
text-align: center;
cursor: pointer;
padding: 20px;
transition: background-color 0.3s;
font-size: 20px;
}
.opt-box{
display: flex;
align-items: center;
justify-content: center;
margin-top: 30px;
}
.opt-box .btn{
width: 120px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
border: 2px solid #D40000;
margin-inline: 18px;
cursor: pointer;
border-radius: 4px;
font-weight: normal;
font-size: 15px;
}
.opt-box .btn:hover{
background-color: #D40000;
color: #fff;
}
</style>
</head>
<body>
<div id="container">
<div id="title"></div>
<div id="disc"></div>
<img id="image" />
<div id="list"></div>
<div class="opt-box">
<span class="btn pre-btn" onclick="previousStep()">查看上一奖项</span>
<span class="btn next-btn" onclick="nextStep()">开始抽奖</span>
</div>
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script>
var params = new URLSearchParams(window.location.search);
var activityId = params.get('activityId');
var valid = params.get('valid');
var userToken = localStorage.getItem("user_token");
// 获取网页上通过id为disc、image和list的DOM元素的引用。
var disc = document.getElementById('disc')
var image = document.getElementById('image')
var list = document.getElementById('list')
// 奖品列表
var steps = null
// 抽到第几个了
var step = 0
// 人员列表
var names = null
// 正在做什么
var state = ''
// 等待DOM加载完成
document.addEventListener('DOMContentLoaded', function() {
var titleDiv = document.getElementById('title');
var activityName = params.get('activityName');
titleDiv.textContent = activityName;
});
function formatDate(dateString) {
var date = new Date(dateString);
var hour = ('0' + date.getHours()).slice(-2);
var minute = ('0' + date.getMinutes()).slice(-2);
var second = ('0' + date.getSeconds()).slice(-2);
// 构建格式化的日期字符串
return hour + ':' + minute + ':' + second;
}
// showPic函数用于显示图片和奖品信息。
// 它将data.disc设置为disc元素的innerHTML,将data.image设置为image元素的src属性,然后显示图片并隐藏列表。
function showPic(data) {
disc.innerHTML = data.prizeTierName + ' ' + data.name + ' ' + data.prizeAmount + '份'
image.src = data.imageUrl ? data.imageUrl : '/pic/defaultPrizeImg.png'
image.style.display = 'inline'
list.style.display = 'none'
while (list.hasChildNodes()) {
list.removeChild(list.firstChild)
}
}
// 查询中奖记录
function showRecords() {
disc.innerHTML = "中奖名单"
image.style.display = 'none'
list.style.display = 'block'
while (list.hasChildNodes()) {
list.removeChild(list.firstChild)
}
$.ajax({
url: '/winning-records/show',
type: 'POST',
dataType: 'json',
contentType: 'application/json',
data: JSON.stringify({activityId: activityId}),
headers: {
'user_token': userToken
},
success: function(result) {
if (result.code != 200) {
alert("查询中奖记录失败!" + result.msg);
} else {
var records = result.data
list.className = "records-item"
for (var i = 0; i < records.length; ++i) {
var item = document.createElement('div')
item.textContent = formatDate(records[i].winningTime) + ' '
+ records[i].winnerName + ' '
+ records[i].prizeName + ' '
+ records[i].prizeTier + ' '
list.appendChild(item)
}
// 分享链接
// 检查是否已经有分享按钮,如果没有则创建
var optBox = document.querySelector('.opt-box');
var existingBtn = optBox.querySelector('.btn.copy-btn');
if (!existingBtn) {
var newBtn = document.createElement('span');
newBtn.className = 'btn copy-btn';
// 设置点击事件处理函数
newBtn.onclick = function() {
// 创建URL对象
var currentUrl = new URL(window.location);
// 创建URLSearchParams对象
var searchParams = currentUrl.searchParams;
// 更新参数
searchParams.delete('valid');
searchParams.append('valid', false);
// 防止分享再分享,拼接多个参数
searchParams.delete('hideButton');
searchParams.append('hideButton', true);
// 更新URL对象的搜索字符串
currentUrl.search = searchParams.toString();
// 获取当前页面的URL
var url = currentUrl;
// 执行复制操作
copyToClipboard(url);
};
// 设置span元素的文本内容
newBtn.textContent = '分享结果';
// 将新的span元素添加到.opt-box容器中
optBox.appendChild(newBtn);
}
// 是否隐藏按钮
var hideButton = params.get('hideButton');
if ((hideButton == null && valid === 'false')
|| hideButton === 'true') {
// 隐藏所有元素
var buttons = document.querySelectorAll('.btn.pre-btn, .btn.next-btn');
buttons.forEach(function(button) {
button.style.display = 'none';
});
}
}
}
});
}
// 复制当前地址
function copyToClipboard(textToCopy) {
// navigator clipboard 需要https等安全上下文
if (navigator.clipboard && window.isSecureContext) {
// navigator clipboard 向剪贴板写文本
return navigator.clipboard.writeText(textToCopy)
.then(() => {alert("链接已复制到剪贴板");});
} else {
// 创建text area
let textArea = document.createElement("textarea");
textArea.value = textToCopy;
// 使text area不在viewport,同时设置不可见
textArea.style.position = "absolute";
textArea.style.opacity = 0;
textArea.style.left = "-999999px";
textArea.style.top = "-999999px";
document.body.appendChild(textArea);
textArea.focus();
textArea.select();
return new Promise((res, rej) => {
// 执行复制命令并移除文本框
document.execCommand('copy') ? res() : rej();
textArea.remove();
alert("链接已复制到剪贴板");
});
}
}
// showBlink函数用于显示闪烁的文字。
// 它首先设置奖品信息,隐藏图片,显示列表,然后创建与data.count相等数量的span元素。
function showBlink(data) {
disc.innerHTML = data.prizeTierName + ' ' + data.name + ' ' + data.prizeAmount + '份'
image.style.display = 'none'
list.style.display = 'block'
var spans = []
for (var i = 0; i < data.prizeAmount; ++i) {
var span = document.createElement('span')
list.appendChild(span)
spans.push(span)
}
// doBlink函数用于实现文字的闪烁效果,通过window.requestAnimationFrame来循环调用。
function doBlink() {
if (state == 'showBlink') {
// Math.random() 生成一个 [0, 1) 范围内的伪随机数,
// 所以表达式的结果将在 (-0.5, 0.5] 范围内,这会导致 names 数组的元素随机排序。
names.sort(function(a, b) {
return 0.5 - Math.random()
})
// 只展示当前奖项个数的随机人名
for (var i = 0; i < data.prizeAmount; ++i) {
spans[i].innerHTML = names[i].userName
}
window.requestAnimationFrame(doBlink)
}
}
// 告诉浏览器------你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。
window.requestAnimationFrame(doBlink)
}
// 通过后端展示奖品维度中奖者,主要是防止页面刷新
function showWinnerListWithPrize(data) {
$.ajax({
url: '/winning-records/show',
type: 'POST',
dataType: 'json',
contentType: 'application/json',
headers: {
'user_token': userToken
},
data: JSON.stringify({activityId: activityId, prizeId: data.prizeId}),
success: function(result) {
if (result.code != 200) {
alert("查询中奖记录失败!" + result.msg);
} else {
var luckRecords = result.data
// 从names中移除幸运儿
var luck = []
// 获得需要删除的用户id
var needRemoves = luckRecords.map(function(luckRecord) {
return luckRecord.winnerId;
});
console.log("needRemoves" + JSON.stringify(needRemoves))
console.log("names1" + JSON.stringify(names))
// 从names中移除包含在luckRecords中的name对应的元素
for (var i = 0; i < names.length; i++) {
if (needRemoves.includes(names[i].userId)) {
var tmp = names.splice(i, 1) // 移除当前项,并更新索引i,因为后面的项会向前移动
luck = luck.concat(tmp)
console.log("tmp" + JSON.stringify(tmp))
i-- // 减1以确保索引i仍然指向当前的项
}
}
console.log("names2" + JSON.stringify(names))
console.log("luck" + JSON.stringify(luck))
// 只存放当前奖项的人员信息,下次抽奖会被覆盖
data.list = luck.map(x => x)
for (var i = 0; i < data.list.length; ++i) {
var span = document.createElement('span')
span.innerHTML = data.list[i].userName
list.appendChild(span)
}
}
}
});
}
// showList函数用于显示静态的文字列表。它设置奖品信息,隐藏图片,显示列表,并将data.list中的数据添加到列表中。
function showList(data) {
disc.innerHTML = data.prizeTierName + ' ' + data.name + ' ' + data.prizeAmount + '份'
image.style.display = 'none'
list.style.display = 'block'
while (list.hasChildNodes()) {
list.removeChild(list.firstChild)
}
// data.list是前端保存的临时数据,若当前抽奖页刷新,则数据丢失
// 因此判断一下是否为空,为空需要查询后端
data.list = data.list || []
if (data.list.length > 0) {
for (var i = 0; i < data.list.length; ++i) {
var span = document.createElement('span')
span.innerHTML = data.list[i].userName
list.appendChild(span)
}
} else {
showWinnerListWithPrize(data)
}
}
// 这是改变next-btn按钮文本的函数
function changeNextButtonText(text) {
var nextButton = document.querySelector('.btn.next-btn');
nextButton.textContent = text;
}
function saveLuck() {
var data = steps[step]
var luckUserList = []
for (var i = 0; i < data.list.length; ++i) {
luckUserList.push({
userId: data.list[i].userId,
userName: data.list[i].userName
})
}
var drawPrizesData = {
activityId: activityId,
prizeId: data.prizeId,
winningTime: new Date(),
winnerList: luckUserList
};
// 发送AJAX请求
$.ajax({
url: '/draw-prize',
type: 'POST',
dataType: 'json',
contentType: 'application/json',
headers: {
'user_token': userToken
},
data: JSON.stringify(drawPrizesData),
success: function(result) {
if (result.code != 200) {
alert("抽奖失败!" + result.msg);
}
},
error:function(err){
console.log(err);
if(err!=null && err.status==401){
alert("用户未登录, 即将跳转到登录页!");
// 已经被拦截器拦截了, 未登录
location.href ="/blogin.html";
}
}
});
}
// nextStep函数用于根据当前状态显示下一个步骤的内容。
// 它根据state的值和data.list的长度来决定是显示图片、闪烁文字还是静态文字列表,并更新step和state。
function nextStep() {
var data = steps[step]
console.log("steps[" + step + "]:" + JSON.stringify(data))
if (state == 'showPic') {
if (data.valid) {
// 还没抽:抽奖
state = 'showBlink'
showBlink(data)
changeNextButtonText('点我确定')
} else {
// 抽过了:展示
state = 'showList'
showList(data)
changeNextButtonText('已抽完,下一步')
}
} else if (state == 'showBlink') {
// 从names中移除幸运儿,并将幸运儿存放起来
var luck = names.splice(0, data.prizeAmount)
// 只存放当前奖项的人员信息,下次抽奖会被覆盖
data.list = luck.map(x => x)
console.log("data.list:" + JSON.stringify(data.list))
// 调用后端抽奖接口,保存中奖信息
saveLuck()
data.valid = false
state = 'showList'
showList(data)
changeNextButtonText('已抽完,下一步')
} else if (state == 'showList') {
if (step < (steps.length - 1)) {
++step
state = ''
nextStep()
changeNextButtonText('开始抽奖')
} else {
// 抽奖结束,展示全量中奖列表
if (step == (steps.length - 1)) {
++step
}
showRecords()
changeNextButtonText('已全部抽完')
}
} else {
state = 'showPic'
showPic(data)
changeNextButtonText('开始抽奖')
}
}
// previousStep函数用于返回到上一个步骤。如果当前步骤不是第一个步骤,则减少step的值,然后调用nextStep函数。
function previousStep() {
if (step > 0) {
--step
}
state = ''
nextStep()
}
// 抽奖页面加载逻辑
function drawPage() {
// 判断活动是否有效:进行中
if (valid == "true") {
// 判断是用户还是管理员,只有管理员有权限抽奖
var identity = localStorage.getItem("user_identity");
if (identity == "ADMIN") {
// 在页面加载完成后,调用reloadConf函数并传入nextStep作为回调函数,以初始化页面显示。
reloadConf(nextStep)
} else {
alert("抽奖未结束,由于您不是管理员,无权限限参与抽奖。请耐心等待抽奖结束后查看中奖名单");
}
} else {
showRecords()
}
}
// reloadConf函数用于重新加载配置文件。
// 如果提供了回调函数func,则在文件读取完成后调用它。
function reloadConf(func) {
// 发送AJAX请求
$.ajax({
url: '/activity-detail/find',
type: 'GET',
data: { activityId: activityId },
dataType: 'json',
headers: {
'user_token': userToken
},
success: function(result) {
if (result.code != 200) {
alert("获取活动详情失败!" + result.msg);
} else {
names = result.data.users;
steps = result.data.prizes;
if (func) func();
}
},
error:function(err){
console.log(err);
if(err!=null && err.status==401){
alert("用户未登录, 即将跳转到登录页!");
// 已经被拦截器拦截了, 未登录
location.href ="/blogin.html";
}
}
});
}
// 抽奖页起始逻辑
drawPage()
</script>
</body>
</html>