diff --git a/sky/sky-common/src/main/java/com/sky/context/BaseContext.java b/sky/sky-common/src/main/java/com/sky/context/BaseContext.java new file mode 100644 index 0000000..01c08a0 --- /dev/null +++ b/sky/sky-common/src/main/java/com/sky/context/BaseContext.java @@ -0,0 +1,43 @@ +package com.sky.context; + +/** + * BaseContext 类提供了一个基于 ThreadLocal 的机制来存储和检索当前线程的上下文信息。 + * 这种设计模式常用于保持线程安全的状态,例如在一次请求的生命周期内跟踪用户会话。 + */ +public class BaseContext { + + /** + * threadLocal 是一个 ThreadLocal 对象,用于存储每个线程的局部变量。 + * 它保证了每个线程都拥有自己的变量副本,避免了多线程环境下的共享状态问题。 + */ + private static ThreadLocal threadLocal = new ThreadLocal<>(); + + /** + * 设置当前线程的上下文ID。 + * 这个ID可以是用户ID、会话ID或其他任何需要在请求处理过程中保持的标识符。 + * + * @param id 要设置的上下文ID。 + */ + public static void setCurrentId(Long id) { + threadLocal.set(id); + } + + /** + * 获取当前线程的上下文ID。 + * 如果当前线程没有设置过ID,这个方法将返回 null。 + * + * @return 当前线程的上下文ID。 + */ + public static Long getCurrentId() { + return threadLocal.get(); + } + + /** + * 移除当前线程的上下文ID。 + * 这个方法通常在请求处理结束时被调用,以清理线程局部变量,避免内存泄漏。 + */ + public static void removeCurrentId() { + threadLocal.remove(); + } + +} \ No newline at end of file diff --git a/sky/sky-common/src/main/java/com/sky/exception/AddressBookBusinessException.java b/sky/sky-common/src/main/java/com/sky/exception/AddressBookBusinessException.java new file mode 100644 index 0000000..ba5eb52 --- /dev/null +++ b/sky/sky-common/src/main/java/com/sky/exception/AddressBookBusinessException.java @@ -0,0 +1,19 @@ +package com.sky.exception; + +/** + * AddressBookBusinessException 类是一个自定义的业务异常类,专门用于处理地址簿应用中特定的业务逻辑错误。 + * 这个异常类继承自 BaseException,使得它可以保持一致的异常处理机制,同时允许针对地址簿业务逻辑错误进行特定的异常处理。 + */ +public class AddressBookBusinessException extends BaseException { + + /** + * 构造函数,初始化 AddressBookBusinessException 实例。 + * + * @param msg 错误信息,描述了异常的具体原因。 + * 这个信息通常用于日志记录和错误报告,以便开发者或用户了解异常的具体情况。 + */ + public AddressBookBusinessException(String msg) { + super(msg); // 调用父类 BaseException 的构造函数,传递错误信息。 + } + +} \ No newline at end of file diff --git a/sky/sky-common/src/main/java/com/sky/exception/OrderBusinessException.java b/sky/sky-common/src/main/java/com/sky/exception/OrderBusinessException.java new file mode 100644 index 0000000..e86a305 --- /dev/null +++ b/sky/sky-common/src/main/java/com/sky/exception/OrderBusinessException.java @@ -0,0 +1,19 @@ +package com.sky.exception; + +/** + * OrderBusinessException 类是一个自定义的业务异常类,专门用于处理订单应用中特定的业务逻辑错误。 + * 这个异常类继承自 BaseException,使得它可以保持一致的异常处理机制,同时允许针对订单业务逻辑错误进行特定的异常处理。 + */ +public class OrderBusinessException extends BaseException { + + /** + * 构造函数,初始化 OrderBusinessException 实例。 + * + * @param msg 错误信息,描述了异常的具体原因。 + * 这个信息通常用于日志记录和错误报告,以便开发者或用户了解异常的具体情况。 + */ + public OrderBusinessException(String msg) { + super(msg); // 调用父类 BaseException 的构造函数,传递错误信息。 + } + +} \ No newline at end of file diff --git a/sky/sky-common/src/main/java/com/sky/exception/ShoppingCartBusinessException.java b/sky/sky-common/src/main/java/com/sky/exception/ShoppingCartBusinessException.java new file mode 100644 index 0000000..39bb895 --- /dev/null +++ b/sky/sky-common/src/main/java/com/sky/exception/ShoppingCartBusinessException.java @@ -0,0 +1,19 @@ +package com.sky.exception; + +/** + * ShoppingCartBusinessException 类是一个自定义的业务异常类,专门用于处理购物车应用中特定的业务逻辑错误。 + * 这个异常类继承自 BaseException,使得它可以保持一致的异常处理机制,同时允许针对购物车业务逻辑错误进行特定的异常处理。 + */ +public class ShoppingCartBusinessException extends BaseException { + + /** + * 构造函数,用于创建一个包含特定错误消息的 ShoppingCartBusinessException 实例。 + * + * @param msg 详细描述异常原因的错误消息。 + * 这个错误消息对于调试和日志记录至关重要,因为它提供了异常发生的上下文信息。 + */ + public ShoppingCartBusinessException(String msg) { + super(msg); // 调用父类 BaseException 的构造函数,并传递错误消息。 + } + +} \ No newline at end of file diff --git a/sky/sky-common/src/main/java/com/sky/properties/JwtProperties.java b/sky/sky-common/src/main/java/com/sky/properties/JwtProperties.java new file mode 100644 index 0000000..aaa9290 --- /dev/null +++ b/sky/sky-common/src/main/java/com/sky/properties/JwtProperties.java @@ -0,0 +1,52 @@ +package com.sky.properties; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.stereotype.Component; + +/** + * JwtProperties 类用于存储和配置 JWT(JSON Web Tokens)相关的属性。 + * 这个类通过 Spring Boot 的 @ConfigurationProperties 注解与 application.properties 或 application.yml 文件中的配置项绑定。 + */ +@Component +@ConfigurationProperties(prefix = "sky.jwt") // 指定配置文件中属性的前缀 +@Data +public class JwtProperties { + + /** + * adminSecretKey 用于管理端员工生成 JWT 令牌的密钥。 + * 这个密钥用于签名和验证 JWT,确保令牌的安全性。 + */ + private String adminSecretKey; + + /** + * adminTtl 表示管理端员工 JWT 令牌的有效期(单位:毫秒)。 + * 这个值定义了令牌在过期前的有效时长。 + */ + private long adminTtl; + + /** + * adminTokenName 表示管理端员工 JWT 令牌的名称。 + * 这个名称用于在 HTTP 请求头中标识 JWT 令牌。 + */ + private String adminTokenName; + + /** + * userSecretKey 用于用户端微信用户生成 JWT 令牌的密钥。 + * 这个密钥同样用于签名和验证 JWT,确保令牌的安全性。 + */ + private String userSecretKey; + + /** + * userTtl 表示用户端微信用户 JWT 令牌的有效期(单位:毫秒)。 + * 这个值定义了用户令牌在过期前的有效时长。 + */ + private long userTtl; + + /** + * userTokenName 表示用户端微信用户 JWT 令牌的名称。 + * 这个名称用于在 HTTP 请求头中标识 JWT 令牌。 + */ + private String userTokenName; + +} \ No newline at end of file diff --git a/sky/sky-common/src/main/java/com/sky/result/Result.java b/sky/sky-common/src/main/java/com/sky/result/Result.java new file mode 100644 index 0000000..2dd24a0 --- /dev/null +++ b/sky/sky-common/src/main/java/com/sky/result/Result.java @@ -0,0 +1,76 @@ +package com.sky.result; + +import lombok.Data; + +import java.io.Serializable; + +/** + * Result 类用于封装后端统一返回的结果。 + * 这个类实现了 Serializable 接口,以便在网络传输或持久化时能够被序列化。 + * + * @param 表示返回数据的类型,可以是任意类型。 + */ +@Data +public class Result implements Serializable { + + private static final long serialVersionUID = 1L; // 序列化版本号,用于确保反序列化时的兼容性 + + /** + * 编码:1 表示成功,0 和其它数字表示失败。 + * 这个字段用于指示操作的结果状态。 + */ + private Integer code; + + /** + * 错误信息,用于描述操作失败的原因。 + * 这个字段在操作失败时提供详细的错误信息,便于调试和用户反馈。 + */ + private String msg; + + /** + * 数据字段,存储返回的具体数据。 + * 这个字段可以是任意类型的数据,通常用于返回操作成功时的结果。 + */ + private T data; + + /** + * 静态方法,创建一个表示成功的 Result 对象,不带数据。 + * + * @param 返回数据的类型 + * @return 一个成功的 Result 对象,状态码为 1。 + */ + public static Result success() { + Result result = new Result(); + result.code = 1; // 设置状态码为成功 + return result; + } + + /** + * 静态方法,创建一个表示成功的 Result 对象,并包含返回数据。 + * + * @param object 要返回的数据 + * @param 返回数据的类型 + * @return 一个成功的 Result 对象,状态码为 1,包含返回的数据。 + */ + public static Result success(T object) { + Result result = new Result(); + result.data = object; // 设置返回的数据 + result.code = 1; // 设置状态码为成功 + return result; + } + + /** + * 静态方法,创建一个表示失败的 Result 对象,并包含错误信息。 + * + * @param msg 错误信息,描述失败的原因 + * @param 返回数据的类型 + * @return 一个失败的 Result 对象,状态码为 0,包含错误信息。 + */ + public static Result error(String msg) { + Result result = new Result(); + result.msg = msg; // 设置错误信息 + result.code = 0; // 设置状态码为失败 + return result; + } + +} \ No newline at end of file diff --git a/sky/sky-common/src/main/java/com/sky/utils/WeChatPayUtil.java b/sky/sky-common/src/main/java/com/sky/utils/WeChatPayUtil.java new file mode 100644 index 0000000..585ccf1 --- /dev/null +++ b/sky/sky-common/src/main/java/com/sky/utils/WeChatPayUtil.java @@ -0,0 +1,233 @@ +package com.sky.utils; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import com.sky.properties.WeChatProperties; +import com.wechat.pay.contrib.apache.httpclient.WechatPayHttpClientBuilder; +import com.wechat.pay.contrib.apache.httpclient.util.PemUtil; +import org.apache.commons.lang.RandomStringUtils; +import org.apache.http.HttpHeaders; +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.entity.ContentType; +import org.apache.http.entity.StringEntity; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.util.EntityUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.math.BigDecimal; +import java.security.PrivateKey; +import java.security.Signature; +import java.security.cert.X509Certificate; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Base64; +import java.util.List; + +/** + * 微信支付工具类,提供微信支付相关的操作,如下单、退款等。 + */ +@Component +public class WeChatPayUtil { + + /** + * 微信支付下单接口地址。 + */ + public static final String JSAPI = "https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi"; + + /** + * 申请退款接口地址。 + */ + public static final String REFUNDS = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"; + + @Autowired + private WeChatProperties weChatProperties; + + /** + * 获取调用微信接口的客户端工具对象。 + * 使用商户API私钥和微信支付平台证书进行签名和验签。 + * + * @return CloseableHttpClient 客户端工具对象。 + */ + private CloseableHttpClient getClient() { + PrivateKey merchantPrivateKey = null; + try { + // 加载商户API私钥 + merchantPrivateKey = PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath()))); + // 加载微信支付平台证书 + X509Certificate x509Certificate = PemUtil.loadCertificate(new FileInputStream(new File(weChatProperties.getWeChatPayCertFilePath()))); + List wechatPayCertificates = Arrays.asList(x509Certificate); + + // 构建HttpClient,自动处理签名和验签 + WechatPayHttpClientBuilder builder = WechatPayHttpClientBuilder.create() + .withMerchant(weChatProperties.getMchid(), weChatProperties.getMchSerialNo(), merchantPrivateKey) + .withWechatPay(wechatPayCertificates); + CloseableHttpClient httpClient = builder.build(); + return httpClient; + } catch (FileNotFoundException e) { + e.printStackTrace(); + return null; + } + } + + /** + * 发送POST请求到指定URL。 + * + * @param url 请求的URL地址 + * @param body 请求的JSON体 + * @return 响应结果字符串 + * @throws Exception 可能抛出的异常 + */ + private String post(String url, String body) throws Exception { + CloseableHttpClient httpClient = getClient(); + HttpPost httpPost = new HttpPost(url); + httpPost.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()); + httpPost.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()); + httpPost.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo()); + httpPost.setEntity(new StringEntity(body, "UTF-8")); + + CloseableHttpResponse response = httpClient.execute(httpPost); + try { + String bodyAsString = EntityUtils.toString(response.getEntity()); + return bodyAsString; + } finally { + httpClient.close(); + response.close(); + } + } + + /** + * 发送GET请求到指定URL。 + * + * @param url 请求的URL地址 + * @return 响应结果字符串 + * @throws Exception 可能抛出的异常 + */ + private String get(String url) throws Exception { + CloseableHttpClient httpClient = getClient(); + HttpGet httpGet = new HttpGet(url); + httpGet.addHeader(HttpHeaders.ACCEPT, ContentType.APPLICATION_JSON.toString()); + httpGet.addHeader(HttpHeaders.CONTENT_TYPE, ContentType.APPLICATION_JSON.toString()); + httpGet.addHeader("Wechatpay-Serial", weChatProperties.getMchSerialNo()); + + CloseableHttpResponse response = httpClient.execute(httpGet); + try { + String bodyAsString = EntityUtils.toString(response.getEntity()); + return bodyAsString; + } finally { + httpClient.close(); + response.close(); + } + } + + /** + * 使用JSAPI进行微信支付下单。 + * + * @param orderNum 商户订单号 + * @param total 总金额(元) + * @param description 商品描述 + * @param openid 微信用户的openid + * @return 响应结果字符串 + * @throws Exception 可能抛出的异常 + */ + private String jsapi(String orderNum, BigDecimal total, String description, String openid) throws Exception { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("appid", weChatProperties.getAppid()); + jsonObject.put("mchid", weChatProperties.getMchid()); + jsonObject.put("description", description); + jsonObject.put("out_trade_no", orderNum); + jsonObject.put("notify_url", weChatProperties.getNotifyUrl()); + + JSONObject amount = new JSONObject(); + amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue()); + amount.put("currency", "CNY"); + + jsonObject.put("amount", amount); + + JSONObject payer = new JSONObject(); + payer.put("openid", openid); + + jsonObject.put("payer", payer); + + String body = jsonObject.toJSONString(); + return post(JSAPI, body); + } + + /** + * 小程序支付接口。 + * + * @param orderNum 商户订单号 + * @param total 金额,单位元 + * @param description 商品描述 + * @param openid 微信用户的openid + * @return 包含支付参数的JSONObject + * @throws Exception 可能抛出的异常 + */ + public JSONObject pay(String orderNum, BigDecimal total, String description, String openid) throws Exception { + String bodyAsString = jsapi(orderNum, total, description, openid); + JSONObject jsonObject = JSON.parseObject(bodyAsString); + String prepayId = jsonObject.getString("prepay_id"); + if (prepayId != null) { + String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); + String nonceStr = RandomStringUtils.randomNumeric(32); + ArrayList list = new ArrayList<>(); + list.add(weChatProperties.getAppid()); + list.add(timeStamp); + list.add(nonceStr); + list.add("prepay_id=" + prepayId); + StringBuilder stringBuilder = new StringBuilder(); + for (Object o : list) { + stringBuilder.append(o).append("\n"); + } + String signMessage = stringBuilder.toString(); + byte[] message = signMessage.getBytes(); + Signature signature = Signature.getInstance("SHA256withRSA"); + signature.initSign(PemUtil.loadPrivateKey(new FileInputStream(new File(weChatProperties.getPrivateKeyFilePath())))); + signature.update(message); + String packageSign = Base64.getEncoder().encodeToString(signature.sign()); + + JSONObject jo = new JSONObject(); + jo.put("timeStamp", timeStamp); + jo.put("nonceStr", nonceStr); + jo.put("package", "prepay_id=" + prepayId); + jo.put("signType", "RSA"); + jo.put("paySign", packageSign); + + return jo; + } + return jsonObject; + } + + /** + * 申请退款接口。 + * + * @param outTradeNo 商户订单号 + * @param outRefundNo 商户退款单号 + * @param refund 退款金额 + * @param total 原订单金额 + * @return 响应结果字符串 + * @throws Exception 可能抛出的异常 + */ + public String refund(String outTradeNo, String outRefundNo, BigDecimal refund, BigDecimal total) throws Exception { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("out_trade_no", outTradeNo); + jsonObject.put("out_refund_no", outRefundNo); + + JSONObject amount = new JSONObject(); + amount.put("refund", refund.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue()); + amount.put("total", total.multiply(new BigDecimal(100)).setScale(2, BigDecimal.ROUND_HALF_UP).intValue()); + amount.put("currency", "CNY"); + + jsonObject.put("amount", amount); + jsonObject.put("notify_url", weChatProperties.getRefundNotifyUrl()); + + String body = jsonObject.toJSONString(); + + return post(REFUNDS, body); + } +} \ No newline at end of file