一、圖片上傳功能

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>