first commit

This commit is contained in:
2026-07-13 08:57:05 +08:00
commit 3ecbd0957b
2536 changed files with 200576 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
# Ss-Component 模块
主要实现各种第三方Spring组件服务。
+70
View File
@@ -0,0 +1,70 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.zxdmy.excite</groupId>
<artifactId>shensong</artifactId>
<version>2.0.0</version>
</parent>
<artifactId>ss-component</artifactId>
<version>2.0.0</version>
<name>ss-component</name>
<description>ss-component</description>
<dependencies>
<!-- 加载Common依赖,一些常用的工具包 -->
<dependency>
<groupId>com.zxdmy.excite</groupId>
<artifactId>ss-common</artifactId>
<version>2.0.0</version>
</dependency>
<!-- 七牛云依赖 -->
<!-- https://mvnrepository.com/artifact/com.qiniu/qiniu-java-sdk -->
<dependency>
<groupId>com.qiniu</groupId>
<artifactId>qiniu-java-sdk</artifactId>
<version>7.9.3</version>
</dependency>
<!-- 阿里云短信服务依赖 -->
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>dysmsapi20170525</artifactId>
<version>2.0.8</version>
</dependency>
<!-- 阿里云OSS服务依赖 -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.13.2</version>
</dependency>
<!-- 第三方登录依赖 -->
<dependency>
<groupId>me.zhyd.oauth</groupId>
<artifactId>JustAuth</artifactId>
<version>1.16.5</version>
</dependency>
<!-- 支付宝支付依赖 -->
<dependency>
<groupId>com.alipay.sdk</groupId>
<artifactId>alipay-easysdk</artifactId>
<version>2.2.1</version>
</dependency>
<!-- 微信支付依赖 -->
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-pay</artifactId>
<version>4.2.2.B</version>
</dependency>
</dependencies>
</project>
@@ -0,0 +1,362 @@
package com.zxdmy.excite.component.alipay;
import cn.hutool.core.util.IdUtil;
import com.alipay.easysdk.factory.Factory;
import com.alipay.easysdk.kernel.Config;
import com.alipay.easysdk.kernel.util.ResponseChecker;
import com.alipay.easysdk.payment.app.models.AlipayTradeAppPayResponse;
import com.alipay.easysdk.payment.common.models.AlipayDataDataserviceBillDownloadurlQueryResponse;
import com.alipay.easysdk.payment.common.models.AlipayTradeFastpayRefundQueryResponse;
import com.alipay.easysdk.payment.common.models.AlipayTradeQueryResponse;
import com.alipay.easysdk.payment.common.models.AlipayTradeRefundResponse;
import com.alipay.easysdk.payment.facetoface.models.AlipayTradePrecreateResponse;
import com.alipay.easysdk.payment.page.models.AlipayTradePagePayResponse;
import com.alipay.easysdk.payment.wap.models.AlipayTradeWapPayResponse;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.zxdmy.excite.common.exception.ServiceException;
import com.zxdmy.excite.common.service.IGlobalConfigService;
import com.zxdmy.excite.component.bo.AlipayBO;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 支付宝支付服务实现类
* https://github.com/alipay/alipay-easysdk/tree/master/java
*
* @author 沈松
* @since 2022-01-02 0002 23:31
*/
@Service
@AllArgsConstructor
public class AlipayService {
private IGlobalConfigService configService;
private static final String DEFAULT_SERVICE = "alipay";
private static final String DEFAULT_KEY = "alipay";
/**
* 保存信息至数据库
*
* @param alipayBO 支付宝支付培配置信息
* @return 保存结果:T|F
*/
public boolean saveConfig(AlipayBO alipayBO) {
// 必填信息不能为空
if (null == alipayBO.getAppId() || null == alipayBO.getMerchantPrivateKey()) {
throw new ServiceException("APPID和应用私钥不能为空,请检查!");
}
// 如果支付宝公钥为空,则三个证书不能为空
if (null == alipayBO.getAlipayPublicKey())
if (null == alipayBO.getMerchantCertPath() || null == alipayBO.getAlipayCertPath() || null == alipayBO.getAlipayRootCertPath())
throw new ServiceException("证书模式下,[应用公钥证书][支付宝公钥证书][支付宝根证书]不能为空,请检查!");
// 保存信息至数据库
try {
return configService.save(DEFAULT_SERVICE, alipayBO.getKey() != null ? alipayBO.getKey() : DEFAULT_KEY, alipayBO, true);
} catch (JsonProcessingException e) {
throw new ServiceException(e.getMessage());
}
}
/**
* 从数据库读取配置信息(指定KEY)
*
* @param confKey 本条支付信息的KEY,如果为null,读取默认KEY
* @return 支付宝支付配置信息
*/
public AlipayBO getConfig(String confKey) {
if (null == confKey) {
return this.getConfig();
}
return (AlipayBO) configService.get(DEFAULT_SERVICE, confKey, new AlipayBO());
}
/**
* 从数据库读取配置信息(默认KEY)
*
* @return 支付宝支付配置信息
*/
public AlipayBO getConfig() {
return this.getConfig(DEFAULT_KEY);
}
/**
* 统一支付服务接口(当面付、电脑网站支付、手机网站支付、APP支付)
*
* @param confKey 支付宝配置信息的KEY。如果加载默认的KEY,输入null
* @param payType 支付类型:当面付(faceToFace),电脑网站支付(page),手机网站支付(wap),APP支付(app)
* @param subject 商品名称
* @param outTradeNo 商户订单号:商户内唯一
* @param totalAmount 总金额(单位:元),实例:12.34
* @param returnUrl 支付成功后跳转页面(只针对网站支付有效)
* @param quitUrl 支付取消跳转页面(只针对手机网站支付有效)
* @return 返回结果:当面付为二维码链接,其他均为网站Body代码
*/
public String pay(String confKey, String payType, String subject, String outTradeNo, String totalAmount, String returnUrl, String quitUrl) {
// 必填信息不能为空
if (null == subject || null == outTradeNo || null == totalAmount) {
throw new ServiceException("商品名称、商户订单号、商品价格不能为空!");
}
// 1. 设置参数
Factory.setOptions(this.getOptions(confKey));
try {
// 2. 调用API发起创建支付
switch (payType.toLowerCase()) {
// 当面付
case "facetoface":
// 创建订单
AlipayTradePrecreateResponse response = Factory.Payment.FaceToFace().preCreate(subject, outTradeNo, totalAmount);
// 成功?
if (ResponseChecker.success(response))
return response.getQrCode();
break;
// 电脑网站支付
case "page":
if (null == returnUrl) {
throw new ServiceException("电脑网站支付的跳转地址(returnUrl)不能为空!");
}
// 调用接口
AlipayTradePagePayResponse response1 = Factory.Payment.Page().pay(subject, outTradeNo, totalAmount, returnUrl);
// 成功?
if (ResponseChecker.success(response1))
return response1.getBody();
break;
// 手机网站支付
case "wap":
if (null == returnUrl || null == quitUrl) {
throw new ServiceException("手机网站支付的失败退出地址(quitUrl)、成功跳转地址(returnUrl)不能为空!");
}
AlipayTradeWapPayResponse response2 = Factory.Payment.Wap().pay(subject, outTradeNo, totalAmount, quitUrl, returnUrl);
// 成功
if (ResponseChecker.success(response2))
return response2.getBody();
break;
// APP支付
case "app":
AlipayTradeAppPayResponse response3 = Factory.Payment.App().pay(subject, outTradeNo, totalAmount);
// 成功
if (ResponseChecker.success(response3))
return response3.getBody();
break;
// 其他类型:错误
default:
throw new ServiceException("输入的支付类型[" + payType + "]不在许可支付类型范围内。许可类型:当面付(faceToFace),电脑网站支付(page),手机网站支付(wap),APP支付(app)");
}
} catch (Exception e) {
System.err.println("调用遭遇异常,原因:" + e.getMessage());
throw new ServiceException(e.getMessage());
}
return null;
}
/**
* 查询支付订单接口
*
* @param confKey 可选:支付宝配置信息的KEY。如果加载默认的KEY,输入null
* @param tradeNo 特殊可选:支付宝交易号(订单号)
* @param outTradeNo 特殊可选:商家订单号
* @return 查询成功:{ 0:Y,1:支付宝交易号,2:商家订单号,3:交易状态,4:订单金额,5:买家ID,6:买家支付宝账号 } <br> 查询失败:{ E,错误代码,错误描述 }
* @apiNote tradeNo 和 outTradeNo 不能同时为空。同时存在优先取 tradeNo。
*/
public String[] queryPay(String confKey, String tradeNo, String outTradeNo) {
// 判断
if (null == tradeNo && null == outTradeNo) {
throw new ServiceException("tradeNo 和 outTradeNo 不能同时为空!");
}
// 设置参数
Factory.setOptions(this.getOptions(confKey));
try {
// 执行查询
AlipayTradeQueryResponse response = Factory.Payment.Common().optional("trade_no", tradeNo).query(outTradeNo);
// 请求成功(即返回信息中没有sub_code)
if (ResponseChecker.success(response)) {
return new String[]{
"Y",
response.tradeNo,
response.outTradeNo,
response.tradeStatus,
response.totalAmount,
response.buyerUserId,
response.buyerLogonId
};
} else {
return new String[]{"E", response.subCode, response.subMsg};
}
} catch (Exception e) {
System.err.println("调用遭遇异常,原因:" + e.getMessage());
throw new ServiceException(e.getMessage());
}
}
/**
* 退款接口(支持部分退款)
*
* @param confKey 可选:支付宝配置信息的KEY。如果加载默认的KEY,输入null
* @param tradeNo 特殊可选:商户订单号
* @param outTradeNo 特殊可选:商户订单号
* @param refundAmount 必填:退款金额
* @param reason 可选:退款原因
* @return 本次请求退款成功:{ 0:Y,1:支付宝交易号,2:商家订单号,3:退款请求号,4:总退款金额 } <br>
* 历史请求退款成功:{ 0:N,1:支付宝交易号,2:商家订单号,3:退款请求号,4:退款金额 } <br>
* 退款发生错误:{ 0:E,1:错误代码,2:错误描述 }
* @apiNote tradeNo 和 outTradeNo 不能同时为空。同时存在优先取 tradeNo。
*/
public String[] refund(String confKey, String tradeNo, String outTradeNo, String refundAmount, String reason) {
// 判断
if (null == tradeNo && null == outTradeNo) {
throw new ServiceException("tradeNo 和 outTradeNo 不能同时为空!");
}
// 设置参数
Factory.setOptions(this.getOptions(confKey));
try {
// 生成唯一的款请求号
String outRequestNo = IdUtil.simpleUUID();
// 发起请求
AlipayTradeRefundResponse response = Factory.Payment.Common()
// 支付宝交易号
.optional("trade_no", tradeNo)
// 退款原因
.optional("refund_reason", reason)
// 退款请求号
.optional("out_request_no", outRequestNo)
// 执行退款
.refund(outTradeNo, refundAmount);
// 如果请求成功(即返回信息中没有sub_code)
if (ResponseChecker.success(response)) {
return new String[]{
// 本次请求退款状态(即资金有改变,详情:https://opensupport.alipay.com/support/knowledge/27585/201602348776
response.fundChange,
response.tradeNo,
response.outTradeNo,
outRequestNo,
response.refundFee
};
} else {
return new String[]{"E", response.subCode, response.subMsg};
}
} catch (Exception e) {
System.err.println("调用遭遇异常,原因:" + e.getMessage());
throw new ServiceException(e.getMessage());
}
}
/**
* 查询退款
*
* @param confKey 可选:支付宝配置信息的KEY。如果加载默认的KEY,输入null
* @param outTradeNo 必填:商户订单号
* @param outRequestNo 必填:退款请求号
* @return 已退款:{ Y,支付宝交易号,商家订单号,退款请求号,订单金额,退款金额,退款原因 } <br> 未退款:{ N,描述 } <br> 发生错误:{ E,错误代码,错误描述 }
*/
public String[] queryRefund(String confKey, String outTradeNo, String outRequestNo) {
// 设置参数
Factory.setOptions(this.getOptions(confKey));
// 如果请求号为空,则表示全额退款,设置请求号位商家订单号
if (null == outRequestNo) {
outRequestNo = outTradeNo;
}
try {
// 发起请求
AlipayTradeFastpayRefundQueryResponse response = Factory.Payment.Common().queryRefund(outTradeNo, outRequestNo);
// 如果请求成功(即返回信息中没有sub_code)
if (ResponseChecker.success(response)) {
// 如果该接口返回了查询数据,则代表退款成功(详情:https://opensupport.alipay.com/support/knowledge/27585/201602348776
if (null != response.refundAmount) {
return new String[]{
"Y",
response.tradeNo,
response.outTradeNo,
response.outRequestNo,
response.totalAmount,
response.refundAmount,
response.refundReason
};
} else {
return new String[]{
"N",
"该订单未退款或输入的退款请求号有误,请检查!"
};
}
} else {
return new String[]{"E", response.subCode, response.subMsg};
}
} catch (Exception e) {
System.err.println("调用遭遇异常,原因:" + e.getMessage());
throw new ServiceException(e.getMessage());
}
}
/**
* 下载对账单(不能查询当天或当月的)
* https://developer.aliyun.com/article/710922
*
* @param confKey 可选:支付宝配置信息的KEY。如果加载默认的KEY,输入null
* @param date 必填:交易的具体日期(如2022-01-01)或月份(2021-12
* @return 获取成功:{Y,下载URL} <br> 发生错误:{ E,错误代码,错误描述 }
*/
public String[] downloadBill(String confKey, String date) {
// 设置参数
Factory.setOptions(this.getOptions(confKey));
try {
// 发送请求
AlipayDataDataserviceBillDownloadurlQueryResponse response = Factory.Payment.Common().downloadBill("trade", date);
// 如果请求成功(即返回信息中没有sub_code)
if (ResponseChecker.success(response)) {
return new String[]{
"Y",
response.billDownloadUrl
};
} else {
return new String[]{"E", response.subCode, response.subMsg};
}
} catch (Exception e) {
System.err.println("调用遭遇异常,原因:" + e.getMessage());
throw new ServiceException(e.getMessage());
}
}
/**
* 读取配置信息
*
* @param confKey 支付宝配置信息的KEY。如果加载默认的KEY,输入null
* @return 配置信息
*/
private Config getOptions(String confKey) {
// 从数据库中读取配置信息
AlipayBO alipayBO = this.getConfig(confKey);
// 将配置信息写入支付宝支付配置类中
Config config = new Config();
config.protocol = alipayBO.getProtocol();
config.gatewayHost = alipayBO.getGatewayHost();
config.signType = alipayBO.getSignType();
config.appId = alipayBO.getAppId();
// 为避免私钥随源码泄露,推荐从文件中读取私钥字符串而不是写入源码中
config.merchantPrivateKey = alipayBO.getMerchantPrivateKey();
// 如果三个证书不为空,则优先设置三个证书
if (null != alipayBO.getMerchantCertPath() && null != alipayBO.getAlipayCertPath() && null != alipayBO.getAlipayRootCertPath()) {
//注:证书文件路径支持设置为文件系统中的路径或CLASS_PATH中的路径,优先从文件系统中加载,加载失败后会继续尝试从CLASS_PATH中加载
config.merchantCertPath = alipayBO.getMerchantCertPath();
config.alipayCertPath = alipayBO.getAlipayCertPath();
config.alipayRootCertPath = alipayBO.getAlipayRootCertPath();
} else if (null != alipayBO.getAlipayPublicKey()) {
//注:如果采用非证书模式,则无需赋值上面的三个证书路径,改为赋值如下的支付宝公钥字符串即可
config.alipayPublicKey = alipayBO.getAlipayPublicKey();
} else {
throw new ServiceException("配置信息有误:证书模式配置不完整或未配置公钥模式,请检查!");
}
//可设置异步通知接收服务地址(可选)
config.notifyUrl = alipayBO.getNotifyUrl();
//可设置AES密钥,调用AES加解密相关接口时需要(可选)
config.encryptKey = alipayBO.getEncryptKey();
return config;
}
}
@@ -0,0 +1,127 @@
package com.zxdmy.excite.component.aliyun;
import cn.hutool.core.codec.Base64;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.model.PolicyConditions;
import com.aliyun.oss.model.PutObjectResult;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.zxdmy.excite.common.exception.ServiceException;
import com.zxdmy.excite.common.service.IGlobalConfigService;
import com.zxdmy.excite.component.bo.AliyunOssBO;
import com.zxdmy.excite.component.bo.AliyunSmsBO;
import com.zxdmy.excite.component.vo.AliyunOssPolicyVO;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.Date;
/**
* 阿里云对象存储服务
*
* @author 沈松
* @since 2022/1/5 20:37
*/
@Service
@AllArgsConstructor
public class AliyunOssService {
IGlobalConfigService configService;
private static final String DEFAULT_SERVICE = "aliyunOss";
private static final String DEFAULT_KEY = "aliyunOss";
public boolean saveConfig(AliyunOssBO aliyunOssBO) throws JsonProcessingException {
if (null == aliyunOssBO.getBucketName() || null == aliyunOssBO.getBucketDomain() || null == aliyunOssBO.getEndpoint() || null == aliyunOssBO.getAccessKeyId() || null == aliyunOssBO.getAccessKeySecret()) {
throw new ServiceException("部分参数为空,请检查!");
}
if (null == aliyunOssBO.getKey()) {
return configService.save(DEFAULT_SERVICE, DEFAULT_KEY, aliyunOssBO, true);
} else {
return configService.save(DEFAULT_SERVICE, aliyunOssBO.getKey(), aliyunOssBO, true);
}
}
public AliyunOssBO getConfig(String confKey) {
AliyunOssBO aliyunOssBO = new AliyunOssBO();
return (AliyunOssBO) configService.get(DEFAULT_SERVICE, confKey, aliyunOssBO);
}
public AliyunOssBO getConfig() {
return this.getConfig(DEFAULT_KEY);
}
/**
* 前端直传用的方法
*
* @return
*/
public AliyunOssPolicyVO createUploadToken(String confKey, String fileName, Long expireTime) {
// 读取秘钥等信息
AliyunOssBO aliyunOssBO = this.getConfig(confKey);
// 创建OSSClient实例。
OSS oss = new OSSClientBuilder().build(aliyunOssBO.getEndpoint(), aliyunOssBO.getAccessKeyId(), aliyunOssBO.getAccessKeySecret());
PolicyConditions policyConditions = new PolicyConditions();
// 策略:最大上传文件1G
policyConditions.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expireEndDate = new Date(expireEndTime);
try {
// 生成上传策略
String postPolicy = oss.generatePostPolicy(expireEndDate, policyConditions);
// 生成签名
String postSignature = oss.calculatePostSignature(postPolicy);
// 策略转码
String base64Policy = Base64.encode(postPolicy);
// 构造返回数据
return new AliyunOssPolicyVO()
.setAccessKeyId(aliyunOssBO.getAccessKeyId())
.setPolicy(base64Policy)
.setSignature(postSignature)
.setHost(aliyunOssBO.getBucketDomain())
.setCallback("")
.setKey(fileName);
} catch (Exception e) {
throw new ServiceException(e.getMessage());
} finally {
oss.shutdown();
}
}
/**
* 前端直传用的方法
*
* @return
*/
public AliyunOssPolicyVO createUploadToken(String fileName, Long expireTime) {
return this.createUploadToken(DEFAULT_KEY, fileName, expireTime);
}
public boolean uploadFile(String confKey, File file) {
// 读取配置
AliyunOssBO aliyunOssBO = this.getConfig(confKey);
// 创建OSSClient实例。
OSS oss = new OSSClientBuilder().build(aliyunOssBO.getEndpoint(), aliyunOssBO.getAccessKeyId(), aliyunOssBO.getAccessKeySecret());
try {
PutObjectResult putObjectRequest = oss.putObject(aliyunOssBO.getBucketName(), file.getName(), file);
return putObjectRequest.getResponse().isSuccessful();
} catch (Exception e) {
System.out.println(e.getMessage());
return false;
} finally {
oss.shutdown();
}
}
public boolean uploadFile(File file) {
return this.uploadFile(DEFAULT_KEY, file);
}
}
@@ -0,0 +1,184 @@
package com.zxdmy.excite.component.aliyun;
import com.aliyun.dysmsapi20170525.Client;
import com.aliyun.dysmsapi20170525.models.SendSmsRequest;
import com.aliyun.dysmsapi20170525.models.SendSmsResponse;
import com.aliyun.teaopenapi.models.Config;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.zxdmy.excite.common.exception.ServiceException;
import com.zxdmy.excite.common.service.IGlobalConfigService;
import com.zxdmy.excite.component.bo.AliyunSmsBO;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* 阿里云短信服务(Short Message Service)实现
*
* @author 沈松
* @since 2022-01-02 0002 23:33
*/
@Service
@AllArgsConstructor
public class AliyunSmsService {
IGlobalConfigService configService;
private static final String DEFAULT_SERVICE = "aliyunSms";
/**
* 保存阿里云短信服务的开发者信息、消息模板相关信息
* 注意:同一个系统的消息模板可能有多个,所以
*
* @param aliyunSmsBO 实体信息,其中KEY必填
* @return 保存结果
* @throws JsonProcessingException 异常
*/
public boolean saveAliyunSmsConfig(AliyunSmsBO aliyunSmsBO) throws JsonProcessingException {
// 如果必填项为空,则返回错误信息
if (null == aliyunSmsBO.getKey() || null == aliyunSmsBO.getAccessKeyId() || null == aliyunSmsBO.getAccessKeySecret() || null == aliyunSmsBO.getSignName() || null == aliyunSmsBO.getTemplateCode()) {
throw new ServiceException("部分必填信息为空,请检查!");
}
// 保存
return configService.save(DEFAULT_SERVICE, aliyunSmsBO.getKey(), aliyunSmsBO, true);
}
/**
* 通过配置KEY获取短信配置信息
*
* @param confKey 阿里云短信服务的配置key
* @return 阿里云短信服务实体
*/
public AliyunSmsBO getAliyunSmsConfig(String confKey) {
AliyunSmsBO aliyunSmsBO = new AliyunSmsBO();
return (AliyunSmsBO) configService.get(DEFAULT_SERVICE, confKey, aliyunSmsBO);
}
/**
* 发送短信服务
*
* @param confKey confKey,当前短信模板配置的key
* @param phone 手机号数组
* @param templateValue 消息模板中的参数值
* @return 结果
*/
public boolean sendSmsOne(String confKey, String phone, String[] templateValue) {
// 从数据库读取配置信息
AliyunSmsBO aliyunSmsBO = this.getAliyunSmsConfig(confKey);
if (null == aliyunSmsBO) {
throw new ServiceException("阿里云短信服务配置信息有误/不存在,请核实");
}
// 如果输入的参数个数和配置信息里的参数个数不相等,则返回错误信息
if (templateValue.length != aliyunSmsBO.getTemplateParam().length) {
throw new ServiceException("templateValue个数有误,请核实");
}
// 发起短信并获取发送结果
SendSmsResponse response = this.sendSms(aliyunSmsBO, phone, templateValue, null);
// 结果不为空,进一步判断
if (response != null) {
// 打印结果
// System.out.println(response.body.code); // 发送成功为OK
// System.out.println(response.body.message); // 发送成功为OK
// System.out.println(response.body.requestId); // 请求ID
// System.out.println(response.body.bizId); // 请求ID
return "OK".equals(response.body.code);
}
return false;
}
/**
* 批量发送短信
* 其中官网中有批量发送短信的接口SendBatchSms。
* 但我们这里还是逐个发送
*
* @param confKey 短信模板的key
* @param smsMap 短信map,其中key为手机号,value为String[]数组,其大小与消息模板参数个数一致,并依次对应
* @return 结果map
*/
public Map<String, Boolean> sendSmsBatch(String confKey, Map<String, String[]> smsMap) {
// 从数据库读取配置信息
AliyunSmsBO aliyunSmsBO = this.getAliyunSmsConfig(confKey);
if (null == aliyunSmsBO) {
throw new ServiceException("阿里云短信服务配置信息有误,请核实");
}
int templateLength = aliyunSmsBO.getTemplateParam().length;
// 发起短信并获取发送结果
SendSmsResponse response;
// 记录结果的map
Map<String, Boolean> result = new HashMap<>();
// 遍历map,逐个构造并发送短信
for (String phone : smsMap.keySet()) {
// 参数值对等,则可以发送
if (smsMap.get(phone).length == templateLength) {
response = this.sendSms(aliyunSmsBO, phone, smsMap.get(phone), null);
// 发送成功
if (response != null && "OK".equals(response.body.code)) {
result.put(phone, true);
}
// 发送失败
else {
result.put(phone, false);
}
}
// 参数值不对等,直接标记失败
else {
result.put(phone, false);
}
}
return result;
}
/**
* 私有方法:配置并发送短信实现类
*
* @param aliyunSmsBO 配置信息
* @param phone 接收手机号
* @param templateValue 模板JSON格式的参数
* @param outId 流水号
* @return 发送结果
*/
private SendSmsResponse sendSms(AliyunSmsBO aliyunSmsBO, String phone, String[] templateValue, String outId) {
// 阿里云短信服务信息配置
Config config = new Config()
// 必填,开发者的 AccessKey ID
.setAccessKeyId(aliyunSmsBO.getAccessKeyId())
// 必填,开发者的 AccessKey Secret
.setAccessKeySecret(aliyunSmsBO.getAccessKeySecret())
// 默认,暂时不支持多Region,默认即可
.setRegionId("cn-hangzhou")
// 默认,产品域名,开发者无需替换
.setEndpoint("dysmsapi.aliyuncs.com")
// 可选,启用https协议
.setProtocol("https");
// 构造请求参数
StringBuilder templateParam = new StringBuilder("{");
for (int i = 0; i < aliyunSmsBO.getTemplateParam().length; i++) {
templateParam.append("\"").append(aliyunSmsBO.getTemplateParam()[i]).append("\":\"").append(templateValue[i]).append("\",");
}
templateParam.append("}");
// 短信请求信息配置
SendSmsRequest sendSmsRequest = new SendSmsRequest()
// 必填,接收短信的手机号
.setPhoneNumbers(phone)
// 必填,设置签名名称,应严格按"签名名称"填写,格式如:阿里云
.setSignName(aliyunSmsBO.getSignName())
// 必填,设置模板CODE,应严格按"模板CODE"填写,格式:SMS_88888888
.setTemplateCode(aliyunSmsBO.getTemplateCode())
// 可选,设置模板参数, 假如模板中存在变量需要替换则为必填项
.setTemplateParam(templateParam.toString())
// 可选,设置流水号
//.setOutId("10086")
;
// 发起请求
SendSmsResponse response;
try {
response = new Client(config).sendSms(sendSmsRequest);
} catch (Exception e) {
System.out.println(e.getMessage());
return null;
}
return response;
}
}
@@ -0,0 +1,88 @@
package com.zxdmy.excite.component.bo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 支付宝支付配置信息
*
* @author 沈松
* @since 2022/1/10 22:13
*/
@Data
@Accessors(chain = true)
public class AlipayBO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 本条支付宝支付信息的KEY
*/
private String key;
/**
* 必填:应用AppId,例如:2019091767145019
*/
private String appId;
/**
* 必填:应用名称
*/
private String appName;
/**
* 必填:应用私钥(Java语言),例如:MIIEvQIBADANB ... ...
*/
private String merchantPrivateKey;
// 接口加签方式分:证书模式(收、退款)和公钥模式(只能收款)
// 证书模式文档:https://opendocs.alipay.com/common/02kipl
// 优先级:证书模式 非证书模式
/**
* 证书模式:【应用公钥证书】文件路径,路径优先级:文件系统 > CLASS_PATH
*/
private String merchantCertPath;
/**
* 证书模式:【支付宝公钥证书】文件路径,路径优先级:文件系统 > CLASS_PATH
*/
private String alipayCertPath;
/**
* 证书模式:【支付宝根证书】文件路径,路径优先级:文件系统 > CLASS_PATH
*/
private String alipayRootCertPath;
/**
* 公钥模式:只需要填写此【支付宝公钥】即可,无需赋值上面的三个证书路径
*/
private String alipayPublicKey;
/**
* 可选:异步通知接收服务地址,例如:https://www.test.com/callback
*/
private String notifyUrl;
/**
* 可选:AES密钥(接口内容加密方式),调用AES加解密相关接口时需要
*/
private String encryptKey;
/**
* 必填:协议类型:http | https 二选一
*/
private String protocol = "https";
/**
* 必填:gatewayHost,默认:openapi.alipay.com
*/
private String gatewayHost = "openapi.alipay.com";
/**
* 必填:签名类型,默认RSA2
*/
private String signType = "RSA2";
}
@@ -0,0 +1,57 @@
package com.zxdmy.excite.component.bo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 阿里云OSS配置信息
*
* @author 沈松
* @since 2022/1/5 20:18
*/
@Data
@Accessors(chain = true)
public class AliyunOssBO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 本条OSS配置信息的key
*/
private String key;
/**
* OSS上的命名空间
*/
private String bucketName;
/**
* Bucket的域名,由阿里云官方生成,该域名用来访问空间中的文件。
*/
private String bucketDomain;
/**
* 自己通过CNAME域名解析绑定的域名
*/
private String bindDomain;
/**
* 访问域名(地域节点),上传使用。
* 详情请参考:https://help.aliyun.com/document_detail/31837.html
*/
private String endpoint;
/**
* 阿里云账号(建议使用子账号)的AccessKey
*/
private String accessKeyId;
/**
* 阿里云账号(建议使用子账号)的AccessKey的秘钥
*/
private String accessKeySecret;
}
@@ -0,0 +1,49 @@
package com.zxdmy.excite.component.bo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 阿里云短信服务配置信息
*
* @author 沈松
* @since 2022/1/3 20:28
*/
@Data
@Accessors(chain = true)
public class AliyunSmsBO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 本条短信配置信息的key
*/
private String key;
/**
* 必填。开发者的 AccessKey ID
*/
private String accessKeyId;
/**
* 必填。开发者的 AccessKey Secret
*/
private String accessKeySecret;
/**
* 必填。短信模板的签名,严格按照"签名名称"填写,格式如:阿里云
*/
private String signName;
/**
* 必填。模板CODE,应严格按"模板CODE"填写,格式:SMS_88888888
*/
private String templateCode;
/**
* 选填。如果消息模板中有参数,则必填。设置模板参数,如:[code,data]
*/
private String[] templateParam;
}
@@ -0,0 +1,59 @@
package com.zxdmy.excite.component.bo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 第三方登录的配置信息实体
*
* @author 沈松
* @since 2022/1/6 17:30
*/
@Data
@Accessors(chain = true)
public class JustAuthBO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 本条登录类型的KEY,如:qq、weibo等
*/
private String key;
/**
* 客户端id:对应各平台的appKey
*/
private String appID;
/**
* 客户端Secret:对应各平台的appSecret
*/
private String appSecret;
/**
* 登录成功后的回调地址
*/
private String redirectUri;
/**
* 只针对qq登录生效:是否需要申请unionid
*/
private Boolean unionId;
/**
* 企业微信第三方授权用户类型:member|admin
*/
private String usertype;
/**
* 企业微信登录:授权方的网页应用ID
*/
private String agentId;
/**
* 支付宝的公钥,只针对支付宝、阿里云登陆有效
*/
private String publicKey;
}
@@ -0,0 +1,70 @@
package com.zxdmy.excite.component.bo;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotBlank;
import java.io.Serializable;
/**
* <p>
* 描述
* </p>
*
* @author 沈松
* @since 2022/1/3 16:07
*/
@Data
@Accessors(chain = true)
public class QiniuOssBO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 本条七牛云配置信息的key(主键)
*/
private String key;
/**
* 七牛云账号的秘钥管理之:AccessKey
*/
@NotBlank(message = "AccessKey不能为空")
private String accessKey;
/**
* 七牛云账号的秘钥管理之:SecretKey
*/
@NotBlank(message = "SecretKey不能为空")
private String secretKey;
/**
* 七牛云空间名称
*/
@NotBlank(message = "空间名称不能为空")
private String bucket;
/**
* 七牛云该空间(bucket)绑定的CDN加速域名
*/
@NotBlank(message = "CDN加速域名不能为空")
private String domain;
/**
* 地域选择:https://developer.qiniu.com/kodo/1671/region-endpoint-fq
* 华东:z0; 华北:z1; 华南:z2; 北美:na0; 东南亚:as0; 华东-浙江2:cn-east-2
* 可以为空,填写其他字符,由系统自己选
*/
// @NotBlank(message = "地域不能为空")
private String region;
/**
* 七牛云该空间(bucket)绑定的CDN加速域名的协议 http | https
*/
@NotBlank(message = "CDN加速域名协议不能为空")
private String protocol;
/*
* 上传Token
*/
private String uploadToken;
}
@@ -0,0 +1,77 @@
package com.zxdmy.excite.component.bo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* <p>
* 描述
* </p>
*
* @author 沈松
* @since 2022/1/9 20:27
*/
@Data
@Accessors(chain = true)
public class WeChatPayBO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 本条微信支付配置信息的key(主键)
*/
private String key;
/**
* 设置微信公众号或者小程序等的appid
*/
private String appId;
/**
* 微信支付商户号
*/
private String mchId;
/**
* 设置APIv2密钥(微信支付商户密钥
*/
private String mchKey;
/**
* APIv3密钥(微信支付商户密钥
*/
private String apiV3Key;
/**
* 服务商模式下的子商户公众账号ID,普通模式请不要配置,请在配置文件中将对应项删除
*/
private String subAppId;
/**
* 服务商模式下的子商户号,普通模式请不要配置,最好是请在配置文件中将对应项删除
*/
private String subMchId;
/**
* V2apiclient_cert.p12文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
*/
private String keyPath;
/**
* V3:key.pem文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
*/
private String privateKeyPath;
/**
* V3:cert.pem文件的绝对路径,或者如果放在项目中,请以classpath:开头指定
*/
private String privateCertPath;
/**
* 通知地址.通知url必须为外网可访问的url,不能携带参数。公网域名必须为https。
*/
private String notifyUrl;
}
@@ -0,0 +1,281 @@
package com.zxdmy.excite.component.justauth;
import cn.hutool.core.util.DesensitizedUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.zxdmy.excite.common.exception.ServiceException;
import com.zxdmy.excite.common.service.IGlobalConfigService;
import com.zxdmy.excite.component.bo.JustAuthBO;
import lombok.AllArgsConstructor;
import me.zhyd.oauth.config.AuthConfig;
import me.zhyd.oauth.enums.scope.AuthGiteeScope;
import me.zhyd.oauth.model.AuthCallback;
import me.zhyd.oauth.model.AuthResponse;
import me.zhyd.oauth.model.AuthUser;
import me.zhyd.oauth.request.*;
import me.zhyd.oauth.utils.AuthScopeUtils;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* 第三方登录服务接口
*
* @author 沈松
* @since 2022/1/6 12:02
*/
@Service
@AllArgsConstructor
public class JustAuthService {
IGlobalConfigService configService;
private static final String DEFAULT_SERVICE = "justAuth";
private static final Set<String> CONF_KEY_SET = new HashSet<String>() {{
add("qq");
add("weibo");
add("wechat_open");
add("wechat_mp");
add("wechat_enterprise");
add("baidu");
add("dingtalk");
add("aliyun");
add("alipay");
add("huawei");
add("feishu");
add("gitee");
add("github");
}};
/**
* 保存配置信息
*
* @param authBO 配置信息类
* @return 保存结果
*/
public boolean saveConfig(JustAuthBO authBO) throws JsonProcessingException {
// 配置信息的key必须存在于当前的库中
if (null != authBO.getKey() && CONF_KEY_SET.contains(authBO.getKey())) {
// 三大必填信息不能为空
if (null == authBO.getAppID() || null == authBO.getAppSecret() || null == authBO.getRedirectUri()) {
throw new ServiceException("APPID、APPSecret和RedirectUri不能为空!请检查!");
} else {
return configService.save(DEFAULT_SERVICE, authBO.getKey(), authBO, true);
}
} else {
throw new ServiceException("当前输入的配置KEY[" + authBO.getKey() + "]不在许可范围内,请检查!");
}
}
/**
* 根据配置的KEY获取一条配置信息
*
* @param confKey 配置的KEY
* @return 指定KEY的配置信息
*/
public JustAuthBO getConfig(String confKey) {
// 配置信息的key必须存在于当前的库中
if (!CONF_KEY_SET.contains(confKey)) {
throw new ServiceException("当前输入的配置KEY[" + confKey + "]不在许可范围内,请检查!");
}
JustAuthBO justAuthBO = new JustAuthBO();
return (JustAuthBO) configService.get(DEFAULT_SERVICE, confKey, justAuthBO);
}
/**
* 获取所有的登录配置信息
*
* @param desensitized 是否脱敏
* @return 配置列表
*/
public List<JustAuthBO> getConfigList(Boolean desensitized) {
JustAuthBO justAuthBO = new JustAuthBO();
List<Object> objectList = configService.getList(DEFAULT_SERVICE, justAuthBO);
ObjectMapper objectMapper = new ObjectMapper();
List<JustAuthBO> justAuthBOList = new ArrayList<>();
for (Object o : objectList) {
justAuthBO = objectMapper.convertValue(o, justAuthBO.getClass());
if (desensitized) {
justAuthBO.setAppSecret(DesensitizedUtil.idCardNum(justAuthBO.getAppSecret(), 2, 2));
justAuthBO.setPublicKey(DesensitizedUtil.idCardNum(justAuthBO.getPublicKey(), 2, 2));
}
justAuthBOList.add(justAuthBO);
}
return justAuthBOList;
}
/**
* 生成登录授权的URL
*
* @param confKey 配置的KEY,也即授权来源(如qq、weibo等)
* @param state 状态码(字符串)
* @return 登录的链接
*/
public String createAuthorizeUrl(String confKey, String state) {
if (!CONF_KEY_SET.contains(confKey)) {
return null;
}
return this.createAuthRequest(confKey).authorize(state);
}
/**
* 根据回调信息获取登录用户的信息
*
* @param confKey 配置的KEY,也即授权来源(如qq、weibo等)
* @param callback 接口回调信息
* @return 授权成功后的用户信息,根据授权平台的不同,获取的数据完整性也不同
*/
public AuthUser getAuthUser(String confKey, AuthCallback callback) {
if (!CONF_KEY_SET.contains(confKey)) {
return null;
}
AuthRequest authRequest = this.createAuthRequest(confKey);
// 根据返回的参数,执行登录请求(获取用户信息)
AuthResponse<AuthUser> response = authRequest.login(callback);
// 判断结果
if (response.ok()) {
return response.getData();
} else {
return null;
}
}
/**
* 根据具体地授权来源,获取授权请求工具类
*
* @param confKey 配置的KEY,也即授权来源(如qq、weibo等)
* @return 授权工具类
*/
public AuthRequest createAuthRequest(String confKey) {
if (!CONF_KEY_SET.contains(confKey)) {
throw new ServiceException("当前输入的配置KEY[" + confKey + "]不在许可范围内,请检查!");
}
JustAuthBO authBO = this.getConfig(confKey);
AuthRequest authRequest;
switch (confKey) {
// QQ登录
case "qq":
authRequest = new AuthQqRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.unionId(authBO.getUnionId())
.build());
break;
// 微博登录
case "weibo":
authRequest = new AuthWeiboRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.build());
break;
// 微信开放平台登录
case "wechat_open":
authRequest = new AuthWeChatOpenRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.build());
break;
// 微信公众号登录
case "wechat_mp":
authRequest = new AuthWeChatMpRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.build());
break;
// 微信企业号登录
case "wechat_enterprise":
authRequest = new AuthWeChatEnterpriseQrcodeRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.agentId(authBO.getAgentId())
.build());
break;
// 百度登录
case "baidu":
authRequest = new AuthBaiduRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.build());
break;
// 钉钉登录
case "dingtalk":
authRequest = new AuthDingTalkRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.build());
break;
// 阿里云登录
case "aliyun":
authRequest = new AuthAliyunRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.alipayPublicKey(authBO.getPublicKey())
.build());
break;
// 支付宝登录
case "alipay":
authRequest = new AuthAlipayRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.alipayPublicKey(authBO.getPublicKey())
.build());
break;
// 华为登录
case "huawei":
authRequest = new AuthHuaweiRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.build());
break;
// 飞书登录
case "feishu":
authRequest = new AuthFeishuRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.build());
break;
// Gitee登录
case "gitee":
authRequest = new AuthGiteeRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.scopes(AuthScopeUtils.getScopes(AuthGiteeScope.values()))
.build());
break;
// GitHub登录
case "github":
authRequest = new AuthGithubRequest(AuthConfig.builder()
.clientId(authBO.getAppID())
.clientSecret(authBO.getAppSecret())
.redirectUri(authBO.getRedirectUri())
.build());
break;
default:
throw new ServiceException("配置的KEY不存在,请检查!");
}
return authRequest;
}
/**
* 读取当前auth服务支持的KEY的集合
*
* @return 支持的KEY的集合
*/
public Set<String> getConfKeySet() {
return CONF_KEY_SET;
}
}
@@ -0,0 +1,16 @@
package com.zxdmy.excite.component.oss;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "aliyun.oss")
public class AliOSSProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
private String region;
}
@@ -0,0 +1,303 @@
package com.zxdmy.excite.component.oss;
import com.alibaba.fastjson.JSONObject;
import com.aliyun.oss.*;
import com.aliyun.oss.model.*;
import com.zxdmy.excite.common.exception.ServiceException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.UUID;
/**
* 阿里云 OSS 工具类
*/
@Component
public class AliOSSUtils{
@Autowired//将OSS的配置注入进来,后面需要用到
private AliOSSProperties aliOSSProperties;
@Resource
protected HttpServletRequest request;
/**
* oss文件下载
* @param path 文件下载本地地址
* @param pathOss oss地址
* @return
* @throws IOException
*/
public Boolean download(String path,String pathOss) throws IOException {
//获取阿里云OSS参数
String endpoint = aliOSSProperties.getEndpoint();
String accessKeyId = aliOSSProperties.getAccessKeyId();
String accessKeySecret = aliOSSProperties.getAccessKeySecret();
String bucketName = aliOSSProperties.getBucketName();
// 创建连接
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId,accessKeySecret);
// content-disposition
try {
// 下载Object到本地文件,并保存到指定的本地路径中。如果指定的本地文件存在会覆盖,不存在则新建。
// 如果未指定本地路径,则下载后的文件默认保存到示例程序所属项目对应本地路径中。
ossClient.getObject(new GetObjectRequest(bucketName, pathOss), new File(path));
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return false;
}
/**
* 删除oss文件
* @param path 文件地址
* @return
* @throws IOException
*/
public Boolean delete(String path) throws IOException {
//获取阿里云OSS参数
String endpoint = aliOSSProperties.getEndpoint();
String accessKeyId = aliOSSProperties.getAccessKeyId();
String accessKeySecret = aliOSSProperties.getAccessKeySecret();
String bucketName = aliOSSProperties.getBucketName();
// 创建连接
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId,accessKeySecret);
try {
// 删除文件。
// 文件完整路径中不能包含Bucket名称。
List<String> keys = new ArrayList<String>();
keys.add(path);
DeleteObjectsResult deleteObjectsResult = ossClient.deleteObjects(new DeleteObjectsRequest(bucketName).withKeys(keys).withEncodingType("url"));
List<String> deletedObjects = deleteObjectsResult.getDeletedObjects();
try {
for(String obj : deletedObjects) {
String deleteObj = URLDecoder.decode(obj, "UTF-8");
System.out.println(deleteObj);
return !(ossClient.doesObjectExist(bucketName,path));
}
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return false;
}
/**
* 查找oss文件
* @param path 文件地址
* @return
* @throws IOException
*/
public Boolean query(String path) throws IOException {
//获取阿里云OSS参数
String endpoint = aliOSSProperties.getEndpoint();
String accessKeyId = aliOSSProperties.getAccessKeyId();
String accessKeySecret = aliOSSProperties.getAccessKeySecret();
String bucketName = aliOSSProperties.getBucketName();
// 创建连接
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId,accessKeySecret);
try {
// 判断文件是否存在。如果返回值为true,则文件存在,否则存储空间或者文件不存在。
// 设置是否进行重定向或者镜像回源。默认值为true,表示忽略302重定向和镜像回源;如果设置isINoss为false,则进行302重定向或者镜像回源。
//boolean isINoss = true;
boolean found = ossClient.doesObjectExist(bucketName, path);
//boolean found = ossClient.doesObjectExist(bucketName, objectName, isINoss);
return found;
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return false;
}
/**
* 拷贝oss地址文件位置至其他oss地址位置(源Bucket相同)
* @param path
* @param newPath
* @return
* @throws IOException
*/
public Boolean copy(String path,String newPath) throws IOException {
//获取阿里云OSS参数
String endpoint = aliOSSProperties.getEndpoint();
String accessKeyId = aliOSSProperties.getAccessKeyId();
String accessKeySecret = aliOSSProperties.getAccessKeySecret();
String bucketName = aliOSSProperties.getBucketName();
String newBucketName = aliOSSProperties.getBucketName();
// 创建连接
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId,accessKeySecret);
try {
ObjectMetadata objectMetadata = ossClient.getObjectMetadata(bucketName, path);
// 获取被拷贝文件的大小。
long contentLength = objectMetadata.getContentLength();
long gb = 1024 * 1024 * 1024;
//小文件上传
if(contentLength < gb) {
// 拷贝文件。
CopyObjectResult result = ossClient.copyObject(bucketName, path, newBucketName, newPath);
System.out.println("ETag: " + result.getETag() + " LastModified: " + result.getLastModified());
// 判断文件是否存在。如果返回值为true,则文件存在,否则存储空间或者文件不存在。
// 设置是否进行重定向或者镜像回源。默认值为true,表示忽略302重定向和镜像回源;如果设置isINoss为false,则进行302重定向或者镜像回源。
}
//大文件上传(分片上传[大于1GB])
else {
// 设置分片大小为10 MB。单位为字节。
long partSize = 1024 * 1024 * 10;
// 计算分片总数。
int partCount = (int) (contentLength / partSize);
if (contentLength % partSize != 0) {
partCount++;
}
System.out.println("total part count:" + partCount);
// 初始化拷贝任务。可以通过InitiateMultipartUploadRequest指定目标文件元信息。
InitiateMultipartUploadRequest initiateMultipartUploadRequest = new InitiateMultipartUploadRequest(newBucketName, newPath);
// 拷贝源文件ContentType和UserMetadata,分片拷贝默认不拷贝源文件的ContentType和UserMetadata。
ObjectMetadata metadata = new ObjectMetadata();
metadata.setContentType(objectMetadata.getContentType());
metadata.setUserMetadata(objectMetadata.getUserMetadata());
initiateMultipartUploadRequest.setObjectMetadata(metadata);
InitiateMultipartUploadResult initiateMultipartUploadResult = ossClient.initiateMultipartUpload(initiateMultipartUploadRequest);
String uploadId = initiateMultipartUploadResult.getUploadId();
// 分片拷贝。
List<PartETag> partETags = new ArrayList<PartETag>();
for (int i = 0; i < partCount; i++) {
// 计算每个分片的大小。
long skipBytes = partSize * i;
long size = Math.min(partSize, contentLength - skipBytes);
// 创建UploadPartCopyRequest。可以通过UploadPartCopyRequest指定限定条件。
UploadPartCopyRequest uploadPartCopyRequest =
new UploadPartCopyRequest(bucketName, path, newBucketName, newPath);
uploadPartCopyRequest.setUploadId(uploadId);
uploadPartCopyRequest.setPartSize(size);
uploadPartCopyRequest.setBeginIndex(skipBytes);
uploadPartCopyRequest.setPartNumber(i + 1);
//Map headers = new HashMap();
// 指定拷贝的源地址。
// headers.put(OSSHeaders.COPY_OBJECT_SOURCE, "/examplebucket/desexampleobject.txt");
// 指定源Object的拷贝范围。例如设置bytes=0~1023,表示拷贝1~1024字节的内容。
// headers.put(OSSHeaders.COPY_SOURCE_RANGE, "bytes=0~1023");
// 如果源Object的ETag值和您提供的ETag相等,则执行拷贝操作,并返回200 OK。
// headers.put(OSSHeaders.COPY_OBJECT_SOURCE_IF_MATCH, "5B3C1A2E053D763E1B002CC607C5****");
// 如果源Object的ETag值和您提供的ETag不相等,则执行拷贝操作,并返回200 OK。
// headers.put(OSSHeaders.COPY_OBJECT_SOURCE_IF_NONE_MATCH, "5B3C1A2E053D763E1B002CC607C5****");
// 如果指定的时间等于或者晚于文件实际修改时间,则正常拷贝文件,并返回200 OK。
// headers.put(OSSHeaders.COPY_OBJECT_SOURCE_IF_UNMODIFIED_SINCE, "2021-12-09T07:01:56.000Z");
// 如果源Object在用户指定的时间以后被修改过,则执行拷贝操作。
// headers.put(OSSHeaders.COPY_OBJECT_SOURCE_IF_MODIFIED_SINCE, "2021-12-09T07:01:56.000Z");
// uploadPartCopyRequest.setHeaders(headers);
UploadPartCopyResult uploadPartCopyResult = ossClient.uploadPartCopy(uploadPartCopyRequest);
// 将返回的分片ETag保存到partETags中。
partETags.add(uploadPartCopyResult.getPartETag());
}
// 提交分片拷贝任务。
CompleteMultipartUploadRequest completeMultipartUploadRequest = new CompleteMultipartUploadRequest(
newBucketName, newPath, uploadId, partETags);
ossClient.completeMultipartUpload(completeMultipartUploadRequest);
}
boolean found = ossClient.doesObjectExist(newBucketName, newPath);
return found;
} catch (OSSException oe) {
System.out.println("Caught an OSSException, which means your request made it to OSS, "
+ "but was rejected with an error response for some reason.");
System.out.println("Error Message:" + oe.getErrorMessage());
System.out.println("Error Code:" + oe.getErrorCode());
System.out.println("Request ID:" + oe.getRequestId());
System.out.println("Host ID:" + oe.getHostId());
} catch (ClientException ce) {
System.out.println("Caught an ClientException, which means the client encountered "
+ "a serious internal problem while trying to communicate with OSS, "
+ "such as not being able to access the network.");
System.out.println("Error Message:" + ce.getMessage());
} finally {
if (ossClient != null) {
ossClient.shutdown();
}
}
return false;
}
}
@@ -0,0 +1,18 @@
package com.zxdmy.excite.component.oss;
import lombok.Data;
import lombok.EqualsAndHashCode;
@Data
@EqualsAndHashCode(callSuper = false)
public class OSSVO {
private String region;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
private String endpoint;
private String fileName;
private String prefix;
}
@@ -0,0 +1,428 @@
package com.zxdmy.excite.component.qiniu;
import cn.hutool.core.util.DesensitizedUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.*;
import com.qiniu.storage.model.FetchRet;
import com.qiniu.storage.model.FileInfo;
import com.qiniu.util.Auth;
import com.zxdmy.excite.common.exception.ServiceException;
import com.zxdmy.excite.common.service.IGlobalConfigService;
import com.zxdmy.excite.component.vo.QiniuFileVO;
import com.zxdmy.excite.component.bo.QiniuOssBO;
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
/**
* 七牛云对象存储服务实现类
* 更多接口,可以根据 https://developer.qiniu.com/kodo/1239/java 实现。
*
* @author 沈松
* @since 2022-01-02 0002 23:33
*/
@Service
@AllArgsConstructor
public class QiniuOssService {
IGlobalConfigService configService;
private static final String DEFAULT_SERVICE = "qiniu";
private static final String DEFAULT_KEY = "qiniuOss";
/**
* 将七牛云的信息保存至数据库
*
* @param qiniuOssBO 七牛云信息实体。如果指定key,则存储的配置信息的key即为所指定
* @return 结果
*/
public boolean saveQiniuConfig(QiniuOssBO qiniuOssBO) {
// 如果必填信息为空,则返回错误
if (null == qiniuOssBO.getSecretKey() || null == qiniuOssBO.getAccessKey() || null == qiniuOssBO.getBucket() || null == qiniuOssBO.getDomain()) {
throw new ServiceException("AK、SK、空间名称或者域名为空,请核实!");
}
// 核实无误,填充数据
if (null == qiniuOssBO.getProtocol()) {
qiniuOssBO.setProtocol("http");
}
try {
if (null == qiniuOssBO.getKey()) {
return configService.save(DEFAULT_SERVICE, DEFAULT_KEY, qiniuOssBO, true);
}
return configService.save(DEFAULT_SERVICE, qiniuOssBO.getKey(), qiniuOssBO, true);
} catch (Exception e) {
throw new ServiceException(e.getMessage());
}
}
/**
* 从数据库读取七牛云配置信息(指定key)
*
* @param confKey 配置文件的key,不同的key对应不同的存储空间
* @return 七牛云配置信息实体
*/
public QiniuOssBO getQiniuConfig(String confKey) {
QiniuOssBO qiniuOssBO = new QiniuOssBO();
qiniuOssBO = (QiniuOssBO) configService.get(DEFAULT_SERVICE, confKey, qiniuOssBO);
return qiniuOssBO;
}
/**
* 从数据库读取七牛云配置信息(默认key)
*
* @return 七牛云配置信息实体
*/
public QiniuOssBO getQiniuConfig() {
return this.getQiniuConfig(DEFAULT_KEY);
}
/**
* 从数据库读取七牛云配置信息,并脱敏(指定key)
*
* @param confKey 配置文件的key,不同的key对应不同的存储空间
* @return 七牛云配置信息实体
*/
public QiniuOssBO getQiniuConfigHide(String confKey) {
QiniuOssBO qiniuOssBO = new QiniuOssBO();
qiniuOssBO = (QiniuOssBO) configService.get(DEFAULT_SERVICE, confKey, qiniuOssBO);
qiniuOssBO.setAccessKey(DesensitizedUtil.idCardNum(qiniuOssBO.getAccessKey(), 2, 2));
qiniuOssBO.setSecretKey(DesensitizedUtil.idCardNum(qiniuOssBO.getSecretKey(), 2, 2));
return qiniuOssBO;
}
/**
* 从数据库读取七牛云配置信息,并脱敏(默认key)
*
* @return 七牛云配置信息实体
*/
public QiniuOssBO getQiniuConfigHide() {
return this.getQiniuConfigHide(DEFAULT_KEY);
}
/**
* 获取全部的七牛云配置信息
*
* @param desensitized 是否脱敏:true:脱敏, false:不脱敏
* @return 读取全部配置信息
*/
public List<QiniuOssBO> getQiniuConfigList(Boolean desensitized) {
QiniuOssBO qiniuOssBO = new QiniuOssBO();
List<Object> objectList = configService.getList(DEFAULT_SERVICE, qiniuOssBO);
ObjectMapper objectMapper = new ObjectMapper();
List<QiniuOssBO> qiniuOssBOList = new ArrayList<>();
for (Object o : objectList) {
qiniuOssBO = objectMapper.convertValue(o, qiniuOssBO.getClass());
if (desensitized) {
qiniuOssBO.setAccessKey(DesensitizedUtil.idCardNum(qiniuOssBO.getAccessKey(), 2, 2));
qiniuOssBO.setSecretKey(DesensitizedUtil.idCardNum(qiniuOssBO.getSecretKey(), 2, 2));
}
qiniuOssBOList.add(qiniuOssBO);
}
return qiniuOssBOList;
}
/**
* 生成上传的Token(指定key
*
* @param confKey 配置文件的key,不同的key对应不同的存储空间
* @param fileKey 文件的key,又称文件名,可以带路径
* @param expireSeconds 过期时间
* @return 结果
*/
public QiniuOssBO createUploadToken(String confKey, String fileKey, Long expireSeconds) {
// 如果必填信息为空,则返回
if (null == fileKey || "".equals(fileKey)) {
throw new ServiceException("文件的key为空,请核实!");
}
// 读取配置信息
QiniuOssBO qiniuOssBO = new QiniuOssBO();
qiniuOssBO = (QiniuOssBO) configService.get(DEFAULT_SERVICE, confKey, qiniuOssBO);
// 生成Token
Auth auth = Auth.create(qiniuOssBO.getAccessKey(), qiniuOssBO.getSecretKey());
String upToken = auth.uploadToken(qiniuOssBO.getBucket(), fileKey, expireSeconds, null);
// 写入Token,并返回
qiniuOssBO.setUploadToken(upToken);
return qiniuOssBO;
}
/**
* 生成上传的Token(默认key
*
* @param fileKey 文件的key,又称文件名
* @param expireSeconds 过期时间
* @return 结果
*/
public QiniuOssBO createUploadToken(String fileKey, Long expireSeconds) {
return this.createUploadToken(DEFAULT_KEY, fileKey, expireSeconds);
}
/**
* 后端上传文件(指定key)
*
* @param confKey 配置文件的key,不同的key对应不同的存储空间
* @param file 文件
* @return 结果
*/
public QiniuFileVO uploadFile(String confKey, File file) {
if (null == file) {
throw new ServiceException("上传的文件为空,请检查!");
}
// 生成Token
QiniuOssBO qiniuOssBO = this.createUploadToken(confKey, file.getName(), 3600L);
// 配置信息
Configuration configuration = new Configuration(this.createRegion(qiniuOssBO.getRegion()));
// 构造上传管理类
UploadManager uploadManager = new UploadManager(configuration);
// 进行上传
try {
Response response = uploadManager.put(file, file.getName(), qiniuOssBO.getUploadToken());
if (response.isOK()) {
return new QiniuFileVO()
.setProtocol(qiniuOssBO.getProtocol())
.setDomain(qiniuOssBO.getDomain())
.setKey(qiniuOssBO.getKey());
}
return null;
} catch (QiniuException ex) {
throw new ServiceException(ex.response.toString());
}
}
/**
* 后端上传文件(默认key)
*
* @param file 文件
* @return 七牛云配置信息实体
*/
public QiniuFileVO uploadFile(File file) {
return this.uploadFile(DEFAULT_KEY, file);
}
/**
* 抓取网络资源到空间(指定key)
*
* @param confKey 配置文件的key,不同的key对应不同的存储空间
* @param fileKey 文件名
* @param remoteSrcUrl 远程链接
* @return 文件信息
*/
public QiniuFileVO uploadFileFromUrl(String confKey, String fileKey, String remoteSrcUrl) {
QiniuOssBO qiniuOssBO = this.getQiniuConfig(confKey);
if (qiniuOssBO == null) {
throw new ServiceException("key为[" + confKey + "]的配置信息为空,请检查!");
}
BucketManager bucketManager = this.createBucketManager(qiniuOssBO);
//抓取网络资源到空间
try {
FetchRet fetchRet = bucketManager.fetch(remoteSrcUrl, qiniuOssBO.getBucket(), fileKey);
return new QiniuFileVO()
.setProtocol(qiniuOssBO.getProtocol())
.setDomain(qiniuOssBO.getDomain())
.setKey(fetchRet.key)
.setHash(fetchRet.hash)
.setMimeType(fetchRet.mimeType)
.setSize(fetchRet.fsize);
} catch (QiniuException ex) {
throw new ServiceException(ex.response.toString());
}
}
/**
* 抓取网络资源到空间(默认KEY)
*
* @param fileKey 文件名
* @param remoteSrcUrl 远程链接
* @return 文件信息
*/
public QiniuFileVO uploadFileFromUrl(String fileKey, String remoteSrcUrl) {
return this.uploadFileFromUrl(DEFAULT_KEY, fileKey, remoteSrcUrl);
}
/**
* 获取七牛云空间的全部文件(指定key)
*
* @param confKey 配置文件的key,不同的key对应不同的存储空间
* @param prefix 文件前缀
* @param delimiter 文件分隔符
* @return 文件信息列表
*/
public List<FileInfo> getQiniuFileListAll(String confKey, String prefix, String delimiter) {
QiniuOssBO qiniuOssBO = this.getQiniuConfig(confKey);
if (qiniuOssBO == null) {
throw new ServiceException("key为[" + confKey + "]的配置信息为空,请检查!");
}
BucketManager bucketManager = this.createBucketManager(qiniuOssBO);
BucketManager.FileListIterator fileListIterator = bucketManager.createFileListIterator(qiniuOssBO.getBucket(), prefix, 1000, delimiter);
// 记录返回值
List<FileInfo> fileInfoList = new ArrayList<>();
while (fileListIterator.hasNext()) {
fileInfoList.addAll(Arrays.asList(fileListIterator.next()));
}
return fileInfoList;
}
/**
* 获取七牛云空间的全部文件(默认key)
*
* @param prefix 文件前缀
* @param delimiter 文件分隔符
* @return 文件信息列表
*/
public List<FileInfo> getQiniuFileListAll(String prefix, String delimiter) {
return this.getQiniuFileListAll(DEFAULT_KEY, prefix, delimiter);
}
/**
* 获取七牛云空间的分页文件列表(分页),如果页数超限,则返回第一页(指定key)
*
* @param confKey 配置文件的key,不同的key对应不同的存储空间
* @param page 页号
* @param limit 每页条数
* @param prefix 前缀
* @param delimiter 分隔符
* @return 页面类Page
*/
public Page<FileInfo> getQiniuFileListPage(String confKey, Integer page, Integer limit, String prefix, String delimiter) {
// 定义返回Page实体
Page<FileInfo> fileInfoPage = new Page<>();
// 获取配置
QiniuOssBO qiniuOssBO = this.getQiniuConfig(confKey);
if (qiniuOssBO == null) {
throw new ServiceException("key为[" + confKey + "]的配置信息为空,请检查!");
}
// 生成管理器
BucketManager bucketManager = this.createBucketManager(qiniuOssBO);
// 获取列表迭代器
BucketManager.FileListIterator fileListIterator = bucketManager.createFileListIterator(qiniuOssBO.getBucket(), prefix, limit, delimiter);
// 遍历直至找到指定页数
long total = 0;
while (fileListIterator.hasNext()) {
page--;
if (page == 0) {
FileInfo[] items = fileListIterator.next();
fileInfoPage.setRecords(Arrays.asList(items));
total += items.length;
} else {
total += fileListIterator.next().length;
}
}
if (fileInfoPage.getRecords().size() == 0) {
fileInfoPage.setRecords(Arrays.asList(fileListIterator.next()));
}
fileInfoPage.setCurrent(page).setSize(limit).setTotal(total);
return fileInfoPage;
}
/**
* 获取七牛云空间的分页文件列表(分页),如果页数超限,则返回第一页(默认key)
*
* @param page 页号
* @param limit 每页条数
* @param prefix 前缀
* @param delimiter 分隔符
* @return 页面类Page
*/
public Page<FileInfo> getQiniuFileListPage(Integer page, Integer limit, String prefix, String delimiter) {
return this.getQiniuFileListPage(DEFAULT_KEY, page, limit, prefix, delimiter);
}
/**
* 获取下载链接(一般针对私有资源使用)(指定key)
*
* @param confKey 配置文件的key,不同的key对应不同的存储空间
* @param fileKey 下载资源在七牛云存储的key
* @param attName 下载文件的名字。null:与存储的文件名相同
* @param expireSeconds 有效期(秒),一般取3600(即1小时)
* @return 下载链接
*/
public String getDownloadUrl(String confKey, String fileKey, String attName, Long expireSeconds) {
// 获取配置
QiniuOssBO qiniuOssBO = this.getQiniuConfig(confKey);
if (qiniuOssBO == null) {
throw new ServiceException("key为[" + confKey + "]的配置信息为空,请检查!");
}
DownloadUrl url = new DownloadUrl(qiniuOssBO.getDomain(), "https".equals(qiniuOssBO.getProtocol()), fileKey);
if (null != attName) {
url.setAttname(attName);
}
Auth auth = Auth.create(qiniuOssBO.getAccessKey(), qiniuOssBO.getSecretKey());
long deadline = expireSeconds + new Date().getTime();
String downloadUrl = "";
try {
downloadUrl = url.buildURL(auth, deadline);
} catch (QiniuException e) {
throw new ServiceException(e.response.toString());
}
return downloadUrl;
}
/**
* 获取下载链接(一般针对私有资源使用)(默认key)
*
* @param fileKey 下载资源在七牛云存储的key
* @param attName 下载文件的名字。null:与存储的文件名相同
* @param expireSeconds 有效期(秒),一般取3600(即1小时)
* @return 下载链接
*/
public String getDownloadUrl(String fileKey, String attName, Long expireSeconds) {
return this.getDownloadUrl(DEFAULT_KEY, fileKey, attName, expireSeconds);
}
/**
* 根据配置信息生成空间管理器
*
* @param qiniuOssBO 配置信息
* @return 空间管理器
*/
private BucketManager createBucketManager(QiniuOssBO qiniuOssBO) {
// 生成Auth授权类
Auth auth = Auth.create(qiniuOssBO.getAccessKey(), qiniuOssBO.getSecretKey());
// 配置信息,
Configuration configuration = new Configuration(this.createRegion(qiniuOssBO.getRegion()));
// 返回
return new BucketManager(auth, configuration);
}
/**
* 根据空间地址设置Region
* 详情:https://developer.qiniu.com/kodo/1671/region-endpoint-fq
*
* @param re 空间地域:华东:z0; 华北:z1; 华南:z2; 北美:na0; 东南亚:as0; 华东-浙江2:cn-east-2
* @return 地域类
*/
private Region createRegion(String re) {
// 设置区域:如果不为空
Region region;
switch (re) {
case "z0":
region = Region.huadong();
break;
case "z1":
region = Region.huabei();
break;
case "z2":
region = Region.huanan();
break;
case "na0":
region = Region.beimei();
break;
case "as0":
region = Region.xinjiapo();
break;
case "cn-east-2":
region = Region.huadongZheJiang2();
break;
default:
region = new Region.Builder().autoRegion("https://uc.qbox.me");
}
return region;
}
}
@@ -0,0 +1,56 @@
package com.zxdmy.excite.component.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* 此[表现层对象]用来返回前端上传时,需要用到的各种参数。
* 应用场景:后端生成Policy,前端直传阿里云OSS。上传最大值:5G。
* 参考资料:
* https://help.aliyun.com/document_detail/31927.htm
* https://help.aliyun.com/document_detail/31926.htm
*
* @author 沈松
* @since 2022/1/5 21:06
*/
@Data
@Accessors(chain = true)
public class AliyunOssPolicyVO implements Serializable {
/***
* Bucket域名,即Endpoint访问域名。格式为:https://{bucketName}.{endpoint},如:https://bucket-name.oss-cn-hangzhou.aliyuncs.com
*/
private String host;
/**
* 文件完整名(如果有前缀则需要携带),如:hello.zipuser/10001/hello.rar
*/
private String key;
/**
* 用户表单上传的策略(Policy),Policy为经过Base64编码过的字符串。详情请参见 https://help.aliyun.com/document_detail/31988.htm?spm=a2c4g.11186623.0.0.69de5458ikCoa2#section-d5z-1ww-wdb
*/
private String policy;
/**
* 用户请求的AccessKey ID。
*/
private String accessKeyId;
/**
* 设置阿里云OSS服务端返回状态码为200,不设置则默认返回状态码204。
*/
private final String success_action_status = "200";
/**
* 上传后的阿里云OSS服务器回调功能。即上传完成后,阿里云向此地址发送一个回调。如果不涉及上传回调,请不要填充值。
*/
private String callback;
/**
* 对Policy签名后的字符串。详情请参见:https://help.aliyun.com/document_detail/31988.htm?spm=a2c4g.11186623.0.0.69de5458ikCoa2#section-wny-mww-wdb
*/
private String signature;
}
@@ -0,0 +1,71 @@
package com.zxdmy.excite.component.vo;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
/**
* <p>
* 描述
* </p>
*
* @author 沈松
* @since 2022/1/4 11:51
*/
@Data
@Accessors(chain = true)
public class QiniuFileVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 文件名
*/
public String key;
/**
* 文件hash值
*/
public String hash;
/**
* 文件的md5值
*/
public String md5;
/**
* 文件大小,单位:字节
*/
public long size;
/**
* 文件上传时间,单位为:100纳秒
*/
public long putTime;
/**
* 文件的mimeType
*/
public String mimeType;
/**
* 文件的状态,0表示启用,1表示禁用
*/
public int status;
/**
* 七牛云该空间(bucket)绑定的CDN加速域名
*/
private String domain;
/**
* 七牛云该空间(bucket)绑定的CDN加速域名的协议 http | https
*/
private String protocol;
/*
* 上传Token
*/
private String uploadToken;
}
@@ -0,0 +1,245 @@
package com.zxdmy.excite.component.wechat;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.github.binarywang.wxpay.bean.request.WxPayRefundV3Request;
import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderV3Request;
import com.github.binarywang.wxpay.bean.result.*;
import com.github.binarywang.wxpay.bean.result.enums.TradeTypeEnum;
import com.github.binarywang.wxpay.config.WxPayConfig;
import com.github.binarywang.wxpay.exception.WxPayException;
import com.github.binarywang.wxpay.service.WxPayService;
import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
import com.zxdmy.excite.common.exception.ServiceException;
import com.zxdmy.excite.common.service.IGlobalConfigService;
import com.zxdmy.excite.common.utils.HttpServletRequestUtil;
import com.zxdmy.excite.component.bo.WeChatPayBO;
import lombok.AllArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
/**
* 微信支付服务实现类
* 只实现V3版本的接口(文档地址:https://pay.weixin.qq.com/wiki/doc/apiv3/apis/index.shtml
*
* @author 沈松
* @since 2022-01-02 0002 23:34
*/
@Service
@AllArgsConstructor
public class WeChatPayService {
private IGlobalConfigService configService;
private static final String DEFAULT_SERVICE = "wechatPay";
private static final String DEFAULT_KEY = "wechatPay";
// 千万不要使用这个垃圾的微信支付沙盒环境.....
private static final Boolean SAND_BOX_ENV = false;
/**
* 保存微信支付配置信息至数据库
*
* @param weChatPayBO 微信支付实体
* @return 结果:T|F
* @throws JsonProcessingException 异常
*/
public boolean saveConfig(WeChatPayBO weChatPayBO) throws JsonProcessingException {
// 必填信息不能为空
if (null == weChatPayBO.getAppId() || null == weChatPayBO.getMchId() || null == weChatPayBO.getMchKey()) {
throw new ServiceException("APPID、商户号、商户秘钥不能为空!");
}
// 保存并返回结果
return configService.save(DEFAULT_SERVICE, null != weChatPayBO.getKey() ? weChatPayBO.getKey() : DEFAULT_KEY, weChatPayBO, true);
}
/**
* 从数据库中读取配置信息
*
* @param confKey 微信支付的配置KEY
* @return 微信支付配置信息
*/
public WeChatPayBO getConfig(String confKey) {
if (null == confKey) {
return this.getConfig();
}
return (WeChatPayBO) configService.get(DEFAULT_SERVICE, confKey, new WeChatPayBO());
}
/**
* 从数据库中读取配置信息(默认KEY)
*
* @return 微信支付配置信息
*/
public WeChatPayBO getConfig() {
return this.getConfig(DEFAULT_KEY);
}
/**
* 下单接口(只设置了必填信息)(V3版本)
*
* @param confKey 必填:微信支付配置信息的KEY。如果使用默认KEY,输入null
* @param tradeType 必填:交易类型:jsapi(含小程序)、app、h5、native
* @param description 必填:商品描述(商品标题)
* @param outTradeNo 必填:商家订单号
* @param total 必填:商品金额(单位:分)
* @param openId 特殊必填:支付用户的OpenId,JSAPI支付时必填。
* @return 支付返回结果:<br>APP支付、JSAPI支付为[预支付交易会话标识] <br>Native支付为[二维码链接] <br>H5支付为[支付跳转链接]
*/
public String pay(String confKey, String tradeType, String description, String outTradeNo, Integer total, String openId) {
// 构建统一下单请求参数对象
WxPayUnifiedOrderV3Request wxPayUnifiedOrderV3Request = new WxPayUnifiedOrderV3Request();
// 对象中写入数据
wxPayUnifiedOrderV3Request
// 【1】必填信息
// 应用ID:必填,可不填,通过配置信息读取
// .setAppid(weChatPayBO.getAppId())
// 直连商户号:必填,可不填,通过配置信息读取
// .setMchid(weChatPayBO.getMchId())
// 商品描述:必填
.setDescription(description)
// 商户订单号:必填,同一个商户号下唯一
.setOutTradeNo(outTradeNo)
// 通知地址:必填,公网域名必须为https,外网可访问。
// 可不填,通过配置信息读取
.setNotifyUrl(this.getConfig(confKey).getNotifyUrl())
// 订单金额:单位(分)
.setAmount(new WxPayUnifiedOrderV3Request.Amount().setTotal(total))
// 【2】选填信息
// 附加信息
.setAttach("附加信息")
// 订单优惠标记
.setGoodsTag("ABCD")
// ...
;
try {
// 根据请求类型,返回指定类型,其中包含:【3】条件必填信息
switch (tradeType.toLowerCase()) {
// Native支付
case "native":
return this.getWxPayService(confKey).unifiedOrderV3(TradeTypeEnum.NATIVE, wxPayUnifiedOrderV3Request).getCodeUrl();
// JSAPI支付
case "jsapi":
// 用户在直连商户appid下的唯一标识。 下单前需获取到用户的Openid
wxPayUnifiedOrderV3Request.setPayer(new WxPayUnifiedOrderV3Request.Payer().setOpenid(openId));
return this.getWxPayService(confKey).unifiedOrderV3(TradeTypeEnum.JSAPI, wxPayUnifiedOrderV3Request).getPrepayId();
// H5支付
case "h5":
wxPayUnifiedOrderV3Request.setSceneInfo(
new WxPayUnifiedOrderV3Request.SceneInfo()
// 用户终端IP
.setPayerClientIp(HttpServletRequestUtil.getRemoteIP())
.setH5Info(
new WxPayUnifiedOrderV3Request.H5Info()
// 场景类型
.setType("wechat")
)
);
return this.getWxPayService(confKey).unifiedOrderV3(TradeTypeEnum.H5, wxPayUnifiedOrderV3Request).getH5Url();
// APP支付
case "app":
return this.getWxPayService(confKey).unifiedOrderV3(TradeTypeEnum.APP, wxPayUnifiedOrderV3Request).getPrepayId();
default:
throw new ServiceException("输入的[" + tradeType + "]不合法,只能为native、jsapi、h5、app其一,请核实!");
}
} catch (WxPayException e) {
throw new ServiceException(e.getMessage());
}
}
/**
* 订单查询接口(新版V3)
*
* @param confKey 必填:微信支付配置信息的KEY。如果使用默认KEY,输入null
* @param transactionId 微信订单号
* @param outTradeNo 商户系统内部的订单号,当没提供transactionId时需要传这个。
* @return 查询订单 返回结果对象
*/
public WxPayOrderQueryV3Result query(String confKey, String transactionId, String outTradeNo) {
try {
return this.getWxPayService(confKey).queryOrderV3(transactionId, outTradeNo);
} catch (WxPayException e) {
throw new ServiceException(e.getMessage());
}
}
/**
* 退款接口
*
* @param confKey 必填:微信支付配置信息的KEY。如果使用默认KEY,输入null
* @param outTradeNo 商户订单号
* @param outRefundNo 商户退款单号
* @param total 订单总金额(单位:分)
* @param refund 退款金额(单位:分)
*/
public WxPayRefundV3Result refund(String confKey, String outTradeNo, String outRefundNo, int total, int refund) {
WxPayRefundV3Request wxPayRefundV3Request = new WxPayRefundV3Request();
wxPayRefundV3Request
.setOutTradeNo(outTradeNo)
.setOutRefundNo(outRefundNo)
.setAmount(new WxPayRefundV3Request.Amount()
.setTotal(total)
.setRefund(refund)
.setCurrency("CNY")
);
try {
return this.getWxPayService(confKey).refundV3(wxPayRefundV3Request);
} catch (WxPayException e) {
throw new ServiceException(e.getMessage());
}
}
/**
* 获取微信支付相关接口的服务
* (后续的几个服务方法,实现了基本的实例)
* (此接口也可以直接在controller中使用)
*
* @return 微信支付服务接口
*/
public WxPayService getWxPayService(String confKey) {
if (null == confKey) {
return this.getWxPayService();
}
// 读取配置信息
WeChatPayBO weChatPayBO = this.getConfig(confKey);
// 生成配置
WxPayConfig payConfig = new WxPayConfig();
// 填充基本配置信息
payConfig.setAppId(StringUtils.trimToNull(weChatPayBO.getAppId()));
payConfig.setMchId(StringUtils.trimToNull(weChatPayBO.getMchId()));
payConfig.setMchKey(StringUtils.trimToNull(weChatPayBO.getMchKey()));
payConfig.setApiV3Key(StringUtils.trimToNull(weChatPayBO.getApiV3Key()));
payConfig.setSubAppId(StringUtils.trimToNull(weChatPayBO.getSubAppId()));
payConfig.setSubMchId(StringUtils.trimToNull(weChatPayBO.getSubMchId()));
payConfig.setKeyPath(StringUtils.trimToNull(weChatPayBO.getKeyPath()));
payConfig.setPrivateCertPath(StringUtils.trimToNull(weChatPayBO.getPrivateCertPath()));
payConfig.setPrivateKeyPath(StringUtils.trimToNull(weChatPayBO.getPrivateKeyPath()));
payConfig.setNotifyUrl(StringUtils.trimToNull(weChatPayBO.getNotifyUrl()));
// 创建配置服务
WxPayService wxPayService = new WxPayServiceImpl();
wxPayService.setConfig(payConfig);
// 可以指定是否使用沙箱环境
payConfig.setUseSandboxEnv(SAND_BOX_ENV);
if (SAND_BOX_ENV) {
try {
payConfig.setMchKey(wxPayService.getSandboxSignKey());
wxPayService.setConfig(payConfig);
} catch (WxPayException e) {
throw new ServiceException(e.getMessage());
}
}
// 返回结果
return wxPayService;
}
/**
* 获取微信支付相关接口的服务(默认KEY)
*
* @return 微信支付服务接口
*/
public WxPayService getWxPayService() {
return this.getWxPayService(DEFAULT_KEY);
}
}