From ded05d4e785a6635a79202eb4ac211533a3c93cf Mon Sep 17 00:00:00 2001 From: "925116093-qq.com" <925116093@qq.com> Date: Sat, 7 Jun 2025 18:16:03 +0800 Subject: [PATCH] 202506071815 --- build_verification.bat | 41 + pom.xml | 16 + ruoyi-admin/pom.xml | 6 + .../controller/common/CommonController.java | 82 +- .../controller/common/QiniuController.java | 294 +++++++ .../src/main/resources/application.yml | 15 + ruoyi-auth-common/pom.xml | 33 + .../ruoyi/auth/common/domain/OauthUser.java | 293 +++++++ .../common/enums/OauthVerificationUse.java | 59 ++ .../auth/common/mapper/OauthUserMapper.java | 112 +++ .../common/service/IOauthUserService.java | 102 +++ .../service/OauthVerificationCodeService.java | 15 + .../ruoyi/auth/common/service/TfaService.java | 73 ++ .../service/impl/OauthUserServiceImpl.java | 149 ++++ .../auth/common/utils/RandomCodeUtil.java | 28 + .../mapper/common/OauthUserMapper.xml | 169 ++++ .../com/ruoyi/common/utils/DateUtils.java | 2 +- .../framework/config/SecurityConfig.java | 5 +- ruoyi-oauth-wx/pom.xml | 29 + .../oauth/wx/constant/WxMiniAppConstant.java | 42 + .../oauth/wx/constant/WxPubConstant.java | 41 + .../wx/controller/WxLoginController.java | 69 ++ .../Impl/WxMiniAppLoginServiceImpl.java | 76 ++ .../service/Impl/WxPubLoginServiceImpl.java | 77 ++ .../oauth/wx/service/WxLoginService.java | 34 + ...itional-spring-configuration-metadata.json | 20 + ruoyi-system/pom.xml | 14 + .../com/ruoyi/system/config/QiniuConfig.java | 92 ++ .../system/controller/AppletController.java | 709 ++++++++++++++++ .../controller/ServiceGoodsController.java | 104 ++- .../controllerUtil/AppletControllerUtil.java | 798 ++++++++++++++++++ .../system/controllerUtil/WechatApiUtil.java | 362 ++++++++ .../system/controllerUtil/WechatPayUtil.java | 787 +++++++++++++++++ .../com/ruoyi/system/mapper/UsersMapper.java | 15 + .../ruoyi/system/service/IUsersService.java | 15 + .../system/service/impl/UsersServiceImpl.java | 25 + .../ruoyi/system/utils/QiniuUploadUtil.java | 360 ++++++++ .../mapper/system/ServiceCateMapper.xml | 4 +- .../resources/mapper/system/UsersMapper.xml | 13 +- ruoyi-ui/src/components/Editor/index.vue | 13 +- ruoyi-ui/src/components/FileUpload/index.vue | 12 +- ruoyi-ui/src/components/ImageUpload/index.vue | 31 +- .../src/views/system/GoodsShangPin/index.vue | 180 ++-- .../views/system/IntegralProduct/index.vue | 78 +- .../src/views/system/ServiceGoods/index.vue | 730 +++++++++++++--- 45 files changed, 5985 insertions(+), 239 deletions(-) create mode 100644 build_verification.bat create mode 100644 ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/QiniuController.java create mode 100644 ruoyi-auth-common/pom.xml create mode 100644 ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/domain/OauthUser.java create mode 100644 ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/enums/OauthVerificationUse.java create mode 100644 ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/mapper/OauthUserMapper.java create mode 100644 ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/IOauthUserService.java create mode 100644 ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/OauthVerificationCodeService.java create mode 100644 ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/TfaService.java create mode 100644 ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/impl/OauthUserServiceImpl.java create mode 100644 ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/utils/RandomCodeUtil.java create mode 100644 ruoyi-auth-common/src/main/resources/mapper/common/OauthUserMapper.xml create mode 100644 ruoyi-oauth-wx/pom.xml create mode 100644 ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/constant/WxMiniAppConstant.java create mode 100644 ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/constant/WxPubConstant.java create mode 100644 ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/controller/WxLoginController.java create mode 100644 ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/Impl/WxMiniAppLoginServiceImpl.java create mode 100644 ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/Impl/WxPubLoginServiceImpl.java create mode 100644 ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/WxLoginService.java create mode 100644 ruoyi-oauth-wx/src/main/resources/META-INF/additional-spring-configuration-metadata.json create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/config/QiniuConfig.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/controller/AppletController.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/AppletControllerUtil.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatApiUtil.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatPayUtil.java create mode 100644 ruoyi-system/src/main/java/com/ruoyi/system/utils/QiniuUploadUtil.java diff --git a/build_verification.bat b/build_verification.bat new file mode 100644 index 0000000..b9ff7cb --- /dev/null +++ b/build_verification.bat @@ -0,0 +1,41 @@ +@echo off +echo 开始验证项目编译... + +echo. +echo [1/4] 编译后端项目... +cd /d "%~dp0" +call mvn clean compile -DskipTests=true +if %errorlevel% neq 0 ( + echo 后端编译失败! + pause + exit /b 1 +) + +echo. +echo [2/4] 安装前端依赖... +cd ruoyi-ui +call npm install +if %errorlevel% neq 0 ( + echo 前端依赖安装失败! + pause + exit /b 1 +) + +echo. +echo [3/4] 编译前端项目... +call npm run build:prod +if %errorlevel% neq 0 ( + echo 前端编译失败! + pause + exit /b 1 +) + +echo. +echo [4/4] 验证完成! +echo 所有修改已通过编译验证。 +echo. +echo 下一步可以启动服务进行功能测试: +echo 1. 启动后端:mvn spring-boot:run -pl ruoyi-admin +echo 2. 启动前端:cd ruoyi-ui && npm run dev +echo. +pause \ No newline at end of file diff --git a/pom.xml b/pom.xml index 8f73aa3..7c1c335 100644 --- a/pom.xml +++ b/pom.xml @@ -30,6 +30,7 @@ 4.1.2 2.3 0.9.1 + 7.15.1 9.0.102 1.2.13 @@ -204,6 +205,13 @@ ${ruoyi.version} + + + com.ruoyi + ruoyi-auth-common + ${ruoyi.version} + + com.ruoyi @@ -232,6 +240,13 @@ 1.2.83 + + + com.qiniu + qiniu-java-sdk + ${qiniu.version} + + @@ -242,6 +257,7 @@ ruoyi-quartz ruoyi-generator ruoyi-common + ruoyi-oauth-wx pom diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml index 7d75d03..fd4744e 100644 --- a/ruoyi-admin/pom.xml +++ b/ruoyi-admin/pom.xml @@ -61,6 +61,12 @@ ruoyi-generator + + + com.qiniu + qiniu-java-sdk + + diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java index eb21326..a9ccb70 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java @@ -20,6 +20,8 @@ import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.common.utils.file.FileUtils; import com.ruoyi.framework.config.ServerConfig; +import com.ruoyi.system.utils.QiniuUploadUtil; +import com.ruoyi.system.config.QiniuConfig; /** * 通用请求处理 @@ -34,6 +36,9 @@ public class CommonController @Autowired private ServerConfig serverConfig; + + @Autowired + private QiniuConfig qiniuConfig; private static final String FILE_DELIMETER = ","; @@ -71,23 +76,41 @@ public class CommonController /** * 通用上传请求(单个) + * 根据配置选择使用七牛云上传或本地上传 */ @PostMapping("/upload") public AjaxResult uploadFile(MultipartFile file) throws Exception { try { - // 上传文件路径 - String filePath = RuoYiConfig.getUploadPath(); - // 上传并返回新文件名称 - String fileName = FileUploadUtils.upload(filePath, file); - String url = "https://img.huafurenjia.cn" + fileName; - 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; + log.info("1上传文件开始"); + // 检查是否启用七牛云上传 + if (qiniuConfig.isEnabled()) + { + log.info("2上传文件开始"); + // 使用七牛云上传 + String fileUrl = QiniuUploadUtil.uploadFile(file); + AjaxResult ajax = AjaxResult.success(); + ajax.put("url", fileUrl); + ajax.put("fileName", fileUrl); + ajax.put("newFileName", FileUtils.getName(file.getOriginalFilename())); + ajax.put("originalFilename", file.getOriginalFilename()); + return ajax; + } + else + { + log.info("3上传文件开始"); + // 使用本地上传 + String filePath = RuoYiConfig.getUploadPath(); + String fileName = FileUploadUtils.upload(filePath, file); + String url = fileName; + 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 (Exception e) { @@ -97,28 +120,45 @@ public class CommonController /** * 通用上传请求(多个) + * 根据配置选择使用七牛云上传或本地上传 */ @PostMapping("/uploads") public AjaxResult uploadFiles(List files) throws Exception { try { - // 上传文件路径 - String filePath = RuoYiConfig.getUploadPath(); List urls = new ArrayList(); List fileNames = new ArrayList(); List newFileNames = new ArrayList(); List originalFilenames = new ArrayList(); - for (MultipartFile file : files) + + if (qiniuConfig.isEnabled()) { - // 上传并返回新文件名称 - String fileName = FileUploadUtils.upload(filePath, file); - String url = "https://img.huafurenjia.cn" + fileName; - urls.add(url); - fileNames.add(fileName); - newFileNames.add(FileUtils.getName(fileName)); - originalFilenames.add(file.getOriginalFilename()); + // 使用七牛云上传 + for (MultipartFile file : files) + { + String fileUrl = QiniuUploadUtil.uploadFile(file); + urls.add(fileUrl); + fileNames.add(fileUrl); + newFileNames.add(FileUtils.getName(file.getOriginalFilename())); + originalFilenames.add(file.getOriginalFilename()); + } } + else + { + // 使用本地上传 + String filePath = RuoYiConfig.getUploadPath(); + for (MultipartFile file : files) + { + String fileName = FileUploadUtils.upload(filePath, file); + String url = "https://img.huafurenjia.cn" + fileName; + urls.add(url); + fileNames.add(fileName); + newFileNames.add(FileUtils.getName(fileName)); + originalFilenames.add(file.getOriginalFilename()); + } + } + AjaxResult ajax = AjaxResult.success(); ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER)); ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER)); diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/QiniuController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/QiniuController.java new file mode 100644 index 0000000..e5f8387 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/QiniuController.java @@ -0,0 +1,294 @@ +package com.ruoyi.web.controller.common; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.file.FileUtils; +import com.ruoyi.system.config.QiniuConfig; +import com.ruoyi.system.utils.QiniuUploadUtil; +import com.qiniu.storage.model.FileInfo; + +/** + * 七牛云上传控制器 + * + * @author ruoyi + */ +@RestController +@RequestMapping("/common/qiniu") +public class QiniuController { + + @Autowired + private QiniuConfig qiniuConfig; + + private static final String FILE_DELIMETER = ","; + + /** + * 检查七牛云上传是否已启用 + * + * @return 启用状态 + */ + @GetMapping("/status") + public AjaxResult getQiniuStatus() { + AjaxResult result = AjaxResult.success(); + result.put("enabled", qiniuConfig.isEnabled()); + result.put("domain", qiniuConfig.getDomain()); + result.put("bucketName", qiniuConfig.getBucketName()); + return result; + } + + /** + * 获取七牛云上传凭证(用于前端直传) + * + * @param key 指定的文件key,可选 + * @return 上传凭证 + */ + @GetMapping("/token") + public AjaxResult getUploadToken(@RequestParam(required = false) String key) { + try { + if (!qiniuConfig.isEnabled()) { + return AjaxResult.error("七牛云上传未启用"); + } + + String token = QiniuUploadUtil.getUploadToken(key); + if (StringUtils.isEmpty(token)) { + return AjaxResult.error("获取上传凭证失败"); + } + + AjaxResult result = AjaxResult.success(); + result.put("token", token); + result.put("domain", "https://" + qiniuConfig.getDomain()); + return result; + + } catch (Exception e) { + return AjaxResult.error("获取上传凭证失败: " + e.getMessage()); + } + } + + /** + * 七牛云单文件上传 + * + * @param file 上传的文件 + * @return 上传结果 + */ + @PostMapping("/upload") + public AjaxResult uploadFile(@RequestParam("file") MultipartFile file) { + try { + if (!qiniuConfig.isEnabled()) { + return AjaxResult.error("七牛云上传未启用"); + } + + if (file == null || file.isEmpty()) { + return AjaxResult.error("上传文件不能为空"); + } + + // 验证文件格式 + String[] allowedTypes = {"jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"}; + if (!QiniuUploadUtil.isValidFileType(file.getOriginalFilename(), allowedTypes)) { + return AjaxResult.error("不支持的文件格式"); + } + + // 验证文件大小(10MB) + long maxSize = 10 * 1024 * 1024; + if (!QiniuUploadUtil.isFileSizeValid(file.getSize(), maxSize)) { + return AjaxResult.error("文件大小不能超过10MB"); + } + + String fileUrl = QiniuUploadUtil.uploadFile(file); + + AjaxResult result = AjaxResult.success(); + result.put("url", fileUrl); + result.put("fileName", fileUrl); + result.put("newFileName", FileUtils.getName(file.getOriginalFilename())); + result.put("originalFilename", file.getOriginalFilename()); + result.put("size", file.getSize()); + + return result; + + } catch (Exception e) { + return AjaxResult.error("上传失败: " + e.getMessage()); + } + } + + /** + * 七牛云多文件上传 + * + * @param files 上传的文件列表 + * @return 上传结果 + */ + @PostMapping("/uploads") + public AjaxResult uploadFiles(@RequestParam("files") List files) { + try { + if (!qiniuConfig.isEnabled()) { + return AjaxResult.error("七牛云上传未启用"); + } + + if (files == null || files.isEmpty()) { + return AjaxResult.error("上传文件不能为空"); + } + + List urls = new ArrayList<>(); + List fileNames = new ArrayList<>(); + List newFileNames = new ArrayList<>(); + List originalFilenames = new ArrayList<>(); + List fileSizes = new ArrayList<>(); + + String[] allowedTypes = {"jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"}; + long maxSize = 10 * 1024 * 1024; // 10MB + + for (MultipartFile file : files) { + if (file.isEmpty()) { + continue; + } + + // 验证文件格式 + if (!QiniuUploadUtil.isValidFileType(file.getOriginalFilename(), allowedTypes)) { + return AjaxResult.error("文件 " + file.getOriginalFilename() + " 格式不支持"); + } + + // 验证文件大小 + if (!QiniuUploadUtil.isFileSizeValid(file.getSize(), maxSize)) { + return AjaxResult.error("文件 " + file.getOriginalFilename() + " 大小不能超过10MB"); + } + + String fileUrl = QiniuUploadUtil.uploadFile(file); + urls.add(fileUrl); + fileNames.add(fileUrl); + newFileNames.add(FileUtils.getName(file.getOriginalFilename())); + originalFilenames.add(file.getOriginalFilename()); + fileSizes.add(file.getSize()); + } + + AjaxResult result = AjaxResult.success(); + result.put("urls", StringUtils.join(urls, FILE_DELIMETER)); + result.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER)); + result.put("newFileNames", StringUtils.join(newFileNames, FILE_DELIMETER)); + result.put("originalFilenames", StringUtils.join(originalFilenames, FILE_DELIMETER)); + result.put("count", urls.size()); + + return result; + + } catch (Exception e) { + return AjaxResult.error("批量上传失败: " + e.getMessage()); + } + } + + /** + * 删除七牛云文件 + * + * @param fileUrl 文件URL + * @return 删除结果 + */ + @DeleteMapping("/delete") + public AjaxResult deleteFile(@RequestParam("fileUrl") String fileUrl) { + try { + if (!qiniuConfig.isEnabled()) { + return AjaxResult.error("七牛云上传未启用"); + } + + if (StringUtils.isEmpty(fileUrl)) { + return AjaxResult.error("文件URL不能为空"); + } + + boolean success = QiniuUploadUtil.deleteFile(fileUrl); + if (success) { + return AjaxResult.success("文件删除成功"); + } else { + return AjaxResult.error("文件删除失败"); + } + + } catch (Exception e) { + return AjaxResult.error("删除失败: " + e.getMessage()); + } + } + + /** + * 批量删除七牛云文件 + * + * @param fileUrls 文件URL列表(逗号分隔) + * @return 删除结果 + */ + @DeleteMapping("/deleteAll") + public AjaxResult deleteFiles(@RequestParam("fileUrls") String fileUrls) { + try { + if (!qiniuConfig.isEnabled()) { + return AjaxResult.error("七牛云上传未启用"); + } + + if (StringUtils.isEmpty(fileUrls)) { + return AjaxResult.error("文件URL不能为空"); + } + + String[] urlArray = fileUrls.split(","); + int successCount = 0; + int failCount = 0; + + for (String url : urlArray) { + if (StringUtils.isNotEmpty(url.trim())) { + boolean success = QiniuUploadUtil.deleteFile(url.trim()); + if (success) { + successCount++; + } else { + failCount++; + } + } + } + + AjaxResult result = AjaxResult.success(); + result.put("message", String.format("删除完成,成功%d个,失败%d个", successCount, failCount)); + result.put("successCount", successCount); + result.put("failCount", failCount); + + return result; + + } catch (Exception e) { + return AjaxResult.error("批量删除失败: " + e.getMessage()); + } + } + + /** + * 获取文件信息 + * + * @param fileUrl 文件URL + * @return 文件信息 + */ + @GetMapping("/fileInfo") + public AjaxResult getFileInfo(@RequestParam("fileUrl") String fileUrl) { + try { + if (!qiniuConfig.isEnabled()) { + return AjaxResult.error("七牛云上传未启用"); + } + + if (StringUtils.isEmpty(fileUrl)) { + return AjaxResult.error("文件URL不能为空"); + } + + com.qiniu.storage.model.FileInfo fileInfo = QiniuUploadUtil.getFileInfo(fileUrl); + if (fileInfo != null) { + AjaxResult result = AjaxResult.success(); + result.put("fileInfo", fileInfo); + result.put("size", fileInfo.fsize); + result.put("hash", fileInfo.hash); + result.put("mimeType", fileInfo.mimeType); + result.put("putTime", fileInfo.putTime); + return result; + } else { + return AjaxResult.error("获取文件信息失败"); + } + + } catch (Exception e) { + return AjaxResult.error("获取文件信息失败: " + e.getMessage()); + } + } +} \ No newline at end of file diff --git a/ruoyi-admin/src/main/resources/application.yml b/ruoyi-admin/src/main/resources/application.yml index 0cb835d..80a6b6c 100644 --- a/ruoyi-admin/src/main/resources/application.yml +++ b/ruoyi-admin/src/main/resources/application.yml @@ -13,6 +13,21 @@ ruoyi: # 验证码类型 math 数字计算 char 字符验证 captchaType: math +# 七牛云配置 +qiniu: + # 是否启用七牛云上传 true-启用七牛云 false-使用本地上传 + enabled: true + # 七牛云AccessKey(请替换为您的实际Key) + access-key: F88NOtz7vFVGwN_4j9EgRAHm4vS3eiGWLgcjGFlK + # 七牛云SecretKey(请替换为您的实际Key) + secret-key: X10y88HTfb8TvFFac9W_e1XywOcWIsDSPoBx8ZYN + # 存储空间名称(请替换为您的bucket名称) + bucket-name: hfrj-xcx + # 七牛云域名(请替换为您的CDN域名,不包含http://或https://) + domain: img.huafurenjia.cn + # 存储区域 region0-华东 region1-华北 region2-华南 regionNa0-北美 regionAs0-东南亚 + region: region2 + # 高德地图配置 amap: # 高德Web服务API类型Key diff --git a/ruoyi-auth-common/pom.xml b/ruoyi-auth-common/pom.xml new file mode 100644 index 0000000..1efb52a --- /dev/null +++ b/ruoyi-auth-common/pom.xml @@ -0,0 +1,33 @@ + + + + ruoyi-auth + com.ruoyi.geekxd + 3.8.9-G + + 4.0.0 + + ruoyi-auth-common + + + system系统模块 + + + + + + + com.ruoyi + ruoyi-framework + + + + + org.apache.httpcomponents + httpclient + + + + + \ No newline at end of file diff --git a/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/domain/OauthUser.java b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/domain/OauthUser.java new file mode 100644 index 0000000..650b514 --- /dev/null +++ b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/domain/OauthUser.java @@ -0,0 +1,293 @@ +package com.ruoyi.auth.common.domain; + +import org.apache.commons.lang3.builder.ToStringBuilder; +import org.apache.commons.lang3.builder.ToStringStyle; + +import com.ruoyi.common.annotation.Excel; +import com.ruoyi.common.core.domain.BaseEntity; + +import io.swagger.v3.oas.annotations.media.Schema; + +/** + * 第三方认证对象 oauth_user + * + * @author Dftre + * @date 2024-01-18 + */ +@Schema(description = "第三方认证对象") +public class OauthUser extends BaseEntity { + private static final long serialVersionUID = 1L; + + /** 主键 */ + @Schema(title = "主键") + private Long id; + + /** 第三方系统的唯一ID,详细解释请参考:名词解释 */ + @Schema(title = "第三方系统的唯一ID,详细解释请参考:名词解释") + @Excel(name = "第三方系统的唯一ID,详细解释请参考:名词解释") + private String uuid; + + /** 用户ID */ + @Schema(title = "用户ID") + @Excel(name = "用户ID") + private Long userId; + + /** + * 第三方用户来源,可选值:GITHUB、GITEE、QQ,更多请参考:AuthDefaultSource.java(opens new window) + */ + @Schema(title = "第三方用户来源,可选值:GITHUB、GITEE、QQ,更多请参考:AuthDefaultSource.java(opens new window)") + @Excel(name = "第三方用户来源,可选值:GITHUB、GITEE、QQ,更多请参考:AuthDefaultSource.java(opens new window)") + private String source; + + /** 用户的授权令牌 */ + @Schema(title = "用户的授权令牌") + @Excel(name = "用户的授权令牌") + private String accessToken; + + /** 第三方用户的授权令牌的有效期,部分平台可能没有 */ + @Schema(title = "第三方用户的授权令牌的有效期,部分平台可能没有") + @Excel(name = "第三方用户的授权令牌的有效期,部分平台可能没有") + private Long expireIn; + + /** 刷新令牌,部分平台可能没有 */ + @Schema(title = "刷新令牌,部分平台可能没有") + @Excel(name = "刷新令牌,部分平台可能没有") + private String refreshToken; + + /** 第三方用户的 open id,部分平台可能没有 */ + @Schema(title = "第三方用户的 open id,部分平台可能没有") + @Excel(name = "第三方用户的 open id,部分平台可能没有") + private String openId; + + /** 第三方用户的 ID,部分平台可能没有 */ + @Schema(title = "第三方用户的 ID,部分平台可能没有") + @Excel(name = "第三方用户的 ID,部分平台可能没有") + private String uid; + + /** 个别平台的授权信息,部分平台可能没有 */ + @Schema(title = "个别平台的授权信息,部分平台可能没有") + @Excel(name = "个别平台的授权信息,部分平台可能没有") + private String accessCode; + + /** 第三方用户的 union id,部分平台可能没有 */ + @Schema(title = "第三方用户的 union id,部分平台可能没有") + @Excel(name = "第三方用户的 union id,部分平台可能没有") + private String unionId; + + /** 第三方用户授予的权限,部分平台可能没有 */ + @Schema(title = "第三方用户授予的权限,部分平台可能没有") + @Excel(name = "第三方用户授予的权限,部分平台可能没有") + private String scope; + + /** 个别平台的授权信息,部分平台可能没有 */ + @Schema(title = "个别平台的授权信息,部分平台可能没有") + @Excel(name = "个别平台的授权信息,部分平台可能没有") + private String tokenType; + + /** id token,部分平台可能没有 */ + @Schema(title = "id token,部分平台可能没有") + @Excel(name = "id token,部分平台可能没有") + private String idToken; + + /** 小米平台用户的附带属性,部分平台可能没有 */ + @Schema(title = "小米平台用户的附带属性,部分平台可能没有") + @Excel(name = "小米平台用户的附带属性,部分平台可能没有") + private String macAlgorithm; + + /** 小米平台用户的附带属性,部分平台可能没有 */ + @Schema(title = "小米平台用户的附带属性,部分平台可能没有") + @Excel(name = "小米平台用户的附带属性,部分平台可能没有") + private String macKey; + + /** 用户的授权code,部分平台可能没有 */ + @Schema(title = "用户的授权code,部分平台可能没有") + @Excel(name = "用户的授权code,部分平台可能没有") + private String code; + + /** Twitter平台用户的附带属性,部分平台可能没有 */ + @Schema(title = "Twitter平台用户的附带属性,部分平台可能没有") + @Excel(name = "Twitter平台用户的附带属性,部分平台可能没有") + private String oauthToken; + + /** Twitter平台用户的附带属性,部分平台可能没有 */ + @Schema(title = "Twitter平台用户的附带属性,部分平台可能没有") + @Excel(name = "Twitter平台用户的附带属性,部分平台可能没有") + private String oauthTokenSecret; + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setUuid(String uuid) { + this.uuid = uuid; + } + + public String getUuid() { + return uuid; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public Long getUserId() { + return userId; + } + + public void setSource(String source) { + this.source = source; + } + + public String getSource() { + return source; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + public String getAccessToken() { + return accessToken; + } + + public void setExpireIn(Long expireIn) { + this.expireIn = expireIn; + } + + public Long getExpireIn() { + return expireIn; + } + + public void setRefreshToken(String refreshToken) { + this.refreshToken = refreshToken; + } + + public String getRefreshToken() { + return refreshToken; + } + + public void setOpenId(String openId) { + this.openId = openId; + } + + public String getOpenId() { + return openId; + } + + public void setUid(String uid) { + this.uid = uid; + } + + public String getUid() { + return uid; + } + + public void setAccessCode(String accessCode) { + this.accessCode = accessCode; + } + + public String getAccessCode() { + return accessCode; + } + + public void setUnionId(String unionId) { + this.unionId = unionId; + } + + public String getUnionId() { + return unionId; + } + + public void setScope(String scope) { + this.scope = scope; + } + + public String getScope() { + return scope; + } + + public void setTokenType(String tokenType) { + this.tokenType = tokenType; + } + + public String getTokenType() { + return tokenType; + } + + public void setIdToken(String idToken) { + this.idToken = idToken; + } + + public String getIdToken() { + return idToken; + } + + public void setMacAlgorithm(String macAlgorithm) { + this.macAlgorithm = macAlgorithm; + } + + public String getMacAlgorithm() { + return macAlgorithm; + } + + public void setMacKey(String macKey) { + this.macKey = macKey; + } + + public String getMacKey() { + return macKey; + } + + public void setCode(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + + public void setOauthToken(String oauthToken) { + this.oauthToken = oauthToken; + } + + public String getOauthToken() { + return oauthToken; + } + + public void setOauthTokenSecret(String oauthTokenSecret) { + this.oauthTokenSecret = oauthTokenSecret; + } + + public String getOauthTokenSecret() { + return oauthTokenSecret; + } + + @Override + public String toString() { + return new ToStringBuilder(this, ToStringStyle.MULTI_LINE_STYLE) + .append("id", getId()) + .append("uuid", getUuid()) + .append("userId", getUserId()) + .append("source", getSource()) + .append("accessToken", getAccessToken()) + .append("expireIn", getExpireIn()) + .append("refreshToken", getRefreshToken()) + .append("openId", getOpenId()) + .append("uid", getUid()) + .append("accessCode", getAccessCode()) + .append("unionId", getUnionId()) + .append("scope", getScope()) + .append("tokenType", getTokenType()) + .append("idToken", getIdToken()) + .append("macAlgorithm", getMacAlgorithm()) + .append("macKey", getMacKey()) + .append("code", getCode()) + .append("oauthToken", getOauthToken()) + .append("oauthTokenSecret", getOauthTokenSecret()) + .toString(); + } +} diff --git a/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/enums/OauthVerificationUse.java b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/enums/OauthVerificationUse.java new file mode 100644 index 0000000..6b44b6c --- /dev/null +++ b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/enums/OauthVerificationUse.java @@ -0,0 +1,59 @@ +package com.ruoyi.auth.common.enums; + +public enum OauthVerificationUse { + + /** 用于登录 */ + LOGIN("登录", "login"), + /** 用于注册 */ + REGISTER("注册", "register"), + /** 用于禁用 */ + DISABLE("禁用", "disable"), + /** 用于重置信息 */ + RESET("重置", "reset"), + /** 用于绑定信息 */ + BIND("绑定", "bind"), + /** 其他用途 */ + OTHER("其他", "other"); + + private String name; + private String value; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + public static OauthVerificationUse getByValue(String value) { + for (OauthVerificationUse use : OauthVerificationUse.values()) { + if (use.getValue().equals(value)) { + return use; + } + } + return null; + } + + public static OauthVerificationUse getByName(String name) { + for (OauthVerificationUse use : OauthVerificationUse.values()) { + if (use.getName().equals(name)) { + return use; + } + } + return null; + } + + private OauthVerificationUse(String name, String value) { + this.name = name; + this.value = value; + } +} diff --git a/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/mapper/OauthUserMapper.java b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/mapper/OauthUserMapper.java new file mode 100644 index 0000000..656630e --- /dev/null +++ b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/mapper/OauthUserMapper.java @@ -0,0 +1,112 @@ +package com.ruoyi.auth.common.mapper; + +import java.util.List; + +import org.apache.ibatis.annotations.Param; + +import com.ruoyi.auth.common.domain.OauthUser; + +/** + * 第三方认证Mapper接口 + * + * @author Dftre + * @date 2024-01-18 + */ +public interface OauthUserMapper { + /** + * 查询第三方认证 + * + * @param id 第三方认证主键 + * @return 第三方认证 + */ + public OauthUser selectOauthUserById(Long id); + + public OauthUser selectOauthUserByUserId(Long userId); + + /** + * 查询第三方认证 + * 钉钉、抖音:uuid 为用户的 unionid + * 微信公众平台登录、京东、酷家乐、美团:uuid 为用户的 openId + * 微信开放平台登录、QQ:uuid 为用户的 openId,平台支持获取unionid, unionid 在 AuthToken + * 中(如果支持),在登录完成后,可以通过 response.getData().getToken().getUnionId() 获取 + * Google:uuid 为用户的 sub,sub为Google的所有账户体系中用户唯一的身份标识符,详见:OpenID Connect + * + * @param uuid + * @return + */ + public OauthUser selectOauthUserByUUID(String uuid); + + /** + * 查询第三方认证列表 + * + * @param oauthUser 第三方认证 + * @return 第三方认证集合 + */ + public List selectOauthUserList(OauthUser oauthUser); + + /** + * 新增第三方认证 + * + * @param oauthUser 第三方认证 + * @return 结果 + */ + public int insertOauthUser(OauthUser oauthUser); + + /** + * 修改第三方认证 + * + * @param oauthUser 第三方认证 + * @return 结果 + */ + public int updateOauthUser(OauthUser oauthUser); + + /** + * 删除第三方认证 + * + * @param id 第三方认证主键 + * @return 结果 + */ + public int deleteOauthUserById(Long id); + + /** + * 批量删除第三方认证 + * + * @param ids 需要删除的数据主键集合 + * @return 结果 + */ + public int deleteOauthUserByIds(Long[] ids); + + /** + * 校验source平台是否绑定 + * + * @param userId 用户编号 + * @param source 绑定平台 + * @return 结果 + */ + public int checkAuthUser(@Param("userId") Long userId, @Param("source") String source); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public int checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param phonenumber 手机号码 + * @return 结果 + */ + public int checkPhoneUnique(String phonenumber); + + /** + * 校验email是否唯一 + * + * @param email 用户邮箱 + * @return 结果 + */ + public int checkEmailUnique(String email); + +} diff --git a/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/IOauthUserService.java b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/IOauthUserService.java new file mode 100644 index 0000000..bc01edf --- /dev/null +++ b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/IOauthUserService.java @@ -0,0 +1,102 @@ +package com.ruoyi.auth.common.service; + +import java.util.List; + +import com.ruoyi.auth.common.domain.OauthUser; +import com.ruoyi.common.core.domain.entity.SysUser; + +/** + * 第三方认证Service接口 + * + * @author Dftre + * @date 2024-01-18 + */ +public interface IOauthUserService { + /** + * 查询第三方认证 + * + * @param id 第三方认证主键 + * @return 第三方认证 + */ + public OauthUser selectOauthUserById(Long id); + + public OauthUser selectOauthUserByUUID(String uuid); + + public OauthUser selectOauthUserByUserId(Long userId); + + public SysUser selectSysUserByUUID(String uuid); + + /** + * 查询第三方认证列表 + * + * @param oauthUser 第三方认证 + * @return 第三方认证集合 + */ + public List selectOauthUserList(OauthUser oauthUser); + + /** + * 新增第三方认证 + * + * @param oauthUser 第三方认证 + * @return 结果 + */ + public int insertOauthUser(OauthUser oauthUser); + + /** + * 修改第三方认证 + * + * @param oauthUser 第三方认证 + * @return 结果 + */ + public int updateOauthUser(OauthUser oauthUser); + + /** + * 批量删除第三方认证 + * + * @param ids 需要删除的第三方认证主键集合 + * @return 结果 + */ + public int deleteOauthUserByIds(Long[] ids); + + /** + * 删除第三方认证信息 + * + * @param id 第三方认证主键 + * @return 结果 + */ + public int deleteOauthUserById(Long id); + + /** + * 校验source平台是否绑定 + * + * @param userId 用户编号 + * @param source 绑定平台 + * @return 结果 + */ + public boolean checkAuthUser(Long userId, String source); + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public boolean checkUserNameUnique(String userName); + + /** + * 校验手机号码是否唯一 + * + * @param phonenumber 手机号码 + * @return 结果 + */ + public boolean checkPhoneUnique(String phonenumber); + + /** + * 校验email是否唯一 + * + * @param email 用户邮箱 + * @return 结果 + */ + public boolean checkEmailUnique(String email); + +} diff --git a/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/OauthVerificationCodeService.java b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/OauthVerificationCodeService.java new file mode 100644 index 0000000..8fa22f0 --- /dev/null +++ b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/OauthVerificationCodeService.java @@ -0,0 +1,15 @@ +package com.ruoyi.auth.common.service; + +import com.ruoyi.auth.common.enums.OauthVerificationUse; + +/** + * code认证方式接口 + * + * @author zlh + * @date 2024-04-16 + */ +public interface OauthVerificationCodeService { + public boolean sendCode(String o, String code,OauthVerificationUse use) throws Exception; + public boolean checkCode(String o, String code,OauthVerificationUse use) throws Exception; + +} diff --git a/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/TfaService.java b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/TfaService.java new file mode 100644 index 0000000..92d92dd --- /dev/null +++ b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/TfaService.java @@ -0,0 +1,73 @@ +package com.ruoyi.auth.common.service; + +import com.ruoyi.common.core.domain.model.LoginBody; +import com.ruoyi.common.core.domain.model.RegisterBody; + +/** + * 双因素认证(TFA)操作的服务接口。 + * 该接口提供处理TFA绑定、注册和登录流程的方法, + * 包括它们的验证步骤。 + * + *

+ * 双因素认证通过要求用户提供两种不同的认证因素, + * 为认证过程增加了额外的安全层。 + *

+ */ +public interface TfaService { + /** + * 启动将TFA方法绑定到用户账户的流程。 + * + * @param loginBody 包含TFA绑定所需数据的登录信息 + */ + public void doBind(LoginBody loginBody); + + /** + * 使用验证码或其他确认方式验证TFA绑定流程。 + * + * @param loginBody 包含验证数据的登录信息 + */ + public void doBindVerify(LoginBody loginBody); + + /** + * 处理包含TFA设置的注册流程。 + * + * @param registerBody 包含用户详情和TFA设置的注册信息 + */ + public void doRegister(RegisterBody registerBody); + + /** + * 验证包含TFA设置的注册流程。 + * + * @param registerBody 包含验证数据的注册信息 + */ + public void doRegisterVerify(RegisterBody registerBody); + + /** + * 启动TFA登录流程的第一步。 + * + * @param loginBody 包含用户凭证的登录信息 + */ + public void doLogin(LoginBody loginBody, boolean autoRegister); + + /** + * 验证TFA登录流程的第二步并完成认证。 + * + * @param loginBody 包含TFA验证码的登录信息 + * @return 已认证会话的字符串令牌或会话标识符 + */ + public String doLoginVerify(LoginBody loginBody, boolean autoRegister); + + /** + * 启动TFA重置流程的第一步。 + * + * @param registerBody 包含用户凭证的注册信息 + */ + public void doReset(RegisterBody registerBody); + + /** + * 验证TFA重置流程的第二步并完成重置。 + * + * @param registerBody 包含TFA验证码的注册信息 + */ + public void doResetVerify(RegisterBody registerBody); +} diff --git a/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/impl/OauthUserServiceImpl.java b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/impl/OauthUserServiceImpl.java new file mode 100644 index 0000000..4d4ebc0 --- /dev/null +++ b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/service/impl/OauthUserServiceImpl.java @@ -0,0 +1,149 @@ +package com.ruoyi.auth.common.service.impl; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.ruoyi.auth.common.domain.OauthUser; +import com.ruoyi.auth.common.mapper.OauthUserMapper; +import com.ruoyi.auth.common.service.IOauthUserService; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.system.mapper.SysUserMapper; + +/** + * 第三方认证Service业务层处理 + * + * @author Dftre + * @date 2024-01-18 + */ +@Service +public class OauthUserServiceImpl implements IOauthUserService { + @Autowired + private OauthUserMapper oauthUserMapper; + + @Autowired + private SysUserMapper sysUserMapper; + + /** + * 查询第三方认证 + * + * @param id 第三方认证主键 + * @return 第三方认证 + */ + @Override + public OauthUser selectOauthUserById(Long id) { + return oauthUserMapper.selectOauthUserById(id); + } + + @Override + public OauthUser selectOauthUserByUUID(String uuid) { + return oauthUserMapper.selectOauthUserByUUID(uuid); + } + + @Override + public OauthUser selectOauthUserByUserId(Long userId) { + return oauthUserMapper.selectOauthUserByUserId(userId); + } + + /** + * 查询第三方认证列表 + * + * @param oauthUser 第三方认证 + * @return 第三方认证 + */ + @Override + public List selectOauthUserList(OauthUser oauthUser) { + return oauthUserMapper.selectOauthUserList(oauthUser); + } + + /** + * 新增第三方认证 + * + * @param oauthUser 第三方认证 + * @return 结果 + */ + @Override + public int insertOauthUser(OauthUser oauthUser) { + return oauthUserMapper.insertOauthUser(oauthUser); + } + + /** + * 修改第三方认证 + * + * @param oauthUser 第三方认证 + * @return 结果 + */ + @Override + public int updateOauthUser(OauthUser oauthUser) { + return oauthUserMapper.updateOauthUser(oauthUser); + } + + /** + * 批量删除第三方认证 + * + * @param ids 需要删除的第三方认证主键 + * @return 结果 + */ + @Override + public int deleteOauthUserByIds(Long[] ids) { + return oauthUserMapper.deleteOauthUserByIds(ids); + } + + /** + * 删除第三方认证信息 + * + * @param id 第三方认证主键 + * @return 结果 + */ + @Override + public int deleteOauthUserById(Long id) { + return oauthUserMapper.deleteOauthUserById(id); + } + + public SysUser selectSysUserByUUID(String uuid) { + OauthUser oauthUser = oauthUserMapper.selectOauthUserByUUID(uuid); + return sysUserMapper.selectUserById(oauthUser.getUserId()); + } + + /** + * 校验source平台是否绑定 + * + * @param userId 用户编号 + * @param source 绑定平台 + * @return 结果 + */ + public boolean checkAuthUser(Long userId, String source) { + return oauthUserMapper.checkAuthUser(userId, source) > 0; + }; + + /** + * 校验用户名称是否唯一 + * + * @param userName 用户名称 + * @return 结果 + */ + public boolean checkUserNameUnique(String userName) { + return oauthUserMapper.checkUserNameUnique(userName) > 0; + }; + + /** + * 校验手机号码是否唯一 + * + * @param phonenumber 手机号码 + * @return 结果 + */ + public boolean checkPhoneUnique(String phonenumber) { + return oauthUserMapper.checkPhoneUnique(phonenumber) > 0; + }; + + /** + * 校验email是否唯一 + * + * @param email 用户邮箱 + * @return 结果 + */ + public boolean checkEmailUnique(String email) { + return oauthUserMapper.checkEmailUnique(email) > 0; + }; +} diff --git a/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/utils/RandomCodeUtil.java b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/utils/RandomCodeUtil.java new file mode 100644 index 0000000..098e9c4 --- /dev/null +++ b/ruoyi-auth-common/src/main/java/com/ruoyi/auth/common/utils/RandomCodeUtil.java @@ -0,0 +1,28 @@ +package com.ruoyi.auth.common.utils; + +import java.security.SecureRandom; + +public class RandomCodeUtil { + + public static String randomString(String characters, int length) { + StringBuilder result = new StringBuilder(); + SecureRandom random = new SecureRandom(); + + for (int i = 0; i < length; i++) { + int index = random.nextInt(characters.length()); + result.append(characters.charAt(index)); + } + + return result.toString(); + } + + public static String numberCode(int length) { + String characters = "0123456789"; + return randomString(characters, length); + } + + public static String code(int length) { + String characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + return randomString(characters, length); + } +} diff --git a/ruoyi-auth-common/src/main/resources/mapper/common/OauthUserMapper.xml b/ruoyi-auth-common/src/main/resources/mapper/common/OauthUserMapper.xml new file mode 100644 index 0000000..c409d09 --- /dev/null +++ b/ruoyi-auth-common/src/main/resources/mapper/common/OauthUserMapper.xml @@ -0,0 +1,169 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + select id, uuid, user_id, source, access_token, expire_in, refresh_token, open_id, uid, access_code, union_id, scope, token_type, id_token, mac_algorithm, mac_key, code, oauth_token, oauth_token_secret from oauth_user + + + + + + + + + + + + + + + + + + + + insert into oauth_user + + id, + uuid, + user_id, + source, + access_token, + expire_in, + refresh_token, + open_id, + uid, + access_code, + union_id, + scope, + token_type, + id_token, + mac_algorithm, + mac_key, + code, + oauth_token, + oauth_token_secret, + + + #{id}, + #{uuid}, + #{userId}, + #{source}, + #{accessToken}, + #{expireIn}, + #{refreshToken}, + #{openId}, + #{uid}, + #{accessCode}, + #{unionId}, + #{scope}, + #{tokenType}, + #{idToken}, + #{macAlgorithm}, + #{macKey}, + #{code}, + #{oauthToken}, + #{oauthTokenSecret}, + + + + + update oauth_user + + uuid = #{uuid}, + user_id = #{userId}, + source = #{source}, + access_token = #{accessToken}, + expire_in = #{expireIn}, + refresh_token = #{refreshToken}, + open_id = #{openId}, + uid = #{uid}, + access_code = #{accessCode}, + union_id = #{unionId}, + scope = #{scope}, + token_type = #{tokenType}, + id_token = #{idToken}, + mac_algorithm = #{macAlgorithm}, + mac_key = #{macKey}, + code = #{code}, + oauth_token = #{oauthToken}, + oauth_token_secret = #{oauthTokenSecret}, + + where oauth_user.id = #{id} + + + + delete from oauth_user where id = #{id} + + + + delete from oauth_user where id in + + #{id} + + + \ No newline at end of file diff --git a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java index fb2ae21..e43cd35 100644 --- a/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java +++ b/ruoyi-common/src/main/java/com/ruoyi/common/utils/DateUtils.java @@ -96,7 +96,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils public static final String datePath() { Date now = new Date(); - return DateFormatUtils.format(now, "yyyy/MM/dd"); + return DateFormatUtils.format(now, "yyyy-MM-dd"); } /** diff --git a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java index 511842b..a46e921 100644 --- a/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java +++ b/ruoyi-framework/src/main/java/com/ruoyi/framework/config/SecurityConfig.java @@ -110,12 +110,13 @@ public class SecurityConfig // 注解标记允许匿名访问的url .authorizeHttpRequests((requests) -> { permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); - // 对于登录login 注册register 验证码captchaImage 允许匿名访问 + // 对于登录login 注册register 验证码captchaImage 小程序Applet 允许匿名访问 requests.antMatchers("/login", "/register", "/captchaImage").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() - // 除上面外的所有请求全部需要鉴权认证 + .antMatchers("/api/**", "/api").permitAll() + // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated(); }) // 添加Logout filter diff --git a/ruoyi-oauth-wx/pom.xml b/ruoyi-oauth-wx/pom.xml new file mode 100644 index 0000000..f0df644 --- /dev/null +++ b/ruoyi-oauth-wx/pom.xml @@ -0,0 +1,29 @@ + + + + ruoyi-auth + com.ruoyi.geekxd + 3.8.9-G + + 4.0.0 + + ruoyi-oauth-wx + + + 微信认证模块 + + + + + + + com.ruoyi + ruoyi-auth-common + + + + + + \ No newline at end of file diff --git a/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/constant/WxMiniAppConstant.java b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/constant/WxMiniAppConstant.java new file mode 100644 index 0000000..7dc9433 --- /dev/null +++ b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/constant/WxMiniAppConstant.java @@ -0,0 +1,42 @@ +package com.ruoyi.oauth.wx.constant; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class WxMiniAppConstant { + @Value("${oauth.wx.miniapp.appId}") + private String appId; + + @Value("${oauth.wx.miniapp.appSecret}") + private String appSecret; + + @Value("${oauth.wx.miniapp.url}") + private String url; + + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getAppSecret() { + return appSecret; + } + + public void setAppSecret(String appSecret) { + this.appSecret = appSecret; + } + +} diff --git a/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/constant/WxPubConstant.java b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/constant/WxPubConstant.java new file mode 100644 index 0000000..ae7cda4 --- /dev/null +++ b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/constant/WxPubConstant.java @@ -0,0 +1,41 @@ +package com.ruoyi.oauth.wx.constant; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class WxPubConstant { + @Value("${oauth.wx.pub.appId}") + private String appId; + + @Value("${oauth.wx.pub.appSecret}") + private String appSecret; + + @Value("${oauth.wx.pub.url}") + private String url; + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } + + public String getAppId() { + return appId; + } + + public void setAppId(String appId) { + this.appId = appId; + } + + public String getAppSecret() { + return appSecret; + } + + public void setAppSecret(String appSecret) { + this.appSecret = appSecret; + } + +} diff --git a/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/controller/WxLoginController.java b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/controller/WxLoginController.java new file mode 100644 index 0000000..22adeda --- /dev/null +++ b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/controller/WxLoginController.java @@ -0,0 +1,69 @@ +package com.ruoyi.oauth.wx.controller; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.ruoyi.auth.common.domain.OauthUser; +import com.ruoyi.auth.common.service.IOauthUserService; +import com.ruoyi.common.annotation.Anonymous; +import com.ruoyi.common.constant.Constants; +import com.ruoyi.common.core.controller.BaseController; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.oauth.wx.service.Impl.WxMiniAppLoginServiceImpl; +import com.ruoyi.oauth.wx.service.Impl.WxPubLoginServiceImpl; + +@RestController +@RequestMapping("/oauth/wx") +public class WxLoginController extends BaseController { + + @Autowired + private IOauthUserService oauthUserService; + + @Autowired + private WxMiniAppLoginServiceImpl wxMiniAppLoginServiceImpl; + + @Autowired + private WxPubLoginServiceImpl wxPubLoginServiceImpl; + + @Anonymous + @PostMapping("/login/{source}/{code}") + public AjaxResult loginMiniApp(@PathVariable("source") String source, @PathVariable("code") String code) { + String token = null; + AjaxResult ajax = AjaxResult.success(); + if ("miniapp".equals(source)) { + token = wxMiniAppLoginServiceImpl.doLogin(code); + } else if ("pub".equals(source)) { + token = wxPubLoginServiceImpl.doLogin(code); + } else { + return error("错误的登录方式"); + } + ajax.put(Constants.TOKEN, token); + return ajax; + } + + @PostMapping("/register/{source}/{code}") + public AjaxResult register(@PathVariable("source") String source, @PathVariable("code") String code) { + OauthUser oauthUser = oauthUserService.selectOauthUserByUserId(getUserId()); + if (oauthUser != null) { + return error("不可以重复绑定"); + } else { + String msg = ""; + oauthUser = new OauthUser(); + oauthUser.setUserId(getUserId()); + oauthUser.setCode(code); + if ("miniapp".equals(source)) { + msg = wxMiniAppLoginServiceImpl.doRegister(oauthUser); + } else if ("pub".equals(source)) { + msg = wxPubLoginServiceImpl.doRegister(oauthUser); + } else { + return error("错误的注册方式"); + } + return StringUtils.isEmpty(msg) ? success() : error(msg); + } + } + +} diff --git a/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/Impl/WxMiniAppLoginServiceImpl.java b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/Impl/WxMiniAppLoginServiceImpl.java new file mode 100644 index 0000000..ebea34e --- /dev/null +++ b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/Impl/WxMiniAppLoginServiceImpl.java @@ -0,0 +1,76 @@ +package com.ruoyi.oauth.wx.service.Impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.auth.common.domain.OauthUser; +import com.ruoyi.auth.common.service.IOauthUserService; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.framework.web.service.UserDetailsServiceImpl; +import com.ruoyi.oauth.wx.constant.WxMiniAppConstant; +import com.ruoyi.oauth.wx.service.WxLoginService; +import com.ruoyi.system.service.ISysUserService; + +@Service +public class WxMiniAppLoginServiceImpl implements WxLoginService { + @Autowired + private WxMiniAppConstant wxAppConstant; + + @Autowired + private TokenService tokenService; + + @Autowired + private UserDetailsServiceImpl userDetailsServiceImpl; + + @Autowired + private ISysUserService userService; + + @Autowired + private IOauthUserService oauthUserService; + + @Override + public String doLogin(String code) { + String openid = doAuth( + wxAppConstant.getUrl(), + wxAppConstant.getAppId(), + wxAppConstant.getAppSecret(), + code).getString("openid"); + OauthUser selectOauthUser = oauthUserService.selectOauthUserByUUID(openid); + if (selectOauthUser == null) { + return null; + } + SysUser sysUser = userService.selectUserById(selectOauthUser.getUserId()); + if (sysUser == null) { + throw new ServiceException("该微信未绑定用户"); + } + LoginUser loginUser = (LoginUser) userDetailsServiceImpl.createLoginUser(sysUser); + return tokenService.createToken(loginUser); + } + + @Override + public String doRegister(OauthUser oauthUser) { + if (StringUtils.isEmpty(oauthUser.getCode())) { + return "没有凭证"; + } + if (oauthUser.getUserId() == null) { + return "请先注册账号"; + } + JSONObject doAuth = doAuth( + wxAppConstant.getUrl(), + wxAppConstant.getAppId(), + wxAppConstant.getAppSecret(), + oauthUser.getCode()); + oauthUser.setOpenId(doAuth.getString("openid")); + oauthUser.setUuid(doAuth.getString("openid")); + oauthUser.setSource("WXMiniApp"); + oauthUser.setAccessToken(doAuth.getString("sessionKey")); + oauthUserService.insertOauthUser(oauthUser); + return ""; + } + +} diff --git a/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/Impl/WxPubLoginServiceImpl.java b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/Impl/WxPubLoginServiceImpl.java new file mode 100644 index 0000000..350fee1 --- /dev/null +++ b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/Impl/WxPubLoginServiceImpl.java @@ -0,0 +1,77 @@ +package com.ruoyi.oauth.wx.service.Impl; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.auth.common.domain.OauthUser; +import com.ruoyi.auth.common.service.IOauthUserService; +import com.ruoyi.common.core.domain.entity.SysUser; +import com.ruoyi.common.core.domain.model.LoginUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.framework.web.service.TokenService; +import com.ruoyi.framework.web.service.UserDetailsServiceImpl; +import com.ruoyi.oauth.wx.constant.WxPubConstant; +import com.ruoyi.oauth.wx.service.WxLoginService; +import com.ruoyi.system.service.ISysUserService; + +@Service +public class WxPubLoginServiceImpl implements WxLoginService { + + @Autowired + private WxPubConstant wxH5Constant; + + @Autowired + private TokenService tokenService; + + @Autowired + private UserDetailsServiceImpl userDetailsServiceImpl; + + @Autowired + private ISysUserService userService; + + @Autowired + private IOauthUserService oauthUserService; + + @Override + public String doLogin(String code) { + String openid = doAuth( + wxH5Constant.getUrl(), + wxH5Constant.getAppId(), + wxH5Constant.getAppSecret(), + code).getString("openid"); + OauthUser selectOauthUser = oauthUserService.selectOauthUserByUUID(openid); + if (selectOauthUser == null) { + return null; + } + SysUser sysUser = userService.selectUserById(selectOauthUser.getUserId()); + if (sysUser == null) { + throw new ServiceException("该微信未绑定用户"); + } + LoginUser loginUser = (LoginUser) userDetailsServiceImpl.createLoginUser(sysUser); + return tokenService.createToken(loginUser); + } + + @Override + public String doRegister(OauthUser oauthUser) { + if (StringUtils.isEmpty(oauthUser.getCode())) { + return "没有凭证"; + } + if (oauthUser.getUserId() == null) { + return "请先注册账号"; + } + JSONObject doAuth = doAuth( + wxH5Constant.getUrl(), + wxH5Constant.getAppId(), + wxH5Constant.getAppSecret(), + oauthUser.getCode()); + oauthUser.setOpenId(doAuth.getString("openid")); + oauthUser.setUuid(doAuth.getString("openid")); + oauthUser.setSource("WXPub"); + oauthUser.setAccessToken(doAuth.getString("sessionKey")); + oauthUserService.insertOauthUser(oauthUser); + return ""; + } + +} diff --git a/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/WxLoginService.java b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/WxLoginService.java new file mode 100644 index 0000000..08b9455 --- /dev/null +++ b/ruoyi-oauth-wx/src/main/java/com/ruoyi/oauth/wx/service/WxLoginService.java @@ -0,0 +1,34 @@ +package com.ruoyi.oauth.wx.service; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.auth.common.domain.OauthUser; +import com.ruoyi.common.exception.ServiceException; +import com.ruoyi.common.utils.http.HttpClientUtil; + +public interface WxLoginService { + + public String doLogin(String code); + + public String doRegister(OauthUser oauthUser); + + public default JSONObject doAuth(String url, String appid, String secret, String code) { + StringBuilder builder = new StringBuilder(url); + builder.append("?appid=").append(appid) + .append("&secret=").append(secret) + .append("&js_code=").append(code) + .append("&grant_type=").append("authorization_code"); + String getMessageUrl = builder.toString(); + String result = HttpClientUtil.sendHttpGet(getMessageUrl); + JSONObject jsonObject = JSON.parseObject(result); + if (jsonObject.containsKey("openid")) { + String openid = jsonObject.getString("openid"); + String sessionKey = jsonObject.getString("session_key"); + System.out.println("openid:" + openid); + System.out.println("sessionKey:" + sessionKey); + return jsonObject; + } else { + throw new ServiceException(jsonObject.getString("errmsg"), jsonObject.getIntValue("errcode")); + } + } +} diff --git a/ruoyi-oauth-wx/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/ruoyi-oauth-wx/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 0000000..e001d72 --- /dev/null +++ b/ruoyi-oauth-wx/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,20 @@ +{ + "properties": [ + { + "name": "oauth.wx.miniapp.app-id", + "type": "java.lang.String", + "description": "wx73d0202b3c8a6d68" + }, + { + "name": "oauth.wx.miniapp.app-secret", + "type": "java.lang.String", + "description": "c0871da0ca140930420c695147f3694b" + }, + { + "name": "oauth.wx.miniapp.url", + "type": "java.lang.String", + "description": "微信小程序认证地址" + } + + ] +} \ No newline at end of file diff --git a/ruoyi-system/pom.xml b/ruoyi-system/pom.xml index 411b265..72be2a8 100644 --- a/ruoyi-system/pom.xml +++ b/ruoyi-system/pom.xml @@ -23,6 +23,20 @@ ruoyi-common + + + + com.ruoyi + ruoyi-auth-common + + + + + + com.qiniu + qiniu-java-sdk + + \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/config/QiniuConfig.java b/ruoyi-system/src/main/java/com/ruoyi/system/config/QiniuConfig.java new file mode 100644 index 0000000..b09798d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/config/QiniuConfig.java @@ -0,0 +1,92 @@ +package com.ruoyi.system.config; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * 七牛云配置属性 + * + * @author ruoyi + */ +@Component +@ConfigurationProperties(prefix = "qiniu") +public class QiniuConfig { + + /** + * 七牛云AccessKey + */ + private String accessKey; + + /** + * 七牛云SecretKey + */ + private String secretKey; + + /** + * 存储空间名称 + */ + private String bucketName; + + /** + * 七牛云域名 + */ + private String domain; + + /** + * 区域选择(华东:region0,华北:region1,华南:region2,北美:regionNa0,东南亚:regionAs0) + */ + private String region = "region0"; + + /** + * 是否启用七牛云上传 + */ + private boolean enabled = false; + + public String getAccessKey() { + return accessKey; + } + + public void setAccessKey(String accessKey) { + this.accessKey = accessKey; + } + + public String getSecretKey() { + return secretKey; + } + + public void setSecretKey(String secretKey) { + this.secretKey = secretKey; + } + + public String getBucketName() { + return bucketName; + } + + public void setBucketName(String bucketName) { + this.bucketName = bucketName; + } + + public String getDomain() { + return domain; + } + + public void setDomain(String domain) { + this.domain = domain; + } + + public String getRegion() { + return region; + } + + public void setRegion(String region) { + this.region = region; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/AppletController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/AppletController.java new file mode 100644 index 0000000..762098d --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/AppletController.java @@ -0,0 +1,709 @@ +package com.ruoyi.system.controller; + +import com.alibaba.fastjson2.JSONObject; +import com.ruoyi.common.core.domain.AjaxResult; +import com.ruoyi.system.domain.*; +import com.ruoyi.system.service.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RestController; +import javax.servlet.http.HttpServletRequest; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.web.bind.annotation.RequestBody; +import com.ruoyi.system.ControllerUtil.AppletControllerUtil; +import static com.ruoyi.common.core.domain.AjaxResult.error; +import static com.ruoyi.common.core.domain.AjaxResult.success; + +/** + * 小程序控制器 + * + * 提供小程序端所需的API接口 + * 主要功能: + * 1. 服务分类管理 + * 2. 服务商品列表和详情 + * 3. 广告图片获取 + * 4. 配置信息查询 + * 5. 用户信息验证 + * + * @author Mr. Zhang Pan + * @date 2025-05-26 + * @version 1.0 + */ +@RestController +public class AppletController { + + @Autowired + private IServiceCateService serviceCateService; + @Autowired + private IUsersService usersService; + @Autowired + private ISiteConfigService siteConfigService; + @Autowired + private IAdvImgService advImgService; + @Autowired + private IServiceGoodsService serviceGoodsService; + /** + * 获取服务分类列表 + * + * @param request HTTP请求对象 + * @return 分类列表数据 + * + * 接口说明: + * - 获取状态为启用的服务分类 + * - 自动添加图片CDN前缀 + * - 支持用户登录状态验证 + */ + @GetMapping(value = "/api/service/cate") + public AjaxResult getInfo(HttpServletRequest request) { + try { + // 验证用户登录状态(可选) + Map userData = AppletControllerUtil.getUserData(request.getHeader("token"), usersService); + + // 构建查询条件:状态启用且类型为服务 + ServiceCate serviceCateQuery = new ServiceCate(); + serviceCateQuery.setStatus(1L); // 启用状态 + serviceCateQuery.setType(1L); // 服务类型 + + // 查询分类列表 + List categoryList = serviceCateService.selectServiceCateList(serviceCateQuery); + + // 为每个分类添加CDN前缀 + for (ServiceCate category : categoryList) { + category.setIcon(AppletControllerUtil.buildImageUrl(category.getIcon())); + } + + return success(categoryList); + } catch (Exception e) { + return error("获取服务分类列表失败:" + e.getMessage()); + } + } + + /** + * 获取系统配置信息 + * + * @param name 配置项名称 + * @param request HTTP请求对象 + * @return 配置信息数据 + * + * 接口说明: + * - 根据配置名称获取对应的配置值 + * - 配置值以JSON格式返回 + * - 支持动态配置管理 + */ + @GetMapping(value = "/api/public/config/{name}") + public AjaxResult config(@PathVariable("name") String name, HttpServletRequest request) { + try { + // 参数验证 + if (name == null || name.trim().isEmpty()) { + return error("配置名称不能为空"); + } + + // 构建查询条件 + SiteConfig configQuery = new SiteConfig(); + configQuery.setName(name.trim()); + + // 查询配置列表 + List configList = siteConfigService.selectSiteConfigList(configQuery); + + if (!configList.isEmpty()) { + // 解析配置值为JSON对象 + String configValue = configList.get(0).getValue(); + if (configValue != null && !configValue.trim().isEmpty()) { + JSONObject jsonObject = JSONObject.parseObject(configValue); + return success(jsonObject); + } else { + return error("配置值为空"); + } + } else { + return error("未找到指定的配置项:" + name); + } + } catch (Exception e) { + return error("获取配置信息失败:" + e.getMessage()); + } + } + + /** + * 获取服务商品列表 + * 前端参数格式:{cate_id: 18, keywords:"dfffff"} + * 返回格式:按分类分组的商品列表 + */ + @PostMapping(value = "/api/service/lst") + public AjaxResult getServiceGoodsList(@RequestBody Map params, HttpServletRequest request) { + try { + Long cateId = null; + String keywords = null; + + // 处理分类ID参数 + if (params.get("cate_id") != null) { + Object cateIdObj = params.get("cate_id"); + if (cateIdObj instanceof Integer) { + cateId = ((Integer) cateIdObj).longValue(); + } else if (cateIdObj instanceof String) { + try { + cateId = Long.parseLong((String) cateIdObj); + } catch (NumberFormatException e) { + return error("分类ID格式错误"); + } + } + } + + // 处理关键词参数 + if (params.get("keywords") != null) { + keywords = params.get("keywords").toString().trim(); + if (keywords.isEmpty()) { + keywords = null; + } + } + + // 构建返回数据结构 + List> resultList = new java.util.ArrayList<>(); + + if (cateId != null) { + // 查询指定分类 + ServiceCate category = serviceCateService.selectServiceCateById(cateId); + if (category != null) { + Map categoryData = AppletControllerUtil.buildCategoryData(category, keywords, serviceGoodsService); + resultList.add(categoryData); + } + } else { + // 查询所有分类 + ServiceCate serviceCateQuery = new ServiceCate(); + serviceCateQuery.setStatus(1L); + serviceCateQuery.setType(1L); + List categories = serviceCateService.selectServiceCateList(serviceCateQuery); + + for (ServiceCate category : categories) { + Map categoryData = AppletControllerUtil.buildCategoryData(category, keywords, serviceGoodsService); + // 只返回有商品的分类 + List> goods = (List>) categoryData.get("goods"); + if (!goods.isEmpty()) { + resultList.add(categoryData); + } + } + } + + return success(resultList); + } catch (Exception e) { + return error("查询服务商品列表失败:" + e.getMessage()); + } + } + + /** + * 获取服务商品详细信息 + * + * @param id 商品ID + * @param request HTTP请求对象 + * @return 商品详细信息 + * + * 接口说明: + * - 根据商品ID获取详细信息 + * - 返回格式化的商品数据 + * - 包含图片、基础信息等数组数据 + */ + @GetMapping(value = "/api/service/info/id/{id}") + public AjaxResult serviceGoodsQuery(@PathVariable("id") long id, HttpServletRequest request) { + try { + // 参数验证 + if (id <= 0) { + return error("商品ID无效"); + } + + // 查询商品信息 + ServiceGoods serviceGoodsData = serviceGoodsService.selectServiceGoodsById(id); + if (serviceGoodsData != null) { + // 使用工具类转换数据格式 + AppletControllerUtil.ServiceGoodsResponse response = new AppletControllerUtil.ServiceGoodsResponse(serviceGoodsData); + return success(response); + } else { + return error("商品不存在或已下架"); + } + } catch (Exception e) { + return error("查询商品详情失败:" + e.getMessage()); + } + } + /** + * 获取广告图片列表 + * + * @param type 广告类型 + * @param request HTTP请求对象 + * @return 广告图片列表 + * + * 接口说明: + * - 根据广告类型获取对应的图片列表 + * - 自动添加图片CDN前缀 + * - 支持多种广告位配置 + */ + @GetMapping(value = "/api/public/adv/lst/{type}") + public AjaxResult getAdvImgData(@PathVariable("type") long type, HttpServletRequest request) { + try { + // 参数验证 + if (type < 0) { + return error("广告类型无效"); + } + + // 构建查询条件 + AdvImg advImgQuery = new AdvImg(); + advImgQuery.setType(type); + + // 查询广告图片列表 + List advImgList = advImgService.selectAdvImgList(advImgQuery); + + // 为每张图片添加CDN前缀 + for (AdvImg advImg : advImgList) { + advImg.setImage(AppletControllerUtil.buildImageUrl(advImg.getImage())); + } + + return success(advImgList); + } catch (Exception e) { + return error("获取广告图片失败:" + e.getMessage()); + } + } + + /** + * 微信用户登录接口 + * + * @param params 请求参数(包含openid) + * @param request HTTP请求对象 + * @return 登录结果 + * + * 请求参数格式: + * { + * "openid": "微信用户openid" + * } + * + * 返回数据格式: + * { + * "code": 200, + * "msg": "操作成功", + * "data": { + * "success": true, + * "token": "用户token", + * "userInfo": {...}, + * "isNewUser": true/false, + * "message": "登录成功" + * } + * } + * + * 接口说明: + * - 验证微信openid的有效性 + * - 首次登录自动创建用户记录 + * - 再次登录更新用户信息和token + * - 生成用户唯一身份token + * - 支持用户信息同步更新 + */ + @PostMapping(value = "/api/wechat/login") + public AjaxResult wechatLogin(@RequestBody Map params, HttpServletRequest request) { + try { + // 1. 参数验证 + if (params == null || !params.containsKey("openid")) { + return error("请求参数不能为空,需要提供openid"); + } + + String openid = (String) params.get("openid"); + if (openid == null || openid.trim().isEmpty()) { + return error("openid不能为空"); + } + + // 2. 调用登录方法 + Map loginResult = AppletControllerUtil.wechatUserLogin(openid.trim(), usersService); + + // 3. 判断登录结果 + boolean success = (Boolean) loginResult.get("success"); + if (success) { + return success(loginResult); + } else { + return error((String) loginResult.get("message")); + } + + } catch (Exception e) { + return error("微信登录失败:" + e.getMessage()); + } + } + + /** + * 验证用户token接口 + * + * @param request HTTP请求对象 + * @return 验证结果 + * + * 请求头格式: + * Authorization: Bearer + * 或 + * token: + * + * 返回数据格式: + * { + * "code": 200, + * "msg": "操作成功", + * "data": { + * "valid": true, + * "userInfo": {...}, + * "message": "token验证成功" + * } + * } + * + * 接口说明: + * - 验证用户token的有效性 + * - 返回用户基本信息 + * - 检查用户账号状态 + * - 过滤敏感信息 + */ + @PostMapping(value = "/api/user/validate") + public AjaxResult validateToken(HttpServletRequest request) { + try { + // 1. 获取token + String token = request.getHeader("token"); + if (token == null || token.trim().isEmpty()) { + // 尝试从Authorization头获取 + String authHeader = request.getHeader("Authorization"); + if (authHeader != null && authHeader.startsWith("Bearer ")) { + token = authHeader.substring(7); + } + } + + if (token == null || token.trim().isEmpty()) { + return error("未提供token,请先登录"); + } + + // 2. 验证token + Map validateResult = AppletControllerUtil.validateUserToken(token.trim(), usersService); + + // 3. 返回验证结果 + boolean valid = (Boolean) validateResult.get("valid"); + if (valid) { + return success(validateResult); + } else { + return error((String) validateResult.get("message")); + } + + } catch (Exception e) { + return error("验证token失败:" + e.getMessage()); + } + } + + + + + /** + * 获取服务商品详细信息 + * + * @param id 商品ID + * @param request HTTP请求对象 + * @return 商品详细信息 + * + * 接口说明: + * - 根据商品ID获取详细信息 + * - 返回格式化的商品数据 + * - 包含图片、基础信息等数组数据 + */ + @GetMapping(value = "/user/phone/login") + public AjaxResult getUserByPhone(@RequestBody Map params,HttpServletRequest request) { + String code=(String)params.get("code"); + String usercode=(String)params.get("phone"); + + if (code==null){ + + } + if (usercode==null){ + + } + return success( ); + + } + + + /** + * 微信支付统一下单接口 + * + * @param params 支付参数 + * @param request HTTP请求对象 + * @return 支付结果 + * + * 请求参数格式: + * { + * "orderNo": "订单号", + * "openid": "用户openid", + * "totalFee": 支付金额(分), + * "body": "商品描述", + * "notifyUrl": "回调地址" + * } + * + * 返回数据格式: + * { + * "code": 200, + * "msg": "操作成功", + * "data": { + * "success": true, + * "payParams": { + * "timeStamp": "时间戳", + * "nonceStr": "随机字符串", + * "package": "prepay_id=xxx", + * "signType": "MD5", + * "paySign": "签名" + * }, + * "prepayId": "预支付交易会话ID" + * } + * } + */ + @PostMapping(value = "/api/pay/unifiedorder") + public AjaxResult createPayOrder(@RequestBody Map params, HttpServletRequest request) { + try { + // 1. 验证用户登录状态 + String token = request.getHeader("token"); + Map userValidation = AppletControllerUtil.validateUserToken(token, usersService); + if (!(Boolean) userValidation.get("valid")) { + return error("用户未登录或token无效"); + } + + // 2. 调用微信支付统一下单 + Map payResult = com.ruoyi.system.ControllerUtil.WechatPayUtil.unifiedOrder(params); + + // 3. 返回结果 + boolean success = (Boolean) payResult.get("success"); + if (success) { + return success(payResult); + } else { + return error((String) payResult.get("message")); + } + + } catch (Exception e) { + return error("创建支付订单失败:" + e.getMessage()); + } + } + + /** + * 查询支付订单状态接口 + * + * @param orderNo 订单号 + * @param request HTTP请求对象 + * @return 查询结果 + */ + @GetMapping(value = "/api/pay/query/{orderNo}") + public AjaxResult queryPayOrder(@PathVariable("orderNo") String orderNo, HttpServletRequest request) { + try { + // 1. 验证用户登录状态 + String token = request.getHeader("token"); + Map userValidation = AppletControllerUtil.validateUserToken(token, usersService); + if (!(Boolean) userValidation.get("valid")) { + return error("用户未登录或token无效"); + } + + // 2. 查询订单状态 + Map queryResult = com.ruoyi.system.ControllerUtil.WechatPayUtil.queryOrder(orderNo, null); + + // 3. 返回结果 + boolean success = (Boolean) queryResult.get("success"); + if (success) { + return success(queryResult); + } else { + return error((String) queryResult.get("message")); + } + + } catch (Exception e) { + return error("查询订单状态失败:" + e.getMessage()); + } + } + + /** + * 创建代付订单接口 + * + * @param params 代付参数 + * @param request HTTP请求对象 + * @return 代付结果 + * + * 请求参数格式: + * { + * "orderNo": "原订单号", + * "payerOpenid": "代付人openid", + * "payeeOpenid": "被代付人openid", + * "totalFee": 代付金额(分), + * "body": "商品描述", + * "notifyUrl": "回调地址", + * "remark": "代付备注" + * } + */ + @PostMapping(value = "/api/pay/payfor") + public AjaxResult createPayForOrder(@RequestBody Map params, HttpServletRequest request) { + try { + // 1. 验证用户登录状态 + String token = request.getHeader("token"); + Map userValidation = AppletControllerUtil.validateUserToken(token, usersService); + if (!(Boolean) userValidation.get("valid")) { + return error("用户未登录或token无效"); + } + + // 2. 创建代付订单 + Map payForResult = com.ruoyi.system.ControllerUtil.WechatPayUtil.createPayForOrder(params); + + // 3. 返回结果 + boolean success = (Boolean) payForResult.get("success"); + if (success) { + return success(payForResult); + } else { + return error((String) payForResult.get("message")); + } + + } catch (Exception e) { + return error("创建代付订单失败:" + e.getMessage()); + } + } + + /** + * 微信支付回调接口 + * + * @param request HTTP请求对象 + * @return 回调处理结果 + * + * 注意:此接口供微信服务器回调使用,不需要用户认证 + * 返回格式必须是XML格式,用于告知微信处理结果 + */ + @PostMapping(value = "/api/pay/notify") + public String handlePayNotify(HttpServletRequest request) { + try { + // 1. 处理支付回调 + Map notifyResult = com.ruoyi.system.ControllerUtil.WechatPayUtil.handlePayNotify(request); + + // 2. 获取支付信息 + boolean success = (Boolean) notifyResult.get("success"); + if (success) { + Map paymentInfo = (Map) notifyResult.get("paymentInfo"); + boolean isPayFor = (Boolean) notifyResult.get("isPayFor"); + + // 3. 根据业务需要处理支付成功逻辑 + // 例如:更新订单状态、发送通知等 + handlePaymentSuccess(paymentInfo, isPayFor); + + // 4. 返回成功响应给微信 + return (String) notifyResult.get("responseXml"); + } else { + // 5. 返回失败响应给微信 + return (String) notifyResult.get("responseXml"); + } + + } catch (Exception e) { + // 6. 异常时返回失败响应 + return ""; + } + } + + /** + * 申请退款接口 + * + * @param params 退款参数 + * @param request HTTP请求对象 + * @return 退款结果 + * + * 请求参数格式: + * { + * "orderNo": "原订单号", + * "refundNo": "退款单号", + * "totalFee": 订单总金额(分), + * "refundFee": 退款金额(分), + * "refundDesc": "退款原因" + * } + */ + @PostMapping(value = "/api/pay/refund") + public AjaxResult refundOrder(@RequestBody Map params, HttpServletRequest request) { + try { + // 1. 验证用户登录状态 + String token = request.getHeader("token"); + Map userValidation = AppletControllerUtil.validateUserToken(token, usersService); + if (!(Boolean) userValidation.get("valid")) { + return error("用户未登录或token无效"); + } + + // 2. 申请退款 + Map refundResult = com.ruoyi.system.ControllerUtil.WechatPayUtil.refund(params); + + // 3. 返回结果 + boolean success = (Boolean) refundResult.get("success"); + if (success) { + return success(refundResult); + } else { + return error((String) refundResult.get("message")); + } + + } catch (Exception e) { + return error("申请退款失败:" + e.getMessage()); + } + } + + /** + * 企业付款接口(用于代付等场景) + * + * @param params 付款参数 + * @param request HTTP请求对象 + * @return 付款结果 + * + * 请求参数格式: + * { + * "partnerTradeNo": "商户订单号", + * "openid": "用户openid", + * "amount": 付款金额(分), + * "desc": "付款描述" + * } + */ + @PostMapping(value = "/api/pay/transfer") + public AjaxResult transferToUser(@RequestBody Map params, HttpServletRequest request) { + try { + // 1. 验证用户登录状态(这里可能需要管理员权限) + String token = request.getHeader("token"); + Map userValidation = AppletControllerUtil.validateUserToken(token, usersService); + if (!(Boolean) userValidation.get("valid")) { + return error("用户未登录或token无效"); + } + + // 2. 企业付款 + Map transferResult = com.ruoyi.system.ControllerUtil.WechatPayUtil.transferToUser(params); + + // 3. 返回结果 + boolean success = (Boolean) transferResult.get("success"); + if (success) { + return success(transferResult); + } else { + return error((String) transferResult.get("message")); + } + + } catch (Exception e) { + return error("企业付款失败:" + e.getMessage()); + } + } + + /** + * 处理支付成功的业务逻辑 + * + * @param paymentInfo 支付信息 + * @param isPayFor 是否为代付订单 + */ + private void handlePaymentSuccess(Map paymentInfo, boolean isPayFor) { + try { + String orderNo = (String) paymentInfo.get("outTradeNo"); + String transactionId = (String) paymentInfo.get("transactionId"); + String totalFee = (String) paymentInfo.get("totalFee"); + String timeEnd = (String) paymentInfo.get("timeEnd"); + + // 根据业务需求处理支付成功逻辑 + if (isPayFor) { + // 代付订单处理逻辑 + // 1. 更新代付订单状态 + // 2. 处理原订单状态 + // 3. 给被代付人转账等 + System.out.println("处理代付订单支付成功:" + orderNo); + } else { + // 普通订单处理逻辑 + // 1. 更新订单状态 + // 2. 发送支付成功通知 + // 3. 处理库存等业务逻辑 + System.out.println("处理普通订单支付成功:" + orderNo); + } + + } catch (Exception e) { + System.err.println("处理支付成功业务逻辑异常:" + e.getMessage()); + } + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ServiceGoodsController.java b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ServiceGoodsController.java index 820a5e0..2da4046 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/controller/ServiceGoodsController.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controller/ServiceGoodsController.java @@ -26,33 +26,32 @@ import com.ruoyi.common.core.page.TableDataInfo; /** * 服务内容Controller - * + * * @author ruoyi * @date 2025-05-13 */ @RestController @RequestMapping("/system/ServiceGoods") -public class ServiceGoodsController extends BaseController -{ +public class ServiceGoodsController extends BaseController { @Autowired private IServiceGoodsService serviceGoodsService; -@Autowired -private IServiceCateService serviceCateService; + @Autowired + private IServiceCateService serviceCateService; + /** * 查询服务内容列表 */ @PreAuthorize("@ss.hasPermi('system:ServiceGoods:list')") @GetMapping("/list") - public TableDataInfo list(ServiceGoods serviceGoods) - { + public TableDataInfo list(ServiceGoods serviceGoods) { startPage(); List list = serviceGoodsService.selectServiceGoodsList(serviceGoods); - for(ServiceGoods serviceGoodsdata:list){ - ServiceCate serviceCate=serviceCateService.selectServiceCateById(serviceGoodsdata.getCateId()); - if(serviceCate!=null){ + for (ServiceGoods serviceGoodsdata : list) { + ServiceCate serviceCate = serviceCateService.selectServiceCateById(serviceGoodsdata.getCateId()); + if (serviceCate != null) { serviceGoodsdata.setCateName(serviceCate.getTitle()); } - serviceGoodsdata.setIcon("https://img.huafurenjia.cn/"+serviceGoodsdata.getIcon()); + serviceGoodsdata.setIcon("https://img.huafurenjia.cn/" + serviceGoodsdata.getIcon()); } return getDataTable(list); @@ -64,8 +63,7 @@ private IServiceCateService serviceCateService; @PreAuthorize("@ss.hasPermi('system:ServiceGoods:export')") @Log(title = "服务内容", businessType = BusinessType.EXPORT) @PostMapping("/export") - public void export(HttpServletResponse response, ServiceGoods serviceGoods) - { + public void export(HttpServletResponse response, ServiceGoods serviceGoods) { List list = serviceGoodsService.selectServiceGoodsList(serviceGoods); ExcelUtil util = new ExcelUtil(ServiceGoods.class); util.exportExcel(response, list, "服务内容数据"); @@ -76,8 +74,7 @@ private IServiceCateService serviceCateService; */ @PreAuthorize("@ss.hasPermi('system:ServiceGoods:query')") @GetMapping(value = "/selectServiceCateList") - public AjaxResult selectServiceCateList() - { + public AjaxResult selectServiceCateList() { return success(serviceCateService.selectServiceCateList(new ServiceCate())); } @@ -87,8 +84,7 @@ private IServiceCateService serviceCateService; */ @PreAuthorize("@ss.hasPermi('system:ServiceGoods:query')") @GetMapping(value = "/{id}") - public AjaxResult getInfo(@PathVariable("id") Long id) - { + public AjaxResult getInfo(@PathVariable("id") Long id) { return success(serviceGoodsService.selectServiceGoodsById(id)); } @@ -98,8 +94,10 @@ private IServiceCateService serviceCateService; @PreAuthorize("@ss.hasPermi('system:ServiceGoods:add')") @Log(title = "服务内容", businessType = BusinessType.INSERT) @PostMapping - public AjaxResult add(@RequestBody ServiceGoods serviceGoods) - { + public AjaxResult add(@RequestBody ServiceGoods serviceGoods) { + // 验证和处理基检现象数据格式 + validateAndProcessBasicField(serviceGoods); + return toAjax(serviceGoodsService.insertServiceGoods(serviceGoods)); } @@ -109,18 +107,20 @@ private IServiceCateService serviceCateService; @PreAuthorize("@ss.hasPermi('system:ServiceGoods:edit')") @Log(title = "服务内容", businessType = BusinessType.UPDATE) @PutMapping - public AjaxResult edit(@RequestBody ServiceGoods serviceGoods) - { + public AjaxResult edit(@RequestBody ServiceGoods serviceGoods) { + // 验证和处理基检现象数据格式 + validateAndProcessBasicField(serviceGoods); + return toAjax(serviceGoodsService.updateServiceGoods(serviceGoods)); } + /** * 定时任务状态修改 */ @PreAuthorize("@ss.hasPermi('system:ServiceGoods:changeStatus')") @Log(title = "修改状态", businessType = BusinessType.UPDATE) @PutMapping("/changeStatus") - public AjaxResult changeStatus(@RequestBody ServiceGoods serviceGoods) - { + public AjaxResult changeStatus(@RequestBody ServiceGoods serviceGoods) { ServiceGoods newServiceGoods = serviceGoodsService.selectServiceGoodsById(serviceGoods.getId()); newServiceGoods.setStatus(serviceGoods.getStatus()); return toAjax(serviceGoodsService.updateServiceGoods(newServiceGoods)); @@ -131,9 +131,61 @@ private IServiceCateService serviceCateService; */ @PreAuthorize("@ss.hasPermi('system:ServiceGoods:remove')") @Log(title = "服务内容", businessType = BusinessType.DELETE) - @DeleteMapping("/{ids}") - public AjaxResult remove(@PathVariable Long[] ids) - { + @DeleteMapping("/{ids}") + public AjaxResult remove(@PathVariable Long[] ids) { return toAjax(serviceGoodsService.deleteServiceGoodsByIds(ids)); } + + /** + * 验证和处理基检现象数据格式 + * 确保基检现象字段为有效的JSON数组字符串格式 + */ + private void validateAndProcessBasicField(ServiceGoods serviceGoods) { + String basic = serviceGoods.getBasic(); + if (basic != null && !basic.trim().isEmpty()) { + try { + // 尝试解析为JSON数组,验证格式是否正确 + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + Object parsed = mapper.readValue(basic, Object.class); + + if (parsed instanceof java.util.List) { + // 如果是有效的JSON数组,保持不变 + // 可以在这里进行额外的数据清理,比如去除空字符串 + @SuppressWarnings("unchecked") + java.util.List list = (java.util.List) parsed; + list.removeIf(item -> item == null || item.trim().isEmpty()); + + // 重新序列化,确保格式统一 + String cleanedBasic = mapper.writeValueAsString(list); + serviceGoods.setBasic(cleanedBasic); + } else { + // 如果不是数组格式,设置为null + serviceGoods.setBasic(null); + } + } catch (Exception e) { + // 如果JSON解析失败,尝试按逗号分隔的字符串处理(兼容旧数据) + String[] items = basic.split(","); + java.util.List list = new java.util.ArrayList<>(); + for (String item : items) { + String trimmed = item.trim(); + if (!trimmed.isEmpty()) { + list.add(trimmed); + } + } + + if (!list.isEmpty()) { + try { + com.fasterxml.jackson.databind.ObjectMapper mapper = new com.fasterxml.jackson.databind.ObjectMapper(); + String jsonArray = mapper.writeValueAsString(list); + serviceGoods.setBasic(jsonArray); + } catch (Exception ex) { + // 转换失败,设置为null + serviceGoods.setBasic(null); + } + } else { + serviceGoods.setBasic(null); + } + } + } + } } diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/AppletControllerUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/AppletControllerUtil.java new file mode 100644 index 0000000..40a6424 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/AppletControllerUtil.java @@ -0,0 +1,798 @@ +package com.ruoyi.system.ControllerUtil; + +import com.alibaba.fastjson2.JSONArray; +import com.ruoyi.system.domain.ServiceCate; +import com.ruoyi.system.domain.ServiceGoods; +import com.ruoyi.system.domain.Users; +import com.ruoyi.system.service.IServiceGoodsService; +import com.ruoyi.system.service.IUsersService; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * 小程序控制器工具类 + * + * 提供小程序相关的数据处理、响应实体转换等工具方法 + * 主要功能: + * 1. 服务商品响应数据格式化 + * 2. 分类数据构建 + * 3. 用户数据处理 + * 4. 图片和基础信息数组解析 + * + * @author Mr. Zhang Pan + * @date 2025-01-03 + * @version 1.0 + */ +public class AppletControllerUtil { + + /** + * 服务商品详情响应实体类 + * + * 用于格式化服务商品数据,统一小程序端的数据结构 + * 将数据库实体ServiceGoods转换为前端需要的JSON格式 + * + * 主要特性: + * - 自动处理图片URL添加CDN前缀 + * - 支持JSON数组和逗号分隔的字符串解析 + * - 统一数据类型转换(BigDecimal转String等) + * - 设置合理的默认值避免空值异常 + */ + public static class ServiceGoodsResponse { + + /** 商品ID */ + private Long id; + + /** 商品标题 */ + private String title; + + /** 商品图标(自动添加CDN前缀) */ + private String icon; + + /** 商品轮播图数组(自动添加CDN前缀) */ + private List imgs; + + /** 商品副标题 */ + private String sub_title; + + /** 商品简介 */ + private String info; + + /** 商品价格(字符串格式) */ + private String price; + + /** 列表价格显示(字符串格式) */ + private String price_in; + + /** 销量 */ + private Integer sales; + + /** 库存 */ + private Integer stock; + + /** 状态 */ + private String status; + + /** 商品详情描述 */ + private String description; + + /** 规格类型 1:单规格 2:多规格 */ + private Integer sku_type; + + /** SKU规格对象 */ + private Map sku; + + /** 纬度 */ + private String latitude; + + /** 经度 */ + private String longitude; + + /** 类型 1:服务 2:商品 */ + private Integer type; + + /** 分类ID */ + private Long cate_id; + + /** 服务项目 */ + private String project; + + /** 排序 */ + private Integer sort; + + /** 物料费用 */ + private String material; + + /** 邮费(字符串格式) */ + private String postage; + + /** 基检现象数组 */ + private List basic; + + /** 保证金(字符串格式) */ + private String margin; + + /** 所需技能ID */ + private String skill_ids; + + /** 创建时间 */ + private String created_at; + + /** 更新时间 */ + private String updated_at; + + /** 删除时间 */ + private String deleted_at; + + /** 评论对象(预留扩展) */ + private Map comment; + + /** + * 构造方法 - 将ServiceGoods实体转换为响应格式 + * + * @param goods ServiceGoods实体对象 + * + * 主要处理: + * 1. 基础字段映射和类型转换 + * 2. 图片URL添加CDN前缀 + * 3. 数组字段解析(支持JSON和逗号分隔) + * 4. 空值处理和默认值设置 + * 5. 特殊对象构建(SKU、评论等) + */ + public ServiceGoodsResponse(ServiceGoods goods) { + // 基础字段映射 + this.id = goods.getId(); + this.title = goods.getTitle(); + this.icon = buildImageUrl(goods.getIcon()); + this.sub_title = goods.getSubTitle(); + this.info = goods.getInfo(); + + // 价格字段处理 - BigDecimal转String避免精度问题 + this.price = goods.getPrice() != null ? goods.getPrice().toString() : "0.00"; + this.price_in = goods.getPriceZn() != null ? goods.getPriceZn() : ""; + + // 数值字段处理 - Long转Integer并设置默认值 + this.sales = goods.getSales() != null ? goods.getSales().intValue() : 0; + this.stock = goods.getStock() != null ? goods.getStock().intValue() : 0; + this.sort = goods.getSort() != null ? goods.getSort().intValue() : 1; + + // 状态和类型字段 + this.status = goods.getStatus() != null ? goods.getStatus() : "1"; + this.type = goods.getType() != null ? goods.getType().intValue() : 1; + this.sku_type = goods.getSkuType() != null ? goods.getSkuType().intValue() : 1; + + // 文本字段 + this.description = goods.getDescription(); + this.latitude = goods.getLatitude(); + this.longitude = goods.getLongitude(); + this.cate_id = goods.getCateId(); + this.project = goods.getProject(); + this.material = goods.getMaterial(); + this.skill_ids = goods.getSkillIds(); + + // 金额字段处理 - BigDecimal转String + this.postage = goods.getPostage() != null ? goods.getPostage().toString() : ""; + this.margin = goods.getMargin() != null ? goods.getMargin().toString() : ""; + + // 时间字段处理 + this.created_at = goods.getCreatedAt() != null ? goods.getCreatedAt().toString() : ""; + this.updated_at = goods.getUpdatedAt() != null ? goods.getUpdatedAt().toString() : ""; + this.deleted_at = goods.getDeletedAt() != null ? goods.getDeletedAt().toString() : null; + + // 数组字段解析 + this.imgs = parseStringToImageList(goods.getImgs()); + this.basic = parseStringToList(goods.getBasic()); + + // 构建SKU对象 - 默认单规格 + this.sku = new HashMap<>(); + this.sku.put("type", "single"); + + // 构建评论对象 - 预留扩展 + this.comment = new HashMap<>(); + } + + // Getter和Setter方法(完整实现) + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public String getTitle() { return title; } + public void setTitle(String title) { this.title = title; } + + public String getIcon() { return icon; } + public void setIcon(String icon) { this.icon = icon; } + + public List getImgs() { return imgs; } + public void setImgs(List imgs) { this.imgs = imgs; } + + public String getSub_title() { return sub_title; } + public void setSub_title(String sub_title) { this.sub_title = sub_title; } + + public String getInfo() { return info; } + public void setInfo(String info) { this.info = info; } + + public String getPrice() { return price; } + public void setPrice(String price) { this.price = price; } + + public String getPrice_in() { return price_in; } + public void setPrice_in(String price_in) { this.price_in = price_in; } + + public Integer getSales() { return sales; } + public void setSales(Integer sales) { this.sales = sales; } + + public Integer getStock() { return stock; } + public void setStock(Integer stock) { this.stock = stock; } + + public String getStatus() { return status; } + public void setStatus(String status) { this.status = status; } + + public String getDescription() { return description; } + public void setDescription(String description) { this.description = description; } + + public Integer getSku_type() { return sku_type; } + public void setSku_type(Integer sku_type) { this.sku_type = sku_type; } + + public Map getSku() { return sku; } + public void setSku(Map sku) { this.sku = sku; } + + public String getLatitude() { return latitude; } + public void setLatitude(String latitude) { this.latitude = latitude; } + + public String getLongitude() { return longitude; } + public void setLongitude(String longitude) { this.longitude = longitude; } + + public Integer getType() { return type; } + public void setType(Integer type) { this.type = type; } + + public Long getCate_id() { return cate_id; } + public void setCate_id(Long cate_id) { this.cate_id = cate_id; } + + public String getProject() { return project; } + public void setProject(String project) { this.project = project; } + + public Integer getSort() { return sort; } + public void setSort(Integer sort) { this.sort = sort; } + + public String getMaterial() { return material; } + public void setMaterial(String material) { this.material = material; } + + public String getPostage() { return postage; } + public void setPostage(String postage) { this.postage = postage; } + + public List getBasic() { return basic; } + public void setBasic(List basic) { this.basic = basic; } + + public String getMargin() { return margin; } + public void setMargin(String margin) { this.margin = margin; } + + public String getSkill_ids() { return skill_ids; } + public void setSkill_ids(String skill_ids) { this.skill_ids = skill_ids; } + + public String getCreated_at() { return created_at; } + public void setCreated_at(String created_at) { this.created_at = created_at; } + + public String getUpdated_at() { return updated_at; } + public void setUpdated_at(String updated_at) { this.updated_at = updated_at; } + + public String getDeleted_at() { return deleted_at; } + public void setDeleted_at(String deleted_at) { this.deleted_at = deleted_at; } + + public Map getComment() { return comment; } + public void setComment(Map comment) { this.comment = comment; } + } + + /** + * CDN域名常量 + * 用于图片URL统一添加CDN前缀 + */ + private static final String CDN_BASE_URL = "https://img.huafurenjia.cn/"; + + /** + * 构建完整的图片URL + * + * @param imagePath 图片路径 + * @return 完整的图片URL(包含CDN前缀) + * + * 处理逻辑: + * - 如果图片路径为空,返回空字符串 + * - 自动添加CDN域名前缀 + * - 确保URL格式正确 + */ + public static String buildImageUrl(String imagePath) { + if (imagePath == null || imagePath.trim().isEmpty()) { + return ""; + } + return CDN_BASE_URL + imagePath.trim(); + } + + /** + * 解析字符串为图片URL列表 + * + * @param imgsString 图片字符串(支持JSON数组或逗号分隔) + * @return 图片URL列表(已添加CDN前缀) + * + * 支持的格式: + * 1. JSON数组格式:["img1.jpg", "img2.jpg"] + * 2. 逗号分隔格式:img1.jpg,img2.jpg + * + * 处理特性: + * - 自动识别数据格式 + * - 过滤空值和空白字符 + * - 自动添加CDN前缀 + * - 异常容错处理 + */ + public static List parseStringToImageList(String imgsString) { + List imageList = new java.util.ArrayList<>(); + + if (imgsString == null || imgsString.trim().isEmpty()) { + return imageList; + } + + try { + // 尝试解析JSON数组格式 + JSONArray jsonArray = JSONArray.parseArray(imgsString); + for (int i = 0; i < jsonArray.size(); i++) { + String img = jsonArray.getString(i); + if (img != null && !img.trim().isEmpty()) { + imageList.add(buildImageUrl(img.trim())); + } + } + } catch (Exception e) { + // JSON解析失败,按逗号分割处理 + String[] imgArray = imgsString.split(","); + for (String img : imgArray) { + String trimmedImg = img.trim(); + if (!trimmedImg.isEmpty()) { + imageList.add(buildImageUrl(trimmedImg)); + } + } + } + + return imageList; + } + + /** + * 解析字符串为普通字符串列表 + * + * @param dataString 数据字符串(支持JSON数组或逗号分隔) + * @return 字符串列表 + * + * 用于解析基检现象、标签等文本数组数据 + * + * 支持的格式: + * 1. JSON数组格式:["现象1", "现象2"] + * 2. 逗号分隔格式:现象1,现象2 + * + * 处理特性: + * - 自动识别数据格式 + * - 过滤空值和空白字符 + * - 异常容错处理 + */ + public static List parseStringToList(String dataString) { + List dataList = new java.util.ArrayList<>(); + + if (dataString == null || dataString.trim().isEmpty()) { + return dataList; + } + + try { + // 尝试解析JSON数组格式 + JSONArray jsonArray = JSONArray.parseArray(dataString); + for (int i = 0; i < jsonArray.size(); i++) { + String item = jsonArray.getString(i); + if (item != null && !item.trim().isEmpty()) { + dataList.add(item.trim()); + } + } + } catch (Exception e) { + // JSON解析失败,按逗号分割处理 + String[] dataArray = dataString.split(","); + for (String item : dataArray) { + String trimmedItem = item.trim(); + if (!trimmedItem.isEmpty()) { + dataList.add(trimmedItem); + } + } + } + + return dataList; + } + + /** + * 构建分类数据 + * + * @param category 分类信息 + * @param keywords 搜索关键词(可选) + * @param serviceGoodsService 服务商品Service + * @return 格式化的分类数据 + * + * 返回数据结构: + * { + * "id": 分类ID, + * "title": 分类标题, + * "icon": 分类图标URL, + * "type": 分类类型, + * "cate_id": 分类ID, + * "goods": [商品列表] + * } + * + * 功能特性: + * - 查询分类下的所有商品 + * - 支持关键词搜索过滤 + * - 自动添加图片CDN前缀 + * - 返回统一的数据格式 + */ + public static Map buildCategoryData(ServiceCate category, String keywords, + IServiceGoodsService serviceGoodsService) { + Map categoryData = new HashMap<>(); + + // 分类基础信息 + categoryData.put("id", category.getId()); + categoryData.put("title", category.getTitle()); + categoryData.put("icon", buildImageUrl(category.getIcon())); + categoryData.put("type", category.getType()); + categoryData.put("cate_id", category.getId()); + + // 查询该分类下的商品 + ServiceGoods serviceGoodsQuery = new ServiceGoods(); + serviceGoodsQuery.setCateId(category.getId()); + + // 如果有关键词,添加搜索条件 + if (keywords != null && !keywords.trim().isEmpty()) { + serviceGoodsQuery.setTitle(keywords.trim()); + } + + // 查询商品列表 + List goodsList = serviceGoodsService.selectServiceGoodsList(serviceGoodsQuery); + List> goodsDataList = new java.util.ArrayList<>(); + + // 构建商品数据列表 + for (ServiceGoods goods : goodsList) { + Map goodsData = new HashMap<>(); + goodsData.put("id", goods.getId()); + goodsData.put("title", goods.getTitle()); + goodsData.put("icon", buildImageUrl(goods.getIcon())); + goodsData.put("type", goods.getType()); + goodsData.put("cate_id", goods.getCateId()); + goodsDataList.add(goodsData); + } + + categoryData.put("goods", goodsDataList); + return categoryData; + } + + /** + * 获取用户数据 + * + * @param token 用户令牌 + * @param usersService 用户Service + * @return 用户数据Map + * + * 返回数据结构: + * - code: 200(成功) / 302(未登录) / 400(令牌无效) + * - user: 用户信息对象(成功时返回) + * + * 状态码说明: + * - 200: 令牌有效,用户存在 + * - 302: 未提供令牌,需要登录 + * - 400: 令牌无效或用户不存在 + * + * 主要用途: + * - 验证用户登录状态 + * - 获取用户基础信息 + * - 权限验证前置处理 + */ + public static Map getUserData(String token, IUsersService usersService) { + Map resultMap = new HashMap<>(); + + if (token == null || token.trim().isEmpty()) { + // 未提供令牌 + resultMap.put("code", 302); + resultMap.put("message", "未登录,请先登录"); + } else { + // 查询用户信息 + Users user = usersService.selectUsersByRememberToken(token.trim()); + if (user != null) { + // 用户存在且令牌有效 + resultMap.put("code", 200); + resultMap.put("user", user); + resultMap.put("message", "获取用户信息成功"); + } else { + // 令牌无效或用户不存在 + resultMap.put("code", 400); + resultMap.put("message", "令牌无效或用户不存在"); + } + } + + return resultMap; + } + + /** + * 微信用户登录方法 + * + * @param openid 微信用户openid + * @param usersService 用户Service + * @return 登录结果Map + * + * 返回数据结构: + * { + * "success": true/false, // 登录是否成功 + * "token": "用户token", // 登录成功时返回 + * "userInfo": {...}, // 用户信息 + * "message": "提示信息" // 操作结果消息 + * } + * + * 登录流程: + * 1. 验证openid的有效性 + * 2. 查询或创建用户记录 + * 3. 生成用户token + * 4. 更新用户登录信息 + * 5. 返回登录结果 + * + * 业务逻辑: + * - 首次登录:创建新用户记录 + * - 再次登录:更新登录时间和token + * - 支持用户信息同步更新 + */ + public static Map wechatUserLogin(String openid, IUsersService usersService) { + Map result = new HashMap<>(); + + try { + // 1. 参数验证 + if (openid == null || openid.trim().isEmpty()) { + result.put("success", false); + result.put("message", "openid不能为空"); + return result; + } + + String trimmedOpenid = openid.trim(); + + // 2. 验证openid有效性 + Map validationResult = WechatApiUtil.validateOpenid(trimmedOpenid); + if (!(Boolean) validationResult.get("valid")) { + result.put("success", false); + result.put("message", "openid验证失败:" + validationResult.get("errorMsg")); + return result; + } + + // 3. 获取微信用户信息 + Map wechatUserInfo = (Map) validationResult.get("userInfo"); + + // 4. 查询数据库中是否已存在该用户 + Users existingUser = usersService.selectUsersByOpenid(trimmedOpenid); + + Users userRecord; + boolean isNewUser = false; + + if (existingUser == null) { + // 4.1 首次登录,创建新用户记录 + userRecord = createNewWechatUser(trimmedOpenid, wechatUserInfo); + isNewUser = true; + } else { + // 4.2 用户已存在,更新用户信息 + userRecord = updateExistingWechatUser(existingUser, wechatUserInfo); + } + + // 5. 生成新的用户token + String userToken = WechatApiUtil.generateUserToken(trimmedOpenid); + userRecord.setRememberToken(userToken); + + // 6. 保存或更新用户记录到数据库 + if (isNewUser) { + // 插入新用户 + int insertResult = usersService.insertUsers(userRecord); + if (insertResult <= 0) { + result.put("success", false); + result.put("message", "创建用户记录失败"); + return result; + } + } else { + // 更新现有用户 + int updateResult = usersService.updateUsers(userRecord); + if (updateResult <= 0) { + result.put("success", false); + result.put("message", "更新用户记录失败"); + return result; + } + } + + // 7. 构建返回数据 + Map responseUserInfo = buildUserResponseInfo(userRecord, wechatUserInfo); + + result.put("success", true); + result.put("token", userToken); + result.put("userInfo", responseUserInfo); + result.put("isNewUser", isNewUser); + result.put("loginTime", System.currentTimeMillis()); + result.put("message", isNewUser ? "注册并登录成功" : "登录成功"); + + } catch (Exception e) { + result.put("success", false); + result.put("message", "登录过程中发生错误:" + e.getMessage()); + } + + return result; + } + + /** + * 创建新的微信用户记录 + * + * @param openid 微信openid + * @param wechatUserInfo 微信用户信息 + * @return 新创建的用户记录 + * + * 用户字段设置: + * - 基础信息:openid、昵称、头像等 + * - 状态信息:启用状态、注册时间等 + * - 默认值:密码、角色等系统字段 + */ + private static Users createNewWechatUser(String openid, Map wechatUserInfo) { + Users newUser = new Users(); + + // 基础信息 + newUser.setOpenid(openid); + newUser.setNickname((String) wechatUserInfo.get("nickname")); + newUser.setAvatar((String) wechatUserInfo.get("avatar")); + + // 用户名使用openid(可以根据业务需求调整) + newUser.setName("wx_" + openid.substring(openid.length() - 8)); + + // 设置默认密码(微信登录不需要密码) + newUser.setPassword(""); // 实际项目中可能需要加密的默认密码 + + // 状态信息 + newUser.setStatus(1); // 启用状态:1启用 0关闭 + newUser.setType("1"); // 用户类型:1普通用户 2师傅 + + // 时间信息 + java.util.Date now = new java.util.Date(); + newUser.setCreateTime(now); + newUser.setUpdateTime(now); + + // 其他信息 + newUser.setRemark("微信小程序用户"); + + return newUser; + } + + /** + * 更新现有微信用户记录 + * + * @param existingUser 现有用户记录 + * @param wechatUserInfo 微信用户信息 + * @return 更新后的用户记录 + * + * 更新策略: + * - 同步微信最新信息:昵称、头像等 + * - 更新登录时间 + * - 保持原有的系统信息不变 + */ + private static Users updateExistingWechatUser(Users existingUser, Map wechatUserInfo) { + // 更新微信相关信息 + existingUser.setNickname((String) wechatUserInfo.get("nickname")); + existingUser.setAvatar((String) wechatUserInfo.get("avatar")); + + // 更新登录时间 + existingUser.setUpdateTime(new java.util.Date()); + + return existingUser; + } + + /** + * 构建用户响应信息 + * + * @param userRecord 用户数据库记录 + * @param wechatUserInfo 微信用户信息 + * @return 格式化的用户信息 + * + * 返回字段: + * - 基础信息:用户ID、昵称、头像等 + * - 微信信息:openid等 + * - 状态信息:用户状态、注册时间等 + * - 过滤敏感信息:密码等 + */ + private static Map buildUserResponseInfo(Users userRecord, Map wechatUserInfo) { + Map userInfo = new HashMap<>(); + + // 基础用户信息 + userInfo.put("userId", userRecord.getId()); + userInfo.put("username", userRecord.getName()); + userInfo.put("nickname", userRecord.getNickname()); + userInfo.put("avatar", userRecord.getAvatar()); + userInfo.put("openid", userRecord.getOpenid()); + + // 状态信息 + userInfo.put("status", userRecord.getStatus()); + userInfo.put("userType", userRecord.getType()); + userInfo.put("createTime", userRecord.getCreateTime()); + userInfo.put("updateTime", userRecord.getUpdateTime()); + + // 微信附加信息 + userInfo.put("gender", wechatUserInfo.get("gender")); + userInfo.put("city", wechatUserInfo.get("city")); + userInfo.put("province", wechatUserInfo.get("province")); + userInfo.put("country", wechatUserInfo.get("country")); + + return userInfo; + } + + /** + * 验证用户token有效性 + * + * @param token 用户token + * @param usersService 用户Service + * @return 验证结果Map + * + * 返回数据结构: + * { + * "valid": true/false, // token是否有效 + * "userInfo": {...}, // 用户信息(有效时返回) + * "message": "提示信息" // 验证结果消息 + * } + * + * 验证逻辑: + * 1. 检查token格式是否正确 + * 2. 从数据库查询token对应的用户 + * 3. 验证用户状态是否正常 + * 4. 检查token是否过期(可选) + * 5. 返回验证结果 + */ + public static Map validateUserToken(String token, IUsersService usersService) { + Map result = new HashMap<>(); + + try { + // 1. 参数验证 + if (token == null || token.trim().isEmpty()) { + result.put("valid", false); + result.put("message", "token不能为空"); + return result; + } + + String trimmedToken = token.trim(); + + // 2. 检查token格式 + if (!WechatApiUtil.isValidTokenFormat(trimmedToken)) { + result.put("valid", false); + result.put("message", "token格式无效"); + return result; + } + + // 3. 从数据库查询token对应的用户 + Users user = usersService.selectUsersByRememberToken(trimmedToken); + if (user == null) { + result.put("valid", false); + result.put("message", "token无效或已过期"); + return result; + } + + // 4. 检查用户状态 + if (!"1".equals(user.getStatus())) { + result.put("valid", false); + result.put("message", "用户账号已被禁用"); + return result; + } + + // 5. 构建用户信息(过滤敏感字段) + Map userInfo = new HashMap<>(); + userInfo.put("userId", user.getId()); + userInfo.put("username", user.getName()); + userInfo.put("nickname", user.getNickname()); + userInfo.put("avatar", user.getAvatar()); + userInfo.put("openid", user.getOpenid()); + userInfo.put("status", user.getStatus()); + userInfo.put("userType", user.getType()); + userInfo.put("createTime", user.getCreateTime()); + userInfo.put("updateTime", user.getUpdateTime()); + + result.put("valid", true); + result.put("userInfo", userInfo); + result.put("message", "token验证成功"); + + } catch (Exception e) { + result.put("valid", false); + result.put("message", "验证token时发生错误:" + e.getMessage()); + } + + return result; + } +} diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatApiUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatApiUtil.java new file mode 100644 index 0000000..c7c4d6f --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatApiUtil.java @@ -0,0 +1,362 @@ +package com.ruoyi.system.ControllerUtil; + +import com.alibaba.fastjson2.JSONObject; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import java.security.MessageDigest; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +/** + * 微信API工具类 + * + * 提供微信小程序相关的API调用功能 + * 主要功能: + * 1. 微信用户openid验证 + * 2. 用户token生成和管理 + * 3. 微信API调用封装 + * 4. 用户信息处理 + * + * @author Mr. Zhang Pan + * @date 2025-01-03 + * @version 1.0 + */ +public class WechatApiUtil { + + /** + * 微信小程序配置常量 + * 注意:实际使用时需要在配置文件中配置这些值 + */ + private static final String WECHAT_APPID = "wx73d0202b3c8a6d68"; // 微信小程序AppID + private static final String WECHAT_SECRET = "c0871da0ca140930420c695147f3694b"; // 微信小程序Secret + private static final String WECHAT_API_BASE_URL = "https://api.weixin.qq.com"; // 微信API基础URL + + /** + * Token配置常量 + */ + private static final String TOKEN_PREFIX = "WECHAT_USER_"; // Token前缀 + private static final long TOKEN_EXPIRE_TIME = 7 * 24 * 60 * 60 * 1000; // Token过期时间(7天) + + /** + * RestTemplate实例,用于HTTP请求 + */ + private static final RestTemplate restTemplate = new RestTemplate(); + + /** + * 验证微信用户openid的有效性 + * + * @param openid 微信用户的openid + * @return 验证结果Map + * + * 返回数据结构: + * { + * "valid": true/false, // openid是否有效 + * "userInfo": {...}, // 用户信息(验证成功时返回) + * "errorMsg": "错误信息" // 错误信息(验证失败时返回) + * } + * + * 验证逻辑: + * 1. 检查openid格式是否正确 + * 2. 调用微信API验证openid有效性 + * 3. 获取用户基本信息 + * 4. 返回验证结果 + */ + public static Map validateOpenid(String openid) { + Map result = new HashMap<>(); + + try { + // 1. 参数验证 + if (openid == null || openid.trim().isEmpty()) { + result.put("valid", false); + result.put("errorMsg", "openid不能为空"); + return result; + } + + // 2. 检查openid格式(微信openid通常以o开头,长度28位) + String trimmedOpenid = openid.trim(); + if (!isValidOpenidFormat(trimmedOpenid)) { + result.put("valid", false); + result.put("errorMsg", "openid格式不正确"); + return result; + } + + // 3. 调用微信API验证openid(这里可以调用微信的用户信息接口) + // 注意:实际项目中可能需要access_token,这里简化处理 + Map wechatUserInfo = getWechatUserInfo(trimmedOpenid); + + if (wechatUserInfo != null && (Boolean) wechatUserInfo.get("success")) { + result.put("valid", true); + result.put("userInfo", wechatUserInfo.get("userInfo")); + result.put("errorMsg", null); + } else { + result.put("valid", false); + result.put("errorMsg", wechatUserInfo != null ? + (String) wechatUserInfo.get("errorMsg") : "无法获取用户信息"); + } + + } catch (Exception e) { + result.put("valid", false); + result.put("errorMsg", "验证openid时发生错误:" + e.getMessage()); + } + + return result; + } + + /** + * 检查openid格式是否正确 + * + * @param openid 微信openid + * @return 格式是否正确 + * + * 验证规则: + * - 不能为空 + * - 长度通常为28位 + * - 以字母开头 + * - 只包含字母、数字、下划线、连字符 + */ + private static boolean isValidOpenidFormat(String openid) { + if (openid == null || openid.isEmpty()) { + return false; + } + + // 微信openid格式验证:长度28位,以字母开头,包含字母数字下划线连字符 + return openid.matches("^[a-zA-Z][a-zA-Z0-9_-]{27}$"); + } + + /** + * 从微信API获取用户信息 + * + * @param openid 微信用户openid + * @return 用户信息Map + * + * 注意:这里是模拟实现,实际项目中需要: + * 1. 获取access_token + * 2. 调用微信用户信息接口 + * 3. 处理API返回结果 + */ + private static Map getWechatUserInfo(String openid) { + Map result = new HashMap<>(); + + try { + // 实际项目中这里应该调用微信API + // String url = WECHAT_API_BASE_URL + "/cgi-bin/user/info?access_token=" + accessToken + "&openid=" + openid; + // ResponseEntity response = restTemplate.getForEntity(url, String.class); + + // 这里模拟微信API返回的用户信息 + // 实际使用时需要替换为真实的微信API调用 + if (isValidOpenidFormat(openid)) { + Map userInfo = new HashMap<>(); + userInfo.put("openid", openid); + userInfo.put("nickname", "微信用户"); // 实际从微信API获取 + userInfo.put("avatar", ""); // 实际从微信API获取 + userInfo.put("gender", 0); // 实际从微信API获取 + userInfo.put("city", ""); // 实际从微信API获取 + userInfo.put("province", ""); // 实际从微信API获取 + userInfo.put("country", ""); // 实际从微信API获取 + + result.put("success", true); + result.put("userInfo", userInfo); + } else { + result.put("success", false); + result.put("errorMsg", "无效的openid"); + } + + } catch (Exception e) { + result.put("success", false); + result.put("errorMsg", "获取用户信息失败:" + e.getMessage()); + } + + return result; + } + + /** + * 生成用户token + * + * @param openid 微信用户openid + * @return 生成的token字符串 + * + * Token生成规则: + * 1. 使用UUID + openid + 时间戳生成基础字符串 + * 2. 通过MD5加密生成32位token + * 3. 添加前缀标识 + * 4. 确保token的唯一性 + * + * Token格式:WECHAT_USER_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + */ + public static String generateUserToken(String openid) { + try { + // 1. 构建基础字符串:UUID + openid + 当前时间戳 + String baseString = UUID.randomUUID().toString().replace("-", "") + + openid + + System.currentTimeMillis(); + + // 2. 使用MD5加密生成32位token + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] digest = md5.digest(baseString.getBytes("UTF-8")); + + // 3. 将字节数组转换为16进制字符串 + StringBuilder token = new StringBuilder(); + for (byte b : digest) { + token.append(String.format("%02x", b)); + } + + // 4. 添加前缀并返回 + return TOKEN_PREFIX + token.toString(); + + } catch (Exception e) { + // 如果加密失败,使用UUID作为备用方案 + return TOKEN_PREFIX + UUID.randomUUID().toString().replace("-", ""); + } + } + + /** + * 验证token格式是否正确 + * + * @param token 用户token + * @return 格式是否正确 + * + * 验证规则: + * - 不能为空 + * - 必须以指定前缀开头 + * - 长度符合要求 + * - 只包含字母和数字 + */ + public static boolean isValidTokenFormat(String token) { + if (token == null || token.isEmpty()) { + return false; + } + + // 检查前缀 + if (!token.startsWith(TOKEN_PREFIX)) { + return false; + } + + // 检查长度(前缀 + 32位MD5) + int expectedLength = TOKEN_PREFIX.length() + 32; + if (token.length() != expectedLength) { + return false; + } + + // 检查token部分只包含字母和数字 + String tokenPart = token.substring(TOKEN_PREFIX.length()); + return tokenPart.matches("^[a-zA-Z0-9]+$"); + } + + /** + * 构建微信登录成功的响应数据 + * + * @param openid 微信用户openid + * @param token 生成的用户token + * @param userInfo 用户信息 + * @return 响应数据Map + * + * 返回数据结构: + * { + * "openid": "用户openid", + * "token": "用户token", + * "userInfo": {...}, + * "loginTime": 登录时间戳, + * "expireTime": token过期时间戳 + * } + */ + public static Map buildLoginResponse(String openid, String token, Map userInfo) { + Map response = new HashMap<>(); + + long currentTime = System.currentTimeMillis(); + + response.put("openid", openid); + response.put("token", token); + response.put("userInfo", userInfo); + response.put("loginTime", currentTime); + response.put("expireTime", currentTime + TOKEN_EXPIRE_TIME); + response.put("message", "登录成功"); + + return response; + } + + /** + * 获取access_token(如果需要调用微信API) + * + * @return access_token信息 + * + * 注意:实际项目中需要: + * 1. 调用微信获取access_token接口 + * 2. 缓存access_token避免频繁调用 + * 3. 处理access_token过期刷新 + */ + public static Map getAccessToken() { + Map result = new HashMap<>(); + + try { + // 构建请求URL + String url = WECHAT_API_BASE_URL + "/cgi-bin/token" + + "?grant_type=client_credential" + + "&appid=" + WECHAT_APPID + + "&secret=" + WECHAT_SECRET; + + // 发起HTTP请求 + ResponseEntity response = restTemplate.getForEntity(url, String.class); + + if (response.getStatusCode().is2xxSuccessful()) { + JSONObject jsonResponse = JSONObject.parseObject(response.getBody()); + + if (jsonResponse.containsKey("access_token")) { + result.put("success", true); + result.put("access_token", jsonResponse.getString("access_token")); + result.put("expires_in", jsonResponse.getInteger("expires_in")); + } else { + result.put("success", false); + result.put("errorMsg", "获取access_token失败:" + jsonResponse.getString("errmsg")); + } + } else { + result.put("success", false); + result.put("errorMsg", "HTTP请求失败,状态码:" + response.getStatusCode()); + } + + } catch (Exception e) { + result.put("success", false); + result.put("errorMsg", "获取access_token异常:" + e.getMessage()); + } + + return result; + } + + /** + * 解析token获取用户标识 + * + * @param token 用户token + * @return 解析结果 + * + * 注意:这里简化处理,实际项目中可能需要: + * 1. 解密token + * 2. 验证token有效期 + * 3. 从缓存或数据库获取用户信息 + */ + public static Map parseToken(String token) { + Map result = new HashMap<>(); + + try { + if (!isValidTokenFormat(token)) { + result.put("valid", false); + result.put("errorMsg", "token格式无效"); + return result; + } + + // 实际项目中这里应该从数据库或缓存中查询token对应的用户信息 + result.put("valid", true); + result.put("token", token); + result.put("message", "token验证成功"); + + } catch (Exception e) { + result.put("valid", false); + result.put("errorMsg", "解析token异常:" + e.getMessage()); + } + + return result; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatPayUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatPayUtil.java new file mode 100644 index 0000000..884fc91 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/controllerUtil/WechatPayUtil.java @@ -0,0 +1,787 @@ +package com.ruoyi.system.ControllerUtil; + +import com.alibaba.fastjson2.JSONObject; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.RestTemplate; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import javax.servlet.http.HttpServletRequest; +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.math.BigDecimal; +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 微信支付工具类 + * + * 提供微信支付相关的完整功能实现 + * 主要功能: + * 1. 统一下单支付 + * 2. 订单状态查询 + * 3. 找朋友代付功能 + * 4. 支付结果回调处理 + * 5. 退款申请和查询 + * 6. 签名生成和验证 + * 7. 支付安全验证 + * + * @author Mr. Zhang Pan + * @date 2025-01-03 + * @version 1.0 + */ +public class WechatPayUtil { + + /** + * 微信支付配置常量 + * 注意:实际使用时需要在配置文件中配置这些值 + */ + private static final String WECHAT_APP_ID = "your_wechat_appid"; + private static final String WECHAT_MCH_ID = "your_merchant_id"; + private static final String WECHAT_API_KEY = "your_api_key"; + private static final String WECHAT_CERT_PATH = "/path/to/cert.p12"; // 微信支付证书路径 + + /** + * 微信支付API地址 + */ + private static final String WECHAT_PAY_URL = "https://api.mch.weixin.qq.com/pay/unifiedorder"; + private static final String WECHAT_QUERY_URL = "https://api.mch.weixin.qq.com/pay/orderquery"; + private static final String WECHAT_REFUND_URL = "https://api.mch.weixin.qq.com/secapi/pay/refund"; + private static final String WECHAT_TRANSFER_URL = "https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers"; // 企业付款 + + /** + * 其他配置常量 + */ + private static final String TRADE_TYPE_JSAPI = "JSAPI"; + private static final String CURRENCY = "CNY"; // 货币类型 + private static final String SUCCESS_CODE = "SUCCESS"; + private static final String FAIL_CODE = "FAIL"; + + /** + * RestTemplate实例 + */ + private static final RestTemplate restTemplate = new RestTemplate(); + + /** + * 统一下单 - 微信小程序支付 + * + * @param orderInfo 订单信息 + * @return 支付参数Map + */ + public static Map unifiedOrder(Map orderInfo) { + Map result = new HashMap<>(); + + try { + // 1. 参数验证 + String validationError = validateOrderParams(orderInfo); + if (validationError != null) { + result.put("success", false); + result.put("message", validationError); + return result; + } + + // 2. 构建统一下单参数 + Map params = buildUnifiedOrderParams(orderInfo); + + // 3. 生成签名 + String sign = generateSign(params, WECHAT_API_KEY); + params.put("sign", sign); + + // 4. 发送请求 + String xmlRequest = mapToXml(params); + ResponseEntity response = restTemplate.postForEntity(WECHAT_PAY_URL, xmlRequest, String.class); + + // 5. 解析响应 + Map responseMap = xmlToMap(response.getBody()); + + if (SUCCESS_CODE.equals(responseMap.get("return_code")) && + SUCCESS_CODE.equals(responseMap.get("result_code"))) { + + // 6. 构建小程序支付参数 + Map payParams = buildMiniProgramPayParams(responseMap.get("prepay_id")); + + result.put("success", true); + result.put("payParams", payParams); + result.put("prepayId", responseMap.get("prepay_id")); + result.put("message", "统一下单成功"); + + } else { + result.put("success", false); + result.put("message", "统一下单失败:" + + (responseMap.get("err_code_des") != null ? responseMap.get("err_code_des") : + responseMap.get("return_msg"))); + } + + } catch (Exception e) { + result.put("success", false); + result.put("message", "统一下单异常:" + e.getMessage()); + } + + return result; + } + + /** + * 查询订单状态 + * + * @param orderNo 订单号 + * @param transactionId 微信订单号(可选,与orderNo二选一) + * @return 查询结果 + */ + public static Map queryOrder(String orderNo, String transactionId) { + Map result = new HashMap<>(); + + try { + // 1. 参数验证 + if ((orderNo == null || orderNo.trim().isEmpty()) && + (transactionId == null || transactionId.trim().isEmpty())) { + result.put("success", false); + result.put("message", "订单号和微信订单号不能同时为空"); + return result; + } + + // 2. 构建查询参数 + Map params = new HashMap<>(); + params.put("appid", WECHAT_APP_ID); + params.put("mch_id", WECHAT_MCH_ID); + params.put("nonce_str", generateNonceStr()); + + if (orderNo != null && !orderNo.trim().isEmpty()) { + params.put("out_trade_no", orderNo.trim()); + } + if (transactionId != null && !transactionId.trim().isEmpty()) { + params.put("transaction_id", transactionId.trim()); + } + + // 3. 生成签名 + String sign = generateSign(params, WECHAT_API_KEY); + params.put("sign", sign); + + // 4. 发送请求 + String xmlRequest = mapToXml(params); + ResponseEntity response = restTemplate.postForEntity(WECHAT_QUERY_URL, xmlRequest, String.class); + + // 5. 解析响应 + Map responseMap = xmlToMap(response.getBody()); + + if (SUCCESS_CODE.equals(responseMap.get("return_code")) && + SUCCESS_CODE.equals(responseMap.get("result_code"))) { + + Map orderInfo = new HashMap<>(); + orderInfo.put("tradeState", responseMap.get("trade_state")); + orderInfo.put("tradeStateDesc", responseMap.get("trade_state_desc")); + orderInfo.put("transactionId", responseMap.get("transaction_id")); + orderInfo.put("outTradeNo", responseMap.get("out_trade_no")); + orderInfo.put("totalFee", responseMap.get("total_fee")); + orderInfo.put("cashFee", responseMap.get("cash_fee")); + orderInfo.put("timeEnd", responseMap.get("time_end")); + + result.put("success", true); + result.put("orderInfo", orderInfo); + result.put("message", "查询订单成功"); + + } else { + result.put("success", false); + result.put("message", "查询订单失败:" + + (responseMap.get("err_code_des") != null ? responseMap.get("err_code_des") : + responseMap.get("return_msg"))); + } + + } catch (Exception e) { + result.put("success", false); + result.put("message", "查询订单异常:" + e.getMessage()); + } + + return result; + } + + /** + * 找朋友代付功能 - 生成代付订单 + * + * @param payForInfo 代付信息 + * @return 代付订单结果 + */ + public static Map createPayForOrder(Map payForInfo) { + Map result = new HashMap<>(); + + try { + // 1. 参数验证 + String validationError = validatePayForParams(payForInfo); + if (validationError != null) { + result.put("success", false); + result.put("message", validationError); + return result; + } + + // 2. 生成代付订单号 + String payForOrderNo = generatePayForOrderNo((String) payForInfo.get("orderNo")); + + // 3. 构建代付订单信息 + Map payForOrderInfo = new HashMap<>(); + payForOrderInfo.put("orderNo", payForOrderNo); + payForOrderInfo.put("openid", payForInfo.get("payerOpenid")); + payForOrderInfo.put("totalFee", payForInfo.get("totalFee")); + payForOrderInfo.put("body", "代付-" + payForInfo.get("body")); + payForOrderInfo.put("notifyUrl", payForInfo.get("notifyUrl")); + payForOrderInfo.put("attach", buildPayForAttach(payForInfo)); + + // 4. 设置30分钟过期时间 + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, 30); + SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss"); + payForOrderInfo.put("timeExpire", sdf.format(calendar.getTime())); + + // 5. 调用统一下单 + Map unifiedResult = unifiedOrder(payForOrderInfo); + + if ((Boolean) unifiedResult.get("success")) { + result.put("success", true); + result.put("payForOrderNo", payForOrderNo); + result.put("payParams", unifiedResult.get("payParams")); + result.put("prepayId", unifiedResult.get("prepayId")); + result.put("expireTime", payForOrderInfo.get("timeExpire")); + result.put("message", "代付订单创建成功"); + } else { + result.put("success", false); + result.put("message", "代付订单创建失败:" + unifiedResult.get("message")); + } + + } catch (Exception e) { + result.put("success", false); + result.put("message", "创建代付订单异常:" + e.getMessage()); + } + + return result; + } + + /** + * 支付结果通知回调处理 + * + * @param request HTTP请求对象 + * @return 处理结果 + */ + public static Map handlePayNotify(HttpServletRequest request) { + Map result = new HashMap<>(); + + try { + // 1. 读取请求数据 + StringBuilder xmlData = new StringBuilder(); + BufferedReader reader = new BufferedReader( + new InputStreamReader(request.getInputStream(), StandardCharsets.UTF_8)); + String line; + while ((line = reader.readLine()) != null) { + xmlData.append(line); + } + reader.close(); + + // 2. 解析XML数据 + Map notifyData = xmlToMap(xmlData.toString()); + + // 3. 验证签名 + if (!verifySign(notifyData, WECHAT_API_KEY)) { + result.put("success", false); + result.put("responseXml", buildNotifyResponse(FAIL_CODE, "签名验证失败")); + result.put("message", "签名验证失败"); + return result; + } + + // 4. 检查支付结果 + if (!SUCCESS_CODE.equals(notifyData.get("return_code")) || + !SUCCESS_CODE.equals(notifyData.get("result_code"))) { + result.put("success", false); + result.put("responseXml", buildNotifyResponse(FAIL_CODE, "支付失败")); + result.put("message", "支付失败:" + notifyData.get("err_code_des")); + return result; + } + + // 5. 构建支付信息 + Map paymentInfo = new HashMap<>(); + paymentInfo.put("transactionId", notifyData.get("transaction_id")); + paymentInfo.put("outTradeNo", notifyData.get("out_trade_no")); + paymentInfo.put("totalFee", notifyData.get("total_fee")); + paymentInfo.put("cashFee", notifyData.get("cash_fee")); + paymentInfo.put("timeEnd", notifyData.get("time_end")); + paymentInfo.put("attach", notifyData.get("attach")); + paymentInfo.put("openid", notifyData.get("openid")); + + // 6. 判断是否为代付订单 + String orderNo = notifyData.get("out_trade_no"); + boolean isPayFor = isPayForOrder(orderNo); + + result.put("success", true); + result.put("responseXml", buildNotifyResponse(SUCCESS_CODE, "OK")); + result.put("paymentInfo", paymentInfo); + result.put("isPayFor", isPayFor); + result.put("message", "支付通知处理成功"); + + } catch (Exception e) { + result.put("success", false); + result.put("responseXml", buildNotifyResponse(FAIL_CODE, "处理异常")); + result.put("message", "处理支付通知异常:" + e.getMessage()); + } + + return result; + } + + /** + * 申请退款 + * + * @param refundInfo 退款信息 + * @return 退款结果 + */ + public static Map refund(Map refundInfo) { + Map result = new HashMap<>(); + + try { + // 1. 参数验证 + String validationError = validateRefundParams(refundInfo); + if (validationError != null) { + result.put("success", false); + result.put("message", validationError); + return result; + } + + // 2. 构建退款参数 + Map params = buildRefundParams(refundInfo); + + // 3. 生成签名 + String sign = generateSign(params, WECHAT_API_KEY); + params.put("sign", sign); + + // 4. 发送请求(需要证书) + String xmlRequest = mapToXml(params); + // 注意:退款接口需要使用客户端证书,这里简化处理 + ResponseEntity response = restTemplate.postForEntity(WECHAT_REFUND_URL, xmlRequest, String.class); + + // 5. 解析响应 + Map responseMap = xmlToMap(response.getBody()); + + if (SUCCESS_CODE.equals(responseMap.get("return_code")) && + SUCCESS_CODE.equals(responseMap.get("result_code"))) { + + Map refundResult = new HashMap<>(); + refundResult.put("refundId", responseMap.get("refund_id")); + refundResult.put("outRefundNo", responseMap.get("out_refund_no")); + refundResult.put("refundFee", responseMap.get("refund_fee")); + refundResult.put("settlementRefundFee", responseMap.get("settlement_refund_fee")); + + result.put("success", true); + result.put("refundInfo", refundResult); + result.put("message", "申请退款成功"); + + } else { + result.put("success", false); + result.put("message", "申请退款失败:" + + (responseMap.get("err_code_des") != null ? responseMap.get("err_code_des") : + responseMap.get("return_msg"))); + } + + } catch (Exception e) { + result.put("success", false); + result.put("message", "申请退款异常:" + e.getMessage()); + } + + return result; + } + + /** + * 企业付款到零钱(用于代付完成后的资金转移) + * + * @param transferInfo 付款信息 + * @return 付款结果 + */ + public static Map transferToUser(Map transferInfo) { + Map result = new HashMap<>(); + + try { + // 1. 参数验证 + String validationError = validateTransferParams(transferInfo); + if (validationError != null) { + result.put("success", false); + result.put("message", validationError); + return result; + } + + // 2. 构建付款参数 + Map params = buildTransferParams(transferInfo); + + // 3. 生成签名 + String sign = generateSign(params, WECHAT_API_KEY); + params.put("sign", sign); + + // 4. 发送请求(需要证书) + String xmlRequest = mapToXml(params); + // 注意:企业付款接口需要使用客户端证书,这里简化处理 + ResponseEntity response = restTemplate.postForEntity(WECHAT_TRANSFER_URL, xmlRequest, String.class); + + // 5. 解析响应 + Map responseMap = xmlToMap(response.getBody()); + + if (SUCCESS_CODE.equals(responseMap.get("return_code")) && + SUCCESS_CODE.equals(responseMap.get("result_code"))) { + + Map transferResult = new HashMap<>(); + transferResult.put("partnerTradeNo", responseMap.get("partner_trade_no")); + transferResult.put("paymentNo", responseMap.get("payment_no")); + transferResult.put("paymentTime", responseMap.get("payment_time")); + + result.put("success", true); + result.put("transferInfo", transferResult); + result.put("message", "企业付款成功"); + + } else { + result.put("success", false); + result.put("message", "企业付款失败:" + + (responseMap.get("err_code_des") != null ? responseMap.get("err_code_des") : + responseMap.get("return_msg"))); + } + + } catch (Exception e) { + result.put("success", false); + result.put("message", "企业付款异常:" + e.getMessage()); + } + + return result; + } + + // ========== 私有工具方法 ========== + + /** + * 验证统一下单参数 + */ + private static String validateOrderParams(Map orderInfo) { + if (orderInfo == null) return "订单信息不能为空"; + if (orderInfo.get("orderNo") == null || orderInfo.get("orderNo").toString().trim().isEmpty()) { + return "订单号不能为空"; + } + if (orderInfo.get("openid") == null || orderInfo.get("openid").toString().trim().isEmpty()) { + return "用户openid不能为空"; + } + if (orderInfo.get("totalFee") == null) { + return "支付金额不能为空"; + } + if (orderInfo.get("body") == null || orderInfo.get("body").toString().trim().isEmpty()) { + return "商品描述不能为空"; + } + if (orderInfo.get("notifyUrl") == null || orderInfo.get("notifyUrl").toString().trim().isEmpty()) { + return "回调地址不能为空"; + } + return null; + } + + /** + * 验证代付参数 + */ + private static String validatePayForParams(Map payForInfo) { + if (payForInfo == null) return "代付信息不能为空"; + if (payForInfo.get("orderNo") == null || payForInfo.get("orderNo").toString().trim().isEmpty()) { + return "原订单号不能为空"; + } + if (payForInfo.get("payerOpenid") == null || payForInfo.get("payerOpenid").toString().trim().isEmpty()) { + return "代付人openid不能为空"; + } + if (payForInfo.get("payeeOpenid") == null || payForInfo.get("payeeOpenid").toString().trim().isEmpty()) { + return "被代付人openid不能为空"; + } + if (payForInfo.get("totalFee") == null) { + return "代付金额不能为空"; + } + if (payForInfo.get("body") == null || payForInfo.get("body").toString().trim().isEmpty()) { + return "商品描述不能为空"; + } + if (payForInfo.get("notifyUrl") == null || payForInfo.get("notifyUrl").toString().trim().isEmpty()) { + return "回调地址不能为空"; + } + return null; + } + + /** + * 验证退款参数 + */ + private static String validateRefundParams(Map refundInfo) { + if (refundInfo == null) return "退款信息不能为空"; + if (refundInfo.get("orderNo") == null || refundInfo.get("orderNo").toString().trim().isEmpty()) { + return "订单号不能为空"; + } + if (refundInfo.get("refundNo") == null || refundInfo.get("refundNo").toString().trim().isEmpty()) { + return "退款单号不能为空"; + } + if (refundInfo.get("totalFee") == null) { + return "订单总金额不能为空"; + } + if (refundInfo.get("refundFee") == null) { + return "退款金额不能为空"; + } + return null; + } + + /** + * 验证企业付款参数 + */ + private static String validateTransferParams(Map transferInfo) { + if (transferInfo == null) return "付款信息不能为空"; + if (transferInfo.get("partnerTradeNo") == null || transferInfo.get("partnerTradeNo").toString().trim().isEmpty()) { + return "商户订单号不能为空"; + } + if (transferInfo.get("openid") == null || transferInfo.get("openid").toString().trim().isEmpty()) { + return "用户openid不能为空"; + } + if (transferInfo.get("amount") == null) { + return "付款金额不能为空"; + } + if (transferInfo.get("desc") == null || transferInfo.get("desc").toString().trim().isEmpty()) { + return "付款描述不能为空"; + } + return null; + } + + /** + * 构建统一下单参数 + */ + private static Map buildUnifiedOrderParams(Map orderInfo) { + Map params = new HashMap<>(); + params.put("appid", WECHAT_APP_ID); + params.put("mch_id", WECHAT_MCH_ID); + params.put("nonce_str", generateNonceStr()); + params.put("body", orderInfo.get("body").toString()); + params.put("out_trade_no", orderInfo.get("orderNo").toString()); + params.put("total_fee", orderInfo.get("totalFee").toString()); + params.put("spbill_create_ip", "127.0.0.1"); + params.put("notify_url", orderInfo.get("notifyUrl").toString()); + params.put("trade_type", TRADE_TYPE_JSAPI); + params.put("openid", orderInfo.get("openid").toString()); + + if (orderInfo.get("attach") != null) { + params.put("attach", orderInfo.get("attach").toString()); + } + if (orderInfo.get("timeExpire") != null) { + params.put("time_expire", orderInfo.get("timeExpire").toString()); + } + + return params; + } + + /** + * 构建退款参数 + */ + private static Map buildRefundParams(Map refundInfo) { + Map params = new HashMap<>(); + params.put("appid", WECHAT_APP_ID); + params.put("mch_id", WECHAT_MCH_ID); + params.put("nonce_str", generateNonceStr()); + params.put("out_trade_no", refundInfo.get("orderNo").toString()); + params.put("out_refund_no", refundInfo.get("refundNo").toString()); + params.put("total_fee", refundInfo.get("totalFee").toString()); + params.put("refund_fee", refundInfo.get("refundFee").toString()); + + if (refundInfo.get("refundDesc") != null) { + params.put("refund_desc", refundInfo.get("refundDesc").toString()); + } + if (refundInfo.get("notifyUrl") != null) { + params.put("notify_url", refundInfo.get("notifyUrl").toString()); + } + + return params; + } + + /** + * 构建企业付款参数 + */ + private static Map buildTransferParams(Map transferInfo) { + Map params = new HashMap<>(); + params.put("mch_appid", WECHAT_APP_ID); + params.put("mchid", WECHAT_MCH_ID); + params.put("nonce_str", generateNonceStr()); + params.put("partner_trade_no", transferInfo.get("partnerTradeNo").toString()); + params.put("openid", transferInfo.get("openid").toString()); + params.put("check_name", transferInfo.get("checkName") != null ? + transferInfo.get("checkName").toString() : "NO_CHECK"); + params.put("amount", transferInfo.get("amount").toString()); + params.put("desc", transferInfo.get("desc").toString()); + params.put("spbill_create_ip", "127.0.0.1"); + + if (transferInfo.get("reUserName") != null) { + params.put("re_user_name", transferInfo.get("reUserName").toString()); + } + + return params; + } + + /** + * 构建小程序支付参数 + */ + private static Map buildMiniProgramPayParams(String prepayId) { + Map payParams = new HashMap<>(); + String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); + String nonceStr = generateNonceStr(); + String packageStr = "prepay_id=" + prepayId; + String signType = "MD5"; + + // 构建签名参数 + Map signParams = new HashMap<>(); + signParams.put("appId", WECHAT_APP_ID); + signParams.put("timeStamp", timeStamp); + signParams.put("nonceStr", nonceStr); + signParams.put("package", packageStr); + signParams.put("signType", signType); + + String paySign = generateSign(signParams, WECHAT_API_KEY); + + payParams.put("timeStamp", timeStamp); + payParams.put("nonceStr", nonceStr); + payParams.put("package", packageStr); + payParams.put("signType", signType); + payParams.put("paySign", paySign); + + return payParams; + } + + /** + * 构建代付附加数据 + */ + private static String buildPayForAttach(Map payForInfo) { + JSONObject attach = new JSONObject(); + attach.put("type", "payfor"); + attach.put("originalOrderNo", payForInfo.get("orderNo")); + attach.put("payerOpenid", payForInfo.get("payerOpenid")); + attach.put("payeeOpenid", payForInfo.get("payeeOpenid")); + if (payForInfo.get("remark") != null) { + attach.put("remark", payForInfo.get("remark")); + } + return attach.toString(); + } + + /** + * 生成代付订单号 + */ + private static String generatePayForOrderNo(String originalOrderNo) { + return "PF" + originalOrderNo + System.currentTimeMillis(); + } + + /** + * 判断是否为代付订单 + */ + private static boolean isPayForOrder(String orderNo) { + return orderNo != null && orderNo.startsWith("PF"); + } + + /** + * 构建通知响应XML + */ + private static String buildNotifyResponse(String returnCode, String returnMsg) { + StringBuilder xml = new StringBuilder(); + xml.append(""); + xml.append(""); + xml.append(""); + xml.append(""); + return xml.toString(); + } + + /** + * 生成随机字符串 + */ + private static String generateNonceStr() { + return UUID.randomUUID().toString().replace("-", "").substring(0, 32); + } + + /** + * 生成签名 + */ + private static String generateSign(Map params, String key) { + try { + // 1. 排序参数 + Map sortedParams = new TreeMap<>(params); + + // 2. 拼接参数 + StringBuilder stringBuilder = new StringBuilder(); + for (Map.Entry entry : sortedParams.entrySet()) { + if (entry.getValue() != null && !entry.getValue().isEmpty() && !"sign".equals(entry.getKey())) { + stringBuilder.append(entry.getKey()).append("=").append(entry.getValue()).append("&"); + } + } + stringBuilder.append("key=").append(key); + + // 3. MD5加密 + MessageDigest md5 = MessageDigest.getInstance("MD5"); + byte[] digest = md5.digest(stringBuilder.toString().getBytes(StandardCharsets.UTF_8)); + + // 4. 转换为大写16进制字符串 + StringBuilder result = new StringBuilder(); + for (byte b : digest) { + result.append(String.format("%02X", b)); + } + + return result.toString(); + + } catch (Exception e) { + throw new RuntimeException("生成签名失败", e); + } + } + + /** + * 验证签名 + */ + private static boolean verifySign(Map params, String key) { + String sign = params.get("sign"); + if (sign == null || sign.isEmpty()) { + return false; + } + + Map paramsWithoutSign = new HashMap<>(params); + paramsWithoutSign.remove("sign"); + + String generatedSign = generateSign(paramsWithoutSign, key); + return sign.equals(generatedSign); + } + + /** + * Map转XML + */ + private static String mapToXml(Map params) { + StringBuilder xml = new StringBuilder(); + xml.append(""); + for (Map.Entry entry : params.entrySet()) { + xml.append("<").append(entry.getKey()).append(">"); + } + xml.append(""); + return xml.toString(); + } + + /** + * XML转Map(简化实现) + */ + private static Map xmlToMap(String xml) { + Map map = new HashMap<>(); + try { + // 简化的XML解析实现 + xml = xml.replaceAll("", "").replaceAll("", ""); + String[] elements = xml.split(""); + + for (String element : elements) { + if (element.trim().isEmpty()) continue; + + int startTag = element.indexOf("<"); + int endTag = element.indexOf(">"); + if (startTag >= 0 && endTag > startTag) { + String key = element.substring(startTag + 1, endTag); + String value = element.substring(endTag + 1); + + // 处理CDATA + if (value.startsWith("")) { + value = value.substring(9, value.length() - 3); + } + + map.put(key, value); + } + } + } catch (Exception e) { + throw new RuntimeException("XML解析失败", e); + } + return map; + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UsersMapper.java b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UsersMapper.java index e947ab3..a10ffbd 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UsersMapper.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/mapper/UsersMapper.java @@ -19,8 +19,23 @@ public interface UsersMapper */ public Users selectUsersById(Long id); + /** + * 查询【请填写功能名称】 + * + * @param rememberToken 【请填写rememberToken】 + * @return 【请填写功能名称】 + */ + public Users selectUsersByRememberToken(String rememberToken); + /** + * 查询【请填写功能名称】 + * + * @param openid 【请填写rememberToken】 + * @return 【请填写功能名称】 + */ + public Users selectUsersByOpenid(String openid); + /** * 查询根据电话号码查询用户基本信息 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/IUsersService.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/IUsersService.java index 1aecc71..7a4f54f 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/IUsersService.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/IUsersService.java @@ -18,7 +18,22 @@ public interface IUsersService * @return 【请填写功能名称】 */ public Users selectUsersById(Long id); + /** + * 查询【请填写功能名称】 + * + * @param rememberToken 【请填写rememberToken】 + * @return 【请填写功能名称】 + */ + public Users selectUsersByRememberToken(String rememberToken); + + /** + * 查询【请填写功能名称】 + * + * @param openid 【请填写rememberToken】 + * @return 【请填写功能名称】 + */ + public Users selectUsersByOpenid(String openid); /** * 查询根据电弧号码查询用户基本数据 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UsersServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UsersServiceImpl.java index 1e31261..6d804ee 100644 --- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UsersServiceImpl.java +++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/UsersServiceImpl.java @@ -30,6 +30,31 @@ public class UsersServiceImpl implements IUsersService { return usersMapper.selectUsersById(id); } + + + + /** + * 查询【请填写功能名称】 + * + * @param rememberToken 【请填写rememberToken】 + * @return 【请填写功能名称】 + */ + public Users selectUsersByRememberToken(String rememberToken) { + return usersMapper.selectUsersByRememberToken(rememberToken); + + } + + + /** + * 查询【请填写功能名称】 + * + * @param openid 【请填写rememberToken】 + * @return 【请填写功能名称】 + */ + public Users selectUsersByOpenid(String openid) { + return usersMapper.selectUsersByOpenid(openid); + + } /** * 查询根据电弧号码查询用户基本数据 * diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/utils/QiniuUploadUtil.java b/ruoyi-system/src/main/java/com/ruoyi/system/utils/QiniuUploadUtil.java new file mode 100644 index 0000000..f552288 --- /dev/null +++ b/ruoyi-system/src/main/java/com/ruoyi/system/utils/QiniuUploadUtil.java @@ -0,0 +1,360 @@ +package com.ruoyi.system.utils; + +import com.qiniu.common.QiniuException; +import com.qiniu.http.Response; +import com.qiniu.storage.BucketManager; +import com.qiniu.storage.Configuration; +import com.qiniu.storage.Region; +import com.qiniu.storage.UploadManager; +import com.qiniu.storage.model.DefaultPutRet; +import com.qiniu.storage.model.FileInfo; +import com.qiniu.util.Auth; +import com.alibaba.fastjson2.JSON; +import com.ruoyi.common.utils.DateUtils; +import com.ruoyi.common.utils.StringUtils; +import com.ruoyi.common.utils.uuid.IdUtils; +import com.ruoyi.common.utils.spring.SpringUtils; +import com.ruoyi.system.config.QiniuConfig; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; + +/** + * 七牛云上传工具类 + * + * @author ruoyi + */ +public class QiniuUploadUtil { + + /** + * 获取七牛云配置 + */ + private static QiniuConfig getQiniuConfig() { + return SpringUtils.getBean(QiniuConfig.class); + } + + /** + * 获取上传配置 + */ + private static Configuration getConfiguration() { + QiniuConfig config = getQiniuConfig(); + Region region; + switch (config.getRegion()) { + case "region1": + region = Region.region1(); + break; + case "region2": + region = Region.region2(); + break; + case "regionNa0": + region = Region.regionNa0(); + break; + case "regionAs0": + region = Region.regionAs0(); + break; + default: + region = Region.region0(); + break; + } + return new Configuration(region); + } + + /** + * 获取认证信息 + */ + private static Auth getAuth() { + QiniuConfig config = getQiniuConfig(); + return Auth.create(config.getAccessKey(), config.getSecretKey()); + } + + /** + * 上传文件到七牛云 + * + * @param file 上传的文件 + * @return 文件访问URL + * @throws IOException 上传异常 + */ + public static String uploadFile(MultipartFile file) throws IOException { + if (file == null || file.isEmpty()) { + throw new IOException("上传文件不能为空"); + } + + QiniuConfig config = getQiniuConfig(); + if (!config.isEnabled()) { + throw new IOException("七牛云上传未启用"); + } + + // 生成唯一的文件名 + String fileName = generateFileName(file.getOriginalFilename()); + + // 获取上传凭证 + Auth auth = getAuth(); + String upToken = auth.uploadToken(config.getBucketName()); + + try { + // 上传文件 + UploadManager uploadManager = new UploadManager(getConfiguration()); + Response response = uploadManager.put(file.getInputStream(), fileName, upToken, null, null); + + // 解析返回结果 + DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class); + + // 返回完整的访问URL + return "https://" + config.getDomain() + "/" + putRet.key; + + } catch (QiniuException ex) { + Response r = ex.response; + throw new IOException("七牛云上传失败: " + r.toString()); + } + } + + /** + * 上传字节数据到七牛云 + * + * @param data 字节数据 + * @param fileName 文件名 + * @return 文件访问URL + * @throws IOException 上传异常 + */ + public static String uploadBytes(byte[] data, String fileName) throws IOException { + if (data == null || data.length == 0) { + throw new IOException("上传数据不能为空"); + } + + QiniuConfig config = getQiniuConfig(); + if (!config.isEnabled()) { + throw new IOException("七牛云上传未启用"); + } + + // 生成唯一的文件名 + String uniqueFileName = generateFileName(fileName); + + // 获取上传凭证 + Auth auth = getAuth(); + String upToken = auth.uploadToken(config.getBucketName()); + + try { + // 上传字节数据 + UploadManager uploadManager = new UploadManager(getConfiguration()); + Response response = uploadManager.put(data, uniqueFileName, upToken); + + // 解析返回结果 + DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class); + + // 返回完整的访问URL + return "https://" + config.getDomain() + "/" + putRet.key; + + } catch (QiniuException ex) { + Response r = ex.response; + throw new IOException("七牛云上传失败: " + r.toString()); + } + } + + /** + * 上传输入流到七牛云 + * + * @param inputStream 输入流 + * @param fileName 文件名 + * @return 文件访问URL + * @throws IOException 上传异常 + */ + public static String uploadStream(InputStream inputStream, String fileName) throws IOException { + if (inputStream == null) { + throw new IOException("输入流不能为空"); + } + + QiniuConfig config = getQiniuConfig(); + if (!config.isEnabled()) { + throw new IOException("七牛云上传未启用"); + } + + // 生成唯一的文件名 + String uniqueFileName = generateFileName(fileName); + + // 获取上传凭证 + Auth auth = getAuth(); + String upToken = auth.uploadToken(config.getBucketName()); + + try { + // 上传输入流 + UploadManager uploadManager = new UploadManager(getConfiguration()); + Response response = uploadManager.put(inputStream, uniqueFileName, upToken, null, null); + + // 解析返回结果 + DefaultPutRet putRet = JSON.parseObject(response.bodyString(), DefaultPutRet.class); + + // 返回完整的访问URL + return "https://" + config.getDomain() + "/" + putRet.key; + + } catch (QiniuException ex) { + Response r = ex.response; + throw new IOException("七牛云上传失败: " + r.toString()); + } + } + + /** + * 删除七牛云文件 + * + * @param fileUrl 文件URL + * @return 是否删除成功 + */ + public static boolean deleteFile(String fileUrl) { + try { + QiniuConfig config = getQiniuConfig(); + if (!config.isEnabled()) { + return false; + } + + // 从URL中提取文件key + String key = extractKeyFromUrl(fileUrl); + if (StringUtils.isEmpty(key)) { + return false; + } + + // 创建BucketManager + Auth auth = getAuth(); + BucketManager bucketManager = new BucketManager(auth, getConfiguration()); + + // 删除文件 + bucketManager.delete(config.getBucketName(), key); + return true; + + } catch (QiniuException ex) { + System.err.println("删除文件失败: " + ex.getMessage()); + return false; + } + } + + /** + * 获取文件信息 + * + * @param fileUrl 文件URL + * @return 文件信息 + */ + public static FileInfo getFileInfo(String fileUrl) { + try { + QiniuConfig config = getQiniuConfig(); + if (!config.isEnabled()) { + return null; + } + + // 从URL中提取文件key + String key = extractKeyFromUrl(fileUrl); + if (StringUtils.isEmpty(key)) { + return null; + } + + // 创建BucketManager + Auth auth = getAuth(); + BucketManager bucketManager = new BucketManager(auth, getConfiguration()); + + // 获取文件信息 + return bucketManager.stat(config.getBucketName(), key); + + } catch (QiniuException ex) { + System.err.println("获取文件信息失败: " + ex.getMessage()); + return null; + } + } + + /** + * 生成唯一的文件名 + * + * @param originalFilename 原始文件名 + * @return 唯一文件名 + */ + private static String generateFileName(String originalFilename) { + // 获取文件扩展名 + String extension = ""; + if (StringUtils.isNotEmpty(originalFilename) && originalFilename.contains(".")) { + extension = originalFilename.substring(originalFilename.lastIndexOf(".")); + } + + // 按日期分目录存储: 年/月/日/UUID.扩展名 + String datePath = DateUtils.datePath(); + String uuid = IdUtils.fastSimpleUUID(); + + return "images/"+datePath + "/" + uuid + extension; + } + + /** + * 从URL中提取文件key + * + * @param fileUrl 文件URL + * @return 文件key + */ + private static String extractKeyFromUrl(String fileUrl) { + if (StringUtils.isEmpty(fileUrl)) { + return null; + } + + QiniuConfig config = getQiniuConfig(); + + // 移除域名部分,获取文件key + String domainPrefix = "https://" + config.getDomain() + "/"; + if (fileUrl.startsWith(domainPrefix)) { + return fileUrl.substring(domainPrefix.length()); + } + + // 如果不是完整URL,可能已经是key + return fileUrl; + } + + /** + * 验证文件格式是否允许上传 + * + * @param fileName 文件名 + * @param allowedExtensions 允许的扩展名数组 + * @return 是否允许上传 + */ + public static boolean isValidFileType(String fileName, String[] allowedExtensions) { + if (StringUtils.isEmpty(fileName) || allowedExtensions == null || allowedExtensions.length == 0) { + return false; + } + + String extension = ""; + if (fileName.contains(".")) { + extension = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(); + } + + for (String allowedExt : allowedExtensions) { + if (allowedExt.toLowerCase().equals(extension)) { + return true; + } + } + + return false; + } + + /** + * 验证文件大小是否超出限制 + * + * @param fileSize 文件大小(字节) + * @param maxSize 最大大小(字节) + * @return 是否超出限制 + */ + public static boolean isFileSizeValid(long fileSize, long maxSize) { + return fileSize <= maxSize; + } + + /** + * 获取上传凭证(用于前端直传) + * + * @param key 指定的文件key,可为null表示不指定 + * @return 上传凭证 + */ + public static String getUploadToken(String key) { + QiniuConfig config = getQiniuConfig(); + if (!config.isEnabled()) { + return null; + } + + Auth auth = getAuth(); + if (StringUtils.isNotEmpty(key)) { + return auth.uploadToken(config.getBucketName(), key); + } else { + return auth.uploadToken(config.getBucketName()); + } + } +} \ No newline at end of file diff --git a/ruoyi-system/src/main/resources/mapper/system/ServiceCateMapper.xml b/ruoyi-system/src/main/resources/mapper/system/ServiceCateMapper.xml index 53bea05..3f3044b 100644 --- a/ruoyi-system/src/main/resources/mapper/system/ServiceCateMapper.xml +++ b/ruoyi-system/src/main/resources/mapper/system/ServiceCateMapper.xml @@ -25,9 +25,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" and title like concat('%',#{title},'%') + and status=#{status} + and type=#{type} - order by id desc + order by sort ASC + + + + +