From 8a1d2cd97377366c7321f874aea52b2b203bafdb Mon Sep 17 00:00:00 2001 From: Richie <280645618@qq.com> Date: Sun, 2 Mar 2025 11:06:22 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=8A=E4=BC=A0=E5=9B=BE?= =?UTF-8?q?=E7=89=87=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 74 ++++---- .../cn/qihangerp/common/utils/FileUtils.java | 174 ++++++++++++++++++ sys-api/pom.xml | 5 + .../sys/controller/ImageUploadController.java | 87 +++++++++ sys-api/src/main/resources/config.properties | 4 + vue/src/components/ImageUpload/index.vue | 2 +- vue/src/views/goods/create.vue | 18 +- 7 files changed, 314 insertions(+), 50 deletions(-) create mode 100644 common/src/main/java/cn/qihangerp/common/utils/FileUtils.java create mode 100644 sys-api/src/main/java/cn/qihangerp/sys/controller/ImageUploadController.java create mode 100644 sys-api/src/main/resources/config.properties diff --git a/README.md b/README.md index 213d90aa..764fd236 100644 --- a/README.md +++ b/README.md @@ -87,20 +87,7 @@ graph TD + Jdk:17 + Nodejs:v16.20.0 -#### 1.2 项目组件 -##### 后端核心组件 -+ SpringBoot:3.0.2 - + spring-boot-starter-security -+ SpringCloudAlibaba:2022.0.0.0 - + Nacos - + SpringCloud Gateway - + spring-cloud-starter-loadbalancer - -##### 前端框架及组件 -+ vue2 -+ element - -#### 1.3、存储及中间件 +#### 1.2、存储及中间件 + MySQL8 + Redis:7.x @@ -108,34 +95,32 @@ graph TD + Nacos:2.2.0(配置中心、注册中心) + Sentinel(分布式流量治理组件) - `java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar` -#### 启动KRaft模式kafka -+ 0 进入kafka解压目录 -+ 1 生成UUID`bin\windows\kafka-storage.bat random-uuid` -+ 2 格式化`bin\windows\kafka-storage.bat format -t ujpyXZx-S9-jGlwxgORmow -c config\kraft\server.properties` -+ 3 启动`bin\windows\kafka-server-start.bat config\kraft\server.properties` + ### 2、项目结构 -#### 2.1 core -项目公共模块包括: +#### 2.1 公共版本 ++`common` +项目公共模块 -+ `common`:公共类型 ++ `security` +公共权限验证模块 -+ `security`:公共权限验证模块 - -#### 2.2 gateway ++ `goods` +商品模块 +#### 2.2 微服务 ++ `gateway` 网关项目,负责微服务接口转发,前端统一通过网关调用其他微服务接口; 采用`gateway`进行api分发,引入Sentinel进行流量治理。 -#### 2.3 sys-api ++ `sys-api` 项目系统微服务,主要功能包括: + 用户 + 菜单 -#### 2.4 oms-api ++ `oms-api` oms主功能微服务,主要功能包括: + 队列消息处理(订单消息、退款消息) @@ -143,7 +128,7 @@ oms主功能微服务,主要功能包括: + 退款接口 + 店铺接口 -#### 2.5 open-api ++ `open-api` 各开放平台微服务 + 淘宝开放平台接口api @@ -158,7 +143,7 @@ oms主功能微服务,主要功能包括: + 拼多多开放平台接口api -+微信视频号小店开放平台接口api ++ 微信小店开放平台接口api @@ -169,10 +154,19 @@ oms主功能微服务,主要功能包括: #### 3.1、启动环境 1. 启动MySQL8 + 2. 启动Redis7 -3. 启动Sentinel1.8.7控制台 -4. 启动Nacos -5. 启动Kafka + +3. 启动Sentinel1.8.7控制台(可以不需要) + `java -Dserver.port=8888 -Dcsp.sentinel.dashboard.server=localhost:8888 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar` +4. 启动Nacos(注册中心) + +5. 启动Kafka(消息队列) +`启动KRaft模式kafka` + + 0 进入kafka解压目录 + + 1 生成UUID`bin\windows\kafka-storage.bat random-uuid` + + 2 格式化`bin\windows\kafka-storage.bat format -t ujpyXZx-S9-jGlwxgORmow -c config\kraft\server.properties` + + 3 启动`bin\windows\kafka-server-start.bat config\kraft\server.properties` #### 3.2、导入数据库 + 创建数据库`qihang-oms` @@ -180,16 +174,16 @@ oms主功能微服务,主要功能包括: #### 3.3、启动服务(项目) -1. 启动开放平台微服务(open-api) -2. 启动sys-api、oms-api微服务 -3. 启动微服务网关(api) +1. 启动开放平台微服务(`open-api`) +2. 启动`sys-api`、`oms-api`微服务 +3. 启动微服务网关(`gateway`) #### 3.4、运行前端 + Nodejs版本:v16.20.0 + 进入`vue`文件夹 + 运行`npm install` + 运行`npm run dev` -+ 浏览网页`http://localhost` ++ 浏览网页`http://localhost:88` ### 4、项目部署 @@ -207,9 +201,9 @@ oms主功能微服务,主要功能包括: # 上传文件至远程服务器 将打包生成在 `dist` 目录下的文件拷贝至 `/usr/share/nginx/html` 目录 -# nginx.cofig 配置 +# nginx.cofig 配置(主要是配置接口转发) server { - listen 80; + listen 88; server_name localhost; location / { root /usr/share/nginx/html; @@ -217,7 +211,7 @@ server { } # 反向代理配置 location /prod-api/ { - proxy_pass http://127.0.0.1:8080/; # 替换成你的后端网关API地址 + proxy_pass http://127.0.0.1:8088/; # 替换成你的后端网关API地址 } } ``` diff --git a/common/src/main/java/cn/qihangerp/common/utils/FileUtils.java b/common/src/main/java/cn/qihangerp/common/utils/FileUtils.java new file mode 100644 index 00000000..38344a52 --- /dev/null +++ b/common/src/main/java/cn/qihangerp/common/utils/FileUtils.java @@ -0,0 +1,174 @@ +package cn.qihangerp.common.utils; + +import com.alibaba.fastjson2.util.IOUtils; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +import java.io.*; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +/** + * 文件处理工具类 + * + * @author qihang + */ +public class FileUtils +{ + public static String FILENAME_PATTERN = "[a-zA-Z0-9_\\-\\|\\.\\u4e00-\\u9fa5]+"; + + /** + * 输出指定文件的byte数组 + * + * @param filePath 文件路径 + * @param os 输出流 + * @return + */ + public static void writeBytes(String filePath, OutputStream os) throws IOException + { + FileInputStream fis = null; + try + { + File file = new File(filePath); + if (!file.exists()) + { + throw new FileNotFoundException(filePath); + } + fis = new FileInputStream(file); + byte[] b = new byte[1024]; + int length; + while ((length = fis.read(b)) > 0) + { + os.write(b, 0, length); + } + } + catch (IOException e) + { + throw e; + } + finally + { + IOUtils.close(os); + IOUtils.close(fis); + } + } + + /** + * 下载文件名重新编码 + * + * @param request 请求对象 + * @param fileName 文件名 + * @return 编码后的文件名 + */ + public static String setFileDownloadHeader(HttpServletRequest request, String fileName) throws UnsupportedEncodingException + { + final String agent = request.getHeader("USER-AGENT"); + String filename = fileName; + if (agent.contains("MSIE")) + { + // IE浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + filename = filename.replace("+", " "); + } + else if (agent.contains("Firefox")) + { + // 火狐浏览器 + filename = new String(fileName.getBytes(), "ISO8859-1"); + } + else if (agent.contains("Chrome")) + { + // google浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + else + { + // 其它浏览器 + filename = URLEncoder.encode(filename, "utf-8"); + } + return filename; + } + + /** + * 下载文件名重新编码 + * + * @param response 响应对象 + * @param realFileName 真实文件名 + */ + public static void setAttachmentResponseHeader(HttpServletResponse response, String realFileName) throws UnsupportedEncodingException + { + String percentEncodedFileName = percentEncode(realFileName); + + StringBuilder contentDispositionValue = new StringBuilder(); + contentDispositionValue.append("attachment; filename=") + .append(percentEncodedFileName) + .append(";") + .append("filename*=") + .append("utf-8''") + .append(percentEncodedFileName); + + response.addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename"); + response.setHeader("Content-disposition", contentDispositionValue.toString()); + response.setHeader("download-filename", percentEncodedFileName); + } + + /** + * 百分号编码工具方法 + * + * @param s 需要百分号编码的字符串 + * @return 百分号编码后的字符串 + */ + public static String percentEncode(String s) throws UnsupportedEncodingException + { + String encode = URLEncoder.encode(s, StandardCharsets.UTF_8.toString()); + return encode.replaceAll("\\+", "%20"); + } + + /** + * 获取图像后缀 + * + * @param photoByte 图像数据 + * @return 后缀名 + */ + public static String getFileExtendName(byte[] photoByte) + { + String strFileExtendName = "jpg"; + if ((photoByte[0] == 71) && (photoByte[1] == 73) && (photoByte[2] == 70) && (photoByte[3] == 56) + && ((photoByte[4] == 55) || (photoByte[4] == 57)) && (photoByte[5] == 97)) + { + strFileExtendName = "gif"; + } + else if ((photoByte[6] == 74) && (photoByte[7] == 70) && (photoByte[8] == 73) && (photoByte[9] == 70)) + { + strFileExtendName = "jpg"; + } + else if ((photoByte[0] == 66) && (photoByte[1] == 77)) + { + strFileExtendName = "bmp"; + } + else if ((photoByte[1] == 80) && (photoByte[2] == 78) && (photoByte[3] == 71)) + { + strFileExtendName = "png"; + } + return strFileExtendName; + } + + /** + * 获取文件名称 /profile/upload/2022/04/16/zhijian.png -- zhijian.png + * + * @param fileName 路径名称 + * @return 没有文件路径的名称 + */ + public static String getName(String fileName) + { + if (fileName == null) + { + return null; + } + int lastUnixPos = fileName.lastIndexOf('/'); + int lastWindowsPos = fileName.lastIndexOf('\\'); + int index = Math.max(lastUnixPos, lastWindowsPos); + return fileName.substring(index + 1); + } + + +} diff --git a/sys-api/pom.xml b/sys-api/pom.xml index a54a4be6..9155d88b 100644 --- a/sys-api/pom.xml +++ b/sys-api/pom.xml @@ -61,6 +61,11 @@ com.alibaba.cloud spring-cloud-starter-alibaba-nacos-discovery + + com.qiniu + qiniu-java-sdk + [7.2.0, 7.2.99] + junit junit diff --git a/sys-api/src/main/java/cn/qihangerp/sys/controller/ImageUploadController.java b/sys-api/src/main/java/cn/qihangerp/sys/controller/ImageUploadController.java new file mode 100644 index 00000000..e0396398 --- /dev/null +++ b/sys-api/src/main/java/cn/qihangerp/sys/controller/ImageUploadController.java @@ -0,0 +1,87 @@ +package cn.qihangerp.sys.controller; + + +import cn.qihangerp.common.common.AjaxResult; +import cn.qihangerp.common.utils.FileUtils; +import com.alibaba.fastjson2.JSONObject; +import com.qiniu.common.QiniuException; +import com.qiniu.common.Zone; +import com.qiniu.http.Response; +import com.qiniu.storage.Configuration; +import com.qiniu.storage.UploadManager; +import com.qiniu.util.Auth; +import org.apache.commons.lang3.StringUtils; +import org.springframework.core.io.support.PropertiesLoaderUtils; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Properties; + +@RestController +public class ImageUploadController { + + @RequestMapping("/images/upload") + public AjaxResult uploadImage(MultipartFile file) throws IOException { + if (file.isEmpty()) return AjaxResult.error(400, "请选择图片"); + String fileSuffix = file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".") + 1, file.getOriginalFilename().length()); + ArrayList a = new ArrayList<>(); + a.add("gif"); + a.add("jpg"); + a.add("jpeg"); + a.add("png"); + if (a.contains(fileSuffix) == false) return AjaxResult.error(400, "不支持的格式"); + String fileName = file.getOriginalFilename(); + //appid配置 + Properties properties = PropertiesLoaderUtils.loadAllProperties("config.properties"); + String qiniu_img_domain = properties.getProperty("qiniu_img_domain"); + String qiniu_access_key = properties.getProperty("qiniu_access_key"); + String qiniu_secret_key = properties.getProperty("qiniu_secret_key"); + String qiniu_bucket = properties.getProperty("qiniu_bucket"); + if(StringUtils.isBlank(qiniu_img_domain)) return AjaxResult.error("请配置七牛云参数"); + if(StringUtils.isBlank(qiniu_access_key)) return AjaxResult.error("请配置七牛云参数"); + if(StringUtils.isBlank(qiniu_secret_key)) return AjaxResult.error("请配置七牛云参数"); + if(StringUtils.isBlank(qiniu_bucket)) return AjaxResult.error("请配置七牛云参数"); + + Auth auth = Auth.create(qiniu_access_key, qiniu_secret_key); + + String upToken = auth.uploadToken(qiniu_bucket);//上传的凭据 + + //构造一个带指定Zone对象的配置类 + Configuration cfg = new Configuration(Zone.zone0()); + + UploadManager uploadManager = new UploadManager(cfg); + //默认不指定key的情况下,以文件内容的hash值作为文件名 + String key = null; +// Response response = uploadManager.put(file, key, upToken); + try { + Response response = uploadManager.put(file.getInputStream(), key, upToken, null, null); + //解析上传成功的结果 + var res = JSONObject.parseObject(response.bodyString()); + + String url = qiniu_img_domain + res.getString("key"); + AjaxResult ajax = AjaxResult.success(); + ajax.put("url", url); + ajax.put("fileName", fileName); + ajax.put("newFileName", FileUtils.getName(fileName)); + ajax.put("originalFilename", file.getOriginalFilename()); + return ajax; + } catch (QiniuException ex) { + Response r = ex.response; + + System.err.println(r.toString()); + try { + System.err.println(r.bodyString()); + return AjaxResult.error(500, r.bodyString()); + } catch (QiniuException ex2) { + //ignore + return AjaxResult.error(500, "图片上传错误"); + } + } catch (IOException e) { + e.printStackTrace(); + return AjaxResult.error(400, "图片格式错误"); + } + } +} diff --git a/sys-api/src/main/resources/config.properties b/sys-api/src/main/resources/config.properties new file mode 100644 index 00000000..5fda5155 --- /dev/null +++ b/sys-api/src/main/resources/config.properties @@ -0,0 +1,4 @@ +qiniu_img_domain=http://img.huayiyungou.com/ +qiniu_access_key= +qiniu_secret_key=utM-_huV78h7GaWWsxKDl97P5EFK5jmb0ba-3HIG +qiniu_bucket=ttxs100-cn-files \ No newline at end of file diff --git a/vue/src/components/ImageUpload/index.vue b/vue/src/components/ImageUpload/index.vue index 1af622a3..1e24b2c1 100644 --- a/vue/src/components/ImageUpload/index.vue +++ b/vue/src/components/ImageUpload/index.vue @@ -77,7 +77,7 @@ export default { dialogVisible: false, hideUpload: false, baseUrl: '',// process.env.VUE_APP_BASE_API, - uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址 + uploadImgUrl: process.env.VUE_APP_BASE_API + "/api/sys-api/images/upload", // 上传的图片服务器地址 headers: { Authorization: "Bearer " + getToken(), }, diff --git a/vue/src/views/goods/create.vue b/vue/src/views/goods/create.vue index cc451833..96411428 100644 --- a/vue/src/views/goods/create.vue +++ b/vue/src/views/goods/create.vue @@ -15,7 +15,7 @@ - + @@ -25,7 +25,7 @@ - + - - - - - - + + + + + + @@ -239,7 +239,7 @@ export default { components: { Treeselect }, data() { return { - uploadImgUrl: process.env.VUE_APP_BASE_API + "/api/oms-api/images/upload", + uploadImgUrl: process.env.VUE_APP_BASE_API + "/api/sys-api/images/upload", headers: { Authorization: "Bearer " + getToken(), },