202506071815

This commit is contained in:
张潘 2025-06-07 18:16:03 +08:00
parent af40a21627
commit ded05d4e78
45 changed files with 5985 additions and 239 deletions

41
build_verification.bat Normal file
View File

@ -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

16
pom.xml
View File

@ -30,6 +30,7 @@
<poi.version>4.1.2</poi.version> <poi.version>4.1.2</poi.version>
<velocity.version>2.3</velocity.version> <velocity.version>2.3</velocity.version>
<jwt.version>0.9.1</jwt.version> <jwt.version>0.9.1</jwt.version>
<qiniu.version>7.15.1</qiniu.version>
<!-- override dependency version --> <!-- override dependency version -->
<tomcat.version>9.0.102</tomcat.version> <tomcat.version>9.0.102</tomcat.version>
<logback.version>1.2.13</logback.version> <logback.version>1.2.13</logback.version>
@ -204,6 +205,13 @@
<version>${ruoyi.version}</version> <version>${ruoyi.version}</version>
</dependency> </dependency>
<!-- 核心模块-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-auth-common</artifactId>
<version>${ruoyi.version}</version>
</dependency>
<!-- 系统模块--> <!-- 系统模块-->
<dependency> <dependency>
<groupId>com.ruoyi</groupId> <groupId>com.ruoyi</groupId>
@ -232,6 +240,13 @@
<version>1.2.83</version> <version>1.2.83</version>
</dependency> </dependency>
<!-- 七牛云存储 -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>${qiniu.version}</version>
</dependency>
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
@ -242,6 +257,7 @@
<module>ruoyi-quartz</module> <module>ruoyi-quartz</module>
<module>ruoyi-generator</module> <module>ruoyi-generator</module>
<module>ruoyi-common</module> <module>ruoyi-common</module>
<module>ruoyi-oauth-wx</module>
</modules> </modules>
<packaging>pom</packaging> <packaging>pom</packaging>

View File

@ -61,6 +61,12 @@
<artifactId>ruoyi-generator</artifactId> <artifactId>ruoyi-generator</artifactId>
</dependency> </dependency>
<!-- 七牛云存储 -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
</dependency>
</dependencies> </dependencies>
<build> <build>

View File

@ -20,6 +20,8 @@ import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.utils.file.FileUploadUtils; import com.ruoyi.common.utils.file.FileUploadUtils;
import com.ruoyi.common.utils.file.FileUtils; import com.ruoyi.common.utils.file.FileUtils;
import com.ruoyi.framework.config.ServerConfig; 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 @Autowired
private ServerConfig serverConfig; private ServerConfig serverConfig;
@Autowired
private QiniuConfig qiniuConfig;
private static final String FILE_DELIMETER = ","; private static final String FILE_DELIMETER = ",";
@ -71,23 +76,41 @@ public class CommonController
/** /**
* 通用上传请求单个 * 通用上传请求单个
* 根据配置选择使用七牛云上传或本地上传
*/ */
@PostMapping("/upload") @PostMapping("/upload")
public AjaxResult uploadFile(MultipartFile file) throws Exception public AjaxResult uploadFile(MultipartFile file) throws Exception
{ {
try try
{ {
// 上传文件路径 log.info("1上传文件开始");
String filePath = RuoYiConfig.getUploadPath(); // 检查是否启用七牛云上传
// 上传并返回新文件名称 if (qiniuConfig.isEnabled())
String fileName = FileUploadUtils.upload(filePath, file); {
String url = "https://img.huafurenjia.cn" + fileName; log.info("2上传文件开始");
AjaxResult ajax = AjaxResult.success(); // 使用七牛云上传
ajax.put("url", url); String fileUrl = QiniuUploadUtil.uploadFile(file);
ajax.put("fileName", fileName); AjaxResult ajax = AjaxResult.success();
ajax.put("newFileName", FileUtils.getName(fileName)); ajax.put("url", fileUrl);
ajax.put("originalFilename", file.getOriginalFilename()); ajax.put("fileName", fileUrl);
return ajax; 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) catch (Exception e)
{ {
@ -97,28 +120,45 @@ public class CommonController
/** /**
* 通用上传请求多个 * 通用上传请求多个
* 根据配置选择使用七牛云上传或本地上传
*/ */
@PostMapping("/uploads") @PostMapping("/uploads")
public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception public AjaxResult uploadFiles(List<MultipartFile> files) throws Exception
{ {
try try
{ {
// 上传文件路径
String filePath = RuoYiConfig.getUploadPath();
List<String> urls = new ArrayList<String>(); List<String> urls = new ArrayList<String>();
List<String> fileNames = new ArrayList<String>(); List<String> fileNames = new ArrayList<String>();
List<String> newFileNames = new ArrayList<String>(); List<String> newFileNames = new ArrayList<String>();
List<String> originalFilenames = new ArrayList<String>(); List<String> originalFilenames = new ArrayList<String>();
for (MultipartFile file : files)
if (qiniuConfig.isEnabled())
{ {
// 上传并返回新文件名称 // 使用七牛云上传
String fileName = FileUploadUtils.upload(filePath, file); for (MultipartFile file : files)
String url = "https://img.huafurenjia.cn" + fileName; {
urls.add(url); String fileUrl = QiniuUploadUtil.uploadFile(file);
fileNames.add(fileName); urls.add(fileUrl);
newFileNames.add(FileUtils.getName(fileName)); fileNames.add(fileUrl);
originalFilenames.add(file.getOriginalFilename()); 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(); AjaxResult ajax = AjaxResult.success();
ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER)); ajax.put("urls", StringUtils.join(urls, FILE_DELIMETER));
ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER)); ajax.put("fileNames", StringUtils.join(fileNames, FILE_DELIMETER));

View File

@ -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<MultipartFile> files) {
try {
if (!qiniuConfig.isEnabled()) {
return AjaxResult.error("七牛云上传未启用");
}
if (files == null || files.isEmpty()) {
return AjaxResult.error("上传文件不能为空");
}
List<String> urls = new ArrayList<>();
List<String> fileNames = new ArrayList<>();
List<String> newFileNames = new ArrayList<>();
List<String> originalFilenames = new ArrayList<>();
List<Long> 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());
}
}
}

View File

@ -13,6 +13,21 @@ ruoyi:
# 验证码类型 math 数字计算 char 字符验证 # 验证码类型 math 数字计算 char 字符验证
captchaType: math 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: amap:
# 高德Web服务API类型Key # 高德Web服务API类型Key

33
ruoyi-auth-common/pom.xml Normal file
View File

@ -0,0 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi-auth</artifactId>
<groupId>com.ruoyi.geekxd</groupId>
<version>3.8.9-G</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-auth-common</artifactId>
<description>
system系统模块
</description>
<dependencies>
<!-- 核心模块-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
</dependency>
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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;
/**
* 第三方用户来源可选值GITHUBGITEEQQ更多请参考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();
}
}

View File

@ -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;
}
}

View File

@ -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
* 微信开放平台登录QQuuid 为用户的 openId平台支持获取unionid unionid AuthToken
* 如果支持在登录完成后可以通过 response.getData().getToken().getUnionId() 获取
* Googleuuid 为用户的 subsub为Google的所有账户体系中用户唯一的身份标识符详见OpenID Connect
*
* @param uuid
* @return
*/
public OauthUser selectOauthUserByUUID(String uuid);
/**
* 查询第三方认证列表
*
* @param oauthUser 第三方认证
* @return 第三方认证集合
*/
public List<OauthUser> 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);
}

View File

@ -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<OauthUser> 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);
}

View File

@ -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;
}

View File

@ -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绑定注册和登录流程的方法
* 包括它们的验证步骤
*
* <p>
* 双因素认证通过要求用户提供两种不同的认证因素
* 为认证过程增加了额外的安全层
* </p>
*/
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);
}

View File

@ -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<OauthUser> 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;
};
}

View File

@ -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);
}
}

View File

@ -0,0 +1,169 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.auth.common.mapper.OauthUserMapper">
<resultMap type="OauthUser" id="OauthUserResult">
<result property="id" column="id" />
<result property="uuid" column="uuid" />
<result property="userId" column="user_id" />
<result property="source" column="source" />
<result property="accessToken" column="access_token" />
<result property="expireIn" column="expire_in" />
<result property="refreshToken" column="refresh_token" />
<result property="openId" column="open_id" />
<result property="uid" column="uid" />
<result property="accessCode" column="access_code" />
<result property="unionId" column="union_id" />
<result property="scope" column="scope" />
<result property="tokenType" column="token_type" />
<result property="idToken" column="id_token" />
<result property="macAlgorithm" column="mac_algorithm" />
<result property="macKey" column="mac_key" />
<result property="code" column="code" />
<result property="oauthToken" column="oauth_token" />
<result property="oauthTokenSecret" column="oauth_token_secret" />
</resultMap>
<sql id="selectOauthUserVo">
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
</sql>
<select id="checkUserNameUnique" parameterType="String" resultType="int">
select count(1) from sys_user where user_name = #{userName} and del_flag = '0' limit 1
</select>
<select id="checkPhoneUnique" parameterType="String" resultType="int">
select count(1) from sys_user where phonenumber = #{phonenumber} and del_flag = '0' limit 1
</select>
<select id="checkEmailUnique" parameterType="String" resultType="int">
select count(1) from sys_user where email = #{email} and del_flag = '0' limit 1
</select>
<select id="checkAuthUser" parameterType="OauthUser" resultType="int">
select count(1) from oauth_user where user_id=#{userId} and source=#{source} limit 1
</select>
<select id="selectOauthUserList" parameterType="OauthUser" resultMap="OauthUserResult">
<include refid="selectOauthUserVo"/>
<where>
<if test="uuid != null and uuid != ''"> and uuid = #{uuid}</if>
<if test="userId != null "> and user_id = #{userId}</if>
<if test="source != null and source != ''"> and source = #{source}</if>
<if test="accessToken != null and accessToken != ''"> and access_token = #{accessToken}</if>
<if test="expireIn != null "> and expire_in = #{expireIn}</if>
<if test="refreshToken != null and refreshToken != ''"> and refresh_token = #{refreshToken}</if>
<if test="openId != null and openId != ''"> and open_id = #{openId}</if>
<if test="uid != null and uid != ''"> and uid = #{uid}</if>
<if test="accessCode != null and accessCode != ''"> and access_code = #{accessCode}</if>
<if test="unionId != null and unionId != ''"> and union_id = #{unionId}</if>
<if test="scope != null and scope != ''"> and scope = #{scope}</if>
<if test="tokenType != null and tokenType != ''"> and token_type = #{tokenType}</if>
<if test="idToken != null and idToken != ''"> and id_token = #{idToken}</if>
<if test="macAlgorithm != null and macAlgorithm != ''"> and mac_algorithm = #{macAlgorithm}</if>
<if test="macKey != null and macKey != ''"> and mac_key = #{macKey}</if>
<if test="code != null and code != ''"> and code = #{code}</if>
<if test="oauthToken != null and oauthToken != ''"> and oauth_token = #{oauthToken}</if>
<if test="oauthTokenSecret != null and oauthTokenSecret != ''"> and oauth_token_secret = #{oauthTokenSecret}</if>
</where>
</select>
<select id="selectOauthUserById" parameterType="Long" resultMap="OauthUserResult">
<include refid="selectOauthUserVo"/>
where oauth_user.id = #{id}
</select>
<select id="selectOauthUserByUserId" parameterType="Long" resultMap="OauthUserResult">
<include refid="selectOauthUserVo"/>
where oauth_user.user_id = #{user_id}
</select>
<select id="selectOauthUserByUUID" parameterType="String" resultMap="OauthUserResult">
<include refid="selectOauthUserVo"/>
where oauth_user.uuid = #{uuid}
</select>
<insert id="insertOauthUser" parameterType="OauthUser">
insert into oauth_user
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">id,</if>
<if test="uuid != null and uuid != ''">uuid,</if>
<if test="userId != null">user_id,</if>
<if test="source != null and source != ''">source,</if>
<if test="accessToken != null and accessToken != ''">access_token,</if>
<if test="expireIn != null">expire_in,</if>
<if test="refreshToken != null">refresh_token,</if>
<if test="openId != null">open_id,</if>
<if test="uid != null">uid,</if>
<if test="accessCode != null">access_code,</if>
<if test="unionId != null">union_id,</if>
<if test="scope != null">scope,</if>
<if test="tokenType != null">token_type,</if>
<if test="idToken != null">id_token,</if>
<if test="macAlgorithm != null">mac_algorithm,</if>
<if test="macKey != null">mac_key,</if>
<if test="code != null">code,</if>
<if test="oauthToken != null">oauth_token,</if>
<if test="oauthTokenSecret != null">oauth_token_secret,</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">#{id},</if>
<if test="uuid != null and uuid != ''">#{uuid},</if>
<if test="userId != null">#{userId},</if>
<if test="source != null and source != ''">#{source},</if>
<if test="accessToken != null and accessToken != ''">#{accessToken},</if>
<if test="expireIn != null">#{expireIn},</if>
<if test="refreshToken != null">#{refreshToken},</if>
<if test="openId != null">#{openId},</if>
<if test="uid != null">#{uid},</if>
<if test="accessCode != null">#{accessCode},</if>
<if test="unionId != null">#{unionId},</if>
<if test="scope != null">#{scope},</if>
<if test="tokenType != null">#{tokenType},</if>
<if test="idToken != null">#{idToken},</if>
<if test="macAlgorithm != null">#{macAlgorithm},</if>
<if test="macKey != null">#{macKey},</if>
<if test="code != null">#{code},</if>
<if test="oauthToken != null">#{oauthToken},</if>
<if test="oauthTokenSecret != null">#{oauthTokenSecret},</if>
</trim>
</insert>
<update id="updateOauthUser" parameterType="OauthUser">
update oauth_user
<trim prefix="SET" suffixOverrides=",">
<if test="uuid != null and uuid != ''">uuid = #{uuid},</if>
<if test="userId != null">user_id = #{userId},</if>
<if test="source != null and source != ''">source = #{source},</if>
<if test="accessToken != null and accessToken != ''">access_token = #{accessToken},</if>
<if test="expireIn != null">expire_in = #{expireIn},</if>
<if test="refreshToken != null">refresh_token = #{refreshToken},</if>
<if test="openId != null">open_id = #{openId},</if>
<if test="uid != null">uid = #{uid},</if>
<if test="accessCode != null">access_code = #{accessCode},</if>
<if test="unionId != null">union_id = #{unionId},</if>
<if test="scope != null">scope = #{scope},</if>
<if test="tokenType != null">token_type = #{tokenType},</if>
<if test="idToken != null">id_token = #{idToken},</if>
<if test="macAlgorithm != null">mac_algorithm = #{macAlgorithm},</if>
<if test="macKey != null">mac_key = #{macKey},</if>
<if test="code != null">code = #{code},</if>
<if test="oauthToken != null">oauth_token = #{oauthToken},</if>
<if test="oauthTokenSecret != null">oauth_token_secret = #{oauthTokenSecret},</if>
</trim>
where oauth_user.id = #{id}
</update>
<delete id="deleteOauthUserById" parameterType="Long">
delete from oauth_user where id = #{id}
</delete>
<delete id="deleteOauthUserByIds" parameterType="String">
delete from oauth_user where id in
<foreach item="id" collection="array" open="(" separator="," close=")">
#{id}
</foreach>
</delete>
</mapper>

View File

@ -96,7 +96,7 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils
public static final String datePath() public static final String datePath()
{ {
Date now = new Date(); Date now = new Date();
return DateFormatUtils.format(now, "yyyy/MM/dd"); return DateFormatUtils.format(now, "yyyy-MM-dd");
} }
/** /**

View File

@ -110,12 +110,13 @@ public class SecurityConfig
// 注解标记允许匿名访问的url // 注解标记允许匿名访问的url
.authorizeHttpRequests((requests) -> { .authorizeHttpRequests((requests) -> {
permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll()); permitAllUrl.getUrls().forEach(url -> requests.antMatchers(url).permitAll());
// 对于登录login 注册register 验证码captchaImage 允许匿名访问 // 对于登录login 注册register 验证码captchaImage 小程序Applet 允许匿名访问
requests.antMatchers("/login", "/register", "/captchaImage").permitAll() requests.antMatchers("/login", "/register", "/captchaImage").permitAll()
// 静态资源可匿名访问 // 静态资源可匿名访问
.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**").permitAll()
// 除上面外的所有请求全部需要鉴权认证 .antMatchers("/api/**", "/api").permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated(); .anyRequest().authenticated();
}) })
// 添加Logout filter // 添加Logout filter

29
ruoyi-oauth-wx/pom.xml Normal file
View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>ruoyi-auth</artifactId>
<groupId>com.ruoyi.geekxd</groupId>
<version>3.8.9-G</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ruoyi-oauth-wx</artifactId>
<description>
微信认证模块
</description>
<dependencies>
<!-- 通用工具-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-auth-common</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}
}

View File

@ -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 "";
}
}

View File

@ -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 "";
}
}

View File

@ -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"));
}
}
}

View File

@ -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": "微信小程序认证地址"
}
]
}

View File

@ -23,6 +23,20 @@
<artifactId>ruoyi-common</artifactId> <artifactId>ruoyi-common</artifactId>
</dependency> </dependency>
<!-- 通用工具-->
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-auth-common</artifactId>
</dependency>
<!-- 七牛云存储 -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -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;
}
}

View File

@ -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<String, Object> userData = AppletControllerUtil.getUserData(request.getHeader("token"), usersService);
// 构建查询条件状态启用且类型为服务
ServiceCate serviceCateQuery = new ServiceCate();
serviceCateQuery.setStatus(1L); // 启用状态
serviceCateQuery.setType(1L); // 服务类型
// 查询分类列表
List<ServiceCate> 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<SiteConfig> 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<String, Object> 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<Map<String, Object>> resultList = new java.util.ArrayList<>();
if (cateId != null) {
// 查询指定分类
ServiceCate category = serviceCateService.selectServiceCateById(cateId);
if (category != null) {
Map<String, Object> categoryData = AppletControllerUtil.buildCategoryData(category, keywords, serviceGoodsService);
resultList.add(categoryData);
}
} else {
// 查询所有分类
ServiceCate serviceCateQuery = new ServiceCate();
serviceCateQuery.setStatus(1L);
serviceCateQuery.setType(1L);
List<ServiceCate> categories = serviceCateService.selectServiceCateList(serviceCateQuery);
for (ServiceCate category : categories) {
Map<String, Object> categoryData = AppletControllerUtil.buildCategoryData(category, keywords, serviceGoodsService);
// 只返回有商品的分类
List<Map<String, Object>> goods = (List<Map<String, Object>>) 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<AdvImg> 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<String, Object> 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<String, Object> 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>
*
* token: <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<String, Object> 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<String, Object> 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<String, Object> params, HttpServletRequest request) {
try {
// 1. 验证用户登录状态
String token = request.getHeader("token");
Map<String, Object> userValidation = AppletControllerUtil.validateUserToken(token, usersService);
if (!(Boolean) userValidation.get("valid")) {
return error("用户未登录或token无效");
}
// 2. 调用微信支付统一下单
Map<String, Object> 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<String, Object> userValidation = AppletControllerUtil.validateUserToken(token, usersService);
if (!(Boolean) userValidation.get("valid")) {
return error("用户未登录或token无效");
}
// 2. 查询订单状态
Map<String, Object> 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<String, Object> params, HttpServletRequest request) {
try {
// 1. 验证用户登录状态
String token = request.getHeader("token");
Map<String, Object> userValidation = AppletControllerUtil.validateUserToken(token, usersService);
if (!(Boolean) userValidation.get("valid")) {
return error("用户未登录或token无效");
}
// 2. 创建代付订单
Map<String, Object> 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<String, Object> notifyResult = com.ruoyi.system.ControllerUtil.WechatPayUtil.handlePayNotify(request);
// 2. 获取支付信息
boolean success = (Boolean) notifyResult.get("success");
if (success) {
Map<String, Object> paymentInfo = (Map<String, Object>) 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 "<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[处理异常]]></return_msg></xml>";
}
}
/**
* 申请退款接口
*
* @param params 退款参数
* @param request HTTP请求对象
* @return 退款结果
*
* 请求参数格式
* {
* "orderNo": "原订单号",
* "refundNo": "退款单号",
* "totalFee": 订单总金额
* "refundFee": 退款金额
* "refundDesc": "退款原因"
* }
*/
@PostMapping(value = "/api/pay/refund")
public AjaxResult refundOrder(@RequestBody Map<String, Object> params, HttpServletRequest request) {
try {
// 1. 验证用户登录状态
String token = request.getHeader("token");
Map<String, Object> userValidation = AppletControllerUtil.validateUserToken(token, usersService);
if (!(Boolean) userValidation.get("valid")) {
return error("用户未登录或token无效");
}
// 2. 申请退款
Map<String, Object> 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<String, Object> params, HttpServletRequest request) {
try {
// 1. 验证用户登录状态这里可能需要管理员权限
String token = request.getHeader("token");
Map<String, Object> userValidation = AppletControllerUtil.validateUserToken(token, usersService);
if (!(Boolean) userValidation.get("valid")) {
return error("用户未登录或token无效");
}
// 2. 企业付款
Map<String, Object> 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<String, Object> 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());
}
}
}

View File

@ -26,33 +26,32 @@ import com.ruoyi.common.core.page.TableDataInfo;
/** /**
* 服务内容Controller * 服务内容Controller
* *
* @author ruoyi * @author ruoyi
* @date 2025-05-13 * @date 2025-05-13
*/ */
@RestController @RestController
@RequestMapping("/system/ServiceGoods") @RequestMapping("/system/ServiceGoods")
public class ServiceGoodsController extends BaseController public class ServiceGoodsController extends BaseController {
{
@Autowired @Autowired
private IServiceGoodsService serviceGoodsService; private IServiceGoodsService serviceGoodsService;
@Autowired @Autowired
private IServiceCateService serviceCateService; private IServiceCateService serviceCateService;
/** /**
* 查询服务内容列表 * 查询服务内容列表
*/ */
@PreAuthorize("@ss.hasPermi('system:ServiceGoods:list')") @PreAuthorize("@ss.hasPermi('system:ServiceGoods:list')")
@GetMapping("/list") @GetMapping("/list")
public TableDataInfo list(ServiceGoods serviceGoods) public TableDataInfo list(ServiceGoods serviceGoods) {
{
startPage(); startPage();
List<ServiceGoods> list = serviceGoodsService.selectServiceGoodsList(serviceGoods); List<ServiceGoods> list = serviceGoodsService.selectServiceGoodsList(serviceGoods);
for(ServiceGoods serviceGoodsdata:list){ for (ServiceGoods serviceGoodsdata : list) {
ServiceCate serviceCate=serviceCateService.selectServiceCateById(serviceGoodsdata.getCateId()); ServiceCate serviceCate = serviceCateService.selectServiceCateById(serviceGoodsdata.getCateId());
if(serviceCate!=null){ if (serviceCate != null) {
serviceGoodsdata.setCateName(serviceCate.getTitle()); serviceGoodsdata.setCateName(serviceCate.getTitle());
} }
serviceGoodsdata.setIcon("https://img.huafurenjia.cn/"+serviceGoodsdata.getIcon()); serviceGoodsdata.setIcon("https://img.huafurenjia.cn/" + serviceGoodsdata.getIcon());
} }
return getDataTable(list); return getDataTable(list);
@ -64,8 +63,7 @@ private IServiceCateService serviceCateService;
@PreAuthorize("@ss.hasPermi('system:ServiceGoods:export')") @PreAuthorize("@ss.hasPermi('system:ServiceGoods:export')")
@Log(title = "服务内容", businessType = BusinessType.EXPORT) @Log(title = "服务内容", businessType = BusinessType.EXPORT)
@PostMapping("/export") @PostMapping("/export")
public void export(HttpServletResponse response, ServiceGoods serviceGoods) public void export(HttpServletResponse response, ServiceGoods serviceGoods) {
{
List<ServiceGoods> list = serviceGoodsService.selectServiceGoodsList(serviceGoods); List<ServiceGoods> list = serviceGoodsService.selectServiceGoodsList(serviceGoods);
ExcelUtil<ServiceGoods> util = new ExcelUtil<ServiceGoods>(ServiceGoods.class); ExcelUtil<ServiceGoods> util = new ExcelUtil<ServiceGoods>(ServiceGoods.class);
util.exportExcel(response, list, "服务内容数据"); util.exportExcel(response, list, "服务内容数据");
@ -76,8 +74,7 @@ private IServiceCateService serviceCateService;
*/ */
@PreAuthorize("@ss.hasPermi('system:ServiceGoods:query')") @PreAuthorize("@ss.hasPermi('system:ServiceGoods:query')")
@GetMapping(value = "/selectServiceCateList") @GetMapping(value = "/selectServiceCateList")
public AjaxResult selectServiceCateList() public AjaxResult selectServiceCateList() {
{
return success(serviceCateService.selectServiceCateList(new ServiceCate())); return success(serviceCateService.selectServiceCateList(new ServiceCate()));
} }
@ -87,8 +84,7 @@ private IServiceCateService serviceCateService;
*/ */
@PreAuthorize("@ss.hasPermi('system:ServiceGoods:query')") @PreAuthorize("@ss.hasPermi('system:ServiceGoods:query')")
@GetMapping(value = "/{id}") @GetMapping(value = "/{id}")
public AjaxResult getInfo(@PathVariable("id") Long id) public AjaxResult getInfo(@PathVariable("id") Long id) {
{
return success(serviceGoodsService.selectServiceGoodsById(id)); return success(serviceGoodsService.selectServiceGoodsById(id));
} }
@ -98,8 +94,10 @@ private IServiceCateService serviceCateService;
@PreAuthorize("@ss.hasPermi('system:ServiceGoods:add')") @PreAuthorize("@ss.hasPermi('system:ServiceGoods:add')")
@Log(title = "服务内容", businessType = BusinessType.INSERT) @Log(title = "服务内容", businessType = BusinessType.INSERT)
@PostMapping @PostMapping
public AjaxResult add(@RequestBody ServiceGoods serviceGoods) public AjaxResult add(@RequestBody ServiceGoods serviceGoods) {
{ // 验证和处理基检现象数据格式
validateAndProcessBasicField(serviceGoods);
return toAjax(serviceGoodsService.insertServiceGoods(serviceGoods)); return toAjax(serviceGoodsService.insertServiceGoods(serviceGoods));
} }
@ -109,18 +107,20 @@ private IServiceCateService serviceCateService;
@PreAuthorize("@ss.hasPermi('system:ServiceGoods:edit')") @PreAuthorize("@ss.hasPermi('system:ServiceGoods:edit')")
@Log(title = "服务内容", businessType = BusinessType.UPDATE) @Log(title = "服务内容", businessType = BusinessType.UPDATE)
@PutMapping @PutMapping
public AjaxResult edit(@RequestBody ServiceGoods serviceGoods) public AjaxResult edit(@RequestBody ServiceGoods serviceGoods) {
{ // 验证和处理基检现象数据格式
validateAndProcessBasicField(serviceGoods);
return toAjax(serviceGoodsService.updateServiceGoods(serviceGoods)); return toAjax(serviceGoodsService.updateServiceGoods(serviceGoods));
} }
/** /**
* 定时任务状态修改 * 定时任务状态修改
*/ */
@PreAuthorize("@ss.hasPermi('system:ServiceGoods:changeStatus')") @PreAuthorize("@ss.hasPermi('system:ServiceGoods:changeStatus')")
@Log(title = "修改状态", businessType = BusinessType.UPDATE) @Log(title = "修改状态", businessType = BusinessType.UPDATE)
@PutMapping("/changeStatus") @PutMapping("/changeStatus")
public AjaxResult changeStatus(@RequestBody ServiceGoods serviceGoods) public AjaxResult changeStatus(@RequestBody ServiceGoods serviceGoods) {
{
ServiceGoods newServiceGoods = serviceGoodsService.selectServiceGoodsById(serviceGoods.getId()); ServiceGoods newServiceGoods = serviceGoodsService.selectServiceGoodsById(serviceGoods.getId());
newServiceGoods.setStatus(serviceGoods.getStatus()); newServiceGoods.setStatus(serviceGoods.getStatus());
return toAjax(serviceGoodsService.updateServiceGoods(newServiceGoods)); return toAjax(serviceGoodsService.updateServiceGoods(newServiceGoods));
@ -131,9 +131,61 @@ private IServiceCateService serviceCateService;
*/ */
@PreAuthorize("@ss.hasPermi('system:ServiceGoods:remove')") @PreAuthorize("@ss.hasPermi('system:ServiceGoods:remove')")
@Log(title = "服务内容", businessType = BusinessType.DELETE) @Log(title = "服务内容", businessType = BusinessType.DELETE)
@DeleteMapping("/{ids}") @DeleteMapping("/{ids}")
public AjaxResult remove(@PathVariable Long[] ids) public AjaxResult remove(@PathVariable Long[] ids) {
{
return toAjax(serviceGoodsService.deleteServiceGoodsByIds(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<String> list = (java.util.List<String>) 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<String> 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);
}
}
}
}
} }

View File

@ -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<String> 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<String, Object> 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<String> basic;
/** 保证金(字符串格式) */
private String margin;
/** 所需技能ID */
private String skill_ids;
/** 创建时间 */
private String created_at;
/** 更新时间 */
private String updated_at;
/** 删除时间 */
private String deleted_at;
/** 评论对象(预留扩展) */
private Map<String, Object> 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<String> getImgs() { return imgs; }
public void setImgs(List<String> 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<String, Object> getSku() { return sku; }
public void setSku(Map<String, Object> 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<String> getBasic() { return basic; }
public void setBasic(List<String> 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<String, Object> getComment() { return comment; }
public void setComment(Map<String, Object> 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<String> parseStringToImageList(String imgsString) {
List<String> 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<String> parseStringToList(String dataString) {
List<String> 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<String, Object> buildCategoryData(ServiceCate category, String keywords,
IServiceGoodsService serviceGoodsService) {
Map<String, Object> 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<ServiceGoods> goodsList = serviceGoodsService.selectServiceGoodsList(serviceGoodsQuery);
List<Map<String, Object>> goodsDataList = new java.util.ArrayList<>();
// 构建商品数据列表
for (ServiceGoods goods : goodsList) {
Map<String, Object> 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<String, Object> getUserData(String token, IUsersService usersService) {
Map<String, Object> 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<String, Object> wechatUserLogin(String openid, IUsersService usersService) {
Map<String, Object> 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<String, Object> validationResult = WechatApiUtil.validateOpenid(trimmedOpenid);
if (!(Boolean) validationResult.get("valid")) {
result.put("success", false);
result.put("message", "openid验证失败" + validationResult.get("errorMsg"));
return result;
}
// 3. 获取微信用户信息
Map<String, Object> wechatUserInfo = (Map<String, Object>) 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> buildUserResponseInfo(Users userRecord, Map<String, Object> wechatUserInfo) {
Map<String, Object> 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<String, Object> validateUserToken(String token, IUsersService usersService) {
Map<String, Object> 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<String, Object> 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;
}
}

View File

@ -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<String, Object> validateOpenid(String openid) {
Map<String, Object> 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<String, Object> 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<String, Object> getWechatUserInfo(String openid) {
Map<String, Object> result = new HashMap<>();
try {
// 实际项目中这里应该调用微信API
// String url = WECHAT_API_BASE_URL + "/cgi-bin/user/info?access_token=" + accessToken + "&openid=" + openid;
// ResponseEntity<String> response = restTemplate.getForEntity(url, String.class);
// 这里模拟微信API返回的用户信息
// 实际使用时需要替换为真实的微信API调用
if (isValidOpenidFormat(openid)) {
Map<String, Object> 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<String, Object> buildLoginResponse(String openid, String token, Map<String, Object> userInfo) {
Map<String, Object> 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<String, Object> getAccessToken() {
Map<String, Object> 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<String> 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<String, Object> parseToken(String token) {
Map<String, Object> 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;
}
}

View File

@ -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<String, Object> unifiedOrder(Map<String, Object> orderInfo) {
Map<String, Object> result = new HashMap<>();
try {
// 1. 参数验证
String validationError = validateOrderParams(orderInfo);
if (validationError != null) {
result.put("success", false);
result.put("message", validationError);
return result;
}
// 2. 构建统一下单参数
Map<String, String> params = buildUnifiedOrderParams(orderInfo);
// 3. 生成签名
String sign = generateSign(params, WECHAT_API_KEY);
params.put("sign", sign);
// 4. 发送请求
String xmlRequest = mapToXml(params);
ResponseEntity<String> response = restTemplate.postForEntity(WECHAT_PAY_URL, xmlRequest, String.class);
// 5. 解析响应
Map<String, String> responseMap = xmlToMap(response.getBody());
if (SUCCESS_CODE.equals(responseMap.get("return_code")) &&
SUCCESS_CODE.equals(responseMap.get("result_code"))) {
// 6. 构建小程序支付参数
Map<String, Object> 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<String, Object> queryOrder(String orderNo, String transactionId) {
Map<String, Object> 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<String, String> 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<String> response = restTemplate.postForEntity(WECHAT_QUERY_URL, xmlRequest, String.class);
// 5. 解析响应
Map<String, String> responseMap = xmlToMap(response.getBody());
if (SUCCESS_CODE.equals(responseMap.get("return_code")) &&
SUCCESS_CODE.equals(responseMap.get("result_code"))) {
Map<String, Object> 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<String, Object> createPayForOrder(Map<String, Object> payForInfo) {
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> handlePayNotify(HttpServletRequest request) {
Map<String, Object> 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<String, String> 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<String, Object> 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<String, Object> refund(Map<String, Object> refundInfo) {
Map<String, Object> result = new HashMap<>();
try {
// 1. 参数验证
String validationError = validateRefundParams(refundInfo);
if (validationError != null) {
result.put("success", false);
result.put("message", validationError);
return result;
}
// 2. 构建退款参数
Map<String, String> params = buildRefundParams(refundInfo);
// 3. 生成签名
String sign = generateSign(params, WECHAT_API_KEY);
params.put("sign", sign);
// 4. 发送请求需要证书
String xmlRequest = mapToXml(params);
// 注意退款接口需要使用客户端证书这里简化处理
ResponseEntity<String> response = restTemplate.postForEntity(WECHAT_REFUND_URL, xmlRequest, String.class);
// 5. 解析响应
Map<String, String> responseMap = xmlToMap(response.getBody());
if (SUCCESS_CODE.equals(responseMap.get("return_code")) &&
SUCCESS_CODE.equals(responseMap.get("result_code"))) {
Map<String, Object> 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<String, Object> transferToUser(Map<String, Object> transferInfo) {
Map<String, Object> result = new HashMap<>();
try {
// 1. 参数验证
String validationError = validateTransferParams(transferInfo);
if (validationError != null) {
result.put("success", false);
result.put("message", validationError);
return result;
}
// 2. 构建付款参数
Map<String, String> params = buildTransferParams(transferInfo);
// 3. 生成签名
String sign = generateSign(params, WECHAT_API_KEY);
params.put("sign", sign);
// 4. 发送请求需要证书
String xmlRequest = mapToXml(params);
// 注意企业付款接口需要使用客户端证书这里简化处理
ResponseEntity<String> response = restTemplate.postForEntity(WECHAT_TRANSFER_URL, xmlRequest, String.class);
// 5. 解析响应
Map<String, String> responseMap = xmlToMap(response.getBody());
if (SUCCESS_CODE.equals(responseMap.get("return_code")) &&
SUCCESS_CODE.equals(responseMap.get("result_code"))) {
Map<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<String, String> buildUnifiedOrderParams(Map<String, Object> orderInfo) {
Map<String, String> 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<String, String> buildRefundParams(Map<String, Object> refundInfo) {
Map<String, String> 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<String, String> buildTransferParams(Map<String, Object> transferInfo) {
Map<String, String> 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<String, Object> buildMiniProgramPayParams(String prepayId) {
Map<String, Object> payParams = new HashMap<>();
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonceStr = generateNonceStr();
String packageStr = "prepay_id=" + prepayId;
String signType = "MD5";
// 构建签名参数
Map<String, String> 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<String, Object> 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>");
xml.append("<return_code><![CDATA[").append(returnCode).append("]]></return_code>");
xml.append("<return_msg><![CDATA[").append(returnMsg).append("]]></return_msg>");
xml.append("</xml>");
return xml.toString();
}
/**
* 生成随机字符串
*/
private static String generateNonceStr() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 32);
}
/**
* 生成签名
*/
private static String generateSign(Map<String, String> params, String key) {
try {
// 1. 排序参数
Map<String, String> sortedParams = new TreeMap<>(params);
// 2. 拼接参数
StringBuilder stringBuilder = new StringBuilder();
for (Map.Entry<String, String> 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<String, String> params, String key) {
String sign = params.get("sign");
if (sign == null || sign.isEmpty()) {
return false;
}
Map<String, String> paramsWithoutSign = new HashMap<>(params);
paramsWithoutSign.remove("sign");
String generatedSign = generateSign(paramsWithoutSign, key);
return sign.equals(generatedSign);
}
/**
* Map转XML
*/
private static String mapToXml(Map<String, String> params) {
StringBuilder xml = new StringBuilder();
xml.append("<xml>");
for (Map.Entry<String, String> entry : params.entrySet()) {
xml.append("<").append(entry.getKey()).append("><![CDATA[")
.append(entry.getValue()).append("]]></").append(entry.getKey()).append(">");
}
xml.append("</xml>");
return xml.toString();
}
/**
* XML转Map简化实现
*/
private static Map<String, String> xmlToMap(String xml) {
Map<String, String> map = new HashMap<>();
try {
// 简化的XML解析实现
xml = xml.replaceAll("<xml>", "").replaceAll("</xml>", "");
String[] elements = xml.split("</\\w+>");
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("<![CDATA[") && value.endsWith("]]>")) {
value = value.substring(9, value.length() - 3);
}
map.put(key, value);
}
}
} catch (Exception e) {
throw new RuntimeException("XML解析失败", e);
}
return map;
}
}

View File

@ -19,8 +19,23 @@ public interface UsersMapper
*/ */
public Users selectUsersById(Long id); public Users selectUsersById(Long id);
/**
* 查询请填写功能名称
*
* @param rememberToken 请填写rememberToken
* @return 请填写功能名称
*/
public Users selectUsersByRememberToken(String rememberToken);
/**
* 查询请填写功能名称
*
* @param openid 请填写rememberToken
* @return 请填写功能名称
*/
public Users selectUsersByOpenid(String openid);
/** /**
* 查询根据电话号码查询用户基本信息 * 查询根据电话号码查询用户基本信息
* *

View File

@ -18,7 +18,22 @@ public interface IUsersService
* @return 请填写功能名称 * @return 请填写功能名称
*/ */
public Users selectUsersById(Long id); public Users selectUsersById(Long id);
/**
* 查询请填写功能名称
*
* @param rememberToken 请填写rememberToken
* @return 请填写功能名称
*/
public Users selectUsersByRememberToken(String rememberToken);
/**
* 查询请填写功能名称
*
* @param openid 请填写rememberToken
* @return 请填写功能名称
*/
public Users selectUsersByOpenid(String openid);
/** /**
* 查询根据电弧号码查询用户基本数据 * 查询根据电弧号码查询用户基本数据
* *

View File

@ -30,6 +30,31 @@ public class UsersServiceImpl implements IUsersService
{ {
return usersMapper.selectUsersById(id); 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);
}
/** /**
* 查询根据电弧号码查询用户基本数据 * 查询根据电弧号码查询用户基本数据
* *

View File

@ -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());
}
}
}

View File

@ -25,9 +25,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<include refid="selectServiceCateVo"/> <include refid="selectServiceCateVo"/>
<where> <where>
<if test="title != null and title != ''"> and title like concat('%',#{title},'%')</if> <if test="title != null and title != ''"> and title like concat('%',#{title},'%')</if>
<if test="status != null and status != ''"> and status=#{status}</if>
<if test="type != null and type != ''"> and type=#{type}</if>
</where> </where>
order by id desc order by sort ASC
</select> </select>
<select id="selectServiceCateById" parameterType="Long" resultMap="ServiceCateResult"> <select id="selectServiceCateById" parameterType="Long" resultMap="ServiceCateResult">
<include refid="selectServiceCateVo"/> <include refid="selectServiceCateVo"/>

View File

@ -4,7 +4,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd"> "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.system.mapper.UsersMapper"> <mapper namespace="com.ruoyi.system.mapper.UsersMapper">
<resultMap type="Users" id="UsersResult"> <resultMap type="Users" id="UsersResult">
<result property="id" column="id" /> <result property="id" column="id" />
<result property="name" column="name" /> <result property="name" column="name" />
<result property="nickname" column="nickname" /> <result property="nickname" column="nickname" />
@ -101,6 +101,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
</select> </select>
<select id="selectUsersByRememberToken" parameterType="String" resultMap="UsersResult">
<include refid="selectUsersVo"/>
where remember_token = #{rememberToken}
</select>
<select id="selectUsersByOpenid" parameterType="String" resultMap="UsersResult">
<include refid="selectUsersVo"/>
where openid = #{openid}
</select>
<select id="selectUsersByPhone" parameterType="String" resultMap="UsersResult"> <select id="selectUsersByPhone" parameterType="String" resultMap="UsersResult">
<include refid="selectUsersVo"/> <include refid="selectUsersVo"/>
where phone = #{phone} where phone = #{phone}

View File

@ -183,8 +183,17 @@ export default {
let quill = this.Quill let quill = this.Quill
// //
let length = quill.getSelection().index let length = quill.getSelection().index
// res.url
quill.insertEmbed(length, "image", process.env.VUE_APP_BASE_API + res.fileName) // URLURL
let imageUrl = res.url || res.fileName;
if (imageUrl && (imageUrl.startsWith('http://') || imageUrl.startsWith('https://'))) {
// URL使
quill.insertEmbed(length, "image", imageUrl)
} else {
// baseUrl
quill.insertEmbed(length, "image", process.env.VUE_APP_BASE_API + res.fileName)
}
// //
quill.setSelection(length + 1) quill.setSelection(length + 1)
} else { } else {

View File

@ -189,7 +189,17 @@ export default {
// //
handleUploadSuccess(res, file) { handleUploadSuccess(res, file) {
if (res.code === 200) { if (res.code === 200) {
this.uploadList.push({ name: res.fileName, url: res.fileName }) // URLURL
let fileUrl = res.url || res.fileName;
let fileName = res.fileName;
// URL使
if (fileUrl && (fileUrl.startsWith('http://') || fileUrl.startsWith('https://'))) {
this.uploadList.push({ name: fileUrl, url: fileUrl })
} else {
// 使fileName
this.uploadList.push({ name: fileName, url: fileName })
}
this.uploadedSuccessfully() this.uploadedSuccessfully()
} else { } else {
this.number-- this.number--

View File

@ -130,10 +130,15 @@ export default {
// //
this.fileList = list.map(item => { this.fileList = list.map(item => {
if (typeof item === "string") { if (typeof item === "string") {
if (item.indexOf(this.baseUrl) === -1 && !isExternal(item)) { // URL使
item = { name: this.baseUrl + item, url: this.baseUrl + item } if (isExternal(item) || item.startsWith('http://') || item.startsWith('https://')) {
item = { name: item, url: item }
} else if (item.indexOf(this.baseUrl) === -1) {
// baseUrl
item = { name: this.baseUrl + item, url: this.baseUrl + item }
} else { } else {
item = { name: item, url: item } //
item = { name: item, url: item }
} }
} }
return item return item
@ -196,7 +201,17 @@ export default {
// //
handleUploadSuccess(res, file) { handleUploadSuccess(res, file) {
if (res.code === 200) { if (res.code === 200) {
this.uploadList.push({ name: res.fileName, url: res.fileName }) // URLURL
let fileUrl = res.url || res.fileName;
let fileName = res.fileName;
// URL使
if (fileUrl && (fileUrl.startsWith('http://') || fileUrl.startsWith('https://'))) {
this.uploadList.push({ name: fileUrl, url: fileUrl })
} else {
// 使fileName
this.uploadList.push({ name: fileName, url: fileName })
}
this.uploadedSuccessfully() this.uploadedSuccessfully()
} else { } else {
this.number-- this.number--
@ -240,7 +255,13 @@ export default {
separator = separator || "," separator = separator || ","
for (let i in list) { for (let i in list) {
if (list[i].url) { if (list[i].url) {
strs += list[i].url.replace(this.baseUrl, "") + separator let url = list[i].url
// URL使URLbaseUrl
if (url.startsWith('http://') || url.startsWith('https://')) {
strs += url + separator
} else {
strs += url.replace(this.baseUrl, "") + separator
}
} }
} }
return strs != '' ? strs.substr(0, strs.length - 1) : '' return strs != '' ? strs.substr(0, strs.length - 1) : ''

View File

@ -132,8 +132,8 @@
<el-table-column label="库存" align="center" width="75" prop="stock"> <el-table-column label="库存" align="center" width="75" prop="stock">
<template slot-scope="scope"> <template slot-scope="scope">
<div style="cursor:pointer;color:#409EFF;display:inline-block;"> <div style="cursor:pointer;color:#409EFF;display:inline-block;">
<span @click="openEditDialog(scope.row, 'stock', '销量')">{{ scope.row.stock }}</span> <span @click="openEditDialog(scope.row, 'stock', '库存')">{{ scope.row.stock }}</span>
<svg @click="openEditDialog(scope.row, 'stock', '销量')" width="14" height="14" viewBox="0 0 1024 1024" fill="#bbb" style="margin-left:4px;vertical-align:middle;cursor:pointer;"><path d="M880.64 227.84l-84.48-84.48c-25.6-25.6-67.2-25.6-92.8 0l-59.52 59.52 177.28 177.28 59.52-59.52c25.6-25.6 25.6-67.2 0-92.8zM160 736v128h128l376.32-376.32-128-128L160 736z"/></svg> <svg @click="openEditDialog(scope.row, 'stock', '库存')" width="14" height="14" viewBox="0 0 1024 1024" fill="#bbb" style="margin-left:4px;vertical-align:middle;cursor:pointer;"><path d="M880.64 227.84l-84.48-84.48c-25.6-25.6-67.2-25.6-92.8 0l-59.52 59.52 177.28 177.28 59.52-59.52c25.6-25.6 25.6-67.2 0-92.8zM160 736v128h128l376.32-376.32-128-128L160 736z"/></svg>
<div style="border-bottom:1px dashed #bbb;width:100%;margin-top:2px;"></div> <div style="border-bottom:1px dashed #bbb;width:100%;margin-top:2px;"></div>
</div> </div>
</template> </template>
@ -271,17 +271,17 @@
<el-tab-pane label="规格配置" name="spec"> <el-tab-pane label="规格配置" name="spec">
<el-form-item label="规格"> <el-form-item label="规格">
<el-radio-group v-model="skuType"> <el-radio-group v-model="skuType">
<el-radio-button label="single">单规格</el-radio-button> <el-radio-button :label="1">单规格</el-radio-button>
<el-radio-button label="multi">多规格</el-radio-button> <el-radio-button :label="2">多规格</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<div v-if="skuType === 'single'"> <div v-if="skuType === 1">
<el-form-item label="规格名"> <!-- <el-form-item label="规格名">
<el-input v-model="form.skuName" placeholder="请输入规格名" /> <el-input v-model="form.skuName" placeholder="请输入规格名" />
</el-form-item> </el-form-item>
<el-form-item label="规格值"> <el-form-item label="规格值">
<el-input v-model="form.skuValue" placeholder="请输入规格值" /> <el-input v-model="form.skuValue" placeholder="请输入规格值" />
</el-form-item> </el-form-item> -->
</div> </div>
<div v-else> <div v-else>
<Sku :info="form.sku" ref="skuRef"></Sku> <Sku :info="form.sku" ref="skuRef"></Sku>
@ -302,8 +302,8 @@
</el-tabs> </el-tabs>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"></el-button> <el-button type="primary" @click="submitForm"></el-button>
<el-button @click="cancel"></el-button> <el-button @click="cancel"></el-button>
</div> </div>
</div> </div>
</el-drawer> </el-drawer>
@ -395,6 +395,9 @@ export default {
status: [ status: [
{ required: true, message: "状态不能为空", trigger: "change" } { required: true, message: "状态不能为空", trigger: "change" }
], ],
sku: [
{ required: true, message: "规格信息不能为空", trigger: "blur" }
],
}, },
audioDialogVisible: false, audioDialogVisible: false,
currentAudioUrl: '', currentAudioUrl: '',
@ -405,7 +408,7 @@ export default {
editRow: null, editRow: null,
daterangeCreatedAt: [], daterangeCreatedAt: [],
activeTab: 'base', activeTab: 'base',
skuType: 'single', skuType: 1,
skuList: [ { name: '', value: '' } ], skuList: [ { name: '', value: '' } ],
specList: [ { name: '', values: [''] } ], specList: [ { name: '', values: [''] } ],
skuTable: [], skuTable: [],
@ -441,18 +444,20 @@ export default {
info: null, info: null,
price: null, price: null,
priceZn: null, priceZn: null,
sales: null, sales: 0,
stock: null, stock: 0,
status: null, status: "1",
description: null, description: null,
skuType: null, skuType: 1,
sku: null, sku: "{}",
skuName: null,
skuValue: null,
latitude: null, latitude: null,
longitude: null, longitude: null,
type: null, type: 2, //
cateId: null, cateId: null,
project: null, project: null,
sort: null, sort: 50, //
material: null, material: null,
postage: null, postage: null,
basic: null, basic: null,
@ -462,7 +467,11 @@ export default {
updatedAt: null, updatedAt: null,
deletedAt: null deletedAt: null
} }
this.resetForm("form") // skuType
this.skuType = 1;
//
this.activeTab = 'base';
this.resetForm("form");
}, },
/** 搜索按钮操作 */ /** 搜索按钮操作 */
handleQuery() { handleQuery() {
@ -515,51 +524,103 @@ export default {
const id = row.id || this.ids const id = row.id || this.ids
getServiceGoods(id).then(response => { getServiceGoods(id).then(response => {
this.form = response.data this.form = response.data
// skuType1
this.skuType = this.form.skuType || 1;
// sku
if (this.skuType === 1 && this.form.sku && this.form.sku !== '{}') {
try {
const skuData = JSON.parse(this.form.sku);
if (skuData.name && skuData.value) {
this.form.skuName = skuData.name;
this.form.skuValue = skuData.value;
}
} catch (e) {
console.warn('解析单规格数据失败:', e);
}
}
this.open = true this.open = true
this.title = "修改服务内容" this.title = "修改服务内容"
}) })
}, },
/** 提交按钮 */ /** 提交按钮 */
submitForm() { submitForm() {
if( this.$refs.skuRef.submit()){ //
this.form.sku=this.$refs.skuRef.submit(); this.form.skuType = this.skuType;
}else{
return
}
//
// if(this.$refs.skuRef.submit()){} if (this.skuType === 2) {
//
if (this.$refs.skuRef && this.$refs.skuRef.submit) {
const skuData = this.$refs.skuRef.submit();
if (!skuData) {
this.$modal.msgError("请完善规格信息");
return;
}
this.form.sku = typeof skuData === 'string' ? skuData : JSON.stringify(skuData);
} else {
this.$modal.msgError("规格信息不能为空");
return;
}
} else {
//
if (!this.form.skuName || !this.form.skuValue) {
this.$modal.msgError("请完善单规格信息");
return;
}
const singleSku = {
name: this.form.skuName,
value: this.form.skuValue
};
this.form.sku = JSON.stringify(singleSku);
}
// sku
if (!this.form.sku || this.form.sku === '{}') {
this.$modal.msgError("规格信息不能为空");
return;
}
this.$refs["form"].validate(valid => { this.$refs["form"].validate(valid => {
if (valid) { if (valid) {
if (this.form.id != null) { if (this.form.id != null) {
updateServiceGoods(this.form).then(response => { updateServiceGoods(this.form).then(response => {
this.$modal.msgSuccess("修改成功") this.$modal.msgSuccess("修改成功");
this.open = false //
this.getList() this.getList(); //
}) }).catch(error => {
console.error('修改失败:', error);
this.$modal.msgError("修改失败,请检查输入数据");
});
} else { } else {
addServiceGoods(this.form).then(response => { addServiceGoods(this.form).then(response => {
this.$modal.msgSuccess("新增成功") this.$modal.msgSuccess("新增成功");
this.open = false //
this.getList() this.getList(); //
}) }).catch(error => {
console.error('新增失败:', error);
this.$modal.msgError("新增失败,请检查输入数据");
});
} }
} else {
console.log('表单验证失败');
this.$message.error('请完善必填信息');
} }
}) })
}, },
/** 删除按钮操作 */ /** 删除按钮操作 */
handleDelete(row) { handleDelete(row) {
const ids = row.id || this.ids const ids = row.id || this.ids;
this.$modal.confirm('是否确认删除服务内容编号为"' + ids + '"的数据项?').then(function() { const names = row.title || this.ServiceGoodsList.filter(item => this.ids.includes(item.id)).map(item => item.title).join('、');
return delServiceGoods(ids)
this.$modal.confirm('是否确认删除商品"' + names + '"').then(function() {
return delServiceGoods(ids);
}).then(() => { }).then(() => {
this.getList() this.getList();
this.$modal.msgSuccess("删除成功") this.$modal.msgSuccess("删除成功");
}).catch(() => {}) }).catch((error) => {
console.error('删除失败:', error);
this.$modal.msgError("删除失败,请稍后重试");
});
}, },
getserviceCateList(){ getserviceCateList(){
selectServiceCateList().then(response => { selectServiceCateList().then(response => {
@ -585,20 +646,43 @@ export default {
}, },
async saveEditField() { async saveEditField() {
if (!this.editRow) return; if (!this.editRow) return;
const value = ['price', 'sales', 'stock', 'sort'].includes(this.editField)
? Number(this.editFieldValue) //
: this.editFieldValue; let value = this.editFieldValue;
if (['price', 'margin'].includes(this.editField)) {
//
const numValue = parseFloat(value);
if (isNaN(numValue) || numValue < 0) {
this.$message.error('请输入有效的价格');
return;
}
value = numValue;
} else if (['sales', 'stock', 'sort'].includes(this.editField)) {
//
const numValue = parseInt(value);
if (isNaN(numValue) || numValue < 0) {
this.$message.error('请输入有效的数字');
return;
}
value = numValue;
}
const payload = { const payload = {
id: this.editRow.id, id: this.editRow.id,
[this.editField]: value [this.editField]: value
}; };
try { try {
await updateServiceGoods(payload); await updateServiceGoods(payload);
this.$message.success('修改成功'); this.$message.success('修改成功');
this.editDialogVisible = false; //
this.editRow[this.editField] = value;
this.getList(); this.getList();
} catch (e) { //
this.$message.error('修改失败'); this.editDialogVisible = false;
} catch (error) {
console.error('快捷编辑失败:', error);
this.$message.error('修改失败,请稍后重试');
} }
}, },
addSpec() { addSpec() {

View File

@ -411,21 +411,19 @@
<el-tab-pane label="规格配置" name="spec"> <el-tab-pane label="规格配置" name="spec">
<el-form-item label="规格"> <el-form-item label="规格">
<el-radio-group v-model="skuType"> <el-radio-group v-model="skuType">
<el-radio-button label="single">单规格</el-radio-button> <el-radio-button :label="1">单规格</el-radio-button>
<el-radio-button label="multi">多规格</el-radio-button> <el-radio-button :label="2">多规格</el-radio-button>
</el-radio-group> </el-radio-group>
</el-form-item> </el-form-item>
<div v-if="skuType === 'single'"> <div v-if="skuType === 1">
<!-- <el-form-item label="规格名">
<el-form-item label="规格名">
<el-input v-model="form.skuName" placeholder="请输入规格名" /> <el-input v-model="form.skuName" placeholder="请输入规格名" />
</el-form-item> </el-form-item>
<el-form-item label="规格值"> <el-form-item label="规格值">
<el-input v-model="form.skuValue" placeholder="请输入规格值" /> <el-input v-model="form.skuValue" placeholder="请输入规格值" />
</el-form-item> </el-form-item> -->
</div> </div>
<div v-else> <div v-else>
<Sku :info="form.sku" ref="skuRef"></Sku> <Sku :info="form.sku" ref="skuRef"></Sku>
</div> </div>
</el-tab-pane> </el-tab-pane>
@ -565,7 +563,7 @@ export default {
editFieldLabel: "", editFieldLabel: "",
editRow: null, editRow: null,
activeTab: "base", activeTab: "base",
skuType: "single", skuType: 1,
skuList: [{ name: "", value: "" }], skuList: [{ name: "", value: "" }],
specList: [{ name: "", values: [""] }], specList: [{ name: "", values: [""] }],
skuTable: [], skuTable: [],
@ -605,12 +603,16 @@ export default {
status: null, status: null,
tags: null, tags: null,
cateId: null, cateId: null,
skuType: null, skuType: 1,
sku: null, sku: "{}",
skuName: null,
skuValue: null,
contetnt: null, contetnt: null,
createdAt: null, createdAt: null,
updatedAt: null, updatedAt: null,
}; };
// skuType
this.skuType = 1;
this.resetForm("form"); this.resetForm("form");
}, },
/** 搜索按钮操作 */ /** 搜索按钮操作 */
@ -682,24 +684,60 @@ export default {
const id = row.id || this.ids; const id = row.id || this.ids;
getIntegralProduct(id).then((response) => { getIntegralProduct(id).then((response) => {
this.form = response.data; this.form = response.data;
// skuType1
this.skuType = this.form.skuType || 1;
// sku
if (this.skuType === 1 && this.form.sku && this.form.sku !== '{}') {
try {
const skuData = JSON.parse(this.form.sku);
if (skuData.name && skuData.value) {
this.form.skuName = skuData.name;
this.form.skuValue = skuData.value;
}
} catch (e) {
console.warn('解析单规格数据失败:', e);
}
}
this.open = true; this.open = true;
this.title = "修改积分商品"; this.title = "修改积分商品";
}); });
}, },
/** 提交按钮 */ /** 提交按钮 */
submitForm() { submitForm() {
//
this.form.skuType = this.skuType;
if(this.$refs.skuRef){
//
if(this.$refs.skuRef.submit()){ if (this.skuType === 2) {
this.form.sku=this.$refs.skuRef.submit(); //
}else{ if (this.$refs.skuRef && this.$refs.skuRef.submit) {
return const skuData = this.$refs.skuRef.submit();
if (!skuData) {
this.$modal.msgError("请完善规格信息");
return;
}
this.form.sku = typeof skuData === 'string' ? skuData : JSON.stringify(skuData);
} else {
this.$modal.msgError("规格信息不能为空");
return;
} }
} else {
//
if (!this.form.skuName || !this.form.skuValue) {
this.$modal.msgError("请完善单规格信息");
return;
}
const singleSku = {
name: this.form.skuName,
value: this.form.skuValue
};
this.form.sku = JSON.stringify(singleSku);
}
}else{ // sku
this.form.sku='{}'; if (!this.form.sku || this.form.sku === '{}') {
this.$modal.msgError("规格信息不能为空");
return;
} }
this.$refs["form"].validate((valid) => { this.$refs["form"].validate((valid) => {

View File

@ -134,8 +134,8 @@
<el-table-column label="库存" align="center" width="75" prop="stock"> <el-table-column label="库存" align="center" width="75" prop="stock">
<template slot-scope="scope"> <template slot-scope="scope">
<div style="cursor:pointer;color:#409EFF;display:inline-block;"> <div style="cursor:pointer;color:#409EFF;display:inline-block;">
<span @click="openEditDialog(scope.row, 'stock', '销量')">{{ scope.row.stock }}</span> <span @click="openEditDialog(scope.row, 'stock', '库存')">{{ scope.row.stock }}</span>
<svg @click="openEditDialog(scope.row, 'stock', '销量')" width="14" height="14" viewBox="0 0 1024 1024" fill="#bbb" style="margin-left:4px;vertical-align:middle;cursor:pointer;"><path d="M880.64 227.84l-84.48-84.48c-25.6-25.6-67.2-25.6-92.8 0l-59.52 59.52 177.28 177.28 59.52-59.52c25.6-25.6 25.6-67.2 0-92.8zM160 736v128h128l376.32-376.32-128-128L160 736z"/></svg> <svg @click="openEditDialog(scope.row, 'stock', '库存')" width="14" height="14" viewBox="0 0 1024 1024" fill="#bbb" style="margin-left:4px;vertical-align:middle;cursor:pointer;"><path d="M880.64 227.84l-84.48-84.48c-25.6-25.6-67.2-25.6-92.8 0l-59.52 59.52 177.28 177.28 59.52-59.52c25.6-25.6 25.6-67.2 0-92.8zM160 736v128h128l376.32-376.32-128-128L160 736z"/></svg>
<div style="border-bottom:1px dashed #bbb;width:100%;margin-top:2px;"></div> <div style="border-bottom:1px dashed #bbb;width:100%;margin-top:2px;"></div>
</div> </div>
</template> </template>
@ -156,6 +156,12 @@
</template> </template>
</el-table-column> </el-table-column>
<el-table-column label="基检现象" align="center" prop="basic" width="150">
<template slot-scope="scope">
<span>{{ formatBasicNames(scope.row.basic) }}</span>
</template>
</el-table-column>
<el-table-column label="状态" width="85" align="center"> <el-table-column label="状态" width="85" align="center">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch <el-switch
@ -240,11 +246,11 @@
<el-select <el-select
v-model="form.skillIdsArray" v-model="form.skillIdsArray"
multiple multiple
filterable
placeholder="请选择所需技能" placeholder="请选择所需技能"
style="width: 100%" style="width: 100%"
clearable clearable
filterable
@change="handleSkillChange" @change="handleSkillChange"
> >
<el-option <el-option
@ -254,14 +260,58 @@
:value="skill.id" :value="skill.id"
/> />
</el-select> </el-select>
<!-- 调试信息 --> <div v-if="siteSkillList.length === 0" style="margin-top: 5px; font-size: 12px; color: #999;">
<!-- <div style="margin-top: 10px; font-size: 12px; color: #999;"> <i class="el-icon-warning"></i>
<p>技能列表数量: {{ siteSkillList.length }}</p> 暂无技能数据请检查接口是否正常
<p>已选技能: {{ form.skillIdsArray }}</p> </div>
<p>技能字符串: {{ form.skillIds }}</p> <div v-else-if="form.skillIdsArray && form.skillIdsArray.length > 0" style="margin-top: 5px; font-size: 12px; color: #999;">
</div> --> <i class="el-icon-info"></i>
已选择 {{ form.skillIdsArray.length }} 项技能{{ getSelectedSkillNames() }}
</div>
</el-form-item> </el-form-item>
</el-tab-pane> <el-form-item label="基检现象" prop="basic">
<div class="basic-tags-container" style="border: 1px solid #dcdfe6; border-radius: 4px; padding: 8px; min-height: 40px;">
<el-tag
v-for="item in basicOptions"
:key="item"
:type="(form.basicArray && form.basicArray.includes(item)) ? 'primary' : 'info'"
:effect="(form.basicArray && form.basicArray.includes(item)) ? 'dark' : 'plain'"
:closable="form.basicArray && form.basicArray.includes(item)"
size="small"
style="margin: 2px; cursor: pointer;"
@click="toggleBasicTag(item)"
@close="removeBasicTag(item)"
>
{{ item }}
</el-tag>
<el-tag
v-if="showBasicInput"
size="small"
style="margin: 2px;"
>
<el-input
ref="basicInput"
v-model="newBasicTag"
size="mini"
style="width: 100px;"
@keyup.enter.native="handleBasicInputConfirm"
@blur="handleBasicInputConfirm"
/>
</el-tag>
<el-button
v-else
size="small"
type="primary"
plain
icon="el-icon-plus"
style="margin: 2px;"
@click="showBasicInputBox"
>
添加
</el-button>
</div>
</el-form-item>
</el-tab-pane>
<el-tab-pane label="营销配置" name="marketing"> <el-tab-pane label="营销配置" name="marketing">
<el-form-item label="销量" prop="sales" required> <el-form-item label="销量" prop="sales" required>
<el-input-number <el-input-number
@ -336,8 +386,8 @@
</el-tabs> </el-tabs>
</el-form> </el-form>
<div slot="footer" class="dialog-footer"> <div slot="footer" class="dialog-footer">
<el-button type="primary" @click="submitForm"></el-button> <el-button type="primary" @click="submitForm"></el-button>
<el-button @click="cancel"></el-button> <el-button @click="cancel"></el-button>
</div> </div>
</div> </div>
</el-drawer> </el-drawer>
@ -387,7 +437,9 @@ export default {
serviceCateList: [], serviceCateList: [],
siteSkillList : [], siteSkillList : [],
//
basicOptions: [],
total: 0, total: 0,
// //
@ -422,11 +474,18 @@ export default {
{ required: true, message: "副标题不能为空", trigger: "blur" } { required: true, message: "副标题不能为空", trigger: "blur" }
], ],
price: [ price: [
{ required: true, message: "价格不能为空", trigger: "blur" } { required: true, message: "价格不能为空", trigger: "blur" },
{ pattern: /^(0|[1-9]\d*)(\.\d{1,2})?$/, message: "请输入正确的价格格式", trigger: "blur" }
], ],
sales: [ sales: [
{ required: true, message: "销量不能为空", trigger: "blur" } { required: true, message: "销量不能为空", trigger: "blur" }
], ],
sort: [
{ required: true, message: "排序不能为空", trigger: "blur" }
],
cateId: [
{ required: true, message: "请选择分类", trigger: "change" }
],
status: [ status: [
{ required: true, message: "状态不能为空", trigger: "change" } { required: true, message: "状态不能为空", trigger: "change" }
], ],
@ -444,6 +503,10 @@ export default {
skuList: [ { name: '', value: '' } ], skuList: [ { name: '', value: '' } ],
specList: [ { name: '', values: [''] } ], specList: [ { name: '', values: [''] } ],
skuTable: [], skuTable: [],
//
showBasicInput: false,
newBasicTag: '',
} }
}, },
created() { created() {
@ -451,6 +514,34 @@ export default {
this.getserviceCateList(); this.getserviceCateList();
this.getSiteSkillList(); this.getSiteSkillList();
this.testSkillList(); this.testSkillList();
this.testJsonConversion(); // JSON
},
watch: {
// UI
'form.skillIdsArray': {
handler(newVal, oldVal) {
console.log('技能数组变化监听:', '旧值:', oldVal, '新值:', newVal);
//
if (newVal && !Array.isArray(newVal)) {
console.warn('技能数组不是数组格式,自动修正:', newVal);
this.$set(this.form, 'skillIdsArray', []);
}
},
deep: true,
immediate: true
},
//
'siteSkillList': {
handler(newVal) {
console.log('技能列表变化:', newVal.length, '项技能');
if (newVal.length > 0) {
console.log('技能列表详情:', newVal.map(skill => ({ id: skill.id, title: skill.title })));
}
},
deep: true,
immediate: true
}
}, },
methods: { methods: {
/** 查询服务内容列表 */ /** 查询服务内容列表 */
@ -488,13 +579,14 @@ export default {
skuValue: '', skuValue: '',
latitude: null, latitude: null,
longitude: null, longitude: null,
type: null, type: 1, //
cateId: null, cateId: null,
project: null, project: null,
sort: 0, sort: 50, //
material: null, material: null,
postage: null, postage: null,
basic: null, basic: null,
basicArray: [], //
margin: null, margin: null,
skillIds: null, skillIds: null,
skillIdsArray: [], // skillIdsArray: [], //
@ -502,10 +594,18 @@ export default {
updatedAt: null, updatedAt: null,
deletedAt: null deletedAt: null
} }
// 使Vue.set // 使Vue.set
this.$set(this.form, 'skillIdsArray', []); this.$set(this.form, 'skillIdsArray', []);
this.skuType = 1 this.$set(this.form, 'basicArray', []);
this.resetForm("form") //
this.showBasicInput = false;
this.newBasicTag = '';
//
this.basicOptions = [];
this.skuType = 1;
//
this.activeTab = 'base';
this.resetForm("form");
}, },
/** 搜索按钮操作 */ /** 搜索按钮操作 */
handleQuery() { handleQuery() {
@ -535,37 +635,74 @@ export default {
this.multiple = !selection.length this.multiple = !selection.length
}, },
// //
handlefenleiStatusChange(row) { handlefenleiStatusChange(row) {
let text = row.status === "0" ? "启用" : "停用" const text = row.status === "0" ? "启用" : "停用";
this.$modal.confirm('确认要"' + text + '""' + row.title + '"状态吗?').then(function() { this.$modal.confirm('确认要"' + text + '""' + row.title + '"吗?').then(function() {
return changefenleiStatus(row.id, row.status) return changefenleiStatus(row.id, row.status);
}).then(() => { }).then(() => {
this.$modal.msgSuccess(text + "成功") this.$modal.msgSuccess(text + "成功");
}).catch(function() { }).catch(function() {
row.status = row.status === "0" ? "1" : "0" //
}) row.status = row.status === "0" ? "1" : "0";
});
}, },
/** 新增按钮操作 */ /** 新增按钮操作 */
handleAdd() { handleAdd() {
this.reset() this.reset()
// //
this.$set(this.form, 'skillIdsArray', []); this.$set(this.form, 'skillIdsArray', []);
this.$set(this.form, 'basicArray', []);
//
this.basicOptions = [];
//
this.showBasicInput = false;
this.newBasicTag = '';
console.log('新增时初始化form:', this.form); console.log('新增时初始化form:', this.form);
//
if (this.siteSkillList.length === 0) {
console.warn('技能列表未加载,重新获取...');
this.getSiteSkillList();
}
this.open = true this.open = true
this.title = "添加服务内容" this.title = "添加服务内容"
}, },
getSiteSkillList(){ getSiteSkillList(){
getSiteSkillList().then(response => { console.log('开始获取技能列表...');
return getSiteSkillList().then(response => {
console.log('技能列表响应:', response);
console.log('技能列表数据:', response.data); console.log('技能列表数据:', response.data);
this.$set(this, 'siteSkillList', response.data || []);
// // idtitle
this.$forceUpdate(); let skillData = [];
if (Array.isArray(response.data)) {
skillData = response.data.filter(skill => skill && skill.id !== undefined && skill.title);
// id
skillData = skillData.map(skill => ({
...skill,
id: parseInt(skill.id)
}));
}
this.$set(this, 'siteSkillList', skillData);
console.log('技能列表设置完成,数量:', skillData.length);
console.log('技能列表详情:', skillData);
//
if (skillData.length === 0) {
console.warn('技能列表为空,请检查后端数据');
this.$message.warning('暂无技能数据,请联系管理员添加技能信息');
}
return skillData;
}).catch(error => { }).catch(error => {
console.error('获取技能列表失败:', error); console.error('获取技能列表失败:', error);
this.$set(this, 'siteSkillList', []); this.$set(this, 'siteSkillList', []);
this.$message.error('获取技能列表失败'); this.$message.error('获取技能列表失败,请检查网络连接或联系管理员');
return [];
}) })
}, },
@ -574,14 +711,54 @@ export default {
handleUpdate(row) { handleUpdate(row) {
this.reset() this.reset()
const id = row.id || this.ids const id = row.id || this.ids
getServiceGoods(id).then(response => {
this.form = response.data //
const skillPromise = this.siteSkillList.length > 0 ?
Promise.resolve(this.siteSkillList) :
this.getSiteSkillList();
Promise.all([
getServiceGoods(id),
skillPromise
]).then(([serviceResponse, skillData]) => {
this.form = serviceResponse.data
console.log('编辑数据:', this.form); console.log('编辑数据:', this.form);
// ID console.log('技能列表数据:', skillData);
console.log('原始基检现象数据:', this.form.basic);
// IDJSON
if (this.form.skillIds) { if (this.form.skillIds) {
try { try {
// skillIds let skillArray = [];
const skillArray = this.form.skillIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)) console.log('原始技能数据:', this.form.skillIds, '类型:', typeof this.form.skillIds);
if (typeof this.form.skillIds === 'string') {
// JSON ["30", "32", "45"]
try {
const parsedSkills = JSON.parse(this.form.skillIds);
if (Array.isArray(parsedSkills)) {
skillArray = parsedSkills.map(id => parseInt(id)).filter(id => !isNaN(id));
} else {
// JSON
skillArray = this.form.skillIds.split(',').map(id => {
const numId = parseInt(id.trim());
return isNaN(numId) ? null : numId;
}).filter(id => id !== null);
}
} catch (jsonError) {
// JSON
skillArray = this.form.skillIds.split(',').map(id => {
const numId = parseInt(id.trim());
return isNaN(numId) ? null : numId;
}).filter(id => id !== null);
}
} else if (Array.isArray(this.form.skillIds)) {
// 使
skillArray = this.form.skillIds.map(id => parseInt(id)).filter(id => !isNaN(id));
} else if (typeof this.form.skillIds === 'number') {
//
skillArray = [this.form.skillIds];
}
this.$set(this.form, 'skillIdsArray', skillArray); this.$set(this.form, 'skillIdsArray', skillArray);
console.log('转换后的技能数组:', this.form.skillIdsArray); console.log('转换后的技能数组:', this.form.skillIdsArray);
} catch (e) { } catch (e) {
@ -592,6 +769,52 @@ export default {
this.$set(this.form, 'skillIdsArray', []); this.$set(this.form, 'skillIdsArray', []);
} }
// JSON
if (this.form.basic) {
try {
let basicArray = [];
console.log('原始基检现象数据:', this.form.basic, '类型:', typeof this.form.basic);
if (typeof this.form.basic === 'string') {
// JSON ["", "", ""]
try {
const parsedBasic = JSON.parse(this.form.basic);
if (Array.isArray(parsedBasic)) {
basicArray = parsedBasic.filter(item => item && item.trim());
} else {
// JSON
basicArray = this.form.basic.split(',').map(item => item.trim()).filter(item => item);
}
} catch (jsonError) {
// JSON
basicArray = this.form.basic.split(',').map(item => item.trim()).filter(item => item);
}
} else if (Array.isArray(this.form.basic)) {
// 使
basicArray = this.form.basic.filter(item => item && item.trim());
}
this.$set(this.form, 'basicArray', basicArray);
console.log('转换后的基检现象数组:', this.form.basicArray);
// 便
basicArray.forEach(item => {
if (!this.basicOptions.includes(item)) {
this.basicOptions.push(item);
}
});
} catch (e) {
console.error('基检现象转换错误:', e);
this.$set(this.form, 'basicArray', []);
}
} else {
this.$set(this.form, 'basicArray', []);
}
//
this.showBasicInput = false;
this.newBasicTag = '';
// //
if (this.form.skuType) { if (this.form.skuType) {
this.skuType = parseInt(this.form.skuType); this.skuType = parseInt(this.form.skuType);
@ -599,92 +822,175 @@ export default {
this.skuType = 1; // this.skuType = 1; //
} }
// sku // sku
// if (this.form.sku && typeof this.form.sku === 'string') { if (this.form.sku && typeof this.form.sku === 'string') {
// try { try {
// this.form.sku = JSON.parse(this.form.sku); // JSON
// console.log('sku:', this.form.sku); JSON.parse(this.form.sku);
// } catch (e) { // JSON
// console.log('sku:', e); } catch (e) {
// this.$set(this.form, 'sku', {}); console.log('sku字符串解析失败设置为空对象:', e);
// } this.form.sku = '{}';
// } else if (!this.form.sku) { }
// this.$set(this.form, 'sku', {}); } else if (!this.form.sku) {
// } this.form.sku = '{}';
}
//
if (this.form.price) {
this.form.price = parseFloat(this.form.price);
}
if (this.form.margin) {
this.form.margin = parseFloat(this.form.margin);
}
if (this.form.sales) {
this.form.sales = parseInt(this.form.sales);
}
if (this.form.stock) {
this.form.stock = parseInt(this.form.stock);
}
if (this.form.sort) {
this.form.sort = parseInt(this.form.sort);
}
console.log('编辑时的规格类型:', this.skuType); console.log('编辑时的规格类型:', this.skuType);
console.log('编辑时的规格数据:', this.form.sku); console.log('编辑时的规格数据:', this.form.sku);
// Vue
this.$nextTick(() => {
console.log('=== 编辑数据初始化完成后的状态 ===');
console.log('技能数组:', this.form.skillIdsArray);
console.log('技能字符串:', this.form.skillIds);
console.log('技能列表长度:', this.siteSkillList.length);
console.log('技能列表:', this.siteSkillList.map(s => ({id: s.id, title: s.title})));
console.log('已选择技能名称:', this.getSelectedSkillNames());
//
if (this.form.skillIds) {
console.log('表格显示格式化结果:', this.formatSkillNames(this.form.skillIds));
}
});
this.open = true this.open = true
this.title = "修改服务内容" this.title = "修改服务内容"
}).catch(error => {
console.error('编辑数据加载失败:', error);
this.$message.error('编辑数据加载失败,请重试');
}) })
}, },
/** 提交按钮 */ /** 提交按钮 */
submitForm() { submitForm() {
//
this.form.skuType = this.skuType;
//
if (this.skuType === 2) {
// -
if (!this.$refs.skuRef) {
this.$modal.msgError("多规格组件未初始化");
return;
}
const skuData = this.$refs.skuRef.submit();
if (!skuData) {
this.$modal.msgError("请完整配置多规格信息");
return;
}
this.form.sku = skuData;
console.log('多规格数据:', skuData);
} else {
// -
this.form.sku = '{}';
console.log('单规格数据:', this.form.sku);
}
// ID
console.log('提交前的技能数组:', this.form.skillIdsArray);
if (this.form.skillIdsArray && this.form.skillIdsArray.length > 0) {
this.form.skillIds = this.form.skillIdsArray.join(',')
} else {
this.form.skillIds = null
}
console.log('提交的技能字符串:', this.form.skillIds);
this.$refs["form"].validate(valid => { this.$refs["form"].validate(valid => {
if (valid) { if (valid) {
//
this.form.skuType = this.skuType;
//
if (this.skuType === 2) {
// -
if (!this.$refs.skuRef) {
this.$modal.msgError("多规格组件未初始化");
return;
}
const skuData = this.$refs.skuRef.submit();
if (!skuData) {
this.$modal.msgError("请完整配置多规格信息");
return;
}
this.form.sku = typeof skuData === 'object' ? JSON.stringify(skuData) : skuData;
console.log('多规格数据:', this.form.sku);
} else {
// -
this.form.sku = '{}';
console.log('单规格数据:', this.form.sku);
}
// IDJSON
console.log('提交前的技能数组:', this.form.skillIdsArray);
if (this.form.skillIdsArray && Array.isArray(this.form.skillIdsArray) && this.form.skillIdsArray.length > 0) {
// ID
const validSkillIds = this.form.skillIdsArray
.filter(id => id !== null && id !== undefined && !isNaN(id))
.map(id => id.toString()); // ["30", "32", "45"]
this.form.skillIds = validSkillIds.length > 0 ? JSON.stringify(validSkillIds) : null;
} else {
this.form.skillIds = null;
}
console.log('提交的技能JSON字符串:', this.form.skillIds);
// JSON
console.log('提交前的基检现象数组:', this.form.basicArray);
if (this.form.basicArray && Array.isArray(this.form.basicArray) && this.form.basicArray.length > 0) {
//
const validBasicItems = this.form.basicArray.filter(item => item && item.trim());
this.form.basic = validBasicItems.length > 0 ? JSON.stringify(validBasicItems) : null;
} else {
this.form.basic = null;
}
console.log('提交的基检现象JSON字符串:', this.form.basic);
//
if (this.form.price) {
this.form.price = parseFloat(this.form.price);
}
if (this.form.margin) {
this.form.margin = parseFloat(this.form.margin);
}
if (this.form.sales) {
this.form.sales = parseInt(this.form.sales);
}
if (this.form.stock) {
this.form.stock = parseInt(this.form.stock);
}
if (this.form.sort) {
this.form.sort = parseInt(this.form.sort);
}
console.log('最终提交的表单数据:', this.form); console.log('最终提交的表单数据:', this.form);
//
if (this.form.id != null) { if (this.form.id != null) {
updateServiceGoods(this.form).then(response => { updateServiceGoods(this.form).then(response => {
this.$modal.msgSuccess("修改成功") this.$modal.msgSuccess("修改成功");
this.open = false //
this.getList() this.getList(); //
}) }).catch(error => {
console.error('修改失败:', error);
this.$modal.msgError("修改失败,请检查输入数据");
});
} else { } else {
addServiceGoods(this.form).then(response => { addServiceGoods(this.form).then(response => {
this.$modal.msgSuccess("新增成功") this.$modal.msgSuccess("新增成功");
this.open = false //
this.getList() this.getList(); //
}) }).catch(error => {
console.error('新增失败:', error);
this.$modal.msgError("新增失败,请检查输入数据");
});
} }
} else {
console.log('表单验证失败');
this.$message.error('请完善必填信息');
} }
}) });
}, },
/** 删除按钮操作 */ /** 删除按钮操作 */
handleDelete(row) { handleDelete(row) {
const ids = row.id || this.ids const ids = row.id || this.ids;
this.$modal.confirm('是否确认删除服务内容编号为"' + ids + '"的数据项?').then(function() { const names = row.title || this.ServiceGoodsList.filter(item => this.ids.includes(item.id)).map(item => item.title).join('、');
return delServiceGoods(ids)
this.$modal.confirm('是否确认删除服务内容"' + names + '"').then(function() {
return delServiceGoods(ids);
}).then(() => { }).then(() => {
this.getList() this.getList();
this.$modal.msgSuccess("删除成功") this.$modal.msgSuccess("删除成功");
}).catch(() => {}) }).catch((error) => {
console.error('删除失败:', error);
this.$modal.msgError("删除失败,请稍后重试");
});
}, },
getserviceCateList(){ getserviceCateList(){
selectServiceCateList().then(response => { selectServiceCateList().then(response => {
@ -710,20 +1016,43 @@ export default {
}, },
async saveEditField() { async saveEditField() {
if (!this.editRow) return; if (!this.editRow) return;
const value = ['price', 'sales', 'stock', 'sort'].includes(this.editField)
? Number(this.editFieldValue) //
: this.editFieldValue; let value = this.editFieldValue;
if (['price', 'margin'].includes(this.editField)) {
//
const numValue = parseFloat(value);
if (isNaN(numValue) || numValue < 0) {
this.$message.error('请输入有效的价格');
return;
}
value = numValue;
} else if (['sales', 'stock', 'sort'].includes(this.editField)) {
//
const numValue = parseInt(value);
if (isNaN(numValue) || numValue < 0) {
this.$message.error('请输入有效的数字');
return;
}
value = numValue;
}
const payload = { const payload = {
id: this.editRow.id, id: this.editRow.id,
[this.editField]: value [this.editField]: value
}; };
try { try {
await updateServiceGoods(payload); await updateServiceGoods(payload);
this.$message.success('修改成功'); this.$message.success('修改成功');
this.editDialogVisible = false; //
this.editRow[this.editField] = value;
this.getList(); this.getList();
} catch (e) { //
this.$message.error('修改失败'); this.editDialogVisible = false;
} catch (error) {
console.error('快捷编辑失败:', error);
this.$message.error('修改失败,请稍后重试');
} }
}, },
addSpec() { addSpec() {
@ -775,6 +1104,31 @@ export default {
console.log('技能列表长度:', this.siteSkillList.length); console.log('技能列表长度:', this.siteSkillList.length);
}, 5000); }, 5000);
}, },
// JSON
testJsonConversion() {
console.log('=== 测试JSON格式转换 ===');
// JSON
const testSkillIds = '["30", "32", "45"]';
console.log('测试数据:', testSkillIds);
try {
const parsed = JSON.parse(testSkillIds);
console.log('解析结果:', parsed);
console.log('是否为数组:', Array.isArray(parsed));
const converted = parsed.map(id => parseInt(id)).filter(id => !isNaN(id));
console.log('转换后的数组:', converted);
//
const backToJson = JSON.stringify(converted.map(id => id.toString()));
console.log('转换回JSON:', backToJson);
} catch (e) {
console.error('转换测试失败:', e);
}
},
// //
handleSkuTypeChange(value) { handleSkuTypeChange(value) {
console.log('规格类型变化:', value); console.log('规格类型变化:', value);
@ -795,24 +1149,178 @@ export default {
// //
handleSkillChange(value) { handleSkillChange(value) {
console.log('技能选择变化:', value); console.log('技能选择变化:', value);
this.form.skillIdsArray = value; // value
const skillArray = Array.isArray(value) ? value : [];
this.$set(this.form, 'skillIdsArray', skillArray);
console.log('设置后的技能数组:', this.form.skillIdsArray);
}, },
//
getSelectedSkillNames() {
if (!this.form.skillIdsArray || !this.form.skillIdsArray.length || !this.siteSkillList.length) {
return '无';
}
const selectedSkills = this.form.skillIdsArray.map(id => {
const skill = this.siteSkillList.find(s => s.id === parseInt(id));
return skill ? skill.title : `ID:${id}`;
}).filter(name => name);
return selectedSkills.join(', ');
},
//
toggleBasicTag(tag) {
// basicArray
if (!this.form.basicArray) {
this.$set(this.form, 'basicArray', []);
}
const index = this.form.basicArray.indexOf(tag);
if (index > -1) {
//
this.form.basicArray.splice(index, 1);
} else {
//
this.form.basicArray.push(tag);
}
console.log('基检现象选择变化:', this.form.basicArray);
},
//
removeBasicTag(tag) {
// basicArray
if (!this.form.basicArray) {
this.$set(this.form, 'basicArray', []);
return;
}
const index = this.form.basicArray.indexOf(tag);
if (index > -1) {
this.form.basicArray.splice(index, 1);
console.log('删除基检现象标签:', tag, '剩余:', this.form.basicArray);
}
},
//
showBasicInputBox() {
this.showBasicInput = true;
this.newBasicTag = '';
this.$nextTick(() => {
if (this.$refs.basicInput) {
this.$refs.basicInput.focus();
}
});
},
//
handleBasicInputConfirm() {
const tag = this.newBasicTag.trim();
if (tag) {
// basicArray
if (!this.form.basicArray) {
this.$set(this.form, 'basicArray', []);
}
// 便
if (!this.basicOptions.includes(tag)) {
this.basicOptions.push(tag);
}
//
if (!this.form.basicArray.includes(tag)) {
this.form.basicArray.push(tag);
}
}
this.showBasicInput = false;
this.newBasicTag = '';
},
// //
formatSkillNames(skillIds) { formatSkillNames(skillIds) {
if (!skillIds || !this.siteSkillList.length) { if (!skillIds || !this.siteSkillList.length) {
return '-' return '-'
} }
try { try {
const skillIdArray = skillIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id)) let skillIdArray = [];
// JSON ["30", "32", "45"]
if (typeof skillIds === 'string') {
try {
const parsedSkills = JSON.parse(skillIds);
if (Array.isArray(parsedSkills)) {
skillIdArray = parsedSkills.map(id => parseInt(id)).filter(id => !isNaN(id));
} else {
// JSON
skillIdArray = skillIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
}
} catch (jsonError) {
// JSON
skillIdArray = skillIds.split(',').map(id => parseInt(id.trim())).filter(id => !isNaN(id));
}
} else if (Array.isArray(skillIds)) {
skillIdArray = skillIds.map(id => parseInt(id)).filter(id => !isNaN(id));
}
const skillNames = skillIdArray.map(id => { const skillNames = skillIdArray.map(id => {
const skill = this.siteSkillList.find(s => s.id === id) const skill = this.siteSkillList.find(s => s.id === id)
return skill ? skill.title : id return skill ? skill.title : `ID:${id}`
}).filter(name => name) }).filter(name => name)
return skillNames.length > 0 ? skillNames.join(', ') : '-' return skillNames.length > 0 ? skillNames.join(', ') : '-'
} catch (e) { } catch (e) {
console.warn('格式化技能名称失败:', e, skillIds);
return '-'
}
},
//
formatBasicNames(basic) {
if (!basic || typeof basic !== 'string') {
return '-'
}
try {
let basicArray = [];
// JSON ["", "", ""]
try {
const parsedBasic = JSON.parse(basic);
if (Array.isArray(parsedBasic)) {
basicArray = parsedBasic.filter(item => item && item.trim());
} else {
// JSON
basicArray = basic.split(',').map(item => item.trim()).filter(item => item);
}
} catch (jsonError) {
// JSON
basicArray = basic.split(',').map(item => item.trim()).filter(item => item);
}
return basicArray.length > 0 ? basicArray.join(', ') : '-'
} catch (e) {
console.warn('格式化基检现象失败:', e)
return '-' return '-'
} }
}, },
} }
} }
</script> </script>
<style scoped>
.basic-tags-container {
background-color: #fafafa;
transition: border-color 0.2s cubic-bezier(0.645, 0.045, 0.355, 1);
}
.basic-tags-container:hover {
border-color: #c0c4cc;
}
.basic-tags-container:focus-within {
border-color: #409eff;
}
.basic-tags-container .el-tag {
transition: all 0.2s;
}
.basic-tags-container .el-tag:hover {
transform: scale(1.05);
}
</style>