pull/1121/head
梁宇奇 2025-07-29 11:46:18 +08:00
parent 7baf13de1d
commit 397f161dc1
6 changed files with 297 additions and 4 deletions

View File

@ -1,9 +1,12 @@
package com.ruoyi.web.controller.common;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.aliyun.oss.common.auth.Credentials;
import com.aliyun.oss.common.utils.BinaryUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.constant.CacheConstants;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.redis.RedisCache;
@ -13,17 +16,25 @@ import com.ruoyi.framework.ocr.BusinessLicense;
import com.ruoyi.framework.ocr.IdCard;
import com.ruoyi.framework.ocr.OcrClientWrapper;
import com.ruoyi.framework.oss.OSSClientWrapper;
import com.ruoyi.web.controller.common.vo.BusinessLicenseVO;
import com.ruoyi.web.controller.common.vo.IdCardVO;
import com.ruoyi.web.controller.common.vo.STSCredentialsVO;
import com.ruoyi.web.controller.common.vo.UrlVO;
import com.ruoyi.web.controller.common.vo.*;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.binary.Base64;
import org.codehaus.jettison.json.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
@ -36,6 +47,11 @@ import java.util.concurrent.TimeUnit;
@RestController
@RequestMapping("/rest/v1/common")
public class CommonController {
@Value("${oss.fileExpireTime:3600}")
private Long fileExpireTime;//指定过期时间,单位为秒。
@Value("${oss.callbackUrl:http://121.40.117.244:9310/rest/v1/oss-callback/upload}")
private String callbackUrl;//上传回调URL
@Autowired
private OSSClientWrapper ossClient;
@Autowired
@ -74,6 +90,104 @@ public class CommonController {
return R.ok(vo);
}
@ApiOperation("获取OSS上传签名")
@PostMapping("/oss/signature4upload")
public R<OSSUploadSignRespVO> signature4upload(@Validated @RequestBody OSSUploadSignReqVO vo) {
String accessKeyId = vo.getAccessKeyId();
String accessKeySecret = vo.getAccessKeySecret();
String securityToken = vo.getSecurityToken();
String region = ossProperties.getRegionId();
String bucket = vo.getBucket();
String uploadDir = StrUtil.emptyIfNull(vo.getUploadDir());
try {
//获取x-oss-credential里的date当前日期格式为yyyyMMdd
ZonedDateTime today = ZonedDateTime.now().withZoneSameInstant(java.time.ZoneOffset.UTC);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
String date = today.format(formatter);
//获取x-oss-date
ZonedDateTime now = ZonedDateTime.now().withZoneSameInstant(java.time.ZoneOffset.UTC);
DateTimeFormatter formatter2 = DateTimeFormatter.ofPattern("yyyyMMdd'T'HHmmss'Z'");
String x_oss_date = now.format(formatter2);
// 步骤1创建policy。
String x_oss_credential = accessKeyId + "/" + date + "/" + region + "/oss/aliyun_v4_request";
ObjectMapper mapper = new ObjectMapper();
Map<String, Object> policy = new HashMap<>(2);
policy.put("expiration", generateExpiration(fileExpireTime));
List<Object> conditions = new ArrayList<>(8);
Map<String, String> bucketCondition = new HashMap<>(1);
bucketCondition.put("bucket", bucket);
conditions.add(bucketCondition);
Map<String, String> securityTokenCondition = new HashMap<>(1);
securityTokenCondition.put("x-oss-security-token", securityToken);
conditions.add(securityTokenCondition);
Map<String, String> signatureVersionCondition = new HashMap<>(1);
signatureVersionCondition.put("x-oss-signature-version", "OSS4-HMAC-SHA256");
conditions.add(signatureVersionCondition);
Map<String, String> credentialCondition = new HashMap<>(1);
credentialCondition.put("x-oss-credential", x_oss_credential); // 替换为实际的 access key id
conditions.add(credentialCondition);
Map<String, String> dateCondition = new HashMap<>(1);
dateCondition.put("x-oss-date", x_oss_date);
conditions.add(dateCondition);
conditions.add(Arrays.asList("content-length-range", 1, 10240000));
conditions.add(Arrays.asList("eq", "$success_action_status", "200"));
conditions.add(Arrays.asList("starts-with", "$key", uploadDir));
policy.put("conditions", conditions);
String jsonPolicy = mapper.writeValueAsString(policy);
// 步骤2构造待签名字符串StringToSign
String stringToSign = new String(Base64.encodeBase64(jsonPolicy.getBytes()));
// 步骤3计算SigningKey。
byte[] dateKey = hmacsha256(("aliyun_v4" + accessKeySecret).getBytes(), date);
byte[] dateRegionKey = hmacsha256(dateKey, region);
byte[] dateRegionServiceKey = hmacsha256(dateRegionKey, "oss");
byte[] signingKey = hmacsha256(dateRegionServiceKey, "aliyun_v4_request");
// 步骤4计算Signature。
byte[] result = hmacsha256(signingKey, stringToSign);
String signature = BinaryUtil.toHex(result);
// 步骤5设置回调。
JSONObject jasonCallback = new JSONObject();
jasonCallback.put("callbackUrl", callbackUrl);
jasonCallback.put("callbackBody", "filename=${object}&size=${size}&mimeType=${mimeType}&height=${imageInfo.height}&width=${imageInfo.width}");
jasonCallback.put("callbackBodyType", "application/x-www-form-urlencoded");
String base64CallbackBody = BinaryUtil.toBase64String(jasonCallback.toString().getBytes());
OSSUploadSignRespVO response = new OSSUploadSignRespVO();
// 将数据添加到 map 中
response.setVersion("OSS4-HMAC-SHA256");
// 这里是易错点不能直接传policy需要做一下Base64编码
response.setPolicy(stringToSign);
response.setX_oss_credential(x_oss_credential);
response.setX_oss_date(x_oss_date);
response.setSignature(signature);
response.setSecurity_token(securityToken);
response.setDir(uploadDir);
response.setHost(generateHost(bucket, ossProperties.isHttps()));
response.setCallback(base64CallbackBody);
// 返回带有状态码 200 (OK) 的 ResponseEntity返回给Web端进行PostObject操作
return R.ok(response);
} catch (Exception e) {
log.error("生成OSS签名异常", e);
}
return R.fail();
}
@ApiOperation("身份证OCR")
@PostMapping("/ocr/idCard")
public R<IdCardVO> recognizeIdCard(@Validated @RequestBody UrlVO vo) {
@ -104,4 +218,49 @@ public class CommonController {
return R.ok(BeanUtil.toBean(dto, BusinessLicenseVO.class));
}
/**
*
*
* @param seconds
* @return ISO8601 "2014-12-01T12:00:00.000Z"
*/
private String generateExpiration(long seconds) {
// 获取当前时间戳(以秒为单位)
long now = Instant.now().getEpochSecond();
// 计算过期时间的时间戳
long expirationTime = now + seconds;
// 将时间戳转换为Instant对象并格式化为ISO8601格式
Instant instant = Instant.ofEpochSecond(expirationTime);
// 定义时区为UTC
ZoneId zone = ZoneOffset.UTC;
// 将 Instant 转换为 ZonedDateTime
ZonedDateTime zonedDateTime = instant.atZone(zone);
// 定义日期时间格式例如2023-12-03T13:00:00.000Z
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'");
// 格式化日期时间
String formattedDate = zonedDateTime.format(formatter);
// 输出结果
return formattedDate;
}
private byte[] hmacsha256(byte[] key, String data) {
try {
// 初始化HMAC密钥规格指定算法为HMAC-SHA256并使用提供的密钥。
SecretKeySpec secretKeySpec = new SecretKeySpec(key, "HmacSHA256");
// 获取Mac实例并通过getInstance方法指定使用HMAC-SHA256算法。
Mac mac = Mac.getInstance("HmacSHA256");
// 使用密钥初始化Mac对象。
mac.init(secretKeySpec);
// 执行HMAC计算通过doFinal方法接收需要计算的数据并返回计算结果的数组。
return mac.doFinal(data.getBytes());
} catch (Exception e) {
throw new RuntimeException("Failed to calculate HMAC-SHA256", e);
}
}
private String generateHost(String bucket, Boolean isHttps) {
String url = "http{0}://{1}.{2}";
return StrUtil.indexedFormat(url, BooleanUtil.isTrue(isHttps) ? "s" : "", bucket, ossProperties.getEndPoint());
}
}

View File

@ -0,0 +1,36 @@
package com.ruoyi.web.controller.common.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import javax.validation.constraints.NotEmpty;
/**
* @author liangyq
* @date 2025-07-29
*/
@ApiModel
@Data
public class OSSUploadSignReqVO {
@NotEmpty(message = "accessKeyId不能为空")
@ApiModelProperty(value = "STS.accessKeyId")
private String accessKeyId;
@NotEmpty(message = "accessKeySecret不能为空")
@ApiModelProperty(value = "STS.accessKeySecret")
private String accessKeySecret;
@NotEmpty(message = "securityToken不能为空")
@ApiModelProperty(value = "STS.securityToken")
private String securityToken;
@NotEmpty(message = "桶不能为空")
@ApiModelProperty(value = "bucket")
private String bucket;
@ApiModelProperty(value = "上传到指定文件夹")
private String uploadDir;
}

View File

@ -0,0 +1,42 @@
package com.ruoyi.web.controller.common.vo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @author liangyq
* @date 2025-07-29
*/
@ApiModel
@Data
public class OSSUploadSignRespVO {
@ApiModelProperty(value = "version")
private String version;
@ApiModelProperty(value = "policy")
private String policy;
@ApiModelProperty(value = "x_oss_credential")
private String x_oss_credential;
@ApiModelProperty(value = "x_oss_date")
private String x_oss_date;
@ApiModelProperty(value = "signature")
private String signature;
@ApiModelProperty(value = "security_token")
private String security_token;
@ApiModelProperty(value = "dir")
private String dir;
@ApiModelProperty(value = "host")
private String host;
@ApiModelProperty(value = "callback")
private String callback;
}

View File

@ -0,0 +1,30 @@
package com.ruoyi.web.controller.xkt;
import com.ruoyi.common.core.controller.XktBaseController;
import com.ruoyi.common.core.domain.R;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
/**
* @author liangyq
* @date 2025-07-29
*/
@Api(tags = "OSS回调接口")
@RestController
@RequestMapping("/rest/v1/oss-callback")
public class OSSCallbackController extends XktBaseController {
@RequestMapping("/upload")
public R callback(HttpServletRequest servletRequest) {
String filename = servletRequest.getParameter("filename");
String size = servletRequest.getParameter("size");
String mimeType = servletRequest.getParameter("mimeType");
String height = servletRequest.getParameter("imageInfo.height");
String width = servletRequest.getParameter("imageInfo.width");
logger.info("OSS回调: {}, {}, {}, {}, {}", filename, size, mimeType, height, width);
return success();
}
}

View File

@ -43,4 +43,29 @@ public class AlipayReqDTO {
*/
@JsonProperty("time_expire")
private String timeExpire;
/**
* https://opendocs.alipay.com/open/common/wifww7
*
* balance
* moneyFund
* bankPay
* debitCardExpress
* creditCardExpress
* creditCardCartoon
* creditCard
* cartoon
* pcredit
* pcreditpayInstallment
* , credit_group
* coupon
* point
* + promotion
* voucher
* mdiscount
* honeyPay
* mcard
* pcard
*/
@JsonProperty("enable_pay_channels")
private String enablePayChannels;
}

View File

@ -118,6 +118,7 @@ public class AliPaymentMangerImpl implements PaymentManager, InitializingBean {
reqDTO.setOutTradeNo(tradeNo);
reqDTO.setTotalAmount(amount.toPlainString());
reqDTO.setSubject(subject);
reqDTO.setEnablePayChannels("balance,moneyFund,bankPay,debitCardExpress");
reqDTO.setTimeExpire(DateUtil.formatDateTime(expireTime));
switch (payPage) {