一、圖片上傳功能
1.1 配置
application.properties 配置上傳⽂件路徑
## 圖⽚服務 ##
pic:
local-path: D:/PIC
# spring boot3 升級配置名
spring:
web:
resources:
static-locations: classpath:/static/,file:${pic.local-path}
1.2 接口定義:
com.yj.lottery_system.service 定義 IPictureService 接口類:
package com.yj.lottery_system.service;
import org.springframework.web.multipart.MultipartFile;
public interface IPictureService {
/**
* 保存圖片方法
* @param multipartFile 上傳文件工具類
* @return 圖片索引:上傳後的文件名(唯一性)
*/
String savePicture(MultipartFile multipartFile);
}
1.3 實現接口
com.yj.lottery_system.service.impl 包下 實現 PictureServiceImpl 類
- 創建目錄
- 根據當前文件名,截取後綴加上自己文件名(會成為索引,確保唯一性),保存圖片
- 返回索引(修改後的文件名)
package com.yj.lottery_system.service.impl;
import com.yj.lottery_system.common.errorcode.ServiceErrorCodeConstants;
import com.yj.lottery_system.common.exception.ServiceException;
import com.yj.lottery_system.service.IPictureService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.util.UUID;
@Service
public class PictureServiceImpl implements IPictureService {
@Value("${pic.local-path}")
private String localPath;
@Override
public String savePicture(MultipartFile multipartFile) {
//創建目錄
File dir = new File(localPath);
if(!dir.exists()) {
dir.mkdirs();
}
//創建索引
//獲取當前文件名 截取後綴 自己文件名拼接後綴
String fileName = multipartFile.getOriginalFilename();
assert fileName != null;
String suffix = fileName.substring(fileName.lastIndexOf("."));
fileName = UUID.randomUUID()+suffix;
//圖片保存
try {
multipartFile.transferTo(new File(localPath+"/"+fileName));
} catch (IOException e) {
throw new ServiceException(ServiceErrorCodeConstants.PICTURE_UPLOAD_ERROR);
}
return fileName;
}
}
1.4 測試
隨便寫一個controller在Postman中測試
二、創建獎品
時序圖:
2.1 參數要求
|
參數名
|
描述
|
類型
|
默認值
|
條件
|
|
param
|
包含獎品名稱介紹價值的表單
|
|
|
必須
|
|
prizePic
|
獎品圖片
|
file
|
|
必須
|
2.2 接口規範
[請求] /prize/create POST
param: {"prizeName":"吹⻛機","description":"吹⻛機","price":100}
prizePic: Obj-C.jpg (FILE)
[響應]
{
"code": 200,
"data": 17,
"msg": ""
}
2.3 controller層
com.yj.lottery_system.controller 包下實現 createPrize 方法
- CreatePrizeParam 接收表單數據的參數
- 表單數據使用@RequestPart 註解接收
- 打印日誌
- 調用service,返回即可
package com.yj.lottery_system.controller;
import com.yj.lottery_system.common.pojo.CommonResult;
import com.yj.lottery_system.common.utils.JacksonUtil;
import com.yj.lottery_system.controller.param.CreatePrizeParam;
import com.yj.lottery_system.service.IPictureService;
import com.yj.lottery_system.service.IPrizeService;
import jakarta.annotation.Resource;
import org.slf4j.LoggerFactory;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.slf4j.Logger;
import org.springframework.web.multipart.MultipartFile;
@RestController
public class PrizeController {
private static final Logger log = LoggerFactory.getLogger(PrizeController.class);
@Resource
private IPictureService pictureService;
@Resource
private IPrizeService prizeService;
@RequestMapping("/pic/upload")
public String uploadPic(MultipartFile file){
return pictureService.savePicture(file);
}
/**
* 創建獎品
* RequestPart 接收表單數據
* @param param
* @param file
* @return
*/
@RequestMapping("/prize/create")
public CommonResult<Long> createPrize(@Validated @RequestPart("param") CreatePrizeParam param,
@RequestPart("prizePic") MultipartFile file) {
//日誌打印
log.info("createPrize CreatePrizeParam: {}", JacksonUtil.writeValueAsString(param));
//調用service服務
Long id = prizeService.createPrize(param,file);
return CommonResult.success(id);
}
}
2.3.1 CreatePrizeParam :創建獎品參數類
com.yj.lottery_system.controller.param 包下
- 根據接口規範的請求參數裏面的param包含獎品名,描述以及價值,獎品描述不是必填參數其餘是。
package com.yj.lottery_system.controller.param;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.NonNull;
import java.io.Serializable;
import java.math.BigDecimal;
@Data
public class CreatePrizeParam implements Serializable {
//獎品名
@NotBlank(message = "獎品名不能為空")
private String prizeName;
//獎品描述
private String description;
//獎品價格
@NotNull(message = "獎品價格不能為空")
private BigDecimal price;
}
2.3.2 測試
2.4 service層
2.4.1 創建接口
com.yj.lottery_system.service 包下,IPrizeService 類
package com.yj.lottery_system.service;
import com.yj.lottery_system.controller.param.CreatePrizeParam;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Service
public interface IPrizeService {
/**
* 創建一個獎品
* @param param 獎品屬性
* @param file 獎品圖
* @return 獎品id
*/
Long createPrize(CreatePrizeParam param, MultipartFile file);
}
2.4.1 實現接口
com.yj.lottery_system.service.impl 包下:
- 上傳圖片,調用 IPictureService 服務
- 構造數據,調用dao的Mapper
package com.yj.lottery_system.service.impl;
import com.yj.lottery_system.controller.param.CreatePrizeParam;
import com.yj.lottery_system.dao.dataObject.PrizeDO;
import com.yj.lottery_system.dao.mapper.PrizeMapper;
import com.yj.lottery_system.service.IPictureService;
import com.yj.lottery_system.service.IPrizeService;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
@Service
public class PrizeServiceImpl implements IPrizeService {
@Resource
private IPictureService pictureService;
@Autowired
private PrizeMapper prizeMapper;
@Override
public Long createPrize(CreatePrizeParam param, MultipartFile file) {
//上傳圖片
String fileName = pictureService.savePicture(file);
//構造數據 存入數據庫
PrizeDO prizeDO = new PrizeDO();
prizeDO.setDescription(param.getDescription());
prizeDO.setName(param.getPrizeName());
prizeDO.setPrice(param.getPrice());
prizeDO.setImageUrl(fileName);
//調用dao
prizeMapper.insert(prizeDO);
return prizeDO.getId();
}
}
2.5 dao層
2.5.1 數據類 PrizeDO
com.yj.lottery_system.dao.dataObject 包下 對應數據庫的字段
package com.yj.lottery_system.dao.dataObject;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.math.BigDecimal;
@Data
@EqualsAndHashCode(callSuper = true)
public class PrizeDO extends BaseDO{
//獎品描述
private String description;
//圖片索引
private String imageUrl;
//獎品名
private String name;
//價格
private BigDecimal price;
}
2.5.2 insert 方法
com.yj.lottery_system.dao.mapper 包下:
- 新增數據
- 將數據庫id賦值給類屬性
package com.yj.lottery_system.dao.mapper;
import com.yj.lottery_system.dao.dataObject.PrizeDO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
@Mapper
public interface PrizeMapper {
@Insert("insert into prize (name, image_url, price, description) values" +
" (#{name},#{imageUrl},#{price},#{description})")
@Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
int insert(PrizeDO prizeDO);
}
2.6 工具converter :支持 JSON + 文件混合上傳
我們直接訪問的時候,會有表單數據轉換到後端時出錯,加上下面的工具 com.yj.lottery_system.common.converter 包下
package com.yj.lottery_system.common.converter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter;
import org.springframework.stereotype.Component;
import java.lang.reflect.Type;
@Component
public class MultipartJackson2HttpMessageConverter extends
AbstractJackson2HttpMessageConverter {
protected MultipartJackson2HttpMessageConverter(ObjectMapper objectMapper)
{
// MediaType.APPLICATION_OCTET_STREAM 表⽰這個轉換器⽤於處理⼆進制流數據,通常⽤於⽂件上傳。
super(objectMapper, MediaType.APPLICATION_OCTET_STREAM);
}
@Override
public boolean canWrite(Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
public boolean canWrite(Type type, Class<?> clazz, MediaType mediaType) {
return false;
}
@Override
protected boolean canWrite(MediaType mediaType) {
return false;
}
}
2.7 前端
static/create-prizes.html :
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>創建抽獎活動</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.5.2/css/bootstrap.min.css">
<link rel="stylesheet" href="./css/base.css">
<link rel="stylesheet" href="./css/toastr.min.css">
<style>
body {
font-family: Arial, sans-serif;
background-color: #fff;
margin: 0;
padding: 0;
}
.container {
max-width: 800px;
margin: 30px auto;
padding: 20px 30px;
background-color: #fff;
}
.prize-checkbox {
margin-bottom: 10px;
}
.modal {
display: none; /* 初始狀態下模態框不可見 */
position: fixed;
z-index: 1;
left: 0;
top: 0;
bottom: 0;
right: 0;
overflow: hidden;
overflow-y: auto;
background-color: rgba(0, 0, 0, 0.1);
}
.modal-content {
background-color: #fefefe;
margin: 5% auto;
padding: 20px;
border: 1px solid #888;
width: 610px;
}
.modal-content h2{
display: flex;
justify-content: space-between;
align-items: center;
font-weight: 600;
font-size: 18px;
color: #000000;
height: 50px;
border-bottom: 1px solid #DEDEDE;
margin-bottom: 30px;
}
#prizesContainer{
height: 406px;
margin-bottom: 40px;
overflow-y: auto;
padding: 0 26px;
}
.close {
color: #000;
float: right;
font-size: 28px;
cursor: pointer;
}
.close:hover,
.close:focus {
color: black;
text-decoration: none;
cursor: pointer;
}
.prize-item,
.user-item {
display: flex;
align-items: center;
margin-bottom: 10px;
justify-content: center;
}
.custom-p {
font-size: 16px;
/* 設置字體粗細 */
font-weight: bold;
margin-right: 90px; /* 右側外邊距 */
}
.prize-item input[type="checkbox"],
.user-item input[type="checkbox"] {
margin-right: 99px;
}
.prize-item label,
.user-item label {
margin-right: 11px;
margin-bottom: 0;
width: 200px;
}
.prize-item select {
margin-left: auto; /* 將下拉選擇框放置在末尾 */
}
.prize-item .form-control {
width: 96px;
height: 36px;
line-height: 36px;
margin-right: 56px;
}
.h-title{
font-weight: 600;
font-size: 30px;
letter-spacing: 1px;
color: #000000;
line-height: 50px;
text-align: center;
margin-bottom: 40px;
}
.desc-row{
margin-bottom: 60px;
}
.form-btn-box{
display: flex;
align-items: center;
justify-content: center;
}
.form-btn-box button{
width: 148px;
height: 48px;
}
.pre-btn{
margin-right: 20px;
}
</style>
</head>
<body>
<div class="container">
<h2 class="h-title">創建抽獎活動</h2>
<form id="activityForm">
<div class="form-group">
<label for="activityName">活動名稱</label>
<input type="text" placeholder="請輸入活動名稱" class="form-control" class="form-control" id="activityName" name="activityName" required>
</div>
<div class="form-group desc-row">
<label for="description">活動描述</label>
<textarea id="description" placeholder="請輸入活動描述" rows="5" cols="33" class="form-control" name="description" required></textarea>
</div>
<div class="form-btn-box">
<button id="buttonPrizes" type="button" class="btn btn-primary pre-btn" onclick="showPrizesModal()">圈選獎品</button>
<button id="buttonUsers" type="button" class="btn btn-primary pre-btn" onclick="showUsersModal()">圈選人員</button>
<button type="submit" class="btn btn-primary" id="createActivity">創建活動</button>
</div>
</form>
</div>
<!-- toast提示 -->
<div class="toast"></div>
<!-- 獎品選擇模態框 -->
<div id="prizesModal" class="modal">
<div class="modal-content">
<h2>獎品列表<span class="close" onclick="hidePrizesModal()">×</span></h2>
<div class="prize-item">
<p class="custom-p">勾選</p>
<p class="custom-p">獎品名</p>
<p class="custom-p">數量</p>
<p class="custom-p">獎品等級</p>
</div>
<div id="prizesContainer">
<!-- 獎品列表將動態插入這裏 -->
</div>
<!-- 分頁控件 -->
<div class="pagination" id="prizePagination" style="justify-content: center; margin: 10px 0;">
<button class="btn-outline-primary" onclick="fetchPrizes(1)">首頁</button>
<button class="btn-outline-primary" onclick="prevPrizePage()">上一頁</button>
<span>第 <input id="modalPageInput" type="number" min="1" value="1" style="width: 60px;"> 頁</span>
<button class="btn-outline-primary" onclick="nextPrizePage()">下一頁</button>
<button class="btn-outline-primary" onclick="fetchPrizes(totalPrizePages)">尾頁</button>
</div>
<div class="form-btn-box">
<button type="button" class="btn btn-secondary pre-btn" onclick="hidePrizesModal()">取消</button>
<button type="button" class="btn btn-primary" onclick="submitPrizes()">確定</button>
</div>
</div>
</div>
<!-- 人員選擇模態框 -->
<div id="usersModal" class="modal">
<div class="modal-content">
<h2>人員列表<span class="close" onclick="hideUsersModal()">×</span></h2>
<div id="usersContainer">
<!-- 獎品列表將動態插入這裏 -->
</div>
<div class="form-btn-box">
<button type="button" class="btn btn-secondary pre-btn" onclick="hideUsersModal()">取消</button>
<button type="button" class="btn btn-primary" onclick="submitUsers()">確定</button>
</div>
</div>
</div>
<!-- JavaScript代碼 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.19.3/jquery.validate.min.js"></script>
<script src="./js/toastr.min.js"></script>
<script>
/* ===== 分頁變量 ===== */
var modalCurrentPage = 1; // 當前頁
var modalPageSize = 5; // 每頁條數
var totalPrizePages = 1; // 總頁數
var userToken = localStorage.getItem("user_token");
// 初始時獎品列表為空: 勾選的獎品存放
var selectedPrizes = [];
// 顯示獎品選擇模態框
function showPrizesModal() {
$('#prizesModal').css('display', 'block');
}
// 隱藏獎品選擇模態框
function hidePrizesModal() {
$('#prizesModal').css('display', 'none');
}
// 獲取獎品列表的函數
function fetchPrizes(page) {
if (!page || page < 1) page = 1;
modalCurrentPage = page;
$.ajax({
url: '/prize/find-list',
type: 'GET',
dataType: 'json',
data: { currentPage: modalCurrentPage, pageSize: modalPageSize },
headers: { "user_token": userToken }, // 原封不動
success: function (result) {
if (result.code !== 200) { alert(result.msg); return; }
var prizes = result.data.records;
totalPrizePages = Math.ceil(result.data.total / modalPageSize);
$('#modalPageInput').val(modalCurrentPage);
var prizesContainer = $('#prizesContainer').empty();
prizes.forEach(function (prize) {
// 下面這段就是原來你 forEach 裏的 append,一行沒動
prizesContainer.append(
$('<div class="prize-item">').append(`
<input type="checkbox" id="prize-${prize.prizeId}" name="prize-${prize.prizeId}" value="${prize.prizeId}">
<label for="prize-${prize.prizeId}">${prize.prizeName}</label>
<input class="form-control" type="number" name="quantity-${prize.prizeId}" min="1" value="1">
<select class="form-control" name="level-${prize.prizeId}">
<option value="FIRST_PRIZE" selected>一等獎</option>
<option value="SECOND_PRIZE">二等獎</option>
<option value="THIRD_PRIZE">三等獎</option>
</select>
`)
);
});
},
error: function (err) {
// 原來錯誤處理一行不動
if (err != null && err.status == 401) {
alert("用户未登錄, 即將跳轉到登錄頁!");
window.location.href = "/blogin.html";
window.parent.location.href = "/blogin.html";
}
}
});
}
function prevPrizePage() {
if (modalCurrentPage > 1) fetchPrizes(modalCurrentPage - 1);
else alert("已是第一頁");
}
function nextPrizePage() {
if (modalCurrentPage < totalPrizePages) fetchPrizes(modalCurrentPage + 1);
else alert("已是最後一頁");
}
// 輸入框直接跳轉
$('#modalPageInput').on('keypress', function (e) {
if (e.key === 'Enter') {
const p = parseInt(this.value, 10);
if (!isNaN(p) && p >= 1 && p <= totalPrizePages) fetchPrizes(p);
}
});
// 提交獎品數據的函數
function submitPrizes() {
selectedPrizes = [];
// 將選中的獎品信息存儲在selectedPrizes
$('.prize-item input[type="checkbox"]:checked').each(function() {
var prizeId = +$(this).val();
var prizeAmount = +$('input[name="quantity-' + prizeId + '"]').val();
var prizeTiers = $('select[name="level-' + prizeId + '"]').val();
selectedPrizes.push({
prizeId: prizeId,
prizeAmount: prizeAmount,
prizeTiers: prizeTiers
});
});
// 關閉模態框
hidePrizesModal();
// 修改按鈕
var nextButton = document.getElementById('buttonPrizes');
if (selectedPrizes.length > 0) {
nextButton.textContent = '圈選獎品(已選)';
} else {
nextButton.textContent = '圈選獎品';
}
}
// 初始時人員列表為空, 存放的勾選的人員信息
var selectedUsers = [];
// 顯示人員選擇模態框
function showUsersModal() {
$('#usersModal').css('display', 'block');
}
// 隱藏人員選擇模態框
function hideUsersModal() {
$('#usersModal').css('display', 'none');
}
// 獲取人員列表的函數
function fetchUsers() {
$.ajax({
url: '/base-user/find-list',
type: 'GET',
dataType: 'json',
data: { identity: 'NORMAL' },
headers: {
// jwt
"user_token": userToken
},
success: function(result) {
var users = result.data;
var usersContainer = $('#usersContainer');
usersContainer.empty(); // 清空當前人員列表
users.forEach(function(user) {
console.info(user);
usersContainer.append(
$('<div class="user-item">').append(`
<input type="checkbox" id="user-${user.userId}" name="user-${user.userId}" value="${user.userId}">
<label for="user-${user.userId}">${user.userName}</label>
`)
);
});
},
error:function(err){
console.log(err);
if(err!=null && err.status==401){
alert("用户未登錄, 即將跳轉到登錄頁!");
// 跳轉登錄頁
window.location.href = "/blogin.html";
window.parent.location.href = "/blogin.html";//讓父頁面一起跳轉
}
}
});
}
// 提交用户數據的函數
function submitUsers() {
selectedUsers = [];
// 將選中的獎品信息存儲在selectedUsers
$('.user-item input[type="checkbox"]:checked').each(function() {
var userId = +$(this).val();
var userName = $(this).next('label').text();
selectedUsers.push({
userId: userId,
userName: userName
});
});
// 關閉模態框
hideUsersModal();
// 修改按鈕
var nextButton = document.getElementById('buttonUsers');
if (selectedUsers.length > 0) {
nextButton.textContent = '圈選人員(已選)';
} else {
nextButton.textContent = '圈選人員';
}
}
// 綁定表單提交事件
$('#createActivity').click(function(event){
event.stopPropagation()
$('#activityForm').validate({
rules:{
activityName:"required",
description:{
required:true,
}
},
messages:{
activityName:"請輸入活動名稱",
description:"請輸入活動描述"
},
// 驗證通過才會觸發
submitHandler:function(form){
console.log('selectedPrizes',selectedPrizes)
console.log('selectedUsers',selectedUsers)
// 如果未選擇獎品則進行toast提示
if(selectedPrizes.length==0){
alert('請至少選擇一個獎品')
return false
}
// 如果未選擇人員則進行toast提示
if(selectedUsers.length==0){
alert('請至少選擇一個人員, 人員數量應大於等於獎品總量')
return false
}
// 獲取提交表單信息
var data = {
activityName:'',
description:'',
activityPrizeList:[],
activityUserList:[]
}
data.activityName = $('#activityName').val()
data.description = $('#description').val()
data.activityPrizeList = selectedPrizes
data.activityUserList = selectedUsers
submitActivity(data)
}
})
})
// 提交活動信息接口
function submitActivity(data){
$.ajax({
url: '/activity/create',
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(data),
headers: {
// jwt
"user_token": userToken
},
success: function(result) {
if (result.code != 200) {
alert("創建失敗!" + result.msg);
} else {
alert("創建成功!");
// 向父頁面傳值 https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessage
window.parent.postMessage({
from:'activities-list.html',
id:'#activitiesList'
},'*');
}
},
error:function(err){
console.log(err);
if(err!=null && err.status==401){
alert("用户未登錄, 即將跳轉到登錄頁!");
// 跳轉登錄頁
window.location.href = "/blogin.html";
window.parent.location.href = "/blogin.html";//讓父頁面一起跳轉
}
}
});
}
// 獲取獎品/人員列表並填充模態框
$(document).ready(function() {
fetchPrizes();
fetchUsers();
});
// 顯示獎品選擇模態框
$(document).ready(function() {
$('#activityForm').on('click', 'button圈選獎品', function() {
showPrizesModal(1);
});
});
// 顯示人員選擇模態框
$(document).ready(function() {
$('#activityForm').on('click', 'button圈選人員', function() {
showUsersModal();
});
});
</script>
</body>
</html>