From 923e877759caaa37439e8f52a6ee9471b3046e4b Mon Sep 17 00:00:00 2001 From: YoVinchen Date: Wed, 30 Jul 2025 10:09:35 +0800 Subject: [PATCH] =?UTF-8?q?=E7=85=A7=E7=89=87=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../FlashSaleSystemApplication.java | 1 + .../org/flashsalesystem/config/WebConfig.java | 11 + .../controller/AdminController.java | 39 +++ .../service/FileUploadService.java | 203 +++++++++++++ src/main/resources/application.yml | 15 + .../webapp/WEB-INF/views/admin/flashsales.jsp | 45 ++- .../webapp/WEB-INF/views/admin/products.jsp | 277 ++++++++++++++---- src/main/webapp/WEB-INF/views/cart.jsp | 30 +- src/main/webapp/WEB-INF/views/flashsales.jsp | 28 +- src/main/webapp/WEB-INF/views/index.jsp | 32 +- src/main/webapp/WEB-INF/views/orders.jsp | 54 +++- 11 files changed, 657 insertions(+), 78 deletions(-) create mode 100644 src/main/java/com/org/flashsalesystem/service/FileUploadService.java diff --git a/src/main/java/com/org/flashsalesystem/FlashSaleSystemApplication.java b/src/main/java/com/org/flashsalesystem/FlashSaleSystemApplication.java index eca14a5..06d8c58 100644 --- a/src/main/java/com/org/flashsalesystem/FlashSaleSystemApplication.java +++ b/src/main/java/com/org/flashsalesystem/FlashSaleSystemApplication.java @@ -8,6 +8,7 @@ public class FlashSaleSystemApplication { public static void main(String[] args) { SpringApplication.run(FlashSaleSystemApplication.class, args); + System.out.println("http://localhost:8080"); } } diff --git a/src/main/java/com/org/flashsalesystem/config/WebConfig.java b/src/main/java/com/org/flashsalesystem/config/WebConfig.java index 9eec1e1..321a381 100644 --- a/src/main/java/com/org/flashsalesystem/config/WebConfig.java +++ b/src/main/java/com/org/flashsalesystem/config/WebConfig.java @@ -1,5 +1,6 @@ package com.org.flashsalesystem.config; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.ViewResolver; @@ -16,6 +17,12 @@ import org.springframework.web.servlet.view.JstlView; @Configuration public class WebConfig implements WebMvcConfigurer { + @Value("${flashsale.upload.path}") + private String uploadPath; + + @Value("${flashsale.upload.url-prefix}") + private String urlPrefix; + /** * JSP视图解析器 */ @@ -56,6 +63,10 @@ public class WebConfig implements WebMvcConfigurer { registry.addResourceHandler("/favicon.ico") .addResourceLocations("classpath:/META-INF/resources/"); + + // 添加上传文件的静态资源映射 + registry.addResourceHandler(urlPrefix + "**") + .addResourceLocations("file:" + uploadPath); } /** diff --git a/src/main/java/com/org/flashsalesystem/controller/AdminController.java b/src/main/java/com/org/flashsalesystem/controller/AdminController.java index deb175b..5302c0e 100644 --- a/src/main/java/com/org/flashsalesystem/controller/AdminController.java +++ b/src/main/java/com/org/flashsalesystem/controller/AdminController.java @@ -1,12 +1,14 @@ package com.org.flashsalesystem.controller; import com.org.flashsalesystem.service.AdminService; +import com.org.flashsalesystem.service.FileUploadService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; import java.util.HashMap; import java.util.Map; @@ -23,6 +25,9 @@ public class AdminController { @Autowired private AdminService adminService; + @Autowired + private FileUploadService fileUploadService; + /** * 获取仪表盘统计数据 */ @@ -449,4 +454,38 @@ public class AdminController { return ResponseEntity.badRequest().body(response); } } + + /** + * 上传商品图片 + */ + @Operation(summary = "上传商品图片") + @PostMapping("/products/upload-image") + public ResponseEntity> uploadProductImage(@RequestParam("file") MultipartFile file) { + try { + String imageUrl = fileUploadService.uploadProductImage(file); + + Map response = new HashMap<>(); + response.put("success", true); + response.put("message", "图片上传成功"); + response.put("imageUrl", imageUrl); + + return ResponseEntity.ok(response); + } catch (IllegalArgumentException e) { + log.error("图片上传失败 - 参数错误", e); + + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", e.getMessage()); + + return ResponseEntity.badRequest().body(response); + } catch (Exception e) { + log.error("图片上传失败", e); + + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", "图片上传失败: " + e.getMessage()); + + return ResponseEntity.badRequest().body(response); + } + } } diff --git a/src/main/java/com/org/flashsalesystem/service/FileUploadService.java b/src/main/java/com/org/flashsalesystem/service/FileUploadService.java new file mode 100644 index 0000000..9173db8 --- /dev/null +++ b/src/main/java/com/org/flashsalesystem/service/FileUploadService.java @@ -0,0 +1,203 @@ +package com.org.flashsalesystem.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import javax.annotation.PostConstruct; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * 文件上传服务类 + * 处理商品图片等文件的上传 + */ +@Service +@Slf4j +public class FileUploadService { + + private static final String IMAGE_DIR = "products/"; + private static final long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB + // 允许的图片格式 + private static final String[] ALLOWED_EXTENSIONS = { + "jpg", "jpeg", "png", "gif", "webp" + }; + @Value("${flashsale.upload.path}") + private String uploadPath; + @Value("${flashsale.upload.url-prefix}") + private String urlPrefix; + + /** + * 初始化上传目录 + */ + @PostConstruct + public void init() { + try { + // 创建上传根目录 + Path rootPath = Paths.get(uploadPath); + if (!Files.exists(rootPath)) { + Files.createDirectories(rootPath); + log.info("创建上传根目录: {}", uploadPath); + } + + // 创建商品图片目录 + Path productPath = Paths.get(uploadPath + IMAGE_DIR); + if (!Files.exists(productPath)) { + Files.createDirectories(productPath); + log.info("创建商品图片目录: {}", productPath); + } + } catch (IOException e) { + log.error("初始化上传目录失败", e); + throw new RuntimeException("初始化上传目录失败", e); + } + } + + /** + * 上传商品图片 + * + * @param file 上传的文件 + * + * @return 图片访问URL + */ + public String uploadProductImage(MultipartFile file) { + // 检查文件是否为空 + if (file == null || file.isEmpty()) { + throw new IllegalArgumentException("上传文件不能为空"); + } + + // 检查文件大小 + if (file.getSize() > MAX_FILE_SIZE) { + throw new IllegalArgumentException("文件大小不能超过10MB"); + } + + // 获取文件扩展名 + String originalFilename = file.getOriginalFilename(); + if (originalFilename == null || !originalFilename.contains(".")) { + throw new IllegalArgumentException("文件名格式不正确"); + } + + String extension = originalFilename.substring(originalFilename.lastIndexOf(".") + 1).toLowerCase(); + + // 检查文件格式 + if (!isAllowedExtension(extension)) { + throw new IllegalArgumentException("不支持的图片格式,仅支持: jpg, jpeg, png, gif, webp"); + } + + try { + // 生成新的文件名 + String newFileName = generateFileName(extension); + + // 按日期创建子目录 + String dateDir = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); + String relativePath = IMAGE_DIR + dateDir + "/" + newFileName; + + // 创建目标目录 + Path targetDir = Paths.get(uploadPath + IMAGE_DIR + dateDir); + if (!Files.exists(targetDir)) { + Files.createDirectories(targetDir); + } + + // 保存文件 + Path targetPath = Paths.get(uploadPath + relativePath); + file.transferTo(targetPath.toFile()); + + // 返回访问URL + String imageUrl = urlPrefix + relativePath; + log.info("文件上传成功: {} -> {}", originalFilename, imageUrl); + + return imageUrl; + + } catch (IOException e) { + log.error("文件上传失败", e); + throw new RuntimeException("文件上传失败: " + e.getMessage()); + } + } + + /** + * 删除商品图片 + * + * @param imageUrl 图片URL + */ + public void deleteProductImage(String imageUrl) { + if (imageUrl == null || imageUrl.isEmpty()) { + return; + } + + // 只处理本地上传的图片 + if (!imageUrl.startsWith(urlPrefix)) { + log.warn("非本地图片,跳过删除: {}", imageUrl); + return; + } + + try { + // 从URL中提取相对路径 + String relativePath = imageUrl.substring(urlPrefix.length()); + Path filePath = Paths.get(uploadPath + relativePath); + + if (Files.exists(filePath)) { + Files.delete(filePath); + log.info("删除图片成功: {}", imageUrl); + } else { + log.warn("图片文件不存在: {}", filePath); + } + } catch (IOException e) { + log.error("删除图片失败: {}", imageUrl, e); + } + } + + /** + * 生成唯一的文件名 + * + * @param extension 文件扩展名 + * + * @return 新文件名 + */ + private String generateFileName(String extension) { + return UUID.randomUUID().toString().replace("-", "") + "." + extension; + } + + /** + * 检查文件扩展名是否允许 + * + * @param extension 扩展名 + * + * @return 是否允许 + */ + private boolean isAllowedExtension(String extension) { + for (String allowed : ALLOWED_EXTENSIONS) { + if (allowed.equalsIgnoreCase(extension)) { + return true; + } + } + return false; + } + + /** + * 获取文件的MIME类型 + * + * @param extension 文件扩展名 + * + * @return MIME类型 + */ + public String getMimeType(String extension) { + switch (extension.toLowerCase()) { + case "jpg": + case "jpeg": + return "image/jpeg"; + case "png": + return "image/png"; + case "gif": + return "image/gif"; + case "webp": + return "image/webp"; + default: + return "application/octet-stream"; + } + } +} \ No newline at end of file diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 7bd11a2..a759860 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -60,6 +60,14 @@ spring: prefix: /WEB-INF/views/ suffix: .jsp + # 文件上传配置 + servlet: + multipart: + enabled: true + max-file-size: 10MB + max-request-size: 10MB + location: ${java.io.tmpdir} + # JSON配置 jackson: date-format: yyyy-MM-dd HH:mm:ss @@ -85,6 +93,13 @@ logging: # 自定义配置 flashsale: + # 文件上传配置 + upload: + # 文件上传路径 + path: ${user.home}/flashsale-uploads/ + # 访问URL前缀 + url-prefix: /uploads/ + # 秒杀配置 seckill: # 每个用户每个商品最大购买数量 diff --git a/src/main/webapp/WEB-INF/views/admin/flashsales.jsp b/src/main/webapp/WEB-INF/views/admin/flashsales.jsp index 3ca23cb..219cc47 100644 --- a/src/main/webapp/WEB-INF/views/admin/flashsales.jsp +++ b/src/main/webapp/WEB-INF/views/admin/flashsales.jsp @@ -97,6 +97,7 @@ ID + 图片 活动名称 商品 原价/秒杀价 @@ -109,7 +110,7 @@ - + 加载中... @@ -575,7 +576,7 @@ currentPage = page; // 显示加载状态 - $('#flashSalesTableBody').html(' 加载中...'); + $('#flashSalesTableBody').html(' 加载中...'); // 构建查询参数 const queryData = { @@ -616,12 +617,12 @@ renderFlashSalesTable(response.data.content || response.data.flashSales || []); renderPagination(response.data.totalElements || response.data.total || 0, pageSize); } else { - $('#flashSalesTableBody').html('获取秒杀数据失败: ' + response.message + ''); + $('#flashSalesTableBody').html('获取秒杀数据失败: ' + response.message + ''); } }, error: function (xhr, status, error) { console.error('获取秒杀列表失败:', error); - $('#flashSalesTableBody').html('网络请求失败,请稍后重试'); + $('#flashSalesTableBody').html('网络请求失败,请稍后重试'); } }); } @@ -630,7 +631,7 @@ let html = ''; if (flashSales.length === 0) { - html = '暂无秒杀活动'; + html = '暂无秒杀活动'; } else { flashSales.forEach(flashSale => { const statusText = getStatusText(flashSale.status); @@ -639,6 +640,12 @@ html += ` ` + flashSale.id + ` + + ` + (flashSale.productName || '商品') + ` +
` + (flashSale.productName || '秒杀活动') + `
` + (flashSale.statusDescription || '') + ` @@ -739,7 +746,7 @@ } function refreshFlashSales() { - $('#flashSalesTableBody').html(' 加载中...'); + $('#flashSalesTableBody').html(' 加载中...'); loadFlashSales(currentPage); } @@ -1173,6 +1180,32 @@ return ''; } } + + // 获取商品图片URL + function getProductImageUrl(imageUrl) { + // 如果没有图片URL或为空,返回默认图片 + if (!imageUrl || imageUrl.trim() === '') { + return '${pageContext.request.contextPath}/images/default-product.svg'; + } + + // 如果是相对路径,添加上下文路径 + if (imageUrl.startsWith('/images/')) { + return '${pageContext.request.contextPath}' + imageUrl; + } + + // 如果是上传的图片(以/uploads/开头) + if (imageUrl.startsWith('/uploads/')) { + return '${pageContext.request.contextPath}' + imageUrl; + } + + // 如果是完整的URL(http或https),直接返回 + if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { + return imageUrl; + } + + // 其他情况,当作相对路径处理 + return '${pageContext.request.contextPath}/images/' + imageUrl; + } <%@ include file="../common/footer.jsp" %> diff --git a/src/main/webapp/WEB-INF/views/admin/products.jsp b/src/main/webapp/WEB-INF/views/admin/products.jsp index 73f8197..4c92a01 100644 --- a/src/main/webapp/WEB-INF/views/admin/products.jsp +++ b/src/main/webapp/WEB-INF/views/admin/products.jsp @@ -216,9 +216,18 @@
- - + +
+
+ + + 支持格式: JPG, JPEG, PNG, GIF, WEBP (最大10MB) +
+
+ 预览图片 +
+
@@ -281,8 +290,22 @@
- - + +
+
+ + + 支持格式: JPG, JPEG, PNG, GIF, WEBP (最大10MB) +
+
+ 预览图片 +
+ 当前图片:
+ 当前图片 +
+
+
@@ -631,25 +654,63 @@ loadProducts(1); } - function saveProduct() { - const productData = { - name: $('#productName').val(), - price: $('#productPrice').val(), - stock: $('#productStock').val(), - status: $('#productStatus').val(), - description: $('#productDescription').val(), - imageUrl: $('#productImage').val() - }; + async function saveProduct() { + try { + // 显示加载状态 + const saveBtn = $('#addProductModal .btn-primary'); + const originalText = saveBtn.text(); + saveBtn.prop('disabled', true).html(' 保存中...'); - console.log('保存商品:', productData); + // 检查是否有选择图片 + const imageFile = $('#productImageFile')[0].files[0]; + let imageUrl = ''; - // 模拟API调用 - setTimeout(function () { - $('#addProductModal').modal('hide'); - $('#addProductForm')[0].reset(); - alert('商品添加成功!'); - refreshProducts(); - }, 1000); + if (imageFile) { + try { + imageUrl = await uploadImage(imageFile); + } catch (error) { + alert('图片上传失败: ' + error); + saveBtn.prop('disabled', false).text(originalText); + return; + } + } + + const productData = { + name: $('#productName').val(), + price: parseFloat($('#productPrice').val()), + stock: parseInt($('#productStock').val()), + status: parseInt($('#productStatus').val()), + description: $('#productDescription').val(), + imageUrl: imageUrl + }; + + // 调用API保存商品 + $.ajax({ + url: '${pageContext.request.contextPath}/api/admin/products', + type: 'POST', + contentType: 'application/json', + data: JSON.stringify(productData), + success: function (response) { + if (response.success) { + $('#addProductModal').modal('hide'); + $('#addProductForm')[0].reset(); + $('#addProductImagePreview').addClass('d-none'); + showAlert('success', '商品添加成功!'); + loadProducts(currentPage); + } else { + alert('保存失败: ' + response.message); + } + saveBtn.prop('disabled', false).text(originalText); + }, + error: function (xhr, status, error) { + alert('网络错误: ' + error); + saveBtn.prop('disabled', false).text(originalText); + } + }); + } catch (error) { + console.error('保存商品失败:', error); + alert('保存失败: ' + error); + } } function editProduct(id) { @@ -708,8 +769,21 @@
- - + +
+
+ + + 支持格式: JPG, JPEG, PNG, GIF, WEBP (最大10MB) +
+
+ 预览图片 +
+ 当前图片:
+ 当前图片 +
+
+
`); @@ -722,6 +796,13 @@ $('#editProductStatus').val(product.status); $('#editProductDescription').val(product.description || ''); $('#editProductImage').val(product.imageUrl || ''); + + // 显示当前图片 + if (product.imageUrl) { + const imageUrl = getProductImageUrl(product.imageUrl); + $('#currentProductImage').attr('src', imageUrl); + $('#currentImageContainer').removeClass('d-none'); + } } else { $('#editProductModal .modal-body').html('
获取商品信息失败: ' + response.message + '
'); } @@ -731,47 +812,63 @@ }); } - function updateProduct() { - const productId = $('#editProductId').val(); - const productData = { - name: $('#editProductName').val(), - price: parseFloat($('#editProductPrice').val()), - stock: parseInt($('#editProductStock').val()), - status: parseInt($('#editProductStatus').val()), - description: $('#editProductDescription').val(), - imageUrl: $('#editProductImage').val() - }; + async function updateProduct() { + try { + const productId = $('#editProductId').val(); - console.log('更新商品:', productData); + // 显示加载状态 + const updateBtn = $('#editProductModal .btn-primary'); + const originalText = updateBtn.text(); + updateBtn.prop('disabled', true).html(' 更新中...'); - // 显示加载状态 - const updateBtn = $('#editProductModal .btn-primary'); - const originalText = updateBtn.text(); - updateBtn.prop('disabled', true).html(' 更新中...'); + // 检查是否有新选择的图片 + const imageFile = $('#editProductImageFile')[0].files[0]; + let imageUrl = $('#editProductImage').val(); // 保持原有图片URL - // 调用真实API - $.ajax({ - url: '${pageContext.request.contextPath}/api/admin/products/' + productId, - type: 'PUT', - contentType: 'application/json', - data: JSON.stringify(productData), - success: function (response) { - if (response.success) { - $('#editProductModal').modal('hide'); - showAlert('success', '商品更新成功!'); - // 只更新当前页面,不刷新整个列表 - loadProducts(currentPage); - } else { - showAlert('danger', '更新失败: ' + response.message); + if (imageFile) { + try { + imageUrl = await uploadImage(imageFile); + } catch (error) { + alert('图片上传失败: ' + error); + updateBtn.prop('disabled', false).text(originalText); + return; } - }, - error: function () { - showAlert('danger', '网络请求失败,请稍后重试'); - }, - complete: function () { - updateBtn.prop('disabled', false).text(originalText); } - }); + + const productData = { + name: $('#editProductName').val(), + price: parseFloat($('#editProductPrice').val()), + stock: parseInt($('#editProductStock').val()), + status: parseInt($('#editProductStatus').val()), + description: $('#editProductDescription').val(), + imageUrl: imageUrl + }; + + // 调用真实API + $.ajax({ + url: '${pageContext.request.contextPath}/api/admin/products/' + productId, + type: 'PUT', + contentType: 'application/json', + data: JSON.stringify(productData), + success: function (response) { + if (response.success) { + $('#editProductModal').modal('hide'); + showAlert('success', '商品更新成功!'); + loadProducts(currentPage); + } else { + alert('更新失败: ' + response.message); + } + updateBtn.prop('disabled', false).text(originalText); + }, + error: function (xhr, status, error) { + alert('网络错误: ' + error); + updateBtn.prop('disabled', false).text(originalText); + } + }); + } catch (error) { + console.error('更新商品失败:', error); + alert('更新失败: ' + error); + } } function deleteProduct(id) { @@ -1171,6 +1268,11 @@ return '${pageContext.request.contextPath}' + imageUrl; } + // 如果是上传的图片(以/uploads/开头) + if (imageUrl.startsWith('/uploads/')) { + return '${pageContext.request.contextPath}' + imageUrl; + } + // 如果是完整的URL(http或https),直接返回 if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { return imageUrl; @@ -1179,6 +1281,61 @@ // 其他情况,当作相对路径处理 return '${pageContext.request.contextPath}/images/' + imageUrl; } + + // 图片预览功能 + function previewImage(input, previewId) { + if (input.files && input.files[0]) { + const file = input.files[0]; + const reader = new FileReader(); + + // 检查文件大小 + if (file.size > 10 * 1024 * 1024) { + alert('图片大小不能超过10MB'); + input.value = ''; + return; + } + + // 检查文件类型 + const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp']; + if (!allowedTypes.includes(file.type)) { + alert('不支持的图片格式,仅支持: JPG, JPEG, PNG, GIF, WEBP'); + input.value = ''; + return; + } + + reader.onload = function (e) { + $('#' + previewId).attr('src', e.target.result).removeClass('d-none'); + } + + reader.readAsDataURL(file); + } + } + + // 上传图片到服务器 + function uploadImage(file) { + return new Promise((resolve, reject) => { + const formData = new FormData(); + formData.append('file', file); + + $.ajax({ + url: '${pageContext.request.contextPath}/api/admin/products/upload-image', + type: 'POST', + data: formData, + processData: false, + contentType: false, + success: function (response) { + if (response.success) { + resolve(response.imageUrl); + } else { + reject(response.message || '图片上传失败'); + } + }, + error: function (xhr, status, error) { + reject('网络错误:' + error); + } + }); + }); + } <%@ include file="../common/footer.jsp" %> diff --git a/src/main/webapp/WEB-INF/views/cart.jsp b/src/main/webapp/WEB-INF/views/cart.jsp index 3d0cb02..be9c06e 100644 --- a/src/main/webapp/WEB-INF/views/cart.jsp +++ b/src/main/webapp/WEB-INF/views/cart.jsp @@ -171,7 +171,7 @@
- ` + item.productName + `
@@ -499,7 +499,7 @@ html += `
- ` + product.name + `
@@ -593,6 +593,32 @@ } }); } + + // 获取商品图片URL + function getProductImageUrl(imageUrl) { + // 如果没有图片URL或为空,返回默认图片 + if (!imageUrl || imageUrl.trim() === '') { + return '${pageContext.request.contextPath}/images/default-product.svg'; + } + + // 如果是相对路径,添加上下文路径 + if (imageUrl.startsWith('/images/')) { + return '${pageContext.request.contextPath}' + imageUrl; + } + + // 如果是上传的图片(以/uploads/开头) + if (imageUrl.startsWith('/uploads/')) { + return '${pageContext.request.contextPath}' + imageUrl; + } + + // 如果是完整的URL(http或https),直接返回 + if (imageUrl.startsWith('http://') || imageUrl.startsWith('https://')) { + return imageUrl; + } + + // 其他情况,当作相对路径处理 + return '${pageContext.request.contextPath}/images/' + imageUrl; + }