diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java index 2430af4f6..7c0b956d6 100644 --- a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/CommonController.java @@ -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 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 policy = new HashMap<>(2); + policy.put("expiration", generateExpiration(fileExpireTime)); + + List conditions = new ArrayList<>(8); + + Map bucketCondition = new HashMap<>(1); + bucketCondition.put("bucket", bucket); + conditions.add(bucketCondition); + + Map securityTokenCondition = new HashMap<>(1); + securityTokenCondition.put("x-oss-security-token", securityToken); + conditions.add(securityTokenCondition); + + Map signatureVersionCondition = new HashMap<>(1); + signatureVersionCondition.put("x-oss-signature-version", "OSS4-HMAC-SHA256"); + conditions.add(signatureVersionCondition); + + Map credentialCondition = new HashMap<>(1); + credentialCondition.put("x-oss-credential", x_oss_credential); // 替换为实际的 access key id + conditions.add(credentialCondition); + + Map 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 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()); + } + } diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/vo/OSSUploadSignReqVO.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/vo/OSSUploadSignReqVO.java new file mode 100644 index 000000000..b70319e5f --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/vo/OSSUploadSignReqVO.java @@ -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; + +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/vo/OSSUploadSignRespVO.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/vo/OSSUploadSignRespVO.java new file mode 100644 index 000000000..a93eddb51 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/common/vo/OSSUploadSignRespVO.java @@ -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; + +} diff --git a/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/OSSCallbackController.java b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/OSSCallbackController.java new file mode 100644 index 000000000..c2aea6595 --- /dev/null +++ b/ruoyi-admin/src/main/java/com/ruoyi/web/controller/xkt/OSSCallbackController.java @@ -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(); + } +} diff --git a/xkt/src/main/java/com/ruoyi/xkt/dto/finance/AlipayReqDTO.java b/xkt/src/main/java/com/ruoyi/xkt/dto/finance/AlipayReqDTO.java index f6a4ef51a..2d444f44e 100644 --- a/xkt/src/main/java/com/ruoyi/xkt/dto/finance/AlipayReqDTO.java +++ b/xkt/src/main/java/com/ruoyi/xkt/dto/finance/AlipayReqDTO.java @@ -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; } diff --git a/xkt/src/main/java/com/ruoyi/xkt/manager/impl/AliPaymentMangerImpl.java b/xkt/src/main/java/com/ruoyi/xkt/manager/impl/AliPaymentMangerImpl.java index 6103c0b26..403dc80ca 100644 --- a/xkt/src/main/java/com/ruoyi/xkt/manager/impl/AliPaymentMangerImpl.java +++ b/xkt/src/main/java/com/ruoyi/xkt/manager/impl/AliPaymentMangerImpl.java @@ -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) {