From c4efa7e917fb69358d1402e8425aeaad1f4c9831 Mon Sep 17 00:00:00 2001 From: zk Date: Tue, 10 Mar 2026 18:27:11 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9D=E5=A7=8B=E8=AF=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 5 + client-api/pom.xml | 50 ++++ .../java/org/jiayunet/ClientApplication.java | 21 ++ .../WxPayNotifyMessageAbstractImpl.java | 42 ++++ .../src/main/resources/application-local.yml | 81 +++++++ client-api/src/main/resources/application.yml | 70 ++++++ common/pom.xml | 105 ++++++++ .../org/jiayunet/aop/ControllerLogAspect.java | 56 +++++ .../org/jiayunet/config/JacksonConfig.java | 48 ++++ .../org/jiayunet/config/MybatisConfig.java | 49 ++++ .../java/org/jiayunet/config/OssConfig.java | 36 +++ .../org/jiayunet/config/RedissonConf.java | 35 +++ .../org/jiayunet/config/SecurityConfig.java | 95 ++++++++ .../java/org/jiayunet/config/SmsConfig.java | 37 +++ .../jiayunet/config/UpdateBatchMethod.java | 26 ++ .../java/org/jiayunet/config/WebConfig.java | 28 +++ .../java/org/jiayunet/config/WxPayConfig.java | 142 +++++++++++ .../jiayunet/constant/PreRedisKeyName.java | 35 +++ .../java/org/jiayunet/email/EmailAbility.java | 111 +++++++++ .../jiayunet/exception/BusinessException.java | 77 ++++++ .../exception/BusinessExpCodeEnum.java | 50 ++++ .../exception/BusinessExpCodeOperations.java | 10 + .../interceptor/BlackListInterceptor.java | 29 +++ .../JwtAuthenticationTokenFilter.java | 165 +++++++++++++ .../LoggingOriginalRequestFilter.java | 32 +++ .../interceptor/PreventReplayInterceptor.java | 78 ++++++ .../interceptor/SqlLoggerInterceptor.java | 119 ++++++++++ .../jiayunet/interceptor/TraceIdFilter.java | 51 ++++ .../org/jiayunet/mapper/CommonMapper.java | 78 ++++++ .../java/org/jiayunet/oss/AliOssAbility.java | 132 +++++++++++ .../org/jiayunet/pojo/UnifiedResponse.java | 44 ++++ .../interceptor/RedisPreventReplayInfo.java | 22 ++ .../pojo/login/RedisLoginTokenInfo.java | 45 ++++ .../org/jiayunet/sms/AliYunSmsAbility.java | 92 +++++++ .../java/org/jiayunet/sms/ISmsAbility.java | 43 ++++ .../org/jiayunet/sms/VerificationConfig.java | 125 ++++++++++ .../org/jiayunet/sms/VerifyCodeAttribute.java | 33 +++ .../java/org/jiayunet/tool/AuthenticTool.java | 121 ++++++++++ .../java/org/jiayunet/tool/HttpIpTool.java | 63 +++++ .../main/java/org/jiayunet/tool/HttpTool.java | 224 ++++++++++++++++++ .../java/org/jiayunet/tool/ObjectTool.java | 42 ++++ .../org/jiayunet/tool/UserSecurityTool.java | 23 ++ .../jiayunet/tool/VerifyImageCodeUtils.java | 207 ++++++++++++++++ .../jiayunet/tool/server/RedisServerTool.java | 121 ++++++++++ .../jiayunet/web/GlobalExceptionAdvice.java | 87 +++++++ .../web/UnifiedResponseBodyAdvice.java | 44 ++++ .../org/jiayunet/wxPay/WxJsPayAbility.java | 223 +++++++++++++++++ .../jiayunet/wxPay/WxNativePayAbility.java | 65 +++++ .../jiayunet/wxPay/WxPayNotifyController.java | 121 ++++++++++ .../wxPay/WxPayNotifyMessageAbstract.java | 36 +++ .../jiayunet/wxPay/WxTransferPayAbility.java | 64 +++++ .../jiayunet/wxPay/server/TransferServer.java | 172 ++++++++++++++ .../server/model/BillNoQueryRequest.java | 16 ++ .../wxPay/server/model/CancelRequest.java | 23 ++ .../wxPay/server/model/CancelResponse.java | 52 ++++ .../wxPay/server/model/JsRequestPay.java | 33 +++ .../server/model/TransferBillsRequest.java | 73 ++++++ .../server/model/TransferBillsResponse.java | 94 ++++++++ .../server/model/TransferNotification.java | 93 ++++++++ .../server/model/TransferResultResponse.java | 116 +++++++++ common/src/main/resources/logback-spring.xml | 65 +++++ manager/pom.xml | 28 +++ .../org/jiayunet/constant/OssBucketEnum.java | 36 +++ .../org/jiayunet/constant/OssPathEnum.java | 47 ++++ .../jiayunet/constant/SmsVerifyCodeEnum.java | 48 ++++ .../controller/HealthCheckController.java | 19 ++ .../jiayunet/controller/OssController.java | 30 +++ .../org/jiayunet/mapper/OssFileMapper.java | 11 + .../java/org/jiayunet/mapper/UserMapper.java | 15 ++ .../java/org/jiayunet/pojo/po/OssFile.java | 55 +++++ .../main/java/org/jiayunet/pojo/po/User.java | 91 +++++++ .../java/org/jiayunet/pojo/vo/OssUrlVo.java | 20 ++ .../java/org/jiayunet/server/OssServer.java | 111 +++++++++ pom.xml | 82 +++++++ settings.xml | 15 ++ 75 files changed, 5083 insertions(+) create mode 100644 .gitignore create mode 100644 client-api/pom.xml create mode 100644 client-api/src/main/java/org/jiayunet/ClientApplication.java create mode 100644 client-api/src/main/java/org/jiayunet/server/WxPayNotifyMessageAbstractImpl.java create mode 100644 client-api/src/main/resources/application-local.yml create mode 100644 client-api/src/main/resources/application.yml create mode 100644 common/pom.xml create mode 100644 common/src/main/java/org/jiayunet/aop/ControllerLogAspect.java create mode 100644 common/src/main/java/org/jiayunet/config/JacksonConfig.java create mode 100644 common/src/main/java/org/jiayunet/config/MybatisConfig.java create mode 100644 common/src/main/java/org/jiayunet/config/OssConfig.java create mode 100644 common/src/main/java/org/jiayunet/config/RedissonConf.java create mode 100644 common/src/main/java/org/jiayunet/config/SecurityConfig.java create mode 100644 common/src/main/java/org/jiayunet/config/SmsConfig.java create mode 100644 common/src/main/java/org/jiayunet/config/UpdateBatchMethod.java create mode 100644 common/src/main/java/org/jiayunet/config/WebConfig.java create mode 100644 common/src/main/java/org/jiayunet/config/WxPayConfig.java create mode 100644 common/src/main/java/org/jiayunet/constant/PreRedisKeyName.java create mode 100644 common/src/main/java/org/jiayunet/email/EmailAbility.java create mode 100644 common/src/main/java/org/jiayunet/exception/BusinessException.java create mode 100644 common/src/main/java/org/jiayunet/exception/BusinessExpCodeEnum.java create mode 100644 common/src/main/java/org/jiayunet/exception/BusinessExpCodeOperations.java create mode 100644 common/src/main/java/org/jiayunet/interceptor/BlackListInterceptor.java create mode 100644 common/src/main/java/org/jiayunet/interceptor/JwtAuthenticationTokenFilter.java create mode 100644 common/src/main/java/org/jiayunet/interceptor/LoggingOriginalRequestFilter.java create mode 100644 common/src/main/java/org/jiayunet/interceptor/PreventReplayInterceptor.java create mode 100644 common/src/main/java/org/jiayunet/interceptor/SqlLoggerInterceptor.java create mode 100644 common/src/main/java/org/jiayunet/interceptor/TraceIdFilter.java create mode 100644 common/src/main/java/org/jiayunet/mapper/CommonMapper.java create mode 100644 common/src/main/java/org/jiayunet/oss/AliOssAbility.java create mode 100644 common/src/main/java/org/jiayunet/pojo/UnifiedResponse.java create mode 100644 common/src/main/java/org/jiayunet/pojo/interceptor/RedisPreventReplayInfo.java create mode 100644 common/src/main/java/org/jiayunet/pojo/login/RedisLoginTokenInfo.java create mode 100644 common/src/main/java/org/jiayunet/sms/AliYunSmsAbility.java create mode 100644 common/src/main/java/org/jiayunet/sms/ISmsAbility.java create mode 100644 common/src/main/java/org/jiayunet/sms/VerificationConfig.java create mode 100644 common/src/main/java/org/jiayunet/sms/VerifyCodeAttribute.java create mode 100644 common/src/main/java/org/jiayunet/tool/AuthenticTool.java create mode 100644 common/src/main/java/org/jiayunet/tool/HttpIpTool.java create mode 100644 common/src/main/java/org/jiayunet/tool/HttpTool.java create mode 100644 common/src/main/java/org/jiayunet/tool/ObjectTool.java create mode 100644 common/src/main/java/org/jiayunet/tool/UserSecurityTool.java create mode 100644 common/src/main/java/org/jiayunet/tool/VerifyImageCodeUtils.java create mode 100644 common/src/main/java/org/jiayunet/tool/server/RedisServerTool.java create mode 100644 common/src/main/java/org/jiayunet/web/GlobalExceptionAdvice.java create mode 100644 common/src/main/java/org/jiayunet/web/UnifiedResponseBodyAdvice.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/WxJsPayAbility.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/WxNativePayAbility.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/WxPayNotifyController.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/WxPayNotifyMessageAbstract.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/WxTransferPayAbility.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/server/TransferServer.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/server/model/BillNoQueryRequest.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/server/model/CancelRequest.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/server/model/CancelResponse.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/server/model/JsRequestPay.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/server/model/TransferBillsRequest.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/server/model/TransferBillsResponse.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/server/model/TransferNotification.java create mode 100644 common/src/main/java/org/jiayunet/wxPay/server/model/TransferResultResponse.java create mode 100644 common/src/main/resources/logback-spring.xml create mode 100644 manager/pom.xml create mode 100644 manager/src/main/java/org/jiayunet/constant/OssBucketEnum.java create mode 100644 manager/src/main/java/org/jiayunet/constant/OssPathEnum.java create mode 100644 manager/src/main/java/org/jiayunet/constant/SmsVerifyCodeEnum.java create mode 100644 manager/src/main/java/org/jiayunet/controller/HealthCheckController.java create mode 100644 manager/src/main/java/org/jiayunet/controller/OssController.java create mode 100644 manager/src/main/java/org/jiayunet/mapper/OssFileMapper.java create mode 100644 manager/src/main/java/org/jiayunet/mapper/UserMapper.java create mode 100644 manager/src/main/java/org/jiayunet/pojo/po/OssFile.java create mode 100644 manager/src/main/java/org/jiayunet/pojo/po/User.java create mode 100644 manager/src/main/java/org/jiayunet/pojo/vo/OssUrlVo.java create mode 100644 manager/src/main/java/org/jiayunet/server/OssServer.java create mode 100644 pom.xml create mode 100644 settings.xml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e58ef9d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*/target +**/.idea +.idea/ +./log +./logs \ No newline at end of file diff --git a/client-api/pom.xml b/client-api/pom.xml new file mode 100644 index 0000000..2505a5b --- /dev/null +++ b/client-api/pom.xml @@ -0,0 +1,50 @@ + + 4.0.0 + + org.jiayunet + back_end + 1.0-SNAPSHOT + + + client-api + 1.0-SNAPSHOT + + + + + org.jiayunet + manager + 1.0-SNAPSHOT + + + + client-api + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + org.springframework.boot + spring-boot-maven-plugin + 2.6.13 + + true + + + + + repackage + + + + + + + + diff --git a/client-api/src/main/java/org/jiayunet/ClientApplication.java b/client-api/src/main/java/org/jiayunet/ClientApplication.java new file mode 100644 index 0000000..c098333 --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/ClientApplication.java @@ -0,0 +1,21 @@ +package org.jiayunet; + +import org.mybatis.spring.annotation.MapperScan; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.scheduling.annotation.EnableScheduling; + +/** + * @author zk + */ +@SpringBootApplication(scanBasePackages = "org.jiayunet") +@MapperScan("org.jiayunet.**.mapper") +@EnableScheduling +@EnableAspectJAutoProxy +public class ClientApplication { + public static void main(String[] args) { + ApplicationContext context = SpringApplication.run(ClientApplication.class, args); + } +} diff --git a/client-api/src/main/java/org/jiayunet/server/WxPayNotifyMessageAbstractImpl.java b/client-api/src/main/java/org/jiayunet/server/WxPayNotifyMessageAbstractImpl.java new file mode 100644 index 0000000..cf289cb --- /dev/null +++ b/client-api/src/main/java/org/jiayunet/server/WxPayNotifyMessageAbstractImpl.java @@ -0,0 +1,42 @@ +package org.jiayunet.server; + +import com.wechat.pay.java.service.partnerpayments.nativepay.model.Transaction; +import com.wechat.pay.java.service.refund.model.RefundNotification; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jiayunet.wxPay.WxPayNotifyMessageAbstract; +import org.springframework.context.annotation.Primary; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +/** + * @author zk + */ +@Service +@Primary +@Slf4j +@AllArgsConstructor +public class WxPayNotifyMessageAbstractImpl implements WxPayNotifyMessageAbstract { + + + /** + * 支付回调 + * + * @param transaction 支付回调消息 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void payMessageHandle(Transaction transaction) { + String outTradeNo = transaction.getOutTradeNo(); + } + + /** + * 退款回调 + * + * @param refundNotification 退款回调消息 + */ + @Override + public void refundMessageHandle(RefundNotification refundNotification) { + + } +} diff --git a/client-api/src/main/resources/application-local.yml b/client-api/src/main/resources/application-local.yml new file mode 100644 index 0000000..4242c74 --- /dev/null +++ b/client-api/src/main/resources/application-local.yml @@ -0,0 +1,81 @@ +# tomcat 端口配置 +server: + port: 8080 +# 数据源配置 +spring: + datasource: + url: jdbc:mysql://${MYSQL_HOST:8.138.180.255}:${MYSQL_PORT:3306}/${DB_NAME:aiResume}?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true + username: ${MYSQL_USERNAME:root} + password: ${MYSQL_PASSWORD:qwertyuiopasdfghjkl.zxcvbnm} + driver-class-name: com.mysql.cj.jdbc.Driver + redis: + host: ${REDIS_HOST:8.138.180.255} + password: ${REDIS_PASSWORD:qwertyuiopasdfghjkl.zxcvbnm} + port: ${REDIS_PORT:6379} + timeout: 8s + +# 电子邮箱 +email: + status: close + account: ${EMAIL_ACCOUNT:sim18502043706@163.com} + authorization: ${EMAIL_AUTHORIZATION:CHBCVPYGFZCSUNCP} + +# 微信支付 +wx_pay: + # status close:关闭 open:打开 + status: close + merchant_id: ${MERCHANT_ID:1705334978} + privateKey_path: ${PRIVATEKEY_PATH:E:\wxPayCertify\1705334978_20250131_cert\apiclient_key.pem} + merchant_serial_number: ${MERCHANT_SERIAL_NUMBER:354F4A0BD7A11CB756ED3BC380A0E41442DE68F7} + api_v3_key: ${API_V3_KEY:zhyahjkiysetindhGseuHadjKITshons} + public_key_path: ${PUBLIC_KEY_PATH:E:\wxPayCertify\pub_key.pem} + public_key_id: ${PUBLIC_KEY_ID:PUB_KEY_ID_0117053349782025013000337100001480} + #回调域名地址 + notify_domain: ${API_NOTIFY_DOMAIN:http://8.138.180.255:8080/api/} + +app: + # 加密秘钥配置 + secret: + token: ${SECRET_TOKEN:Aa123123} + + #登陆配置 + login: + # 登陆图片验证相关 + login_image_verification: + enable: false + images_code_sources: 123456789ABCDEFGHJKLMNPQRSTUVWXYZ + images_w: 111 + images_h: 36 + # token配置 + token: + exceed_time: 129600 + #设备在线数量 + device_online_quantity: 10 + + # 短信 + sms: + service_provider: aliyun + aliyun: + access_key_id: LTAI5tGRFuJmt6nrxRqZYC7z + access_key_secret: xLC3kNSmCvEtsc9j4O8g3rOs70QjZQ + + oss: + service_provider: aliyun + aliyun: + access_key_id: LTAI5tGRFuJmt6nrxRqZYC7z + access_key_secret: xLC3kNSmCvEtsc9j4O8g3rOs70QjZQ + + # 防刷配置 20秒 20次 + prevent_replay: + if_open: false + interval_time: 20 + limit_number: 20 + + # 微信小程序 + wx: + app_id: wx7d75957cbd35e36b + app_secret: d70ef5baa4b9c046f396736d9a42cdc3 + + #开放接口 + ignore: + urls: "/public/**" \ No newline at end of file diff --git a/client-api/src/main/resources/application.yml b/client-api/src/main/resources/application.yml new file mode 100644 index 0000000..a36e901 --- /dev/null +++ b/client-api/src/main/resources/application.yml @@ -0,0 +1,70 @@ +# tomcat config +server: + tomcat: + accept-count: 100 + threads: + max: 200 + min-spare: 10 + servlet: + context-path: /api +# spring config +spring: + application: + name: client + profiles: + active: ${PROFILES_ACTIVE:local} + servlet: + multipart: + max-file-size: 4MB + max-request-size: 20MB + mvc: + throw-exception-if-no-handler-found: true + web: + resources: + add-mappings: false + cache: + redis: + cache-null-values: true + key-prefix: "${spring.application.name}:" + time-to-live: 24h + jackson: + default-property-inclusion: non_null + property-naming-strategy: LOWER_CAMEL_CASE + serialization: + fail-on-empty-beans: false + write-date-keys-as-timestamps: true + write-date-timestamps-as-nanoseconds: false + write-dates-as-timestamps: true + deserialization: + fail-on-unknown-properties: false + fail-on-numbers-for-enums: true + read-date-timestamps-as-nanoseconds: false +# mybatis plus config +mybatis-plus: + mapper-locations: classpath*:mapper/**/*.xml + global-config: + db-config: + id-type: assign_id + insert-strategy: not_null + update-strategy: not_null + logic-delete-field: isDelete + logic-delete-value: '1' + logic-not-delete-value: '0' + configuration: + map-underscore-to-camel-case: true + auto-mapping-unknown-column-behavior: warning + cache-enabled: false + call-setters-on-nulls: true + jdbc-type-for-null: 'null' + +#日志 + +logging: + level: + org: + mybatis: info + + + + + diff --git a/common/pom.xml b/common/pom.xml new file mode 100644 index 0000000..a51ffa7 --- /dev/null +++ b/common/pom.xml @@ -0,0 +1,105 @@ + + 4.0.0 + + org.jiayunet + back_end + 1.0-SNAPSHOT + + + common + 1.0-SNAPSHOT + + + + org.apache.httpcomponents + httpmime + 4.5.13 + + + org.springframework.boot + spring-boot-starter-web + + + + + org.aspectj + aspectjrt + 1.9.7 + + + org.aspectj + aspectjweaver + 1.9.7 + + + + org.springframework.boot + spring-boot-starter-validation + + + com.baomidou + mybatis-plus-boot-starter + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.redisson + redisson-spring-boot-starter + 3.17.7 + + + mysql + mysql-connector-java + + + com.auth0 + java-jwt + + + com.github.wechatpay-apiv3 + wechatpay-java + 0.2.15 + + + com.aliyun + dysmsapi20170525 + + + com.aliyun.oss + aliyun-sdk-oss + + + org.springframework.boot + spring-boot-starter-security + + + com.sun.mail + javax.mail + + + commons-io + commons-io + 2.11.0 + + + org.apache.httpcomponents + httpclient + 4.5.13 + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + 17 + 17 + + + + + diff --git a/common/src/main/java/org/jiayunet/aop/ControllerLogAspect.java b/common/src/main/java/org/jiayunet/aop/ControllerLogAspect.java new file mode 100644 index 0000000..9651808 --- /dev/null +++ b/common/src/main/java/org/jiayunet/aop/ControllerLogAspect.java @@ -0,0 +1,56 @@ +package org.jiayunet.aop; + +import lombok.extern.slf4j.Slf4j; +import org.aspectj.lang.JoinPoint; +import org.aspectj.lang.annotation.*; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +@Aspect +@Component +@Slf4j +public class ControllerLogAspect { + + + // 定义切点:所有Controller包下的方法 + @Pointcut("execution(* org.jiayunet..controller..*.*(..))") + public void controllerPointcut() { + } + + // 方法执行前记录请求信息 + @Before("controllerPointcut()") + public void logBefore(JoinPoint joinPoint) { + String className = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); + + // 处理参数,过滤掉HttpServletRequest/Response和文件流 + List filteredArgs = Arrays.stream(joinPoint.getArgs()).filter(arg -> !(arg instanceof HttpServletRequest || arg instanceof HttpServletResponse || arg instanceof MultipartFile || (arg != null && arg.getClass().isArray() && MultipartFile.class.isAssignableFrom(arg.getClass().getComponentType())))).collect(Collectors.toList()); + + log.info("接口参数 方法:{} {}" + "参数: {} ", className, methodName, filteredArgs); + } + + // 方法执行后记录返回结果 + @AfterReturning(pointcut = "controllerPointcut()", returning = "result") + public void logAfterReturning(JoinPoint joinPoint, Object result) { + log.info("接口返回:{}", result); + } + + // 方法异常时记录错误信息 + @AfterThrowing(pointcut = "controllerPointcut()", throwing = "ex") + public void logAfterThrowing(JoinPoint joinPoint, Throwable ex) { + String className = joinPoint.getTarget().getClass().getSimpleName(); + String methodName = joinPoint.getSignature().getName(); + + log.error(" 类名: {} 方法: {} 异常: {} 堆栈: {}", className, methodName, ex.getMessage(), getStackTrace(ex)); + } + + private String getStackTrace(Throwable ex) { + return Arrays.stream(ex.getStackTrace()).map(StackTraceElement::toString).collect(Collectors.joining("\n")); + } +} \ No newline at end of file diff --git a/common/src/main/java/org/jiayunet/config/JacksonConfig.java b/common/src/main/java/org/jiayunet/config/JacksonConfig.java new file mode 100644 index 0000000..ae26be7 --- /dev/null +++ b/common/src/main/java/org/jiayunet/config/JacksonConfig.java @@ -0,0 +1,48 @@ +package org.jiayunet.config; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import org.springframework.boot.jackson.JsonComponent; +import org.springframework.context.annotation.Bean; +import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; + +import java.io.IOException; + +/** + * Jackson 全局配置 + *

+ * 将 Long/long 类型序列化为字符串,避免 JavaScript 精度丢失问题 + */ +@JsonComponent +public class JacksonConfig { + + private static final JsonSerializer LONG_SERIALIZER = new JsonSerializer() { + @Override + public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(value.toString()); + } + }; + + private static final JsonDeserializer LONG_DESERIALIZER = new JsonDeserializer() { + @Override + public Long deserialize(JsonParser p, DeserializationContext ctx) throws IOException { + String text = p.getText(); + return (text == null || text.trim().isEmpty()) ? null : Long.parseLong(text.trim()); + } + }; + + @Bean + public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) { + return builder + .serializerByType(Long.class, LONG_SERIALIZER) + .serializerByType(long.class, LONG_SERIALIZER) + .deserializerByType(Long.class, LONG_DESERIALIZER) + .deserializerByType(long.class, LONG_DESERIALIZER) + .build(); + } +} diff --git a/common/src/main/java/org/jiayunet/config/MybatisConfig.java b/common/src/main/java/org/jiayunet/config/MybatisConfig.java new file mode 100644 index 0000000..461be66 --- /dev/null +++ b/common/src/main/java/org/jiayunet/config/MybatisConfig.java @@ -0,0 +1,49 @@ +package org.jiayunet.config; + + + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.injector.DefaultSqlInjector; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import com.baomidou.mybatisplus.extension.injector.methods.InsertBatchSomeColumn; +import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; +import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.util.List; + +/** + * @author zk + */ +@Configuration +public class MybatisConfig { + /** + * SQL注入器 + * 在默认的基础上田间InsertBatchSomeColumn + */ + @Bean + public DefaultSqlInjector easySqlInjector() { + return new DefaultSqlInjector(){ + @Override + public List getMethodList(Class mapperClass, TableInfo tableInfo) { + List methodList = super.getMethodList(mapperClass,tableInfo); + methodList.add(new InsertBatchSomeColumn()); + methodList.add(new UpdateBatchMethod("updateBatchMethod")); + return methodList; + } + }; + } + + /** + * 拦截器实现得分页插件 + */ + @Bean + public MybatisPlusInterceptor mybatisPlusInterceptor() { + MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); + // 添加分页插件 + interceptor.addInnerInterceptor(new PaginationInnerInterceptor()); + return interceptor; + } + +} diff --git a/common/src/main/java/org/jiayunet/config/OssConfig.java b/common/src/main/java/org/jiayunet/config/OssConfig.java new file mode 100644 index 0000000..c4af2f9 --- /dev/null +++ b/common/src/main/java/org/jiayunet/config/OssConfig.java @@ -0,0 +1,36 @@ +package org.jiayunet.config; + +import com.aliyun.oss.common.auth.CredentialsProviderFactory; +import com.aliyun.oss.common.auth.DefaultCredentialProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.Assert; + +@Configuration +public class OssConfig { + + /** + * 阿里云配置 + */ + @Value("${app.oss.aliyun.access_key_id}") + private String accessKeyId; + @Value("${app.oss.aliyun.access_key_secret}") + private String accessKeySecret; + + /** + * 密码凭证提供者 + */ + @Bean + @ConditionalOnProperty(name = "app.oss.service_provider", havingValue = "aliyun") + public DefaultCredentialProvider aliOssCredentialProvider() { + Assert.hasText(accessKeyId, "app.oss.aliyun.access_key_id配置缺失"); + Assert.hasText(accessKeySecret, "app.oss.aliyun.access_key_secret配置缺失"); + return CredentialsProviderFactory.newDefaultCredentialProvider(accessKeyId,accessKeySecret); + } + + + + +} diff --git a/common/src/main/java/org/jiayunet/config/RedissonConf.java b/common/src/main/java/org/jiayunet/config/RedissonConf.java new file mode 100644 index 0000000..812b52d --- /dev/null +++ b/common/src/main/java/org/jiayunet/config/RedissonConf.java @@ -0,0 +1,35 @@ +package org.jiayunet.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.redisson.config.Config; +/** + * @author zk + */ +@Configuration +public class RedissonConf { + /** + * redis 配置 + */ + @Value("${spring.redis.host}") + private String host; + @Value("${spring.redis.password}") + private String password; + @Value("${spring.redis.port}") + private String port; + + + @Bean + public Config redissonConfig() { + Config config = new Config(); + config.useSingleServer() + .setAddress("redis://"+host+":"+port) + .setPassword(password) + .setDatabase(1); + return config; + } + + + +} diff --git a/common/src/main/java/org/jiayunet/config/SecurityConfig.java b/common/src/main/java/org/jiayunet/config/SecurityConfig.java new file mode 100644 index 0000000..acf1ce2 --- /dev/null +++ b/common/src/main/java/org/jiayunet/config/SecurityConfig.java @@ -0,0 +1,95 @@ +package org.jiayunet.config; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.MediaType; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.cors.CorsConfiguration; +import org.springframework.web.cors.CorsConfigurationSource; +import org.springframework.web.cors.UrlBasedCorsConfigurationSource; +import org.jiayunet.interceptor.JwtAuthenticationTokenFilter; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * @author zk + */ +@Configuration +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Value("${app.ignore.urls}") + private String ignoreUrls; + @Autowired + JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + /** + * 处理跨域问题 + */ + CorsConfigurationSource corsConfigurationSource(){ + CorsConfiguration config = new CorsConfiguration(); + config.setAllowedHeaders(List.of("*")); + config.setAllowedMethods(List.of("*")); + config.setAllowedOrigins(List.of("*")); + //config.setAllowCredentials(true); + config.setMaxAge(3600L); + + UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); + source.registerCorsConfiguration("/**", config); + return source; + } + @Override + protected void configure(HttpSecurity http) throws Exception { + + http + .csrf().disable().httpBasic().disable().formLogin().disable().logout().disable() + // 不通过Session获取SecurityContext + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests() + // 对于登录接口 允许匿名访问 + .antMatchers(ignoreUrls.split(",")).permitAll() + // 除上面外的所有请求全部需要鉴权认证 + .anyRequest().authenticated(); + + http.cors().configurationSource(corsConfigurationSource()); + + + // 把token校验过滤器添加到过滤器链中 + http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + + http.exceptionHandling().authenticationEntryPoint((request, response, authException) -> { + Map errorResponse = new HashMap<>(); + errorResponse.put("status", HttpServletResponse.SC_UNAUTHORIZED); + errorResponse.put("message", "Authentication failed"); + + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + + // 将错误消息写入响应体 + ObjectMapper mapper = new ObjectMapper(); + mapper.writeValue(response.getWriter(), errorResponse); + }); + } + + +} diff --git a/common/src/main/java/org/jiayunet/config/SmsConfig.java b/common/src/main/java/org/jiayunet/config/SmsConfig.java new file mode 100644 index 0000000..42ab971 --- /dev/null +++ b/common/src/main/java/org/jiayunet/config/SmsConfig.java @@ -0,0 +1,37 @@ +package org.jiayunet.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.Assert; + +/** + * @author zk + */ + +@Configuration +public class SmsConfig { + + /** + * 阿里云配置 + */ + @Value("${app.sms.aliyun.access_key_id}") + private String accessKeyId; + @Value("${app.sms.aliyun.access_key_secret}") + private String accessKeySecret; + + + @Bean + @ConditionalOnProperty(name = "app.sms.service_provider", havingValue = "aliyun") + public com.aliyun.dysmsapi20170525.Client aliyunSmsClient() throws Exception { + Assert.hasText(accessKeyId, "app.sms.aliyun.access_key_id配置缺失"); + Assert.hasText(accessKeySecret, "app.sms.aliyun.access_key_secret配置缺失"); + + com.aliyun.teaopenapi.models.Config config = + new com.aliyun.teaopenapi.models.Config().setAccessKeyId(accessKeyId).setAccessKeySecret(accessKeySecret); + config.endpoint = "dysmsapi.aliyuncs.com"; + return new com.aliyun.dysmsapi20170525.Client(config); + } + +} diff --git a/common/src/main/java/org/jiayunet/config/UpdateBatchMethod.java b/common/src/main/java/org/jiayunet/config/UpdateBatchMethod.java new file mode 100644 index 0000000..799eb03 --- /dev/null +++ b/common/src/main/java/org/jiayunet/config/UpdateBatchMethod.java @@ -0,0 +1,26 @@ +package org.jiayunet.config; + +import com.baomidou.mybatisplus.core.injector.AbstractMethod; +import com.baomidou.mybatisplus.core.metadata.TableInfo; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; /** + * @author zk + */ +public class UpdateBatchMethod extends AbstractMethod { + protected UpdateBatchMethod(String methodName) { + super(methodName); + } + + @Override + public MappedStatement injectMappedStatement(Class mapperClass, Class modelClass, TableInfo tableInfo) { + String sql = ""; + String additional = tableInfo.isWithVersion() ? tableInfo.getVersionFieldInfo().getVersionOli("item", "item.") : "" + tableInfo.getLogicDeleteSql(true, true); + String setSql = sqlSet(tableInfo.isWithLogicDelete(), false, tableInfo, false, "item", "item."); + String sqlResult = String.format(sql, tableInfo.getTableName(), setSql, tableInfo.getKeyColumn(), "item." + tableInfo.getKeyProperty(), additional); + //log.debug("sqlResult----->{}", sqlResult); + SqlSource sqlSource = languageDriver.createSqlSource(configuration, sqlResult, modelClass); + // 第三个参数必须和RootMapper的自定义方法名一致 + return this.addUpdateMappedStatement(mapperClass, modelClass, "updateBatchMethod", sqlSource); + } + +} diff --git a/common/src/main/java/org/jiayunet/config/WebConfig.java b/common/src/main/java/org/jiayunet/config/WebConfig.java new file mode 100644 index 0000000..d6c3e03 --- /dev/null +++ b/common/src/main/java/org/jiayunet/config/WebConfig.java @@ -0,0 +1,28 @@ +package org.jiayunet.config; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import org.jiayunet.interceptor.BlackListInterceptor; +import org.jiayunet.interceptor.PreventReplayInterceptor; + +/** + * @author zk + */ +@Configuration +public class WebConfig implements WebMvcConfigurer { + @Autowired + private PreventReplayInterceptor preventReplayInterceptor; + @Autowired + private BlackListInterceptor blackListInterceptor; + + + //配置拦截器 + @Override + public void addInterceptors(InterceptorRegistry registry) { + registry.addInterceptor(blackListInterceptor).addPathPatterns("/**").order(1); + registry.addInterceptor(preventReplayInterceptor).addPathPatterns("/**").order(2); + } + +} diff --git a/common/src/main/java/org/jiayunet/config/WxPayConfig.java b/common/src/main/java/org/jiayunet/config/WxPayConfig.java new file mode 100644 index 0000000..992f832 --- /dev/null +++ b/common/src/main/java/org/jiayunet/config/WxPayConfig.java @@ -0,0 +1,142 @@ +package org.jiayunet.config; + +import com.wechat.pay.java.core.RSAPublicKeyConfig; +import com.wechat.pay.java.core.notification.NotificationParser; +import com.wechat.pay.java.core.util.PemUtil; +import com.wechat.pay.java.service.payments.nativepay.NativePayService; +import com.wechat.pay.java.service.payments.jsapi.JsapiService; +import com.wechat.pay.java.service.refund.RefundService; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.Assert; +import org.jiayunet.wxPay.server.TransferServer; + +import java.security.PrivateKey; + + +/** + * @author zk + */ +@Configuration +public class WxPayConfig { + /** + * 商户号 + */ + @Value("${wx_pay.merchant_id}") + private String merchantId = ""; + /** + * 商户API私钥路径 + */ + @Value("${wx_pay.privateKey_path}") + private String privateKeyPath = ""; + /** + * 商户证书序列号 + */ + @Value("${wx_pay.merchant_serial_number}") + private String merchantSerialNumber = ""; + /** + * 商户APIV3密钥 + */ + @Value("${wx_pay.api_v3_key}") + private String apiV3Key = ""; + /** + * 平台公钥地址 + */ + @Value("${wx_pay.public_key_path}") + private String publicKeyPath = ""; + /** + * 平台公钥地址ID + */ + @Value("${wx_pay.public_key_id}") + private String publicKeyId = ""; + + + /** + * 微信自动签名验签配置 + */ + @Bean + @ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") + public RSAPublicKeyConfig rSAAutoCertificateConfig() { + Assert.hasText(merchantId, "微信支付的商户号不能为空"); + Assert.hasText(privateKeyPath, "商户API私钥路径不能为空"); + Assert.hasText(merchantSerialNumber, "商户证书序列号不能为空"); + Assert.hasText(apiV3Key, "商户APIV3密钥不能为空"); + Assert.hasText(publicKeyPath, "微信支付平台公钥地址不能为空"); + Assert.hasText(publicKeyId, "微信支付平台公钥Id不能为空"); + + return new RSAPublicKeyConfig.Builder() + .merchantId(merchantId) + .privateKeyFromPath(privateKeyPath) + .merchantSerialNumber(merchantSerialNumber) + .apiV3Key(apiV3Key) + .publicKeyFromPath(publicKeyPath) + .publicKeyId(publicKeyId) + .build(); + } + + + /** + * 微信JsApi支付接口能能力server + */ + @Bean + @ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") + public JsapiService jsapiService(RSAPublicKeyConfig rSAPublicKeyConfig) { + return new JsapiService.Builder().config(rSAPublicKeyConfig).build(); + } + + + /** + * 微信Native支付接口能能力server + */ + @Bean + @ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") + public NativePayService nativePayService(RSAPublicKeyConfig rSAPublicKeyConfig) { + return new NativePayService.Builder().config(rSAPublicKeyConfig).build(); + } + + /** + * 微信单家付款接口能能力server + */ + @Bean + @ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") + public TransferServer transferServer(RSAPublicKeyConfig rSAPublicKeyConfig) { + return new TransferServer.Builder().config(rSAPublicKeyConfig).build(); + } + + + + /** + * 微信支付退款能力server + */ + @Bean + @ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") + public RefundService refundService(RSAPublicKeyConfig rSAPublicKeyConfig) { + return new RefundService.Builder().config(rSAPublicKeyConfig).build(); + } + + /** + * 通知解析器 + */ + @Bean + @ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") + public NotificationParser notificationParser(RSAPublicKeyConfig rSAPublicKeyConfig) { + return new NotificationParser(rSAPublicKeyConfig); + } + + + /** + * 商户私钥 + */ + + @Bean + @ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") + public PrivateKey privateKey(){ + return PemUtil.loadPrivateKeyFromPath(privateKeyPath); + } + + + + +} diff --git a/common/src/main/java/org/jiayunet/constant/PreRedisKeyName.java b/common/src/main/java/org/jiayunet/constant/PreRedisKeyName.java new file mode 100644 index 0000000..dc537b9 --- /dev/null +++ b/common/src/main/java/org/jiayunet/constant/PreRedisKeyName.java @@ -0,0 +1,35 @@ +package org.jiayunet.constant; + +/** + * redisKey + * + * @author zk + */ +public interface PreRedisKeyName { + + /** + * 防刷key名 + */ + String PREVENT_REPLAY = "preventReplay:ip:"; + /** + * 黑名单ip + */ + String BLACK_LIST = "blackList:ip:"; + /** + * 单次执行任务Id + */ + String EXECUTE_SINGLE_TASK = "execute:single-task-id:"; + + + /** + * token Key名 前缀 + */ + String LOGIN_TOKEN = "login:token:"; + + /** + * 图片验证码Key名 前缀 + */ + String LOGIN_IMAGES_CODE_UUID = "login:images:uuid:"; + + +} diff --git a/common/src/main/java/org/jiayunet/email/EmailAbility.java b/common/src/main/java/org/jiayunet/email/EmailAbility.java new file mode 100644 index 0000000..fef4ed0 --- /dev/null +++ b/common/src/main/java/org/jiayunet/email/EmailAbility.java @@ -0,0 +1,111 @@ +package org.jiayunet.email; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +import javax.mail.Authenticator; +import javax.mail.Message; +import javax.mail.MessagingException; +import javax.mail.PasswordAuthentication; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; +import java.util.Properties; + +/** + * @author zk + */ +/** + * @author zk + */ +@Component +@ConditionalOnProperty(name = "email.status", havingValue = "open") +@Slf4j +public class EmailAbility { + + /** + * 发送邮箱账户 + */ + @Value("${email.account}") + private String emailAccount; + /** + * 发送邮箱授权码 + */ + @Value("${email.authorization}") + private String authorization; + + + /** + * 获取连接 + * @return 连接Session + */ + private Session createSession() { + + Assert.isTrue(StringUtils.hasText(authorization) && StringUtils.hasText(authorization),"邮箱账户授权为配置"); + + //连接配置 勿动 163 + Properties props = new Properties(); + props.put("mail.smtp.host", "smtp.163.com"); + props.put("mail.smtp.port", "25"); + props.put("mail.smtp.auth", "true"); + props.put("mail.smtp.starttls.enable", "true"); + Session session = Session.getInstance(props,new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(emailAccount,authorization); + } + }); + + log.info("获取邮件发送Session成功"); + //session.setDebug(true); + return session; + } + + /** + * 发送邮件 + * @param emailAdder 收信人地址 + * @param subject 邮件主题 + * @param content 邮件类容 + * @return 是否成功 + */ + public boolean send(String emailAdder,String subject, String content) { + + //获取session + Session session = createSession(); + + try { + + //构建发送消息 + MimeMessage message=new MimeMessage(session); + //设置邮件主题 + message.setSubject(subject); + //设置邮件内容 + message.setText(content); + //设置发件人 + message.setFrom(new InternetAddress(emailAccount)); + //设置收件人 + message.setRecipient(Message.RecipientType.TO, new InternetAddress(emailAdder)); + + //发送消息 + Transport.send(message); + + } catch (MessagingException e) { + e.printStackTrace(); + log.error(e.getMessage()); + return false; + } + + return true; + } + + + + + + +} diff --git a/common/src/main/java/org/jiayunet/exception/BusinessException.java b/common/src/main/java/org/jiayunet/exception/BusinessException.java new file mode 100644 index 0000000..f1edc56 --- /dev/null +++ b/common/src/main/java/org/jiayunet/exception/BusinessException.java @@ -0,0 +1,77 @@ +package org.jiayunet.exception; + +/** + * @author zk + */ +public class BusinessException extends RuntimeException { + + private static final String DELIMITER = " "; + private final BusinessExpCodeOperations expCode; + private Object data; + private String description; + private Boolean replace = true; + + public BusinessException(BusinessExpCodeOperations expCode) { + super(String.join(" ", expCode.getCode(), expCode.getMsg())); + this.expCode = expCode; + } + + public BusinessException(BusinessExpCodeOperations expCode, String description) { + super(String.join(" ", expCode.getCode(), expCode.getMsg(), description)); + this.expCode = expCode; + this.description = description; + } + + public BusinessException(BusinessExpCodeOperations expCode, String description, boolean replace) { + super(String.join(" ", expCode.getCode(), expCode.getMsg(), description)); + this.expCode = expCode; + this.description = description; + this.replace = replace; + } + + public BusinessException(BusinessExpCodeOperations expCode, Throwable throwable) { + super(String.join(" ", expCode.getCode(), expCode.getMsg()), throwable); + this.expCode = expCode; + } + + public BusinessException(BusinessExpCodeOperations expCode, String description, Throwable throwable) { + super(String.join(" ", expCode.getCode(), expCode.getMsg(), description), throwable); + this.expCode = expCode; + this.description = description; + } + + public BusinessException(BusinessExpCodeOperations expCode, String description, Object data) { + super(String.join(" ", expCode.getCode(), expCode.getMsg(), description)); + this.expCode = expCode; + this.description = description; + this.data = data; + } + + public BusinessException(BusinessExpCodeOperations expCode, String description, boolean replace, Object data) { + super(String.join(" ", expCode.getCode(), expCode.getMsg(), description)); + this.expCode = expCode; + this.description = description; + this.replace = replace; + this.data = data; + } + + public String getBusinessCode() { + return this.expCode.getCode(); + } + + public String getBusinessMsg() { + return this.expCode.getMsg(); + } + + public Object getData() { + return this.data; + } + + public String getDescription() { + return this.description; + } + + public Boolean getReplace() { + return this.replace; + } +} diff --git a/common/src/main/java/org/jiayunet/exception/BusinessExpCodeEnum.java b/common/src/main/java/org/jiayunet/exception/BusinessExpCodeEnum.java new file mode 100644 index 0000000..c107f21 --- /dev/null +++ b/common/src/main/java/org/jiayunet/exception/BusinessExpCodeEnum.java @@ -0,0 +1,50 @@ +package org.jiayunet.exception; + +/** + * @author zk + */ + +public enum BusinessExpCodeEnum implements BusinessExpCodeOperations { + + UNKNOWN_ERROR("系统忙不过来了, 稍后再试试"), + PARAMS_INCORRECT("接口参数错误"), + TOKEN_EXPIRED("登录已过期, 请重新登录"), + TOKEN_INCORRECT("访问令牌异常, 请重新登录"), + NOT_LOGGED_IN("未登录"), + USER_EXISTED("用户已存在"), + USER_NOT_EXIST("用户不存在"), + ACCOUNT_EXISTED("账号已存在"), + ACCOUNT_NOT_EXIST("账号不存在"), + ACCOUNT_DISABLED("账号已停用"), + ACCOUNT_UNAUTHORIZED("账号未认证"), + PASSWORD_INCORRECT("密码不正确"), + ACCOUNT_OR_PASSWORD_INCORRECT("账号或密码不正确"), + PHONE_EXISTED("手机号已存在"), + PHONE_NOT_EXIST("手机号不存在"), + BAD_CAPTCHA("验证码不正确"), + BAD_EXPIRED("验证码已失效"), + FAILED_SEND_CAPTCHA("验证码发送失败"), + REPEAT_SEND_CAPTCHA("验证码已发送过, 注意查收"), + TOO_MANY_REQUESTS("请求过多, API限流"), + REPEAT_REQUEST("请勿重复请求"), + PERMISSION_DENIED("无权访问"), + READ_ONLY("不可修改或删除"), + DATA_EXISTED("数据已存在"), + DATA_NOT_EXIST("数据不存在"), + DATA_EXIST_RELATION("数据存在关联"), + DATA_DUPLICATED("数据重复"); + + private final String msg; + + BusinessExpCodeEnum(String msg) { + this.msg = msg; + } + + public String getCode() { + return this.name(); + } + + public String getMsg() { + return this.msg; + } +} diff --git a/common/src/main/java/org/jiayunet/exception/BusinessExpCodeOperations.java b/common/src/main/java/org/jiayunet/exception/BusinessExpCodeOperations.java new file mode 100644 index 0000000..77afe0d --- /dev/null +++ b/common/src/main/java/org/jiayunet/exception/BusinessExpCodeOperations.java @@ -0,0 +1,10 @@ +package org.jiayunet.exception; + +/** + * @author zk + */ +public interface BusinessExpCodeOperations { + String getCode(); + + String getMsg(); +} diff --git a/common/src/main/java/org/jiayunet/interceptor/BlackListInterceptor.java b/common/src/main/java/org/jiayunet/interceptor/BlackListInterceptor.java new file mode 100644 index 0000000..25f4d04 --- /dev/null +++ b/common/src/main/java/org/jiayunet/interceptor/BlackListInterceptor.java @@ -0,0 +1,29 @@ +package org.jiayunet.interceptor; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.jiayunet.constant.PreRedisKeyName; +import org.jiayunet.tool.HttpIpTool; +import org.jiayunet.tool.server.RedisServerTool; + +/** + * 禁止黑名单 + * + * @author zk + */ +@Component +public class BlackListInterceptor implements HandlerInterceptor { + @Autowired + private RedisServerTool redisServerTool; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + String key = PreRedisKeyName.BLACK_LIST + HttpIpTool.gteRealIP(request); + + return Boolean.FALSE.equals(redisServerTool.hasKey(key)); + } +} diff --git a/common/src/main/java/org/jiayunet/interceptor/JwtAuthenticationTokenFilter.java b/common/src/main/java/org/jiayunet/interceptor/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..e9d435e --- /dev/null +++ b/common/src/main/java/org/jiayunet/interceptor/JwtAuthenticationTokenFilter.java @@ -0,0 +1,165 @@ +package org.jiayunet.interceptor; + +import java.io.IOException; +import java.time.Instant; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.slf4j.MDC; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; +import org.jiayunet.constant.PreRedisKeyName; +import org.jiayunet.pojo.login.RedisLoginTokenInfo; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; + +import lombok.extern.slf4j.Slf4j; +import org.jiayunet.tool.server.RedisServerTool; + +/** + * jwt过滤器 + * + * @author zk + */ +@Component +@Slf4j +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { + + /** + * 加密密钥 + */ + @Value("${app.secret.token:sh.0807.}") + private String secret; + /** + * 在线设备数量 + */ + @Value("${app.login.device_online_quantity:5}") + private int deviceOnlineQuantity; + /** + * token过期时间 + */ + @Value("${app.login.token.exceed_time:43200}") + private int tokenExceedTime; + /** + * 忽略请求路径 + */ + @Value("${app.ignore.urls}") + private String ignoreUrls; + + /** + * 全局配置路径 + */ + @Value("${server.servlet.context-path}") + private String contextPath; + + @Autowired + private RedisServerTool redisServerTool; + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) + throws ServletException, IOException { + + // 忽略接口放行 + if (ifCurrentUrl(ignoreUrls, request.getRequestURI())) { + filterChain.doFilter(request, response); + return; + } + + // 获取token + String token = request.getHeader("Token"); + if (!StringUtils.hasText(token)) { + // 放行 + filterChain.doFilter(request, response); + return; + } + + + // 验证token + Algorithm algorithm = Algorithm.HMAC256(secret); + DecodedJWT decodedJwt = JWT.require(algorithm).build().verify(token); + Long userId = decodedJwt.getClaim("userId").asLong(); + String uuId = decodedJwt.getClaim("uuId").asString(); + + // 获取redis信息 + String redisKey = PreRedisKeyName.LOGIN_TOKEN + userId; + RedisLoginTokenInfo info = null; + info = redisServerTool.get(redisKey, RedisLoginTokenInfo.class); + Assert.notNull(info, "用户未登录"); + + + // 判断登录有效 + + // 获取登录设备信息 + List devices = info.getLoginDevices(); + // 过滤过期 + devices = devices.stream().filter(v -> v.getLastLoginTime().isBefore(new Date(System.currentTimeMillis() + tokenExceedTime * 1000L).toInstant())).collect(Collectors.toList()); + Map map = devices.stream().collect(Collectors.toMap(RedisLoginTokenInfo.LoginDevice::getUuId, v -> v)); + Assert.isTrue(map.containsKey(uuId), "登录过期"); + + // 续期时间 + map.get(uuId).setLastLoginTime(Instant.now()); + info.setLoginDevices(devices); + redisServerTool.set(redisKey, info, tokenExceedTime, TimeUnit.SECONDS); + + // 登录 + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(info.getUserId(), info, + info.getAuthority().stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList())); + authenticationToken.setDetails(info); + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + + MDC.put("userId", userId.toString()); + + filterChain.doFilter(request, response); + } + + /** + * 判断是否为被忽略的路径 + * + * @param ignoreUrls 忽略路径 + * @param currentUrl 当前路径 + * @return 是否忽略当前 + */ + private Boolean ifCurrentUrl(String ignoreUrls, String currentUrl) { + String charToRemove = "[*/ ]"; + if (!StringUtils.hasText(ignoreUrls)) { + return false; + } + + // 处理本次请求路径 剔除全局路径 + if (!StringUtils.hasText(currentUrl)) { + return true; + } + if (StringUtils.hasText(contextPath)) { + currentUrl = currentUrl.replaceFirst(contextPath, "").replaceAll(charToRemove, ""); + } + + // 处理需要被忽略的字段 + List urls = Arrays.stream(ignoreUrls.split(",")).map(item -> item.replaceAll(charToRemove, "")).filter(item -> !item.isEmpty()).distinct().collect(Collectors.toList()); + + for (String str : urls) { + if (currentUrl.startsWith(str)) { + return true; + } + } + return false; + } +} diff --git a/common/src/main/java/org/jiayunet/interceptor/LoggingOriginalRequestFilter.java b/common/src/main/java/org/jiayunet/interceptor/LoggingOriginalRequestFilter.java new file mode 100644 index 0000000..55cdcc9 --- /dev/null +++ b/common/src/main/java/org/jiayunet/interceptor/LoggingOriginalRequestFilter.java @@ -0,0 +1,32 @@ +package org.jiayunet.interceptor; + +import java.io.IOException; +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; + +import lombok.extern.slf4j.Slf4j; + +/** + * 请求日志 + * + * @author zk + */ +@Order(2) +@Slf4j +@Component +public class LoggingOriginalRequestFilter extends OncePerRequestFilter { + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + String methodName = request.getMethod(); + log.info(">> Request Url: {} {}", methodName, request.getRequestURI()); + + filterChain.doFilter(request, response); + } + +} diff --git a/common/src/main/java/org/jiayunet/interceptor/PreventReplayInterceptor.java b/common/src/main/java/org/jiayunet/interceptor/PreventReplayInterceptor.java new file mode 100644 index 0000000..eeacc1c --- /dev/null +++ b/common/src/main/java/org/jiayunet/interceptor/PreventReplayInterceptor.java @@ -0,0 +1,78 @@ +package org.jiayunet.interceptor; + +import java.time.Duration; +import java.time.Instant; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.servlet.HandlerInterceptor; +import org.jiayunet.constant.PreRedisKeyName; +import org.jiayunet.pojo.interceptor.RedisPreventReplayInfo; +import org.jiayunet.tool.HttpIpTool; +import org.jiayunet.tool.server.RedisServerTool; + +/** + * 防止高频重复请求 + * + * @author zk + */ +@Component +public class PreventReplayInterceptor implements HandlerInterceptor { + /** + * 是否开启 + */ + @Value("${app.prevent_replay.if_open:true}") + private Boolean ifOpen; + /** + * 间隔时间 + */ + @Value("${app.prevent_replay.interval_time:10}") + private Integer intervalTime; + /** + * 单位时间次数 + */ + @Value("${app.prevent_replay.limit_number:20}") + private Integer limitNumber; + + @Autowired + private RedisServerTool redisServerTool; + + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + // 关闭时 不执行 + if (!ifOpen) { + return true; + } + + String redisKeyName = PreRedisKeyName.PREVENT_REPLAY + HttpIpTool.gteRealIP(request); + + // 获取redis中的数据 + RedisPreventReplayInfo replayInfo = redisServerTool.get(redisKeyName, RedisPreventReplayInfo.class); + + replayInfo = Objects.nonNull(replayInfo) ? replayInfo : new RedisPreventReplayInfo(); + + // 计算时间差 并更新数据 + Duration duration = Duration.between(replayInfo.getLastTiming(), Instant.now()); + long seconds = duration.getSeconds(); + replayInfo = seconds <= intervalTime ? replayInfo : new RedisPreventReplayInfo(); + + // 增加次数 写回redis + replayInfo.setFrequency(replayInfo.getFrequency() + 1); + redisServerTool.set(redisKeyName, replayInfo); + + boolean type = replayInfo.getFrequency() <= limitNumber; + + // 加黑 + if (!type) { + redisServerTool.set(PreRedisKeyName.BLACK_LIST + request.getRemoteAddr(), "高频请求", 1, TimeUnit.DAYS); + } + + return type; + } +} diff --git a/common/src/main/java/org/jiayunet/interceptor/SqlLoggerInterceptor.java b/common/src/main/java/org/jiayunet/interceptor/SqlLoggerInterceptor.java new file mode 100644 index 0000000..cc64b5a --- /dev/null +++ b/common/src/main/java/org/jiayunet/interceptor/SqlLoggerInterceptor.java @@ -0,0 +1,119 @@ +package org.jiayunet.interceptor; + + +import lombok.extern.slf4j.Slf4j; +import org.apache.ibatis.executor.statement.StatementHandler; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ParameterMapping; +import org.apache.ibatis.plugin.*; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.SystemMetaObject; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.type.TypeHandlerRegistry; +import org.springframework.stereotype.Component; + +import java.sql.Statement; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.regex.Matcher; + +@Slf4j +@Intercepts({ + @Signature(type = StatementHandler.class, method = "query", args = {Statement.class, org.apache.ibatis.session.ResultHandler.class}), + @Signature(type = StatementHandler.class, method = "update", args = {Statement.class}), + @Signature(type = StatementHandler.class, method = "batch", args = {Statement.class}) +}) +@Component +public class SqlLoggerInterceptor implements Interceptor { + + @Override + public Object intercept(Invocation invocation) throws Throwable { + + StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); + MetaObject metaObject = SystemMetaObject.forObject(statementHandler); + + // 获取 MappedStatement + MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedStatement"); + + BoundSql boundSql = statementHandler.getBoundSql(); + + // 记录执行开始时间 + long startTime = System.currentTimeMillis(); + + try { + // 执行原始方法 + return invocation.proceed(); + } finally { + // 计算执行耗时 + long costTime = System.currentTimeMillis() - startTime; + + // 获取完整的 SQL(包含参数值) + String completeSql = getCompleteSql(mappedStatement.getConfiguration(), boundSql); + + // 输出日志 + log.info("SQL : {} | Time: {} ms", completeSql, costTime); + } + } + + /** + * 获取完整的 SQL 语句(将 ? 替换为实际参数值) + */ + private String getCompleteSql(Configuration configuration, BoundSql boundSql) { + + String sql = boundSql.getSql().replaceAll("\\s+", " ").trim(); + Object parameterObject = boundSql.getParameterObject(); + List parameterMappings = boundSql.getParameterMappings(); + + if (parameterMappings == null || parameterMappings.isEmpty()) { + return sql; + } + + TypeHandlerRegistry typeHandlerRegistry = configuration.getTypeHandlerRegistry(); + + // 替换 SQL 中的 ? 为实际参数值 + if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(getParameterValue(parameterObject))); + } else { + MetaObject metaObject = configuration.newMetaObject(parameterObject); + for (ParameterMapping parameterMapping : parameterMappings) { + String propertyName = parameterMapping.getProperty(); + Object value; + + if (boundSql.hasAdditionalParameter(propertyName)) { + value = boundSql.getAdditionalParameter(propertyName); + } else if (typeHandlerRegistry.hasTypeHandler(parameterObject.getClass())) { + value = parameterObject; + } else { + value = metaObject.getValue(propertyName); + } + + String paramValueStr = getParameterValue(value); + sql = sql.replaceFirst("\\?", Matcher.quoteReplacement(paramValueStr)); + } + } + + return sql; + } + + /** + * 将参数值转换为字符串形式 + */ + private String getParameterValue(Object obj) { + if (obj == null) { + return "NULL"; + } + + if (obj instanceof String) { + return "'" + obj + "'"; + } + + if (obj instanceof Date) { + SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + return "'" + formatter.format(obj) + "'"; + } + + return obj.toString(); + } +} \ No newline at end of file diff --git a/common/src/main/java/org/jiayunet/interceptor/TraceIdFilter.java b/common/src/main/java/org/jiayunet/interceptor/TraceIdFilter.java new file mode 100644 index 0000000..68ea8a2 --- /dev/null +++ b/common/src/main/java/org/jiayunet/interceptor/TraceIdFilter.java @@ -0,0 +1,51 @@ +package org.jiayunet.interceptor; + +import org.slf4j.MDC; +import org.springframework.core.annotation.Order; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.UUID; + + +/** + * 请求链路标识链接器 + */ +@Component +@Order(1) +public class TraceIdFilter extends OncePerRequestFilter { + + + @Override + protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { + + try { + // 判断请求头中是否存在链路id + String traceId = request.getHeader("X-Request-ID"); + + // 不存在 即时生成 + if (!StringUtils.hasText(traceId)) { + traceId = UUID.randomUUID().toString().replace("-", "").substring(0, 16); + } + + // 存在在日志上下文环境中 + MDC.put("traceId", traceId); + + + // 设置到响应头,方便前端追踪 + response.setHeader("X-Trace-ID", traceId); + + filterChain.doFilter(request, response); + + } finally { + // 请求结束时清除 + MDC.clear(); + } + } +} diff --git a/common/src/main/java/org/jiayunet/mapper/CommonMapper.java b/common/src/main/java/org/jiayunet/mapper/CommonMapper.java new file mode 100644 index 0000000..6abd8d7 --- /dev/null +++ b/common/src/main/java/org/jiayunet/mapper/CommonMapper.java @@ -0,0 +1,78 @@ +package org.jiayunet.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springframework.util.CollectionUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * @author zk + */ +public interface CommonMapper extends BaseMapper { + int DEFAULT_BATCH_SIZE = 5000; + + int insertBatchSomeColumn(Collection entityList); + + int updateBatchMethod(Collection entityList); + + + default int batchInsert(List entityList) { + return this.batchInsert(entityList, DEFAULT_BATCH_SIZE); + } + + default int batchInsert(List entityList, int batchSize) { + if (CollectionUtils.isEmpty(entityList)) { + return 0; + } else { + if (batchSize <= 0) { + batchSize = 5000; + } + + List> partition = partition(entityList, batchSize); + + AtomicInteger total = new AtomicInteger(); + partition.forEach((e) -> { + total.addAndGet(this.insertBatchSomeColumn(e)); + }); + return total.get(); + } + } + + default int batchUpdate(List entityList) { + return this.batchUpdate(entityList, 5000); + } + + default int batchUpdate(List entityList, int batchSize) { + if (CollectionUtils.isEmpty(entityList)) { + return 0; + } else { + if (batchSize <= 0) { + batchSize = 5000; + } + + List> partition = this.partition(entityList, batchSize); + AtomicInteger total = new AtomicInteger(); + partition.forEach((e) -> { + total.addAndGet(updateBatchMethod(e)); + }); + return total.get(); + } + } + + + default List> partition(List entityList, Integer batchSize) { + List> partition = new ArrayList<>(); + + for (int i = 0; i < entityList.size(); i += batchSize) { + int toIndex = Math.min(i + batchSize, entityList.size()); + partition.add(entityList.subList(i, toIndex)); + } + + return partition; + } + + +} diff --git a/common/src/main/java/org/jiayunet/oss/AliOssAbility.java b/common/src/main/java/org/jiayunet/oss/AliOssAbility.java new file mode 100644 index 0000000..edaf663 --- /dev/null +++ b/common/src/main/java/org/jiayunet/oss/AliOssAbility.java @@ -0,0 +1,132 @@ +package org.jiayunet.oss; + +import com.aliyun.oss.HttpMethod; +import com.aliyun.oss.model.GeneratePresignedUrlRequest; +import com.aliyun.oss.model.OSSObject; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import com.aliyun.oss.OSS; +import com.aliyun.oss.common.auth.*; +import com.aliyun.oss.OSSClientBuilder; +import com.aliyun.oss.model.PutObjectRequest; +import com.aliyun.oss.model.PutObjectResult; + +import java.io.InputStream; +import java.net.URL; +import java.util.Date; + + +/** + * 阿里云oss能力 + */ +@Slf4j +@Component("ossAbility") +@ConditionalOnProperty(name = "app.oss.service_provider", havingValue = "aliyun") +public class AliOssAbility { + @Autowired + private DefaultCredentialProvider credentialsProvider; + + + /** + * 简单上传 + * + * @param endpoint 接入口地址 https://oss-cn-guangzhou.aliyuncs.co + * @param bucketName Bucket名称 + * @param objectPathName Object完整路径名称 /user/iii.log + */ + public void simpleUpload(String endpoint, String bucketName, String objectPathName, InputStream inputStream) throws Exception { + // 创建OSSClient实例。 + OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); + + try { + // 创建PutObjectRequest对象。 + PutObjectRequest putObjectRequest = new PutObjectRequest(bucketName, objectPathName, inputStream); + + // 如果需要上传时设置存储类型和访问权限,请参考以下示例代码。 + // ObjectMetadata metadata = new ObjectMetadata(); + // metadata.setHeader(OSSHeaders.OSS_STORAGE_CLASS, StorageClass.Standard.toString()); + // metadata.setObjectAcl(CannedAccessControlList.Private); + // putObjectRequest.setMetadata(metadata); + + PutObjectResult result = ossClient.putObject(putObjectRequest); + return; + + } catch (Exception e) { + log.error("aliOss 请求封装出现异常 异常信息:{}", e.getMessage()); + } finally { + if (ossClient != null) { + ossClient.shutdown(); + } + } + throw new Exception("AliOss上传出现问题"); + } + + /** + * 简单下载 + * + * @param endpoint 接入端口地址 + * @param bucketName Bucket名称 + * @param objectPathName Object完整路径名称 /user/iii.log + * @return 输入流 + */ + public byte[] simpleDownload(String endpoint, String bucketName, String objectPathName) throws Exception { + + OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); + try { + // ossObject包含文件所在的存储空间名称、文件名称、文件元数据以及一个输入流。 + OSSObject ossObject = ossClient.getObject(bucketName, objectPathName); + InputStream inputStream = ossObject.getObjectContent(); + byte[] fileBytes = IOUtils.toByteArray(inputStream); + inputStream.close(); + ossObject.close(); + return fileBytes; + } catch (Exception e) { + log.error("aliOss 请求封装出现异常 异常信息:{}", e.getMessage()); + } finally { + if (ossClient != null) { + ossClient.shutdown(); + } + } + throw new Exception("AliOss下载出现问题"); + } + + /** + * 获取代签名的url + * + * @param endpoint 接入端口地址 + * @param bucketName Bucket名称 + * @param objectPathName Object完整路径名称 /user/iii.log + * @param httpMethod 方法 仅支持 get:获取 put:修改 + */ + public String signatureUrl(String endpoint, String bucketName, String objectPathName, int second, HttpMethod httpMethod) throws Exception { + // 创建OSSClient实例。 + OSS ossClient = new OSSClientBuilder().build(endpoint, credentialsProvider); + + try { + // 封装生成签名url需要的信息 + GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(bucketName, objectPathName); + // 过期时间 毫秒 + generatePresignedUrlRequest.setExpiration(new Date(new Date().getTime() + second * 1000L)); + // 设置请求方法 + generatePresignedUrlRequest.setMethod(httpMethod); + + generatePresignedUrlRequest.setContentType("application/octet-stream"); + + // 获取 + URL url = ossClient.generatePresignedUrl(generatePresignedUrlRequest); + + return url.toString().replace("http://", "https://"); + + } catch (Exception e) { + log.error("aliOss 请求封装出现异常 异常信息:{}", e.getMessage()); + } finally { + ossClient.shutdown(); + } + throw new Exception("AliOss 获取签名Url出现异常"); + } + + +} diff --git a/common/src/main/java/org/jiayunet/pojo/UnifiedResponse.java b/common/src/main/java/org/jiayunet/pojo/UnifiedResponse.java new file mode 100644 index 0000000..0dd3979 --- /dev/null +++ b/common/src/main/java/org/jiayunet/pojo/UnifiedResponse.java @@ -0,0 +1,44 @@ +package org.jiayunet.pojo; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 接口数据同一响应格式 + * + * @author zk + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UnifiedResponse { + + private String code; + private String msg; + private T data; + + public static UnifiedResponse normalResponse(K date) { + UnifiedResponse unifiedResponse = new UnifiedResponse(); + unifiedResponse.setCode("0"); + unifiedResponse.setMsg("正常响应"); + unifiedResponse.setData(date); + return unifiedResponse; + } + + public static UnifiedResponse fail(String code, String msg) { + UnifiedResponse unifiedResponse = new UnifiedResponse(); + unifiedResponse.setCode(code); + unifiedResponse.setMsg(msg); + return unifiedResponse; + } + + public static UnifiedResponse fail(String code, String msg, T data) { + UnifiedResponse unifiedResponse = new UnifiedResponse(); + unifiedResponse.setCode(code); + unifiedResponse.setMsg(msg); + unifiedResponse.setData(data); + return unifiedResponse; + } + +} diff --git a/common/src/main/java/org/jiayunet/pojo/interceptor/RedisPreventReplayInfo.java b/common/src/main/java/org/jiayunet/pojo/interceptor/RedisPreventReplayInfo.java new file mode 100644 index 0000000..6a9f53c --- /dev/null +++ b/common/src/main/java/org/jiayunet/pojo/interceptor/RedisPreventReplayInfo.java @@ -0,0 +1,22 @@ +package org.jiayunet.pojo.interceptor; + +import java.time.Instant; + +import lombok.Data; + +/** + * redis中保存的的防止重复请求的信息 + * + * @author zk + */ +@Data +public class RedisPreventReplayInfo { + /** + * 当前时间间隔秒内请求次数 + */ + private Integer frequency = 0; + /** + * 最后间隔更新时间 + */ + private Instant lastTiming = Instant.now(); +} diff --git a/common/src/main/java/org/jiayunet/pojo/login/RedisLoginTokenInfo.java b/common/src/main/java/org/jiayunet/pojo/login/RedisLoginTokenInfo.java new file mode 100644 index 0000000..6db5560 --- /dev/null +++ b/common/src/main/java/org/jiayunet/pojo/login/RedisLoginTokenInfo.java @@ -0,0 +1,45 @@ +package org.jiayunet.pojo.login; + +import java.time.Instant; +import java.util.ArrayList; +import java.util.List; + +import lombok.Data; + +/** + * redis中保存的用户登录信息 + * + * @author zk + */ +@Data +public class RedisLoginTokenInfo { + private Long userId; + /** + * 登录设备授权信息 + */ + private List loginDevices = new ArrayList<>(); + /** + * 拥有权限 + */ + private List authority = new ArrayList<>(); + /** + * 角色 + */ + private List role = new ArrayList<>(); + + @Data + public static class LoginDevice { + /** + * 最后请求时间 + */ + private Instant lastLoginTime; + /** + * 单次登陆唯一标识 + */ + private String uuId; + /** + * 登录ip + */ + private String loginIp; + } +} diff --git a/common/src/main/java/org/jiayunet/sms/AliYunSmsAbility.java b/common/src/main/java/org/jiayunet/sms/AliYunSmsAbility.java new file mode 100644 index 0000000..8997ab6 --- /dev/null +++ b/common/src/main/java/org/jiayunet/sms/AliYunSmsAbility.java @@ -0,0 +1,92 @@ +package org.jiayunet.sms; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import com.aliyun.dysmsapi20170525.models.SendSmsResponse; +import org.springframework.util.Assert; +import org.jiayunet.tool.server.RedisServerTool; + +import java.util.Random; +import java.util.concurrent.TimeUnit; + +/** + * @author zk + */ +@Component("smsAbility") +@ConditionalOnProperty(name = "app.sms.service_provider", havingValue = "aliyun") +@Slf4j +public class AliYunSmsAbility implements ISmsAbility { + @Autowired + private com.aliyun.dysmsapi20170525.Client aliyunSmsClient; + @Autowired + private RedisServerTool redisServerTool; + + @Override + public void sendVerificationCode(String phone, VerifyCodeAttribute attribute) { + String redisKey = attribute.getRedisKeyPre()+phone; + + // 验证是否发送 + boolean existKey = redisServerTool.hasKey(redisKey); + + if (!attribute.getCover()){ + Assert.isTrue(!existKey,"验证码已发送"); + } + + // 生成验证码 + int randomNumber = new Random().nextInt(1000000); + String number = String.format("%06d", randomNumber); + + // 发送验证码 + Assert.isTrue(sendVerificationCode(phone,number,attribute.getSignName(),attribute.getTemplateCode()),"短信发送失败,请稍后重试"); + + // 跟新redisK + redisServerTool.set(redisKey,number,attribute.getEffectiveTime(), TimeUnit.MINUTES); + } + + @Override + public String getVerificationCode(String phone, VerifyCodeAttribute attribute) { + return redisServerTool.get(attribute.getRedisKeyPre()+phone,String.class); + } + + @Override + public void delVerificationCode(String phone, VerifyCodeAttribute attribute) { + redisServerTool.delete(attribute.getRedisKeyPre()+phone); + } + + @Override + public void sendNotification(String phone, String message) { + + } + + /** + * ali 发送验证码 + * @param phone 手机号 + * @param validCode 验证码 + * @param signName 签名 + * @param templateCode 模板 + * @return 发送结果 + */ + private boolean sendVerificationCode(String phone, String validCode, String signName, String templateCode) { + com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = + new com.aliyun.dysmsapi20170525.models.SendSmsRequest().setSignName(signName).setTemplateCode(templateCode) + .setPhoneNumbers(phone).setTemplateParam("{\"code\":\"" + validCode + "\"}"); + + com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions(); + try { + // 复制代码运行请自行打印 API 的返回值 + SendSmsResponse response = aliyunSmsClient.sendSmsWithOptions(sendSmsRequest, runtime); + if (!response.getStatusCode().equals(200)||!response.getBody().getCode().equals("OK")){ + log.error("短信发送失败: 失败原因:{}",response.getBody()); + return false; + } + return true; + }catch (Throwable e){ + log.error("短信发送异常: 异常:{}",e.getMessage()); + return false; + } + } + +} diff --git a/common/src/main/java/org/jiayunet/sms/ISmsAbility.java b/common/src/main/java/org/jiayunet/sms/ISmsAbility.java new file mode 100644 index 0000000..38736a1 --- /dev/null +++ b/common/src/main/java/org/jiayunet/sms/ISmsAbility.java @@ -0,0 +1,43 @@ +package org.jiayunet.sms; + +/** + * 抽象短信发送接口 + * + * @author zk + */ +public interface ISmsAbility { + + + /** + * 发送验证码 + * @param phone 手机号 + * @param attribute 配置 + */ + void sendVerificationCode(String phone, VerifyCodeAttribute attribute); + + /** + * 获取验证码 + * @param phone 手机号 + * @param attribute 配置 + */ + String getVerificationCode(String phone, VerifyCodeAttribute attribute); + + /** + * 删除验证码 + * @param phone 手机号 + * @param attribute 配置 + */ + void delVerificationCode(String phone, VerifyCodeAttribute attribute); + + + + /** + * 发送通知短信 + * + * @param phone 手机号 + * @param message 消息 + */ + void sendNotification(String phone, String message); + + +} diff --git a/common/src/main/java/org/jiayunet/sms/VerificationConfig.java b/common/src/main/java/org/jiayunet/sms/VerificationConfig.java new file mode 100644 index 0000000..dd986b1 --- /dev/null +++ b/common/src/main/java/org/jiayunet/sms/VerificationConfig.java @@ -0,0 +1,125 @@ +package org.jiayunet.sms; +import lombok.Getter; +import org.springframework.util.Assert; + +/** + * 短信发送配配置 + * + * @author zk + */ +@Getter +public class VerificationConfig implements VerifyCodeAttribute { + /** + * 模板签名 + */ + private final String signName; + /** + * 模板code + */ + private final String templateCode; + /** + * redis 名 + */ + private final String redisKeyPre; + /** + * 有效时间 单位分钟 + */ + private final Integer effectiveTime; + /** + * 覆盖已存在的redis值 + */ + private final Boolean cover; + + /** + * 构造器 + */ + public static class Builder { + private String signName; + private String templateCode; + private String redisKeyPre; + private Integer effectiveTime; + private Boolean cover; + + public VerificationConfig.Builder config(VerifyCodeAttribute attribute){ + this.signName = attribute.getSignName(); + this.templateCode = attribute.getTemplateCode(); + this.redisKeyPre = attribute.getRedisKeyPre(); + this.effectiveTime = attribute.getEffectiveTime(); + this.cover = attribute.getCover(); + return this; + } + public VerificationConfig.Builder config(String signName, String templateCode,String redisKeyPre) { + this.signName = signName; + this.templateCode = templateCode; + this.redisKeyPre = redisKeyPre; + this.effectiveTime = 5; + this.cover = true; + return this; + } + public VerificationConfig.Builder config(String signName, String templateCode,String redisKeyPre,Integer effectiveTime) { + this.signName = signName; + this.templateCode = templateCode; + this.redisKeyPre = redisKeyPre; + this.effectiveTime = effectiveTime; + this.cover = true; + return this; + } + public VerificationConfig.Builder config(String signName, String templateCode,String redisKeyPre,Integer effectiveTime,Boolean cover) { + this.signName = signName; + this.templateCode = templateCode; + this.redisKeyPre = redisKeyPre; + this.effectiveTime = effectiveTime; + this.cover = cover; + return this; + } + + + public VerificationConfig.Builder redisKeyPre(String redisKeyPre) { + this.redisKeyPre = redisKeyPre; + return this; + } + + public VerificationConfig.Builder templateCode(String templateCode) { + this.templateCode = templateCode; + return this; + } + + public VerificationConfig.Builder signName(String signName) { + this.signName = signName; + return this; + } + public VerificationConfig.Builder effectiveTime(Integer effectiveTime) { + this.effectiveTime = effectiveTime; + return this; + } + + public VerificationConfig.Builder cover(Boolean cover) { + this.cover = cover; + return this; + } + public VerificationConfig build() { + Assert.hasText(signName,"signName不能为空"); + Assert.hasText(templateCode,"templateCode不能为空"); + Assert.hasText(redisKeyPre,"redisKey不能为空"); + Assert.notNull(effectiveTime,"effectiveTime不能为空"); + Assert.notNull(cover,"cover不能为空"); + return new VerificationConfig(signName, templateCode, redisKeyPre,effectiveTime,cover); + } + } + + /** + * 私有化构造 + * @param signName 签名 + * @param templateCode 短信模板 + * @param redisKeyPre redis键名 + * @param effectiveTime 有效时间 + * @param cover 是否覆盖发送 + */ + private VerificationConfig(String signName, String templateCode, String redisKeyPre, Integer effectiveTime, Boolean cover) { + this.signName = signName; + this.templateCode = templateCode; + this.redisKeyPre = redisKeyPre; + this.effectiveTime = effectiveTime; + this.cover = cover; + } +} diff --git a/common/src/main/java/org/jiayunet/sms/VerifyCodeAttribute.java b/common/src/main/java/org/jiayunet/sms/VerifyCodeAttribute.java new file mode 100644 index 0000000..bc5d714 --- /dev/null +++ b/common/src/main/java/org/jiayunet/sms/VerifyCodeAttribute.java @@ -0,0 +1,33 @@ +package org.jiayunet.sms; + +/** + * @author zk + */ +public interface VerifyCodeAttribute { + /** + * 签名 + * + */ + String getSignName(); + + /** + * 模板code + */ + String getTemplateCode(); + + /** + * redisKey 名字 + */ + String getRedisKeyPre(); + + /** + * 有效时间 + */ + Integer getEffectiveTime(); + + /** + * 是否覆盖发送 + */ + Boolean getCover(); + +} diff --git a/common/src/main/java/org/jiayunet/tool/AuthenticTool.java b/common/src/main/java/org/jiayunet/tool/AuthenticTool.java new file mode 100644 index 0000000..17a016b --- /dev/null +++ b/common/src/main/java/org/jiayunet/tool/AuthenticTool.java @@ -0,0 +1,121 @@ +package org.jiayunet.tool; + +import org.springframework.util.StringUtils; + +import java.util.regex.Pattern; + +/** + * @author zk + */ +public class AuthenticTool { + /** + * 验证身份证号码 + * + * @param idCard 身份证号 + */ + public static Boolean idCard(String idCard) { + // 验证身份证号码 + // 定义18位和15位身份证号码的正则表达式模式 + String REGEX_18 = "^[1-9]\\d{5}(?:18|19|20)\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}[0-9Xx]$"; + String REGEX_15 = "^[1-9]\\d{5}\\d{2}(?:0[1-9]|1[0-2])(?:0[1-9]|[12]\\d|3[01])\\d{3}$"; + String REGEX = "^(?:" + REGEX_15 + "|" + REGEX_18 + ")$"; + Pattern pattern = Pattern.compile(REGEX); + + if (!StringUtils.hasText(idCard)){ + return false; + } + + return pattern.matcher(idCard).matches(); + } + + /** + * 验证企业社会代码 + * + * @param companyAccount 企业社会代码 + */ + public static Boolean companyAccount(String companyAccount) { + // 校验码对应表 + final String CHECK_CODE_ARRAY = "0123456789ABCDEFGHJKLMNPQRTUWXY"; // 不包含I、O、S、V、Z + // 权重数组 + final int[] WEIGHT_ARRAY = {1, 3, 9, 27, 19, 26, 16, 17, 20, 29, 25, 13, 8, 24, 10, 30, 28}; + + if (companyAccount == null || companyAccount.length() != 18) { + System.out.println("请输入18位代码"); + return false; + } + + // 简单验证:只包含数字和大写字母(根据标准,I、O、S、V、Z 不应出现) + if (!companyAccount.matches("^[0-9A-HJ-NP-RT-UW-Y]{18}$")) { + System.out.println("工商社会代码格式验证错误"); + return false; + } + + // 计算校验码 + int sum = 0; + for (int i = 0; i < 17; i++) { + char c = companyAccount.charAt(i); + int num = CHECK_CODE_ARRAY.indexOf(c); + if (num == -1) { // 如果字符不在对应表中 + System.out.println("工商社会代码包含无效字符"); + return false; + } + sum += num * WEIGHT_ARRAY[i]; + } + + // 计算校验码位置 + int mod = sum % 31; + char expectedCheckCode; + if (mod == 0) { + expectedCheckCode = CHECK_CODE_ARRAY.charAt(30); // 31 - 31 = 0 + } else { + expectedCheckCode = CHECK_CODE_ARRAY.charAt(31 - mod); + } + + // 获取实际的校验码 + char actualCheckCode = Character.toUpperCase(companyAccount.charAt(17)); + + // 比较校验码 + if (expectedCheckCode != actualCheckCode) { + return false; + } + + // 验证通过 + return true; + } + + /** + * 公司名 + * + * @param companyName 公司名 + */ + public static Boolean companyName(String companyName) { + String COMPANY_NAME_REGEX = "^[\u4e00-\u9fa5A-Za-z0-9\\-_]{1,150}$"; + Pattern COMPANY_NAME_PATTERN = Pattern.compile(COMPANY_NAME_REGEX); + + + if (!StringUtils.hasText(companyName)){ + return false; + } + + return COMPANY_NAME_PATTERN.matcher(companyName).matches(); + } + + public static Boolean chineseName(String name) { + // 简单的2 - 4个汉字人名验证正则表达式 + String SIMPLE_NAME_REGEX = "^[\u4e00-\u9fa5]{2,6}$"; + // 包含间隔符“·”的中文人名验证正则表达式 + String COMPLEX_NAME_REGEX = "^([\u4e00-\u9fa5]{2,6})(·[\u4e00-\u9fa5]{2,4})?$"; + + Pattern SIMPLE_NAME_PATTERN = Pattern.compile(SIMPLE_NAME_REGEX); + Pattern COMPLEX_NAME_PATTERN = Pattern.compile(COMPLEX_NAME_REGEX); + + if (!StringUtils.hasText(name)){ + return false; + } + + return SIMPLE_NAME_PATTERN.matcher(name).matches()||COMPLEX_NAME_PATTERN.matcher(name).matches(); + + } + + +} diff --git a/common/src/main/java/org/jiayunet/tool/HttpIpTool.java b/common/src/main/java/org/jiayunet/tool/HttpIpTool.java new file mode 100644 index 0000000..860784e --- /dev/null +++ b/common/src/main/java/org/jiayunet/tool/HttpIpTool.java @@ -0,0 +1,63 @@ +package org.jiayunet.tool; + +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +import javax.servlet.http.HttpServletRequest; + +/** + * @author zk + */ +public class HttpIpTool { + + /** + * 获取请求的真实ip + */ + public static String gteRealIP() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + Assert.notNull(request, "当前非Http请求,无法获取request"); + + //存在转发 + String realIp = request.getHeader("X-Forwarded-For"); + if (StringUtils.hasText(realIp)) { + return realIp.split(",")[0].trim(); + } + + //Nginx 直接链接客户端 + realIp = request.getHeader("X-Real-IP"); + if (StringUtils.hasText(realIp)) { + return realIp; + } + + // 客户端直链服务 + return request.getRemoteAddr(); + + } + + /** + * 获取请求真实ip + * @param request http请求头 + */ + public static String gteRealIP(HttpServletRequest request) { + //存在转发 + String realIp = request.getHeader("X-Forwarded-For"); + if (StringUtils.hasText(realIp)) { + return realIp.split(",")[0].trim(); + } + + //Nginx 直接链接客户端 + realIp = request.getHeader("X-Real-IP"); + if (StringUtils.hasText(realIp)) { + return realIp; + } + + // 客户端直链服务 + return request.getRemoteAddr(); + + } + + + +} diff --git a/common/src/main/java/org/jiayunet/tool/HttpTool.java b/common/src/main/java/org/jiayunet/tool/HttpTool.java new file mode 100644 index 0000000..b3bff75 --- /dev/null +++ b/common/src/main/java/org/jiayunet/tool/HttpTool.java @@ -0,0 +1,224 @@ +package org.jiayunet.tool; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.http.HttpEntity; +import org.apache.http.client.methods.CloseableHttpResponse; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.client.utils.URIBuilder; +import org.apache.http.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.entity.mime.MultipartEntityBuilder; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClients; +import org.apache.http.util.EntityUtils; + +import javax.validation.constraints.NotNull; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * @author zk + */ +public class HttpTool { + public static final ObjectMapper objectMapper; + static { + objectMapper = new ObjectMapper(); + objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + /** + * 发送请求Body为json的请求 + * @param body 参数对象 + * @param url 路径 + * @return 返回Str + */ + public static String sendJsonPost(@NotNull Object body, @NotNull String url){ + return sendJsonPost(body,url,new HashMap<>()); + } + + /** + * 发送post 请求 + * @param body body + * @param url url + * @param headerMap 请求头 + * @return + */ + public static String sendJsonPost(@NotNull Object body, @NotNull String url,Map headerMap){ + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + + // 创建一个 HttpPost 对象 + HttpPost httpPost = new HttpPost(url); + + // 创建一个 StringEntity 对象,用于封装 JSON 数据 + String jsonStr = objectMapper.writeValueAsString(body); + StringEntity entity = new StringEntity(jsonStr, StandardCharsets.UTF_8); + httpPost.setEntity(entity); + + // 遍历 Map 并设置请求头 + for (Map.Entry entry : headerMap.entrySet()) { + httpPost.setHeader(entry.getKey(), entry.getValue()); + } + httpPost.setHeader("Content-Type","application/json"); + + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + String bodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + if (response.getStatusLine().getStatusCode() == 200) { + return bodyStr; + } + throw new IOException("Http请求出现错误,响应码:" + response.getStatusLine().getStatusCode()); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * 发送post请求 + * @param bodyMap body + * @param url url + * @return + */ + public static String sendFormDataPost(@NotNull Map bodyMap, @NotNull String url){ + return sendFormDataPost(bodyMap,url,new HashMap<>()); + } + + /** + * 发送post请求 + * @param bodyMap body + * @param url url + * @param headerMap 请求头 + * @return + */ + public static String sendFormDataPost(@NotNull Map bodyMap, @NotNull String url,Map headerMap){ + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + + // 创建一个 HttpPost 对象 + HttpPost httpPost = new HttpPost(url); + + // 创建 MultipartEntityBuilder + MultipartEntityBuilder builder = MultipartEntityBuilder.create(); + // 遍历 Map 并设置请求头 + for (Map.Entry entry : bodyMap.entrySet()) { + builder.addTextBody(entry.getKey(), entry.getValue().toString(), ContentType.TEXT_PLAIN.withCharset(StandardCharsets.UTF_8)); + } + + httpPost.setEntity(builder.build()); + + // 遍历 Map 并设置请求头 + for (Map.Entry entry : headerMap.entrySet()) { + httpPost.setHeader(entry.getKey(), entry.getValue()); + } + + + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + String bodyStr = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + if (response.getStatusLine().getStatusCode() == 200) { + return bodyStr; + } + throw new IOException("Http请求出现错误,响应码:" + response.getStatusLine().getStatusCode()); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * post下载文件 + * @param body body + * @param url url + */ + public static InputStream downloadPost(@NotNull Object body, @NotNull String url){ + return downloadPost(body,url, new HashMap<>()); + } + + /** + * post下载文件 + * @param body body + * @param url url + * @param headerMap 请求头 + */ + public static InputStream downloadPost(@NotNull Object body, @NotNull String url,Map headerMap){ + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + + // 创建一个 HttpPost 对象 + HttpPost httpPost = new HttpPost(url); + + // 创建一个 StringEntity 对象,用于封装 JSON 数据 + String jsonStr = objectMapper.writeValueAsString(body); + StringEntity entity = new StringEntity(jsonStr, StandardCharsets.UTF_8); + httpPost.setEntity(entity); + + // 遍历 Map 并设置请求头 + for (Map.Entry entry : headerMap.entrySet()) { + httpPost.setHeader(entry.getKey(), entry.getValue()); + } + httpPost.setHeader("Content-Type","application/json"); + + try (CloseableHttpResponse response = httpClient.execute(httpPost)) { + if (response.getStatusLine().getStatusCode() == 200) { + HttpEntity responseEntity = response.getEntity(); + byte[] byteArray = EntityUtils.toByteArray(response.getEntity()); + return new ByteArrayInputStream(byteArray); + } + throw new IOException("Http请求出现错误,响应码:" + response.getStatusLine().getStatusCode()); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + + + /** + * 发送请求GET请求 + * @param paramMap 参数Map + * @param url 路径 + * @return 返回Str + */ + public static String sendGet(@NotNull Map paramMap, @NotNull String url){ + + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { + + // 创建一个 HttpPost 对象 + HttpGet httpGet = new HttpGet(url); + + //处理请求参数 + URIBuilder uriBuilder = new URIBuilder(httpGet.getURI()); + for (String key : paramMap.keySet()) { + Object object = paramMap.get(key); + if (Objects.nonNull(object)){ + uriBuilder.addParameter(key,object.toString()); + } + } + httpGet.setURI(uriBuilder.build()); + + //发起请求 + try (CloseableHttpResponse response = httpClient.execute(httpGet)) { + if (response.getStatusLine().getStatusCode() == 200) { + return EntityUtils.toString(response.getEntity(),StandardCharsets.UTF_8); + } + throw new IOException("Http请求出现异常,响应码:" + response.getStatusLine().getStatusCode()); + } + + } catch (IOException | URISyntaxException e) { + throw new RuntimeException(e); + } + } + + +} diff --git a/common/src/main/java/org/jiayunet/tool/ObjectTool.java b/common/src/main/java/org/jiayunet/tool/ObjectTool.java new file mode 100644 index 0000000..1e386b2 --- /dev/null +++ b/common/src/main/java/org/jiayunet/tool/ObjectTool.java @@ -0,0 +1,42 @@ +package org.jiayunet.tool; + +import org.springframework.beans.BeanUtils; +import org.springframework.util.Assert; + +import java.beans.PropertyDescriptor; +import java.lang.reflect.InvocationTargetException; + +/** + * 对象操作工具类 + */ +public class ObjectTool { + + /** + * 复制非null值 + * @param source 数据源 + * @param target 目标对象 + */ + public static void copyNonNullProperties(Object source, Object target) { + Assert.notNull(target, "target must not be null"); + Assert.notNull(source, "source must not be null"); + + PropertyDescriptor[] sourceDescriptors = BeanUtils.getPropertyDescriptors(source.getClass()); + for (PropertyDescriptor descriptor : sourceDescriptors) { + try { + Object value = descriptor.getReadMethod().invoke(source); + if (value != null) { // 只复制非空字段 + PropertyDescriptor targetDescriptor = BeanUtils.getPropertyDescriptor(target.getClass(), descriptor.getName()); + if (targetDescriptor != null && targetDescriptor.getWriteMethod() != null) { + targetDescriptor.getWriteMethod().invoke(target, value); + } + } + } catch (IllegalAccessException | InvocationTargetException e) { + e.printStackTrace(); + } + } + } + + + + +} diff --git a/common/src/main/java/org/jiayunet/tool/UserSecurityTool.java b/common/src/main/java/org/jiayunet/tool/UserSecurityTool.java new file mode 100644 index 0000000..64a39f6 --- /dev/null +++ b/common/src/main/java/org/jiayunet/tool/UserSecurityTool.java @@ -0,0 +1,23 @@ +package org.jiayunet.tool; + +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.util.Assert; + +/** + * 获取当前用户信息 + * + * @author zk + */ +public class UserSecurityTool { + /** + * 获取当前用户ID + * + * @return id + */ + public static Long getUserId() { + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + Assert.notNull(userId, "用户未登录"); + return userId; + } + +} diff --git a/common/src/main/java/org/jiayunet/tool/VerifyImageCodeUtils.java b/common/src/main/java/org/jiayunet/tool/VerifyImageCodeUtils.java new file mode 100644 index 0000000..1dd0f12 --- /dev/null +++ b/common/src/main/java/org/jiayunet/tool/VerifyImageCodeUtils.java @@ -0,0 +1,207 @@ +package org.jiayunet.tool; + +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.io.OutputStream; +import java.security.SecureRandom; +import java.util.Arrays; +import java.util.Random; + +import javax.imageio.ImageIO; + +/** + * 图片验证码工具类 + * + * @author zk + */ +public class VerifyImageCodeUtils { + + /** + * 使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符 + */ + public static final String VERIFY_CODES = "123456789ABCDEFGHJKLMNPQRSTUVWXYZ"; + + private static final Random random = new SecureRandom(); + + /** + * 使用系统默认字符源生成验证码 + * + * @param verifySize 验证码长度 + * @return + */ + public static String generateVerifyCode(int verifySize) { + return generateVerifyCode(verifySize, VERIFY_CODES); + } + + /** + * 使用指定源生成验证码 + * + * @param verifySize 验证码长度 + * @param sources 验证码字符源 + * @return + */ + public static String generateVerifyCode(int verifySize, String sources) { + if (sources == null || sources.length() == 0) { + sources = VERIFY_CODES; + } + int codesLen = sources.length(); + Random rand = new Random(System.currentTimeMillis()); + StringBuilder verifyCode = new StringBuilder(verifySize); + for (int i = 0; i < verifySize; i++) { + verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1))); + } + return verifyCode.toString(); + } + + /** + * 输出指定验证码图片流 + * + * @param w + * @param h + * @param os + * @param code + * @throws IOException + */ + public static void outputImage(int w, int h, OutputStream os, String code) throws IOException { + int verifySize = code.length(); + BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB); + Random rand = new Random(); + Graphics2D g2 = image.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + Color[] colors = new Color[5]; + Color[] colorSpaces = new Color[] {Color.WHITE, Color.CYAN, Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, + Color.ORANGE, Color.PINK, Color.YELLOW}; + float[] fractions = new float[colors.length]; + for (int i = 0; i < colors.length; i++) { + colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)]; + fractions[i] = rand.nextFloat(); + } + Arrays.sort(fractions); + + g2.setColor(Color.GRAY);// 设置边框色 + g2.fillRect(0, 0, w, h); + + Color c = getRandColor(200, 250); + g2.setColor(c);// 设置背景色 + g2.fillRect(0, 2, w, h - 4); + + // 绘制干扰线 + Random random = new Random(); + g2.setColor(getRandColor(160, 200));// 设置线条的颜色 + for (int i = 0; i < 20; i++) { + int x = random.nextInt(w - 1); + int y = random.nextInt(h - 1); + int xl = random.nextInt(6) + 1; + int yl = random.nextInt(12) + 1; + g2.drawLine(x, y, x + xl + 40, y + yl + 20); + } + + // 添加噪点 + float yawpRate = 0.05f;// 噪声率 + int area = (int)(yawpRate * w * h); + for (int i = 0; i < area; i++) { + int x = random.nextInt(w); + int y = random.nextInt(h); + int rgb = getRandomIntColor(); + image.setRGB(x, y, rgb); + } + + shear(g2, w, h, c);// 使图片扭曲 + + g2.setColor(getRandColor(100, 160)); + int fontSize = h - 4; + Font font = new Font("Algerian", Font.ITALIC, fontSize); + g2.setFont(font); + char[] chars = code.toCharArray(); + for (int i = 0; i < verifySize; i++) { + AffineTransform affine = new AffineTransform(); + affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), + (w / verifySize) * i + fontSize / 2, h / 2); + g2.setTransform(affine); + g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10); + } + + g2.dispose(); + ImageIO.write(image, "jpg", os); + } + + private static Color getRandColor(int fc, int bc) { + if (fc > 255) { + fc = 255; + } + if (bc > 255) { + bc = 255; + } + int r = fc + random.nextInt(bc - fc); + int g = fc + random.nextInt(bc - fc); + int b = fc + random.nextInt(bc - fc); + return new Color(r, g, b); + } + + private static int getRandomIntColor() { + int[] rgb = getRandomRgb(); + int color = 0; + for (int c : rgb) { + color = color << 8; + color = color | c; + } + return color; + } + + private static int[] getRandomRgb() { + int[] rgb = new int[3]; + for (int i = 0; i < 3; i++) { + rgb[i] = random.nextInt(255); + } + return rgb; + } + + private static void shear(Graphics g, int w1, int h1, Color color) { + shearX(g, w1, h1, color); + shearY(g, w1, h1, color); + } + + private static void shearX(Graphics g, int w1, int h1, Color color) { + + int period = random.nextInt(2); + + boolean borderGap = true; + int frames = 1; + int phase = random.nextInt(2); + + for (int i = 0; i < h1; i++) { + double d = (double)(period >> 1) + * Math.sin((double)i / (double)period + (6.2831853071795862D * (double)phase) / (double)frames); + g.copyArea(0, i, w1, 1, (int)d, 0); + if (borderGap) { + g.setColor(color); + g.drawLine((int)d, i, 0, i); + g.drawLine((int)d + w1, i, w1, i); + } + } + + } + + private static void shearY(Graphics g, int w1, int h1, Color color) { + + int period = random.nextInt(40) + 10; // 50; + + boolean borderGap = true; + int frames = 20; + int phase = 7; + for (int i = 0; i < w1; i++) { + double d = (double)(period >> 1) + * Math.sin((double)i / (double)period + (6.2831853071795862D * (double)phase) / (double)frames); + g.copyArea(i, 0, 1, h1, 0, (int)d); + if (borderGap) { + g.setColor(color); + g.drawLine(i, (int)d, i, 0); + g.drawLine(i, (int)d + h1, i, h1); + } + + } + } + +} diff --git a/common/src/main/java/org/jiayunet/tool/server/RedisServerTool.java b/common/src/main/java/org/jiayunet/tool/server/RedisServerTool.java new file mode 100644 index 0000000..f4b2761 --- /dev/null +++ b/common/src/main/java/org/jiayunet/tool/server/RedisServerTool.java @@ -0,0 +1,121 @@ +package org.jiayunet.tool.server; + +import java.util.Collection; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * redis 操作工具类 + * + * @author zk + */ +@Component +public class RedisServerTool { + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private ObjectMapper objectMapper; + @Value("${spring.application.name}") + private String appName; + + public void set(Object key, Object value) { + redisTemplate.opsForValue().set(getKeyName(key), toJsonString(value)); + } + + public void set(Object key, Object value, long timeout, TimeUnit unit) { + redisTemplate.opsForValue().set(getKeyName(key), toJsonString(value), timeout, unit); + } + + public V get(Object key, Class vClass) { + String str = redisTemplate.opsForValue().get(getKeyName(key)); + return toObject(str, vClass); + } + public V get(Object key, Class vClass,Boolean ifPreKeyName) { + String keyName = ifPreKeyName ? getKeyName(key) : key.toString(); + String str = redisTemplate.opsForValue().get(keyName); + return toObject(str, vClass); + } + + + public Collection getArr(Object key, Class vClass) { + String str = redisTemplate.opsForValue().get(getKeyName(key)); + return toArrObject(str, vClass); + } + + public boolean hasKey(Object key) { + return Boolean.TRUE.equals(redisTemplate.hasKey(getKeyName(key))); + } + + /** + * 获取key的剩余过期时间(秒) + * @param key key + * @return 剩余秒数,-1表示永不过期,-2表示key不存在 + */ + public Long getExpire(Object key) { + return redisTemplate.getExpire(getKeyName(key), TimeUnit.SECONDS); + } + + public boolean delete(Object key) { + return Boolean.TRUE.equals(redisTemplate.delete(getKeyName(key))); + } + + public boolean setNx(Object key, Object value) { + return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(getKeyName(key), toJsonString(value))); + } + public boolean setNx(Object key, Object value, long timeout, TimeUnit unit) { + return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(getKeyName(key), toJsonString(value),timeout,unit)); + } + + private String toJsonString(Object value) { + try { + return objectMapper.writeValueAsString(value); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private V toObject(String str, Class vClass) { + if (!StringUtils.hasText(str)) { + return null; + } + try { + return objectMapper.readValue(str, vClass); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + private Collection toArrObject(String str, Class vClass) { + if (!StringUtils.hasText(str)) { + return List.of(); + } + try { + return objectMapper.readValue(str, new TypeReference>() { + }); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + /** + * 将key的键名包装上服务名字 + * @param key keu + */ + private String getKeyName(Object key){ + if (StringUtils.hasText(appName)){ + return appName+":"+key.toString(); + } + return key.toString(); + } + +} diff --git a/common/src/main/java/org/jiayunet/web/GlobalExceptionAdvice.java b/common/src/main/java/org/jiayunet/web/GlobalExceptionAdvice.java new file mode 100644 index 0000000..fc4a629 --- /dev/null +++ b/common/src/main/java/org/jiayunet/web/GlobalExceptionAdvice.java @@ -0,0 +1,87 @@ +package org.jiayunet.web; + +import javax.validation.ValidationException; + +import org.springframework.http.HttpStatus; +import org.springframework.util.StringUtils; +import org.springframework.validation.BindException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; +import org.jiayunet.exception.BusinessException; +import org.jiayunet.exception.BusinessExpCodeEnum; +import org.jiayunet.pojo.UnifiedResponse; + + +import lombok.extern.slf4j.Slf4j; + +/** + * @author zk + */ +@RestControllerAdvice +@Slf4j +public class GlobalExceptionAdvice { + + @ExceptionHandler({MethodArgumentTypeMismatchException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public UnifiedResponse methodArgumentTypeMismatchExceptionHandler(MethodArgumentTypeMismatchException ex) { + ex.printStackTrace(); + return this.unifiedExpResponse(BusinessExpCodeEnum.PARAMS_INCORRECT.getCode(), ex.getMessage()); + } + + @ExceptionHandler({BusinessException.class}) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public UnifiedResponse businessCheckExceptionHandler(BusinessException ex) { + ex.printStackTrace(); + + String description = ex.getDescription(); + String msg = ex.getBusinessMsg(); + if (StringUtils.hasText(description)) { + if (!ex.getReplace()) { + msg = msg + "[" + description + "]"; + } else { + msg = description; + } + } + return UnifiedResponse.fail(ex.getBusinessCode(), msg, ex.getData()); + + } + + @ExceptionHandler({IllegalArgumentException.class}) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public UnifiedResponse mybatisPlusException(IllegalArgumentException ex) { + ex.printStackTrace(); + return this.unifiedExpResponse("断言异常", ex.getMessage()); + } + + @ExceptionHandler({ValidationException.class}) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public UnifiedResponse validationException(ValidationException ex) { + ex.printStackTrace(); + return this.unifiedExpResponse("参数异常拦截", ex.getMessage()); + } + + @ExceptionHandler({BindException.class}) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public UnifiedResponse bindException(BindException ex) { + ex.printStackTrace(); + return this.unifiedExpResponse("参数绑定异常", ex.getMessage()); + } + @ExceptionHandler({Exception.class}) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public UnifiedResponse lastExceptionHandler(Exception ex) { + BusinessExpCodeEnum unknownError = BusinessExpCodeEnum.UNKNOWN_ERROR; + ex.printStackTrace(); + return this.unifiedExpResponse(unknownError.getCode(), unknownError.getMsg()); + } + + private UnifiedResponse unifiedExpResponse(String code, String msg) { + return UnifiedResponse.fail(code, msg); + } + + private UnifiedResponse unifiedExpResponse(String errorFrom, String code, String msg) { + return UnifiedResponse.fail(code, msg); + } + +} diff --git a/common/src/main/java/org/jiayunet/web/UnifiedResponseBodyAdvice.java b/common/src/main/java/org/jiayunet/web/UnifiedResponseBodyAdvice.java new file mode 100644 index 0000000..790bc8d --- /dev/null +++ b/common/src/main/java/org/jiayunet/web/UnifiedResponseBodyAdvice.java @@ -0,0 +1,44 @@ +package org.jiayunet.web; + +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.SneakyThrows; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; +import org.jiayunet.pojo.UnifiedResponse; + +/** + * 统一返回数据格式 + * + * @author zk + */ +@RestControllerAdvice +public class UnifiedResponseBodyAdvice implements ResponseBodyAdvice { + @Autowired + private ObjectMapper objectMapper; + + @Override + public boolean supports(MethodParameter returnType, Class> converterType) { + return !ResponseEntity.class.isAssignableFrom(returnType.getParameterType()); + } + + @SneakyThrows + @Override + public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class> selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { + + // 异常 + if (body instanceof UnifiedResponse) { + return body; + } + + UnifiedResponse unifiedResponse = UnifiedResponse.normalResponse(body); + return String.class.equals(returnType.getParameterType()) ? objectMapper.writeValueAsString(unifiedResponse) : unifiedResponse; + } + +} diff --git a/common/src/main/java/org/jiayunet/wxPay/WxJsPayAbility.java b/common/src/main/java/org/jiayunet/wxPay/WxJsPayAbility.java new file mode 100644 index 0000000..675d1c2 --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/WxJsPayAbility.java @@ -0,0 +1,223 @@ +package org.jiayunet.wxPay; + + +import com.wechat.pay.java.service.payments.jsapi.JsapiService; +import com.wechat.pay.java.service.payments.jsapi.model.CloseOrderRequest; +import com.wechat.pay.java.service.payments.jsapi.model.PrepayRequest; +import com.wechat.pay.java.service.payments.jsapi.model.PrepayResponse; +import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByIdRequest; +import com.wechat.pay.java.service.payments.jsapi.model.QueryOrderByOutTradeNoRequest; +import com.wechat.pay.java.service.payments.model.Transaction; +import com.wechat.pay.java.service.refund.RefundService; +import com.wechat.pay.java.service.refund.model.CreateRequest; +import com.wechat.pay.java.service.refund.model.QueryByOutRefundNoRequest; +import com.wechat.pay.java.service.refund.model.Refund; +import lombok.extern.slf4j.Slf4j; +import org.jiayunet.wxPay.server.model.JsRequestPay; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +import java.nio.charset.StandardCharsets; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.UUID; + +/** + * 微信支付能力 + * + * @author zk + */ +@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") +@Component +@Slf4j +public class WxJsPayAbility { + @Autowired + private JsapiService jsapiService; + @Autowired + private RefundService refundService; + @Autowired + private PrivateKey privateKey; + /** + * 回调域名地址 + */ + @Value("${wx_pay.notify_domain}") + private String notifyDomain; + /** + * 商户号 + */ + @Value("${wx_pay.merchant_id}") + private String merchantId; + /** + * 小程序id + */ + @Value("${app.wx.app_id}") + private String appId; + + + /** + * 下单 已默认回调地址 + * + * @param paramRequest 请求参数 + * @return 预支付订单号 + */ + public String prepay(PrepayRequest paramRequest) { + try { + paramRequest.setNotifyUrl(notifyDomain + "/public/wxNotify/pay"); + log.info("以默认设置支付回调地址为:{}", paramRequest.getNotifyUrl()); + paramRequest.setAppid(appId); + paramRequest.setMchid(merchantId); + PrepayResponse prepay = jsapiService.prepay(paramRequest); + return prepay.getPrepayId(); + } catch (Exception e) { + log.error("JSAPI支付下单出现异常,错误信息:{}", e.getMessage()); + } + throw new RuntimeException("JSAPI支付下单出现异常"); + } + + /** + * 关闭订单 + * + * @param outTradeNo 外部订单 + * @param child 商户id + */ + public void closeOrder(String outTradeNo, String child) { + CloseOrderRequest closeRequest = new CloseOrderRequest(); + closeRequest.setMchid(child); + closeRequest.setOutTradeNo(outTradeNo); + try { + jsapiService.closeOrder(closeRequest); + } catch (Exception e) { + log.error("JSAPI支付关闭订单出现异常,outTradeNo:{},mchId:{},错误信息:{}", outTradeNo, child, e.getMessage()); + } + throw new RuntimeException("JSAPI支付关闭订单出现异常"); + } + + /** + * 微信支付订单号查询订单 + * + * @param transactionId 交易订单id + * @param mchId 商户id + * @return 交易信息 + */ + public Transaction queryOrderById(String transactionId, String mchId) { + + QueryOrderByIdRequest request = new QueryOrderByIdRequest(); + request.setMchid(mchId); + request.setTransactionId(transactionId); + try { + return jsapiService.queryOrderById(request); + } catch (Exception e) { + log.error("JSAPI微信支付订单号查询订单出现异常,transactionId:{},mchId:{},错误信息:{}", transactionId, mchId, e.getMessage()); + } + + throw new RuntimeException("JSAPI微信支付订单号查询订单出现异常"); + } + + /** + * 商户订单号查询订单 + * + * @param outTradeNo 外部订单号 + * @param mchId 商户号 + */ + public Transaction queryOrderByOutTradeNo(String outTradeNo, String mchId) { + QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest(); + request.setMchid(mchId); + request.setOutTradeNo(outTradeNo); + try { + return jsapiService.queryOrderByOutTradeNo(request); + } catch (Exception e) { + log.error("JSAPI支付关闭订单出现异常,outTradeNo:{},mchId:{},错误信息:{}", outTradeNo, mchId, e.getMessage()); + } + throw new RuntimeException("JSAPI支付关闭订单出现异常"); + } + + + /** + * 退款申请 + * + * @param request 退款申请参数 + */ + + public Refund refund(CreateRequest request) { + try { + request.setNotifyUrl(notifyDomain + "/public/wxNotify/refund"); + log.info("以默认设置支付回调地址为:{}", request.getNotifyUrl()); + return refundService.create(request); + } catch (Exception e) { + log.error("JSAPI创建退款申请出现异常,请求参数:{},错误信息:{}", request, e.getMessage()); + } + throw new RuntimeException("JSAPI创建退款申请出现异常"); + } + + /** + * 查询单笔退款(通过商户退款单号) + * + * @param outRefundNo 外部订单号 + */ + public Refund queryByOutRefundNo(String outRefundNo) { + try { + QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest(); + request.setOutRefundNo(outRefundNo); + return refundService.queryByOutRefundNo(request); + } catch (Exception e) { + log.error("JSAPI查询单笔退款出现异常,outRefundNo:{},错误信息:{}", outRefundNo, e.getMessage()); + } + throw new RuntimeException("JSAPI查询单笔退款出现异常"); + + } + + + /** + * 签名 + */ + public String createSign(String appId, String timeStamp, String nonceStr, String packageStr) { + + try { + // 构造签名串 + String signStr = String.format("%s\n%s\n%s\n%s\n", appId, timeStamp, nonceStr, packageStr); + + // 加载私钥 + byte[] keyBytes = privateKey.getEncoded(); + PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes); + KeyFactory keyFactory = KeyFactory.getInstance("RSA"); + PrivateKey priKey = keyFactory.generatePrivate(keySpec); + + // 使用 SHA256withRSA 签名 + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initSign(priKey); + signature.update(signStr.getBytes(StandardCharsets.UTF_8)); + byte[] signed = signature.sign(); + + // 返回 Base64 编码的签名 + return Base64.getEncoder().encodeToString(signed); + } catch (Exception e) { + throw new RuntimeException("生成签名失败", e); + } + } + + + + /** + * 前端发起js支付需要的参数 + * @param prepayId 预订单号 + * @return JsRequestPay + */ + public JsRequestPay getJsRequestPay(String prepayId) { + JsRequestPay jsRequestPay = new JsRequestPay(); + jsRequestPay.setAppId(appId); + jsRequestPay.setNonceStr(UUID.randomUUID().toString().replace("-", "").substring(0,24)); + jsRequestPay.setSignType("RSA"); + jsRequestPay.setPackageStr("prepay_id=" + prepayId); + jsRequestPay.setTimeStamp(String.valueOf(System.currentTimeMillis()/1000)); + jsRequestPay.setPaySign(createSign(jsRequestPay.getAppId(),jsRequestPay.getTimeStamp(),jsRequestPay.getNonceStr(),jsRequestPay.getPackageStr())); + + return jsRequestPay; + } + + +} diff --git a/common/src/main/java/org/jiayunet/wxPay/WxNativePayAbility.java b/common/src/main/java/org/jiayunet/wxPay/WxNativePayAbility.java new file mode 100644 index 0000000..110d79f --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/WxNativePayAbility.java @@ -0,0 +1,65 @@ +package org.jiayunet.wxPay; + + +import com.wechat.pay.java.service.payments.model.Transaction; +import com.wechat.pay.java.service.payments.nativepay.NativePayService; +import com.wechat.pay.java.service.payments.nativepay.model.PrepayResponse; +import com.wechat.pay.java.service.payments.nativepay.model.QueryOrderByOutTradeNoRequest; +import com.wechat.pay.java.service.refund.RefundService; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import com.wechat.pay.java.service.payments.nativepay.model.PrepayRequest; +/** + * @author zk + */ +@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") +@Component +@Slf4j +public class WxNativePayAbility { + @Autowired + private NativePayService nativePayService; + /** + * 回调域名地址 + */ + @Value("${wx_pay.notify_domain}") + private String notifyDomain; + + + /** + * 预支付 + * @param request 请求头 + * @return + */ + public String prepay(PrepayRequest request) { + try { + request.setNotifyUrl(notifyDomain+"/public/wxNotify/pay"); + log.info("以默认设置支付回调地址为:{}",request.getNotifyUrl()); + PrepayResponse prepay = nativePayService.prepay(request); + return prepay.getCodeUrl(); + } catch (Exception e) { + log.error("nativePay支付下单出现异常,错误信息:{}", e.getMessage()); + } + throw new RuntimeException("nativePay支付下单出现异常"); + } + + public Transaction queryOrderByOutTradeNo(String outTradeNo, String mchid) { + try { + + QueryOrderByOutTradeNoRequest request = new QueryOrderByOutTradeNoRequest(); + request.setMchid(mchid); + request.setOutTradeNo(outTradeNo); + + return nativePayService.queryOrderByOutTradeNo(request); + + } catch (Exception e) { + log.error("nativePay支付下单出现异常,错误信息:{}", e.getMessage()); + } + throw new RuntimeException("nativePay支付下单出现异常"); + + + } + +} diff --git a/common/src/main/java/org/jiayunet/wxPay/WxPayNotifyController.java b/common/src/main/java/org/jiayunet/wxPay/WxPayNotifyController.java new file mode 100644 index 0000000..42b87d5 --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/WxPayNotifyController.java @@ -0,0 +1,121 @@ +package org.jiayunet.wxPay; + +import com.wechat.pay.java.core.exception.ValidationException; +import com.wechat.pay.java.core.notification.NotificationParser; +import com.wechat.pay.java.core.notification.RequestParam; +import com.wechat.pay.java.service.partnerpayments.nativepay.model.Transaction; +import com.wechat.pay.java.service.refund.model.RefundNotification; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.util.StringUtils; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.jiayunet.wxPay.server.model.TransferNotification; + +import javax.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +@RestController +@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") +@RequestMapping("/public/wxNotify") +@Slf4j +public class WxPayNotifyController { + @Autowired + private NotificationParser parser; + @Autowired + private WxPayNotifyMessageAbstract wxPayNotifyMessageAbstract; + + /** + * 支付结果回调 + */ + @PostMapping("/pay") + public ResponseEntity pay(HttpServletRequest request) { + return communalRequestHandle(request, Transaction.class); + } + + /** + * 退款结果回调 + */ + @PostMapping("/refund") + public ResponseEntity refund(HttpServletRequest request) { + return communalRequestHandle(request, RefundNotification.class); + } + + /** + * 商家转账回调 + */ + + @PostMapping("/transfer") + public ResponseEntity transfer(HttpServletRequest request) { + return communalRequestHandle(request, TransferNotification.class); + } + + + public ResponseEntity communalRequestHandle(HttpServletRequest request, Class tClass) { + String wechatPaySerial = request.getHeader("Wechatpay-Serial"); + String wechatpayNonce = request.getHeader("Wechatpay-Nonce"); + String wechatSignature = request.getHeader("Wechatpay-Signature"); + String wechatTimestamp = request.getHeader("Wechatpay-Timestamp"); + + String requestBody; + try { + requestBody = IOUtils.toString(request.getInputStream(), StandardCharsets.UTF_8); + } catch (IOException e) { + log.error("微信支付回调获取输入流出错", e); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("请求体读取失败"); + } + + if (!StringUtils.hasText(requestBody)) { + log.error("微信支付回调请求体为空"); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("请求体为空"); + } + + // 打印日志 + log.info("收到微信支付回调数据, body数据:{}",requestBody); + log.info("收到微信支付回调数据, 请求头wechatPaySerial数据:{}",wechatPaySerial); + log.info("收到微信支付回调数据, 请求头wechatpayNonce数据:{}",wechatpayNonce); + log.info("收到微信支付回调数据, 请求头wechatSignature数据:{}",wechatSignature); + log.info("收到微信支付回调数据, 请求头wechatTimestamp数据:{}",wechatTimestamp); + + + RequestParam requestParam = new RequestParam.Builder() + .serialNumber(wechatPaySerial) + .nonce(wechatpayNonce) + .signature(wechatSignature) + .timestamp(wechatTimestamp) + .body(requestBody) + .build(); + + try { + T parse = parser.parse(requestParam, tClass); + // 支付回调 + if (Objects.equals(tClass, Transaction.class)) { + wxPayNotifyMessageAbstract.payMessageHandle((Transaction) parse); + } + // 退款回调 + if (tClass == RefundNotification.class) { + wxPayNotifyMessageAbstract.refundMessageHandle((RefundNotification) parse); + } + // 商户转账回调 + if (tClass == TransferNotification.class){ + wxPayNotifyMessageAbstract.transferMessageHandle((TransferNotification) parse); + } + + } catch (ValidationException e) { + log.error("微信退回调数据验签错误, 请求体: {}, 请求头: {}", requestBody, request.getHeaderNames(), e); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("验签失败"); + } catch (Exception e) { + log.error("微信回调业务错误", e); + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("内部错误"); + } + + return ResponseEntity.ok("200"); + } +} \ No newline at end of file diff --git a/common/src/main/java/org/jiayunet/wxPay/WxPayNotifyMessageAbstract.java b/common/src/main/java/org/jiayunet/wxPay/WxPayNotifyMessageAbstract.java new file mode 100644 index 0000000..8e60bc9 --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/WxPayNotifyMessageAbstract.java @@ -0,0 +1,36 @@ +package org.jiayunet.wxPay; + +import com.wechat.pay.java.service.partnerpayments.nativepay.model.Transaction; +import com.wechat.pay.java.service.refund.model.RefundNotification; +import org.jiayunet.wxPay.server.model.TransferNotification; + +/** + * 微信支付回调消息处理接口 + * 使用前需要实集成接口 实现方法 + * @author zk + */ +public interface WxPayNotifyMessageAbstract { + /** + * 支付结果回调消息处理 + * @param transaction 支付回调消息 + */ + default void payMessageHandle(Transaction transaction){ + System.out.println("微信支付的支付结果回调消息未处理"); + } + + /** + * 退款回调消息 + * @param refundNotification 退款回调 + */ + default void refundMessageHandle(RefundNotification refundNotification){ + System.out.println("微信支付的退款回调消息未处理"); + } + + /** + * 商户支付回调消息 + * @param refundNotification 退款回调 + */ + default void transferMessageHandle(TransferNotification refundNotification){ + System.out.println("微信支付的商家转账回调未处理"); + } +} diff --git a/common/src/main/java/org/jiayunet/wxPay/WxTransferPayAbility.java b/common/src/main/java/org/jiayunet/wxPay/WxTransferPayAbility.java new file mode 100644 index 0000000..47b6e3a --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/WxTransferPayAbility.java @@ -0,0 +1,64 @@ +package org.jiayunet.wxPay; + +import com.wechat.pay.java.core.exception.WechatPayException; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; +import org.jiayunet.wxPay.server.TransferServer; +import org.jiayunet.wxPay.server.model.BillNoQueryRequest; +import org.jiayunet.wxPay.server.model.TransferBillsRequest; +import org.jiayunet.wxPay.server.model.TransferBillsResponse; +import org.jiayunet.wxPay.server.model.TransferResultResponse; + +/** + * @author zk + */ +@ConditionalOnProperty(name = "wx_pay.status", havingValue = "open") +@Component +@Slf4j +public class WxTransferPayAbility { + @Autowired + private TransferServer transferServer; + /** + * 回调域名地址 + */ + @Value("${wx_pay.notify_domain}") + private String notifyDomain; + + + + /** + * 发起转账 + * @param request 请求参数 + */ + public TransferBillsResponse transferBills(TransferBillsRequest request) { + try { + request.setNotifyUrl(notifyDomain+"/public/wxNotify/transfer"); + log.info("以默认设置回调地址为:{}",request.getNotifyUrl()); + return transferServer.transferBills(request); + + } catch (WechatPayException e) { + log.error("TransferPay发起转账出现异常,错误信息:{}", e.getMessage()); + throw new IllegalArgumentException(e.getMessage()); + }catch (Exception e){ + log.error("TransferPay发起转账出现异常,错误信息:{}", e.getMessage()); + throw new RuntimeException(e.getMessage()); + } + } + + /** + * 转账订单查询 + * @param request 请求参数 + */ + public TransferResultResponse billNoQuery(BillNoQueryRequest request) { + try { + return transferServer.billNoQuery(request); + } catch (Exception e){ + log.error("TransferPay查询账出现异常,错误信息:{}", e.getMessage()); + throw new RuntimeException("TransferPay查询账出现异常"); + } + } + +} diff --git a/common/src/main/java/org/jiayunet/wxPay/server/TransferServer.java b/common/src/main/java/org/jiayunet/wxPay/server/TransferServer.java new file mode 100644 index 0000000..8a432ff --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/server/TransferServer.java @@ -0,0 +1,172 @@ +package org.jiayunet.wxPay.server; + +import com.wechat.pay.java.core.Config; +import com.wechat.pay.java.core.cipher.PrivacyDecryptor; +import com.wechat.pay.java.core.cipher.PrivacyEncryptor; +import com.wechat.pay.java.core.http.Constant; +import com.wechat.pay.java.core.http.DefaultHttpClientBuilder; +import com.wechat.pay.java.core.http.HostName; +import com.wechat.pay.java.core.http.HttpClient; +import com.wechat.pay.java.core.http.HttpHeaders; +import com.wechat.pay.java.core.http.HttpMethod; +import com.wechat.pay.java.core.http.HttpRequest; +import com.wechat.pay.java.core.http.HttpResponse; +import com.wechat.pay.java.core.http.JsonRequestBody; +import com.wechat.pay.java.core.http.MediaType; +import com.wechat.pay.java.core.http.RequestBody; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.util.StringUtils; +import org.jiayunet.wxPay.server.model.BillNoQueryRequest; +import org.jiayunet.wxPay.server.model.CancelRequest; +import org.jiayunet.wxPay.server.model.CancelResponse; +import org.jiayunet.wxPay.server.model.TransferBillsRequest; +import org.jiayunet.wxPay.server.model.TransferBillsResponse; +import org.jiayunet.wxPay.server.model.TransferResultResponse; + +import static com.wechat.pay.java.core.http.UrlEncoder.urlEncode; +import static com.wechat.pay.java.core.util.GsonUtil.toJson; + +/** + * @author zk + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@Slf4j +public class TransferServer { + private HttpClient httpClient; + private HostName hostName; + private PrivacyEncryptor encryptor; + private PrivacyDecryptor decryptor; + + + /** TransferServer构造器 */ + public static class Builder { + + private HttpClient httpClient; + private HostName hostName; + private PrivacyEncryptor encryptor; + private PrivacyDecryptor decryptor; + + public TransferServer.Builder config(Config config) { + this.httpClient = new DefaultHttpClientBuilder().config(config).build(); + this.encryptor = config.createEncryptor(); + this.decryptor = config.createDecryptor(); + return this; + } + + public TransferServer.Builder hostName(HostName hostName) { + this.hostName = hostName; + return this; + } + + public TransferServer.Builder httpClient(HttpClient httpClient) { + this.httpClient = httpClient; + return this; + } + + public TransferServer.Builder encryptor(PrivacyEncryptor encryptor) { + this.encryptor = encryptor; + return this; + } + + public TransferServer.Builder decryptor(PrivacyDecryptor decryptor) { + this.decryptor = decryptor; + return this; + } + + public TransferServer build() { + return new TransferServer(httpClient, hostName, encryptor, decryptor); + } + } + + /** + * 发起转账 + * @param request 请求参数 + */ + public TransferBillsResponse transferBills(TransferBillsRequest request) { + String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills"; + if (StringUtils.hasText(request.getUserName())){ + request.setUserName(encryptor.encrypt(request.getUserName())); + if (request.getTransferAmount()<=30){ + log.info("微信商户转账0.3元一下不支持实名验证,以默认设置为空"); + request.setUserName(null); + } + } + + + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.addHeader(Constant.ACCEPT, MediaType.APPLICATION_JSON.getValue()); + headers.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue()); + headers.addHeader(Constant.WECHAT_PAY_SERIAL, encryptor.getWechatpaySerial()); + + + HttpRequest httpRequest = + new HttpRequest.Builder() + .httpMethod(HttpMethod.POST) + .url(requestPath) + .headers(headers) + .body(createRequestBody(request)) + .build(); + HttpResponse httpResponse = httpClient.execute(httpRequest, TransferBillsResponse.class); + return httpResponse.getServiceResponse(); + } + + + /** + * 撤销转账 + * @param request 请求参数 + */ + public CancelResponse cancel(CancelRequest request) { + String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/{out_bill_no}/cancel"; + // 添加 path param + requestPath = requestPath.replace("{" + "out_bill_no" + "}", urlEncode(request.getOutBillNo())); + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.addHeader(Constant.ACCEPT, MediaType.APPLICATION_JSON.getValue()); + headers.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue()); + HttpRequest httpRequest = + new HttpRequest.Builder() + .httpMethod(HttpMethod.POST) + .url(requestPath) + .headers(headers) + .body(createRequestBody(request)) + .build(); + HttpResponse httpResponse = httpClient.execute(httpRequest, CancelResponse.class); + return httpResponse.getServiceResponse(); + } + + /** + * 订单查询 + * @param request 参数 + */ + public TransferResultResponse billNoQuery(BillNoQueryRequest request) { + String requestPath = "https://api.mch.weixin.qq.com/v3/fund-app/mch-transfer/transfer-bills/transfer-bill-no/{transfer_bill_no}"; + // 添加 path param + requestPath = requestPath.replace("{" + "transfer_bill_no" + "}", urlEncode(request.getTransferBillNo())); + // 设置请求头 + HttpHeaders headers = new HttpHeaders(); + headers.addHeader(Constant.ACCEPT, MediaType.APPLICATION_JSON.getValue()); + headers.addHeader(Constant.CONTENT_TYPE, MediaType.APPLICATION_JSON.getValue()); + HttpRequest httpRequest = + new HttpRequest.Builder() + .httpMethod(HttpMethod.GET) + .url(requestPath) + .headers(headers) + .build(); + HttpResponse httpResponse = httpClient.execute(httpRequest, TransferResultResponse.class); + return httpResponse.getServiceResponse(); + } + + + + + private RequestBody createRequestBody(Object request) { + return new JsonRequestBody.Builder().body(toJson(request)).build(); + } + +} diff --git a/common/src/main/java/org/jiayunet/wxPay/server/model/BillNoQueryRequest.java b/common/src/main/java/org/jiayunet/wxPay/server/model/BillNoQueryRequest.java new file mode 100644 index 0000000..22da378 --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/server/model/BillNoQueryRequest.java @@ -0,0 +1,16 @@ +package org.jiayunet.wxPay.server.model; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +/** + * @author zk + */ +@Data +public class BillNoQueryRequest { + /** + * 外部订单号 + */ + @SerializedName("transfer_bill_no") + private String transferBillNo; +} diff --git a/common/src/main/java/org/jiayunet/wxPay/server/model/CancelRequest.java b/common/src/main/java/org/jiayunet/wxPay/server/model/CancelRequest.java new file mode 100644 index 0000000..746a773 --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/server/model/CancelRequest.java @@ -0,0 +1,23 @@ +package org.jiayunet.wxPay.server.model; + +import com.google.gson.annotations.Expose; +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author zk + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CancelRequest { + /** + * 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一 + */ + @SerializedName("outBillNo") + @Expose(serialize = false) + private String outBillNo; + +} diff --git a/common/src/main/java/org/jiayunet/wxPay/server/model/CancelResponse.java b/common/src/main/java/org/jiayunet/wxPay/server/model/CancelResponse.java new file mode 100644 index 0000000..c43a673 --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/server/model/CancelResponse.java @@ -0,0 +1,52 @@ +package org.jiayunet.wxPay.server.model; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author zk + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class CancelResponse { + + /** + * 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一 + **/ + @SerializedName("out_bill_no") + private String outBillNo; + + /** + * 【微信转账单号】 商家转账订单的主键,唯一定义此资源的标识 + */ + @SerializedName("transfer_bill_no") + private String transferBillNo; + + /** + * 【单据状态】 CANCELING: 撤销中;CANCELLED:已撤销 + */ + @SerializedName("state") + private CancelState state; + + /** + * 【最后一次单据状态变更时间】 按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE + */ + @SerializedName("update_time") + private String update_time; + + public enum CancelState{ + /** + * 撤销中 + */ + @SerializedName("CANCELING") + CANCELING, + /** + * 已撤销 + */ + @SerializedName("CANCELLED") + CANCELLED + } +} diff --git a/common/src/main/java/org/jiayunet/wxPay/server/model/JsRequestPay.java b/common/src/main/java/org/jiayunet/wxPay/server/model/JsRequestPay.java new file mode 100644 index 0000000..f100ba8 --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/server/model/JsRequestPay.java @@ -0,0 +1,33 @@ +package org.jiayunet.wxPay.server.model; + +import lombok.Data; + +@Data +public class JsRequestPay { + + /** + * 填写下单时传入的appid,且必需与当前实际调起支付的公众号appid一致,否则无法调起支付。 + */ + private String appId; + /** + * Unix 时间戳,是从1970年1月1日(UTC/GMT的午夜)开始所经过的秒数。 + */ + private String timeStamp; + /** + * 随机字符串,不长于32位。该值建议使用随机数算法生成。 + */ + private String nonceStr; + /** + * 订单详情扩展字符串,JSAPI下单接口返回的prepay_id参数值,提交格式如:prepay_id=***。 + */ + private String packageStr; + /** + * 签名类型,固定填RSA。 + */ + private String signType; + /** + * 签名,使用字段appId、timeStamp、nonceStr、package计算得出的签名值 注意:取值RSA格式。 + */ + private String paySign; + +} diff --git a/common/src/main/java/org/jiayunet/wxPay/server/model/TransferBillsRequest.java b/common/src/main/java/org/jiayunet/wxPay/server/model/TransferBillsRequest.java new file mode 100644 index 0000000..68faee2 --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/server/model/TransferBillsRequest.java @@ -0,0 +1,73 @@ +package org.jiayunet.wxPay.server.model; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import lombok.ToString; + +import java.util.List; + +/** + * @author zk + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@ToString +public class TransferBillsRequest { + /** 公众号ID 说明:公众号ID */ + @SerializedName("appid") + private String appid; + + /** 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一 */ + @SerializedName("out_bill_no") + private String outBillNo; + + /**【转账场景ID】 该笔转账使用的转账场景,可前往“商户平台-产品中心-商家转账”中申请。如:1001-现金营销*/ + @SerializedName("transfer_scene_id") + private String transferSceneId; + + /**【收款用户OpenID】 用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID,获取方式详见参数说明。**/ + @SerializedName("openid") + private String openid; + + /**【收款用户姓名】 收款方真实姓名。需要加密传入,支持标准RSA算法和国密算法,公钥由微信侧提供。 + 转账金额 >= 2,000元时,该笔明细必须填写 + 若商户传入收款用户姓名,微信支付会校验收款用户与输入姓名是否一致,并提供电子回单**/ + @SerializedName("user_name") + private String userName; + + /**【转账金额】 转账金额单位为“分”。*/ + @SerializedName("transfer_amount") + private Integer transferAmount; + + /**【转账备注】 转账备注,用户收款时可见该备注信息,UTF8编码,最多允许32个字符**/ + @SerializedName("transfer_remark") + private String transferRemark; + + /**【通知地址】 异步接收微信支付结果通知的回调地址,通知url必须为公网可访问的URL,必须为HTTPS,不能携带参数**/ + @SerializedName("notify_url") + private String notifyUrl; + + /**【用户收款感知】 用户收款时感知到的收款原因将根据转账场景自动展示默认内容。如有其他展示需求,可在本字段传入。各场景展示的默认内容和支持传入的内容,可查看产品文档了解**/ + @SerializedName("user_recv_perception") + private String userRecvPerception; + + @SerializedName("transfer_scene_report_infos") + private List transferSceneReportInfos; + + + @Data + @AllArgsConstructor + @NoArgsConstructor + public static class TransferSceneReportInfo{ + /**【信息类型】 不能超过15个字符,商户所属转账场景下的信息类型,此字段内容为固定值,需严格按照转账场景报备信息字段说明传参。**/ + @SerializedName("info_type") + private String infoType; + /**【信息内容】 不能超过32个字符,商户所属转账场景下的信息内容,商户可按实际业务场景自定义传参,需严格按照转账场景报备信息字段说明传参。**/ + @SerializedName("info_content") + private String infoContent; + } + +} diff --git a/common/src/main/java/org/jiayunet/wxPay/server/model/TransferBillsResponse.java b/common/src/main/java/org/jiayunet/wxPay/server/model/TransferBillsResponse.java new file mode 100644 index 0000000..26f305f --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/server/model/TransferBillsResponse.java @@ -0,0 +1,94 @@ +package org.jiayunet.wxPay.server.model; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * @author zk + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TransferBillsResponse { + /** + * 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一 + **/ + @SerializedName("out_bill_no") + private String outBillNo; + + /** + * 【微信转账单号】 微信转账单号,微信商家转账系统返回的唯一标识 + **/ + @SerializedName("transfer_bill_no") + private String transferBillNo; + + /** + * 【单据创建时间】 单据受理成功时返回,按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE + **/ + @SerializedName("create_time") + private String createTime; + + /** + * 【单据状态】 商家转账订单状态 + **/ + @SerializedName("state") + private StateEnum state; + + /** + * 【失败原因】 订单已失败或者已退资金时,会返回订单失败原因 + **/ + @SerializedName("fail_reason") + private String failReason; + + /** + * 【跳转领取页面的package信息】 跳转微信支付收款页的package信息,APP调起用户确认收款或者JSAPI调起用户确认收款 时需要使用的参数。 + * 单据创建后,用户24小时内不领取将过期关闭,建议拉起用户确认收款页面前,先查单据状态:如单据状态为待收款用户确认,可用之前的package信息拉起;单据到终态时需更换单号重新发起转账。 + */ + @SerializedName("package_info") + private String packageInfo; + + + + /** + * tradeState + */ + public static enum StateEnum { + /** + * 转账已受理 + */ + @SerializedName("ACCEPTED") + ACCEPTED, + /** + * 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试 + */ + @SerializedName("PROCESSING") + PROCESSING, + /** + * 待收款用户确认,可拉起微信收款确认页面进行收款确认 + */ + @SerializedName("WAIT_USER_CONFIRM") + WAIT_USER_CONFIRM, + /** + * 转账中,可拉起微信收款确认页面再次重试确认收款 + */ + @SerializedName("TRANSFERING") + TRANSFERING, + /** + * 转账成功 + */ + @SerializedName("SUCCESS") + SUCCESS, + /** + * 转账已受理 + */ + @SerializedName("FAIL") + FAIL, + /** + * 转账已受理 + */ + @SerializedName("CANCELLED") + CANCELLED, + } +} diff --git a/common/src/main/java/org/jiayunet/wxPay/server/model/TransferNotification.java b/common/src/main/java/org/jiayunet/wxPay/server/model/TransferNotification.java new file mode 100644 index 0000000..783e360 --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/server/model/TransferNotification.java @@ -0,0 +1,93 @@ +package org.jiayunet.wxPay.server.model; + +import com.google.gson.annotations.SerializedName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 商户支付回调 + * + * @author zk + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +public class TransferNotification { + /** + * 【商户单号】商户系统内部的商家单号,在商户系统内部唯一 + */ + @SerializedName("out_bill_no") + private String outBillNo; + + /** + * 【商家转账订单号】微信单号,微信商家转账系统返回的唯一标识 + */ + @SerializedName("transfer_bill_no") + private String transferBillNo; + + /** + * 【单据状态】微信单号,微信商家转账系统返回的唯一标识 + */ + @SerializedName("state") + private TransferState state; + + /** + * 【商户号】微信支付分配的商户号 + */ + @SerializedName("mch_id") + private String mchId; + + /** + * 【转账金额】转账总金额,单位为“分” + */ + @SerializedName("transfer_amount") + private String transferAmount ; + + /** + * 【收款用户OpenID】用户在商户appid下的唯一标识 + */ + @SerializedName("openid") + private String openid ; + + /** + * 【失败原因】单已失败或者已退资金时,会返回订单失败原因 + */ + @SerializedName("fail_reason") + private String failReason ; + + /** + * 【单据创建时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。 + */ + @SerializedName("create_time") + private String createTime ; + /** + * 【最后一次状态变更时间】遵循rfc3339标准格式,格式为yyyy-MM-DDTHH:mm:ss+TIMEZONE,yyyy-MM-DD表示年月日,T出现在字符串中,表示time元素的开头,HH:mm:ss.表示时分秒,TIMEZONE表示时区(+08:00表示东八区时间,领先UTC 8小时,即北京时间)。例如:2015-05-20T13:29:35+08:00表示北京时间2015年05月20日13点29分35秒。 + */ + @SerializedName("update_time") + private String updateTime; + + public static enum TransferState { + @SerializedName("ACCEPTED") + ACCEPTED, + @SerializedName("PROCESSING") + PROCESSING, + @SerializedName("WAIT_USER_CONFIRM") + WAIT_USER_CONFIRM, + @SerializedName("TRANSFERING") + TRANSFERING, + @SerializedName("SUCCESS") + SUCCESS, + @SerializedName("FAIL") + FAIL, + @SerializedName("CANCELING") + CANCELING, + @SerializedName("CANCELLED") + CANCELLED + + + } + + +} + diff --git a/common/src/main/java/org/jiayunet/wxPay/server/model/TransferResultResponse.java b/common/src/main/java/org/jiayunet/wxPay/server/model/TransferResultResponse.java new file mode 100644 index 0000000..ba3aac0 --- /dev/null +++ b/common/src/main/java/org/jiayunet/wxPay/server/model/TransferResultResponse.java @@ -0,0 +1,116 @@ +package org.jiayunet.wxPay.server.model; + +import com.google.gson.annotations.SerializedName; +import lombok.Data; + +/** + * @author zk + */ +@Data +public class TransferResultResponse { + /** + * 微信支付分配的商户号 + **/ + @SerializedName("mch_id") + private String mchId; + /** + * 【商户单号】 商户系统内部的商家单号,要求此参数只能由数字、大小写字母组成,在商户系统内部唯一 + */ + @SerializedName("out_bill_no") + private String outBillNo; + /** + * 【商家转账订单号】 商家转账订单的主键,唯一定义此资源的标识 + */ + @SerializedName("transfer_bill_no") + private String transferBillNo; + /** + * 【商户AppID】 是微信开放平台和微信公众平台为开发者的应用程序(APP、小程序、公众号、企业号corpid即为此AppID)提供的一个唯一标识。此处,可以填写这四种类型中的任意一种APPID,但请确保该appid与商户号有绑定关系。详见:普通商户模式开发必要参数说明。 + */ + @SerializedName("appid") + private String appid; + /** + * 【单据状态】 + */ + @SerializedName("state") + private StateEnum state; + /** + * 【转账金额】 转账金额单位为“分” + */ + @SerializedName("transfer_amount") + private Integer transferAmount; + /** + * 【转账备注】 单条转账备注(微信用户会收到该备注),UTF8编码,最多允许32个字符 + */ + @SerializedName("transfer_remark") + private String transferRemark; + /** + * 【失败原因】 订单已失败或者已退资金时,会返回订单失败原因 + */ + @SerializedName("fail_reason") + private String failReason; + /** + * 【收款用户OpenID】 用户在商户appid下的唯一标识。发起转账前需获取到用户的OpenID,获取方式详见参数说明。 + */ + @SerializedName("openid") + private String openid; + /** + * 【收款用户姓名】 收款方真实姓名。支持标准RSA算法和国密算法,公钥由微信侧提供转账金额 >= 2,000元时,该笔明细必须填写若商户传入收款用户姓名,微信支付会校验用户OpenID与姓名是否一致,并提供电子回单 + */ + @SerializedName("user_name") + private String userName; + /** + * 【单据创建时间】 单据受理成功时返回,按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE + */ + @SerializedName("create_time") + private String createTime; + /** + * 【最后一次状态变更时间】 单据最后更新时间,按照使用rfc3339所定义的格式,格式为yyyy-MM-DDThh:mm:ss+TIMEZONE + */ + @SerializedName("update_time") + private String update_time; + + + /** + * tradeState + */ + public enum StateEnum { + /** + * 转账已受理 + */ + @SerializedName("ACCEPTED") + ACCEPTED, + /** + * 转账锁定资金中。如果一直停留在该状态,建议检查账户余额是否足够,如余额不足,可充值后再原单重试 + */ + @SerializedName("PROCESSING") + PROCESSING, + /** + * 待收款用户确认,可拉起微信收款确认页面进行收款确认 + */ + @SerializedName("WAIT_USER_CONFIRM") + WAIT_USER_CONFIRM, + /** + * 转账中,可拉起微信收款确认页面再次重试确认收款 + */ + @SerializedName("TRANSFERING") + TRANSFERING, + /** + * 转账成功 + */ + @SerializedName("SUCCESS") + SUCCESS, + /** + * 转账已受理 + */ + @SerializedName("FAIL") + FAIL, + /** + * 转账已受理 + */ + @SerializedName("CANCELLED") + CANCELLED, + } + + + +} diff --git a/common/src/main/resources/logback-spring.xml b/common/src/main/resources/logback-spring.xml new file mode 100644 index 0000000..eae6f66 --- /dev/null +++ b/common/src/main/resources/logback-spring.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + ${CONSOLE_LOG_PATTERN} + UTF-8 + + + + + + + + + + + + + + + + + + ${LOG_FILE} + + ${LOG_PATTERN} + UTF-8 + + + ${LOG_PATH}/application-%d{yyyy-MM-dd}.log + 14 + + + + + + ${LOG_PATH}/sql.log + + ${LOG_PATTERN} + UTF-8 + + + ${LOG_PATH}/sql-%d{yyyy-MM-dd}.log + 14 + + + + + + + + + + + + + + \ No newline at end of file diff --git a/manager/pom.xml b/manager/pom.xml new file mode 100644 index 0000000..ed4c39a --- /dev/null +++ b/manager/pom.xml @@ -0,0 +1,28 @@ + + + 4.0.0 + + org.jiayunet + back_end + 1.0-SNAPSHOT + + + manager + 1.0-SNAPSHOT + + + + 17 + 17 + UTF-8 + + + + org.jiayunet + common + 1.0-SNAPSHOT + + + \ No newline at end of file diff --git a/manager/src/main/java/org/jiayunet/constant/OssBucketEnum.java b/manager/src/main/java/org/jiayunet/constant/OssBucketEnum.java new file mode 100644 index 0000000..ca423a9 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/constant/OssBucketEnum.java @@ -0,0 +1,36 @@ +package org.jiayunet.constant; + +import lombok.Getter; + +/** + * Oss桶 + */ +@Getter +public enum OssBucketEnum { + + + HuiDongNi("oss-cn-guangzhou.aliyuncs.com", "jiayupet", 1,"https://jiayupet.oss-cn-guangzhou.aliyuncs.com"); + /** + * 服务端口 + */ + private final String endpoint; + /** + * 桶名 + */ + private final String bucketName; + /** + * 权限 0 私有 1公共读 2 公共读写 + */ + private final int power; + /** + * + */ + private final String domain; + + OssBucketEnum(String endpoint, String bucketName, int power, String domain) { + this.endpoint = endpoint; + this.bucketName = bucketName; + this.power = power; + this.domain = domain; + } +} diff --git a/manager/src/main/java/org/jiayunet/constant/OssPathEnum.java b/manager/src/main/java/org/jiayunet/constant/OssPathEnum.java new file mode 100644 index 0000000..93e7d58 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/constant/OssPathEnum.java @@ -0,0 +1,47 @@ +package org.jiayunet.constant; + +import lombok.Getter; +import org.springframework.util.Assert; + + +import java.util.UUID; + +/** + * oss 文件路径 + */ +@Getter +public enum OssPathEnum { + UserImage("image/user", "用户图片"), + ResumeImage("image/resume", "简历图片"), + SystemImage("image/system", "系统图片"), + ResumeFile("file/resume", "简历文件"), + + ; + /** + * 文件存放目录 + */ + private final String catalogue; + /** + * 备注 + */ + private final String comment; + + OssPathEnum(String catalogue, String comment) { + this.catalogue = catalogue; + this.comment = comment; + } + + /** + * 获取文件路径 + * + * @param fileName 文件全名 + * @return oss 文件存放路径 + */ + public String getPath(String fileName) { + Assert.hasText(fileName, "获取oss文件路径文件名不能为空"); + String name = UUID.randomUUID().toString().replace("-", "").substring(0, 9); + String fileType = fileName.substring(fileName.lastIndexOf(".")); + return catalogue + "/" + name + fileType; + } + +} diff --git a/manager/src/main/java/org/jiayunet/constant/SmsVerifyCodeEnum.java b/manager/src/main/java/org/jiayunet/constant/SmsVerifyCodeEnum.java new file mode 100644 index 0000000..2c478ad --- /dev/null +++ b/manager/src/main/java/org/jiayunet/constant/SmsVerifyCodeEnum.java @@ -0,0 +1,48 @@ +package org.jiayunet.constant; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.util.Assert; +import org.jiayunet.sms.VerifyCodeAttribute; + +/** + * @author zk + */ +@Getter +@AllArgsConstructor +public enum SmsVerifyCodeEnum implements VerifyCodeAttribute { + /** + * 通用验证码 + */ + UNIVERSAL("加鱼宠物", "SMS_480245067", "universal:code:sms:", 5, false); + + + /** + * 模板签名 + */ + private final String signName; + /** + * 模板code + */ + private final String templateCode; + /** + * redis 名 + */ + private final String redisKeyPre; + /** + * 有效时间 单位分钟 + */ + private final Integer effectiveTime; + /** + * 覆盖已存在的redis值 + */ + private final Boolean cover; + + + public String getRedisKey(String phone) { + Assert.hasText(phone, "手机号码不能为空"); + return this.redisKeyPre + phone; + } + + +} diff --git a/manager/src/main/java/org/jiayunet/controller/HealthCheckController.java b/manager/src/main/java/org/jiayunet/controller/HealthCheckController.java new file mode 100644 index 0000000..960481b --- /dev/null +++ b/manager/src/main/java/org/jiayunet/controller/HealthCheckController.java @@ -0,0 +1,19 @@ +package org.jiayunet.controller; + + +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/public/actuator") +public class HealthCheckController { + + /** + * 健康检查 + */ + @RequestMapping("/health") + public String healthCheck() { + return "Service is running"; + } +} + diff --git a/manager/src/main/java/org/jiayunet/controller/OssController.java b/manager/src/main/java/org/jiayunet/controller/OssController.java new file mode 100644 index 0000000..f11c778 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/controller/OssController.java @@ -0,0 +1,30 @@ +package org.jiayunet.controller; + + +import lombok.AllArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.jiayunet.constant.OssPathEnum; +import org.jiayunet.pojo.vo.OssUrlVo; +import org.jiayunet.server.OssServer; + +/** + * oss控制类 + */ +@RestController +@RequestMapping("/oss") +@AllArgsConstructor +@Validated +public class OssController { + private OssServer ossServer; + + @GetMapping("/requestUploadUrl") + public OssUrlVo requestUploadUrl(@RequestParam String fileName, @RequestParam OssPathEnum pathEnum){ + return ossServer.requestUploadUrl(fileName,pathEnum); + } + + +} diff --git a/manager/src/main/java/org/jiayunet/mapper/OssFileMapper.java b/manager/src/main/java/org/jiayunet/mapper/OssFileMapper.java new file mode 100644 index 0000000..e5ed255 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/mapper/OssFileMapper.java @@ -0,0 +1,11 @@ +package org.jiayunet.mapper; + +import org.apache.ibatis.annotations.Mapper; +import org.jiayunet.pojo.po.OssFile; + +/** + * @author zk + */ +@Mapper +public interface OssFileMapper extends CommonMapper{ +} diff --git a/manager/src/main/java/org/jiayunet/mapper/UserMapper.java b/manager/src/main/java/org/jiayunet/mapper/UserMapper.java new file mode 100644 index 0000000..cdf5356 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/mapper/UserMapper.java @@ -0,0 +1,15 @@ +package org.jiayunet.mapper; + + +import org.apache.ibatis.annotations.Mapper; +import org.jiayunet.pojo.po.User; + +/** + * userMapper控制类 + * + * @author zk + */ +@Mapper +public interface UserMapper extends CommonMapper { + +} \ No newline at end of file diff --git a/manager/src/main/java/org/jiayunet/pojo/po/OssFile.java b/manager/src/main/java/org/jiayunet/pojo/po/OssFile.java new file mode 100644 index 0000000..fb6967e --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/po/OssFile.java @@ -0,0 +1,55 @@ +package org.jiayunet.pojo.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.Instant; + +/** + * Oss文件存储 + * @author zk + */ +@Data +@AllArgsConstructor +@NoArgsConstructor +@TableName("bg_oss_file") +public class OssFile { + @TableId(type = IdType.ASSIGN_ID) + private Long id; + /** + * 桶名 + */ + private String bucketName; + /** + * 文件类型 + */ + private String type; + /** + *文件路径 + */ + private String path; + /** + * 文件名 + */ + private String fileName; + /** + * 上传用户 + */ + private Long userId; + /** + * 状态 0 正常 1删除 + */ + private Integer status; + /** + * 创建时间 + */ + private Instant createTime; + /** + * 删除时间 + */ + private Instant deleteTime; +} diff --git a/manager/src/main/java/org/jiayunet/pojo/po/User.java b/manager/src/main/java/org/jiayunet/pojo/po/User.java new file mode 100644 index 0000000..a4c5cb3 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/po/User.java @@ -0,0 +1,91 @@ +package org.jiayunet.pojo.po; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.baomidou.mybatisplus.annotation.TableLogic; +import com.baomidou.mybatisplus.annotation.TableName; +import lombok.Data; + +import java.time.Instant; + +/** + * 用户基本信息类 + * + * @author zk + */ +@Data +@TableName(value = "bg_user") +public class User { + + @TableId(type = IdType.ASSIGN_ID) + private Long id; + /** + * 手机号 + */ + private String mobileNumber; + /** + * 邮箱 + */ + private String email; + /** + * 登陆密码 + */ + private String loginPassword; + /** + * 昵称 + */ + private String nick; + /** + * 真实名字 + */ + private String realName; + /** + * 头像 + */ + private String picture; + /** + * 生日 + */ + private Instant birthday; + /** + * 性别 1男 2女 + */ + private Integer sex; + /** + * 微信头像 + */ + private String wechatAvatar; + /** + * 学员的微信昵称 + */ + private String wechatNick; + /** + * 微信openid + */ + private String wechatOpenid; + /** + * 微信UnionId + */ + private String wechatUnionId; + /** + * 状态 0 正常 1 禁用 + */ + private Integer status; + + /** + * 添加时间 + */ + private Instant createTime; + + /** + * 修改时间 + */ + private Instant updateTime; + + /** + * 删除标识 0正常 非0 删除 + */ + @TableLogic(value = "0", delval = "new()") + private Long isDelete; + +} \ No newline at end of file diff --git a/manager/src/main/java/org/jiayunet/pojo/vo/OssUrlVo.java b/manager/src/main/java/org/jiayunet/pojo/vo/OssUrlVo.java new file mode 100644 index 0000000..fd87db3 --- /dev/null +++ b/manager/src/main/java/org/jiayunet/pojo/vo/OssUrlVo.java @@ -0,0 +1,20 @@ +package org.jiayunet.pojo.vo; + +import lombok.Data; + +/** + * oss 文件路径 + * @author zk + */ +@Data +public class OssUrlVo { + /** + * 上传地址 + */ + private String uploadUrl; + /** + * 下载地址 + */ + private String downloadUrl; + +} diff --git a/manager/src/main/java/org/jiayunet/server/OssServer.java b/manager/src/main/java/org/jiayunet/server/OssServer.java new file mode 100644 index 0000000..3c349fb --- /dev/null +++ b/manager/src/main/java/org/jiayunet/server/OssServer.java @@ -0,0 +1,111 @@ +package org.jiayunet.server; + +import com.aliyun.oss.HttpMethod; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.Assert; +import org.jiayunet.constant.OssBucketEnum; +import org.jiayunet.constant.OssPathEnum; +import org.jiayunet.exception.BusinessException; +import org.jiayunet.exception.BusinessExpCodeEnum; +import org.jiayunet.mapper.OssFileMapper; +import org.jiayunet.oss.AliOssAbility; +import org.jiayunet.pojo.po.OssFile; +import org.jiayunet.pojo.vo.OssUrlVo; +import org.jiayunet.tool.UserSecurityTool; + +import java.io.InputStream; +import java.time.Instant; + +/** + * Oss 服务类 + * + * @author zk + */ +@Service +@Slf4j +@AllArgsConstructor +public class OssServer { + + + private AliOssAbility aliOssAbility; + private OssFileMapper ossFileMapper; + + /** + * 申请上传文件签名授权地址 + * + * @return 地址 + */ + @Transactional(rollbackFor = Exception.class) + public OssUrlVo requestUploadUrl(String fileName, OssPathEnum pathEnum) { + Assert.notNull(pathEnum, " 上传路径不能为空"); + Assert.hasText(fileName, "文件不能为空"); + Assert.isTrue(fileName.lastIndexOf(".") >= 0, "文件名必须包含类型"); + + OssBucketEnum jiaYuPet = OssBucketEnum.HuiDongNi; + Long userId = UserSecurityTool.getUserId(); + String type = fileName.substring(fileName.lastIndexOf(".") + 1); + String path = pathEnum.getPath(fileName); + + + OssUrlVo ossUrlVo = new OssUrlVo(); + + try { + //保存上传信息 + ossFileMapper.insert(new OssFile(null, jiaYuPet.getBucketName(), type, path, fileName, userId, 0, Instant.now(), null)); + + //获取上传Url + String uploadUrl = aliOssAbility.signatureUrl(jiaYuPet.getEndpoint(), jiaYuPet.getBucketName(), path, 60, HttpMethod.PUT); + + //组装下载地址 + String downloadUrl = jiaYuPet.getDomain() + "/" + path; + + ossUrlVo.setUploadUrl(uploadUrl); + ossUrlVo.setDownloadUrl(downloadUrl); + + return ossUrlVo; + + + } catch (Exception e) { + throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR); + } + } + + public OssUrlVo simpleUpload(String fileName, OssPathEnum pathEnum, InputStream inputStream) { + Assert.notNull(pathEnum, " 上传路径不能为空"); + Assert.hasText(fileName, "文件不能为空"); + Assert.isTrue(fileName.lastIndexOf(".") >= 0, "文件名必须包含类型"); + + OssBucketEnum jiaYuPet = OssBucketEnum.HuiDongNi; + Long userId = UserSecurityTool.getUserId(); + String type = fileName.substring(fileName.lastIndexOf(".") + 1); + String path = pathEnum.getPath(fileName); + + + OssUrlVo ossUrlVo = new OssUrlVo(); + + try { + //保存上传信息 + ossFileMapper.insert(new OssFile(null, jiaYuPet.getBucketName(), type, path, fileName, userId, 0, Instant.now(), null)); + + aliOssAbility.simpleUpload(jiaYuPet.getEndpoint(), jiaYuPet.getBucketName(), path, inputStream); + inputStream.close(); + + + //组装下载地址 + String downloadUrl = jiaYuPet.getDomain() + "/" + path; + + ossUrlVo.setUploadUrl(downloadUrl); + ossUrlVo.setDownloadUrl(downloadUrl); + return ossUrlVo; + + } catch (Exception e) { + throw new BusinessException(BusinessExpCodeEnum.UNKNOWN_ERROR); + } + + + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..c09b070 --- /dev/null +++ b/pom.xml @@ -0,0 +1,82 @@ + + + + + + 4.0.0 + org.jiayunet + back_end + pom + 1.0-SNAPSHOT + + + common + client-api + manager + + + + UTF-8 + 17 + 17 + 2.6.13 + 3.5.3.1 + 3.11.0 + 2.0.24 + 3.17.4 + 2.7.15 + 1.6.2 + + + + + org.projectlombok + lombok + + + + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + com.baomidou + mybatis-plus-boot-starter + ${mybatis-plus.version} + + + com.auth0 + java-jwt + ${java-jwt.version} + + + com.aliyun + dysmsapi20170525 + ${aliyun.ssm.version} + + + com.aliyun.oss + aliyun-sdk-oss + ${aliyun.oss.version} + + + com.sun.mail + javax.mail + ${mail.version} + + + + + + + + diff --git a/settings.xml b/settings.xml new file mode 100644 index 0000000..5aa4ee0 --- /dev/null +++ b/settings.xml @@ -0,0 +1,15 @@ + + + + + + + mirror + central + mirror + https://maven.aliyun.com/nexus/content/groups/public + + + \ No newline at end of file