31 changed files with 877 additions and 175 deletions
@ -0,0 +1,43 @@ |
|||||
|
package cc.hiver.mall.controller; |
||||
|
|
||||
|
import cc.hiver.core.common.utils.ResultUtil; |
||||
|
import cc.hiver.core.common.vo.Result; |
||||
|
import cc.hiver.mall.entity.MallRefundRecord; |
||||
|
import cc.hiver.mall.service.mybatis.MallRefundRecordService; |
||||
|
import io.swagger.annotations.Api; |
||||
|
import io.swagger.annotations.ApiOperation; |
||||
|
import lombok.extern.slf4j.Slf4j; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
|
||||
|
/** |
||||
|
* 订单接口 |
||||
|
*/ |
||||
|
@Slf4j |
||||
|
@RestController |
||||
|
@Api(tags = "订单售后接口") |
||||
|
@RequestMapping("/hiver/mall/refund") |
||||
|
public class MallRefundRecordController { |
||||
|
|
||||
|
@Autowired |
||||
|
private MallRefundRecordService mallRefundRecordService; |
||||
|
|
||||
|
/** |
||||
|
* 下单(普通购买 / 发起拼团 / 参团) |
||||
|
*/ |
||||
|
@PostMapping("/create") |
||||
|
@ApiOperation(value = "售后提交接口") |
||||
|
public Result create(@RequestBody MallRefundRecord mallRefundRecord) { |
||||
|
try { |
||||
|
mallRefundRecordService.create(mallRefundRecord); |
||||
|
return ResultUtil.success("提交售后成功"); |
||||
|
} catch (Exception e) { |
||||
|
log.error("下单失败: {}", e.getMessage(), e); |
||||
|
return ResultUtil.error(e.getMessage()); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -0,0 +1,247 @@ |
|||||
|
package cc.hiver.mall.controller; |
||||
|
|
||||
|
import cc.hiver.mall.entity.MallOrder; |
||||
|
import cc.hiver.mall.service.mybatis.MallOrderService; |
||||
|
import cc.hiver.mall.serviceimpl.UnifiedOrderService; |
||||
|
import cc.hiver.mall.utils.KeyUtils; |
||||
|
import cc.hiver.mall.utils.WechatPayConfig; |
||||
|
import cc.hiver.mall.utils.WechatPaySigner; |
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import okhttp3.MediaType; |
||||
|
import okhttp3.OkHttpClient; |
||||
|
import okhttp3.Request; |
||||
|
import okhttp3.Response; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.core.io.Resource; |
||||
|
import org.springframework.core.io.ResourceLoader; |
||||
|
import org.springframework.http.ResponseEntity; |
||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||
|
import org.springframework.web.bind.annotation.RequestBody; |
||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||
|
import org.springframework.web.bind.annotation.RestController; |
||||
|
|
||||
|
import javax.annotation.PostConstruct; |
||||
|
import java.io.ByteArrayOutputStream; |
||||
|
import java.io.IOException; |
||||
|
import java.io.InputStream; |
||||
|
import java.nio.charset.StandardCharsets; |
||||
|
import java.security.PrivateKey; |
||||
|
import java.util.Base64; |
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
import java.util.UUID; |
||||
|
|
||||
|
@RestController |
||||
|
@RequestMapping("/hiver/api/wechat/pay") |
||||
|
public class WechatPayController { |
||||
|
|
||||
|
private static final Logger log = LoggerFactory.getLogger(WechatPayController.class); |
||||
|
|
||||
|
@Autowired |
||||
|
private UnifiedOrderService orderService; |
||||
|
|
||||
|
@Autowired |
||||
|
private WechatPayConfig config; |
||||
|
|
||||
|
@Autowired |
||||
|
private MallOrderService mallOrderService; |
||||
|
|
||||
|
@Autowired |
||||
|
private WechatPaySigner signer; |
||||
|
|
||||
|
@Autowired |
||||
|
private ResourceLoader resourceLoader; |
||||
|
|
||||
|
private PrivateKey privateKey; |
||||
|
|
||||
|
private final OkHttpClient httpClient = new OkHttpClient(); |
||||
|
private final ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
|
||||
|
@PostConstruct |
||||
|
public void init() throws Exception { |
||||
|
Resource resource = resourceLoader.getResource(config.getPrivateKeyPath()); |
||||
|
byte[] keyBytes; |
||||
|
try (InputStream is = resource.getInputStream()) { |
||||
|
keyBytes = readAllBytes(is); |
||||
|
} |
||||
|
|
||||
|
String key = new String(keyBytes, StandardCharsets.UTF_8) |
||||
|
.replace("-----BEGIN PRIVATE KEY-----", "") |
||||
|
.replace("-----END PRIVATE KEY-----", "") |
||||
|
.replaceAll("\\s+", ""); |
||||
|
|
||||
|
byte[] decodedKey = Base64.getDecoder().decode(key); |
||||
|
privateKey = KeyUtils.loadPrivateKey(decodedKey); |
||||
|
} |
||||
|
|
||||
|
private byte[] readAllBytes(InputStream inputStream) throws IOException { |
||||
|
ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
||||
|
byte[] data = new byte[1024]; |
||||
|
int nRead; |
||||
|
while ((nRead = inputStream.read(data, 0, data.length)) != -1) { |
||||
|
buffer.write(data, 0, nRead); |
||||
|
} |
||||
|
return buffer.toByteArray(); |
||||
|
} |
||||
|
|
||||
|
@PostMapping("/unified-order") |
||||
|
public ResponseEntity<Map<String, String>> jsapiPay(@RequestBody Map<String, String> request) throws Exception { |
||||
|
String outTradeNo = "ORDER_" + request.get("outTradeNo"); |
||||
|
String description = request.get("description"); |
||||
|
String openid = request.get("openid"); |
||||
|
long amount = Long.parseLong(request.get("amount")); // 单位:分
|
||||
|
|
||||
|
Map<String, Object> orderResult = orderService.createJsapiOrder(outTradeNo, description, openid, amount); |
||||
|
String prepayId = (String) orderResult.get("prepay_id"); |
||||
|
|
||||
|
if (prepayId == null) { |
||||
|
Map<String, String> payParams = new HashMap<>(); |
||||
|
payParams.put("code","101"); |
||||
|
payParams.put("message","调起微信支付失败"); |
||||
|
return ResponseEntity.ok(payParams); |
||||
|
}else{ |
||||
|
// 2. 生成前端支付参数
|
||||
|
Map<String, String> payParams = buildPaySign(prepayId); |
||||
|
payParams.put("code","200"); |
||||
|
return ResponseEntity.ok(payParams); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
@PostMapping("/paySuccess") |
||||
|
public ResponseEntity<Map<String, String>> paySuccess(@RequestBody Map<String, String> request) throws Exception { |
||||
|
//订单支付成功逻辑
|
||||
|
mallOrderService.paySuccess(request.get("outTradeNo")); |
||||
|
Map<String, String> payParams = new HashMap<>(); |
||||
|
payParams.put("code","200"); |
||||
|
payParams.put("message","支付成功订单更新"); |
||||
|
return ResponseEntity.ok(payParams); |
||||
|
} |
||||
|
|
||||
|
public Map<String, String> buildPaySign(String prepayId) throws Exception { |
||||
|
// 1. 生成参数
|
||||
|
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); // 秒
|
||||
|
String nonceStr = UUID.randomUUID().toString().replace("-", "").substring(0, 32); |
||||
|
String packageValue = "prepay_id=" + prepayId; |
||||
|
String signType = "RSA"; |
||||
|
|
||||
|
// 2. 构建签名字符串
|
||||
|
String message = String.join("\n", |
||||
|
config.getAppId(), |
||||
|
timeStamp, |
||||
|
nonceStr, |
||||
|
packageValue, |
||||
|
"" // 最后一个 \n
|
||||
|
); |
||||
|
|
||||
|
// 3. 使用商户私钥签名(和 API v3 同一套私钥)
|
||||
|
String paySign = KeyUtils.signWithSha256Rsa(message, privateKey); // ← 复用之前的签名方法
|
||||
|
|
||||
|
// 4. 返回给前端
|
||||
|
Map<String, String> result = new HashMap<>(); |
||||
|
result.put("timeStamp", timeStamp); |
||||
|
result.put("nonceStr", nonceStr); |
||||
|
result.put("package", packageValue); |
||||
|
result.put("signType", signType); |
||||
|
result.put("paySign", paySign); |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 微信退款通用方法 |
||||
|
* 传入 orderId,根据订单信息发起微信支付退款(全额退款) |
||||
|
*/ |
||||
|
@PostMapping("/refund") |
||||
|
public ResponseEntity<Map<String, String>> refund(@RequestBody Map<String, String> request) { |
||||
|
Map<String, String> result = new HashMap<>(); |
||||
|
String orderId = request.get("orderId"); |
||||
|
|
||||
|
try { |
||||
|
// 1. 查询订单信息
|
||||
|
MallOrder order = mallOrderService.getById(orderId); |
||||
|
if (order == null) { |
||||
|
result.put("code", "101"); |
||||
|
result.put("message", "订单不存在"); |
||||
|
return ResponseEntity.ok(result); |
||||
|
} |
||||
|
|
||||
|
// 2. 构建退款请求参数
|
||||
|
// 原支付交易号(与下单时一致,带 ORDER_ 前缀)
|
||||
|
String outTradeNo = "ORDER_" + orderId; |
||||
|
// 退款单号(唯一,使用 REFUND_ + orderId + 时间戳避免重复)
|
||||
|
String outRefundNo = "REFUND_" + orderId + "_" + System.currentTimeMillis(); |
||||
|
// 订单总金额(元 → 分)
|
||||
|
long totalFee = order.getTotalAmount() |
||||
|
.multiply(new java.math.BigDecimal("100")) |
||||
|
.longValue(); |
||||
|
// 退款金额(默认全额退款,如果前端传了 refundAmount 则使用前端的值)
|
||||
|
long refundFee = totalFee; |
||||
|
if (request.get("refundAmount") != null) { |
||||
|
refundFee = new java.math.BigDecimal(request.get("refundAmount")) |
||||
|
.multiply(new java.math.BigDecimal("100")) |
||||
|
.longValue(); |
||||
|
} |
||||
|
|
||||
|
// 退款原因
|
||||
|
String reason = request.get("reason") != null ? request.get("reason") : "订单退款"; |
||||
|
|
||||
|
// 3. 构建请求 Body
|
||||
|
Map<String, Object> reqBody = new HashMap<>(); |
||||
|
reqBody.put("out_trade_no", outTradeNo); |
||||
|
reqBody.put("out_refund_no", outRefundNo); |
||||
|
reqBody.put("reason", reason); |
||||
|
|
||||
|
Map<String, Object> amountMap = new HashMap<>(); |
||||
|
amountMap.put("refund", refundFee); |
||||
|
amountMap.put("total", totalFee); |
||||
|
amountMap.put("currency", "CNY"); |
||||
|
reqBody.put("amount", amountMap); |
||||
|
|
||||
|
String jsonBody = objectMapper.writeValueAsString(reqBody); |
||||
|
|
||||
|
// 4. 签名并调用微信退款 API
|
||||
|
String refundUrl = WechatPayConfig.REFUND_URL; |
||||
|
String authorization = signer.sign("POST", refundUrl, jsonBody); |
||||
|
|
||||
|
// 【修改点】使用标准的 create 方法,参数顺序是 (MediaType, String)
|
||||
|
okhttp3.RequestBody body = okhttp3.RequestBody.create( |
||||
|
MediaType.parse("application/json; charset=utf-8"), |
||||
|
jsonBody |
||||
|
); |
||||
|
|
||||
|
Request httpRequest = new Request.Builder() |
||||
|
.url(refundUrl) |
||||
|
.post(body) |
||||
|
.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 " + authorization) |
||||
|
.addHeader("Content-Type", "application/json") |
||||
|
.addHeader("Accept", "application/json") |
||||
|
.addHeader("User-Agent", "MyApp/1.0") |
||||
|
.build(); |
||||
|
|
||||
|
try (Response response = httpClient.newCall(httpRequest).execute()) { |
||||
|
String responseBody = response.body() != null ? response.body().string() : ""; |
||||
|
|
||||
|
if (response.isSuccessful()) { |
||||
|
// 退款申请成功(注意:微信退款是异步的,此处只表示申请提交成功)
|
||||
|
log.info("微信退款申请成功, orderId={}, outRefundNo={}, response={}", orderId, outRefundNo, responseBody); |
||||
|
result.put("code", "200"); |
||||
|
result.put("message", "退款申请成功"); |
||||
|
result.put("outRefundNo", outRefundNo); |
||||
|
result.put("refundResponse", responseBody); |
||||
|
} else { |
||||
|
log.error("微信退款申请失败, orderId={}, httpCode={}, response={}", orderId, response.code(), responseBody); |
||||
|
result.put("code", "102"); |
||||
|
result.put("message", "微信退款申请失败: " + responseBody); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("微信退款异常, orderId={}", orderId, e); |
||||
|
result.put("code", "500"); |
||||
|
result.put("message", "退款异常: " + e.getMessage()); |
||||
|
} |
||||
|
|
||||
|
return ResponseEntity.ok(result); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,13 @@ |
|||||
|
package cc.hiver.mall.dao.mapper; |
||||
|
|
||||
|
import cc.hiver.mall.entity.MallReturnOrderGoods; |
||||
|
import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
||||
|
import org.springframework.stereotype.Repository; |
||||
|
|
||||
|
/** |
||||
|
* 订单商品详情 Mapper 接口 |
||||
|
*/ |
||||
|
@Repository |
||||
|
public interface MallReturnOrderGoodsMapper extends BaseMapper<MallReturnOrderGoods> { |
||||
|
|
||||
|
} |
||||
@ -1,5 +1,7 @@ |
|||||
package cc.hiver.social.controller; |
package cc.hiver.mall.serviceimpl; |
||||
|
|
||||
|
import cc.hiver.mall.utils.WechatPayConfig; |
||||
|
import cc.hiver.mall.utils.WechatPaySigner; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import okhttp3.*; |
import okhttp3.*; |
||||
import org.springframework.beans.factory.annotation.Autowired; |
import org.springframework.beans.factory.annotation.Autowired; |
||||
@ -1,15 +1,50 @@ |
|||||
package cc.hiver.mall.serviceimpl.mybatis; |
package cc.hiver.mall.serviceimpl.mybatis; |
||||
|
|
||||
import cc.hiver.mall.dao.mapper.MallRefundRecordMapper; |
import cc.hiver.mall.dao.mapper.MallRefundRecordMapper; |
||||
|
import cc.hiver.mall.dao.mapper.MallReturnOrderGoodsMapper; |
||||
|
import cc.hiver.mall.entity.MallOrder; |
||||
import cc.hiver.mall.entity.MallRefundRecord; |
import cc.hiver.mall.entity.MallRefundRecord; |
||||
|
import cc.hiver.mall.service.mybatis.MallOrderService; |
||||
import cc.hiver.mall.service.mybatis.MallRefundRecordService; |
import cc.hiver.mall.service.mybatis.MallRefundRecordService; |
||||
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; |
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
import org.springframework.stereotype.Service; |
import org.springframework.stereotype.Service; |
||||
|
|
||||
|
import java.util.Date; |
||||
|
|
||||
/** |
/** |
||||
* 退款记录 Service 实现 |
* 退款记录 Service 实现 |
||||
*/ |
*/ |
||||
@Service |
@Service |
||||
public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMapper, MallRefundRecord> |
public class MallRefundRecordServiceImpl extends ServiceImpl<MallRefundRecordMapper, MallRefundRecord> |
||||
implements MallRefundRecordService { |
implements MallRefundRecordService { |
||||
|
|
||||
|
@Autowired |
||||
|
private MallRefundRecordMapper mallRefundRecordMapper; |
||||
|
|
||||
|
@Autowired |
||||
|
private MallReturnOrderGoodsMapper mallReturnOrderGoodsMapper; |
||||
|
|
||||
|
@Autowired |
||||
|
private MallOrderService mallOrderService; |
||||
|
|
||||
|
@Override |
||||
|
public void create(MallRefundRecord mallRefundRecord) { |
||||
|
//更新订单为 待售后
|
||||
|
LambdaUpdateWrapper<MallOrder> oUw = new LambdaUpdateWrapper<>(); |
||||
|
oUw.eq(MallOrder::getId, mallRefundRecord.getOrderId()) |
||||
|
.set(MallOrder::getStatus, 11); |
||||
|
mallOrderService.update(oUw); |
||||
|
mallRefundRecord.setStatus(3); // 申请售后
|
||||
|
mallRefundRecord.setCreateTime(new Date()); |
||||
|
mallRefundRecordMapper.insert(mallRefundRecord); |
||||
|
|
||||
|
if(!mallRefundRecord.getItems().isEmpty()){ |
||||
|
mallRefundRecord.getItems().forEach(item -> { |
||||
|
mallReturnOrderGoodsMapper.insert(item); |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
} |
||||
} |
} |
||||
|
|||||
@ -0,0 +1,25 @@ |
|||||
|
package cc.hiver.mall.utils; |
||||
|
|
||||
|
import org.bouncycastle.asn1.pkcs.PrivateKeyInfo; |
||||
|
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter; |
||||
|
|
||||
|
import java.security.PrivateKey; |
||||
|
import java.security.Signature; |
||||
|
import java.util.Base64; |
||||
|
|
||||
|
public class KeyUtils { |
||||
|
|
||||
|
public static PrivateKey loadPrivateKey(byte[] keyBytes) throws Exception { |
||||
|
PrivateKeyInfo pkInfo = PrivateKeyInfo.getInstance(keyBytes); |
||||
|
JcaPEMKeyConverter converter = new JcaPEMKeyConverter(); |
||||
|
return converter.getPrivateKey(pkInfo); |
||||
|
} |
||||
|
|
||||
|
public static String signWithSha256Rsa(String message, PrivateKey privateKey) throws Exception { |
||||
|
Signature sign = Signature.getInstance("SHA256withRSA"); |
||||
|
sign.initSign(privateKey); |
||||
|
sign.update(message.getBytes("utf-8")); |
||||
|
byte[] signed = sign.sign(); |
||||
|
return Base64.getEncoder().encodeToString(signed); |
||||
|
} |
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
package cc.hiver.social.controller; |
package cc.hiver.mall.utils; |
||||
|
|
||||
import org.springframework.beans.factory.annotation.Value; |
import org.springframework.beans.factory.annotation.Value; |
||||
import org.springframework.context.annotation.Configuration; |
import org.springframework.context.annotation.Configuration; |
||||
@ -0,0 +1,92 @@ |
|||||
|
package cc.hiver.mall.utils; |
||||
|
|
||||
|
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
|
import okhttp3.MediaType; |
||||
|
import okhttp3.OkHttpClient; |
||||
|
import okhttp3.Request; |
||||
|
import okhttp3.Response; |
||||
|
import org.slf4j.Logger; |
||||
|
import org.slf4j.LoggerFactory; |
||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||
|
import org.springframework.stereotype.Component; |
||||
|
|
||||
|
import java.util.HashMap; |
||||
|
import java.util.Map; |
||||
|
|
||||
|
/** |
||||
|
* 微信支付工具类 |
||||
|
* |
||||
|
* @author Yazhi Li |
||||
|
*/ |
||||
|
@Component |
||||
|
public class WechatPayUtil { |
||||
|
@Autowired |
||||
|
private WechatPaySigner signer; |
||||
|
|
||||
|
private static final Logger log = LoggerFactory.getLogger(WechatPayUtil.class); |
||||
|
|
||||
|
private final OkHttpClient httpClient = new OkHttpClient(); |
||||
|
private final ObjectMapper objectMapper = new ObjectMapper(); |
||||
|
public Boolean refund(String orderId,long totalFee) { |
||||
|
try { |
||||
|
// 2. 构建退款请求参数
|
||||
|
// 原支付交易号(与下单时一致,带 ORDER_ 前缀)
|
||||
|
String outTradeNo = "ORDER_" + orderId; |
||||
|
// 退款单号(唯一,使用 REFUND_ + orderId + 时间戳避免重复)
|
||||
|
String outRefundNo = "REFUND_" + orderId + "_" + System.currentTimeMillis(); |
||||
|
|
||||
|
// 退款原因
|
||||
|
String reason = "订单退款"; |
||||
|
// 3. 构建请求 Body
|
||||
|
Map<String, Object> reqBody = new HashMap<>(); |
||||
|
reqBody.put("out_trade_no", outTradeNo); |
||||
|
reqBody.put("out_refund_no", outRefundNo); |
||||
|
reqBody.put("reason", reason); |
||||
|
|
||||
|
Map<String, Object> amountMap = new HashMap<>(); |
||||
|
amountMap.put("refund", totalFee); |
||||
|
amountMap.put("total", totalFee); |
||||
|
amountMap.put("currency", "CNY"); |
||||
|
reqBody.put("amount", amountMap); |
||||
|
|
||||
|
String jsonBody = objectMapper.writeValueAsString(reqBody); |
||||
|
|
||||
|
// 4. 签名并调用微信退款 API
|
||||
|
String refundUrl = "https://api.mch.weixin.qq.com/v3/refund/domestic/refunds"; |
||||
|
String authorization = signer.sign("POST", refundUrl, jsonBody); |
||||
|
|
||||
|
// 【修改点】使用标准的 create 方法,参数顺序是 (MediaType, String)
|
||||
|
okhttp3.RequestBody body = okhttp3.RequestBody.create( |
||||
|
MediaType.parse("application/json; charset=utf-8"), |
||||
|
jsonBody |
||||
|
); |
||||
|
|
||||
|
Request httpRequest = new Request.Builder() |
||||
|
.url(refundUrl) |
||||
|
.post(body) |
||||
|
.addHeader("Authorization", "WECHATPAY2-SHA256-RSA2048 " + authorization) |
||||
|
.addHeader("Content-Type", "application/json") |
||||
|
.addHeader("Accept", "application/json") |
||||
|
.addHeader("User-Agent", "MyApp/1.0") |
||||
|
.build(); |
||||
|
|
||||
|
try (Response response = httpClient.newCall(httpRequest).execute()) { |
||||
|
String responseBody = response.body() != null ? response.body().string() : ""; |
||||
|
|
||||
|
if (response.isSuccessful()) { |
||||
|
// 退款申请成功(注意:微信退款是异步的,此处只表示申请提交成功)
|
||||
|
log.info("微信退款申请成功, orderId={}, outRefundNo={}, response={}", orderId, outRefundNo, responseBody); |
||||
|
return true; |
||||
|
} else { |
||||
|
log.error("微信退款申请失败, orderId={}, httpCode={}, response={}", orderId, response.code(), responseBody); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} catch (Exception e) { |
||||
|
log.error("微信退款异常, orderId={}", orderId, e); |
||||
|
return false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
} |
||||
@ -1,129 +0,0 @@ |
|||||
package cc.hiver.social.controller; |
|
||||
|
|
||||
import cc.hiver.mall.service.mybatis.MallOrderService; |
|
||||
import org.springframework.beans.factory.annotation.Autowired; |
|
||||
import org.springframework.core.io.Resource; |
|
||||
import org.springframework.core.io.ResourceLoader; |
|
||||
import org.springframework.http.ResponseEntity; |
|
||||
import org.springframework.web.bind.annotation.PostMapping; |
|
||||
import org.springframework.web.bind.annotation.RequestBody; |
|
||||
import org.springframework.web.bind.annotation.RequestMapping; |
|
||||
import org.springframework.web.bind.annotation.RestController; |
|
||||
|
|
||||
import javax.annotation.PostConstruct; |
|
||||
import java.io.ByteArrayOutputStream; |
|
||||
import java.io.IOException; |
|
||||
import java.io.InputStream; |
|
||||
import java.nio.charset.StandardCharsets; |
|
||||
import java.security.PrivateKey; |
|
||||
import java.util.Base64; |
|
||||
import java.util.HashMap; |
|
||||
import java.util.Map; |
|
||||
import java.util.UUID; |
|
||||
|
|
||||
@RestController |
|
||||
@RequestMapping("/hiver/api/wechat/pay") |
|
||||
public class WechatPayController { |
|
||||
@Autowired |
|
||||
private UnifiedOrderService orderService; |
|
||||
|
|
||||
@Autowired |
|
||||
private WechatPayConfig config; |
|
||||
|
|
||||
@Autowired |
|
||||
private MallOrderService mallOrderService; |
|
||||
|
|
||||
@Autowired |
|
||||
private ResourceLoader resourceLoader; |
|
||||
|
|
||||
private PrivateKey privateKey; |
|
||||
|
|
||||
@PostConstruct |
|
||||
public void init() throws Exception { |
|
||||
Resource resource = resourceLoader.getResource(config.getPrivateKeyPath()); |
|
||||
byte[] keyBytes; |
|
||||
try (InputStream is = resource.getInputStream()) { |
|
||||
keyBytes = readAllBytes(is); |
|
||||
} |
|
||||
|
|
||||
String key = new String(keyBytes, StandardCharsets.UTF_8) |
|
||||
.replace("-----BEGIN PRIVATE KEY-----", "") |
|
||||
.replace("-----END PRIVATE KEY-----", "") |
|
||||
.replaceAll("\\s+", ""); |
|
||||
|
|
||||
byte[] decodedKey = Base64.getDecoder().decode(key); |
|
||||
privateKey = KeyUtils.loadPrivateKey(decodedKey); |
|
||||
} |
|
||||
|
|
||||
private byte[] readAllBytes(InputStream inputStream) throws IOException { |
|
||||
ByteArrayOutputStream buffer = new ByteArrayOutputStream(); |
|
||||
byte[] data = new byte[1024]; |
|
||||
int nRead; |
|
||||
while ((nRead = inputStream.read(data, 0, data.length)) != -1) { |
|
||||
buffer.write(data, 0, nRead); |
|
||||
} |
|
||||
return buffer.toByteArray(); |
|
||||
} |
|
||||
|
|
||||
@PostMapping("/unified-order") |
|
||||
public ResponseEntity<Map<String, String>> jsapiPay(@RequestBody Map<String, String> request) throws Exception { |
|
||||
String outTradeNo = "ORDER_" + request.get("outTradeNo"); |
|
||||
String description = request.get("description"); |
|
||||
String openid = request.get("openid"); |
|
||||
long amount = Long.parseLong(request.get("amount")); // 单位:分
|
|
||||
|
|
||||
Map<String, Object> orderResult = orderService.createJsapiOrder(outTradeNo, description, openid, amount); |
|
||||
String prepayId = (String) orderResult.get("prepay_id"); |
|
||||
|
|
||||
if (prepayId == null) { |
|
||||
Map<String, String> payParams = new HashMap<>(); |
|
||||
payParams.put("code","101"); |
|
||||
payParams.put("message","调起微信支付失败"); |
|
||||
return ResponseEntity.ok(payParams); |
|
||||
}else{ |
|
||||
// 2. 生成前端支付参数
|
|
||||
Map<String, String> payParams = buildPaySign(prepayId); |
|
||||
payParams.put("code","200"); |
|
||||
return ResponseEntity.ok(payParams); |
|
||||
} |
|
||||
} |
|
||||
|
|
||||
@PostMapping("/paySuccess") |
|
||||
public ResponseEntity<Map<String, String>> paySuccess(@RequestBody Map<String, String> request) throws Exception { |
|
||||
//订单支付成功逻辑
|
|
||||
mallOrderService.paySuccess(request.get("outTradeNo")); |
|
||||
Map<String, String> payParams = new HashMap<>(); |
|
||||
payParams.put("code","200"); |
|
||||
payParams.put("message","支付成功订单更新"); |
|
||||
return ResponseEntity.ok(payParams); |
|
||||
} |
|
||||
|
|
||||
public Map<String, String> buildPaySign(String prepayId) throws Exception { |
|
||||
// 1. 生成参数
|
|
||||
String timeStamp = String.valueOf(System.currentTimeMillis() / 1000); // 秒
|
|
||||
String nonceStr = UUID.randomUUID().toString().replace("-", "").substring(0, 32); |
|
||||
String packageValue = "prepay_id=" + prepayId; |
|
||||
String signType = "RSA"; |
|
||||
|
|
||||
// 2. 构建签名字符串
|
|
||||
String message = String.join("\n", |
|
||||
config.getAppId(), |
|
||||
timeStamp, |
|
||||
nonceStr, |
|
||||
packageValue, |
|
||||
"" // 最后一个 \n
|
|
||||
); |
|
||||
|
|
||||
// 3. 使用商户私钥签名(和 API v3 同一套私钥)
|
|
||||
String paySign = KeyUtils.signWithSha256Rsa(message, privateKey); // ← 复用之前的签名方法
|
|
||||
|
|
||||
// 4. 返回给前端
|
|
||||
Map<String, String> result = new HashMap<>(); |
|
||||
result.put("timeStamp", timeStamp); |
|
||||
result.put("nonceStr", nonceStr); |
|
||||
result.put("package", packageValue); |
|
||||
result.put("signType", signType); |
|
||||
result.put("paySign", paySign); |
|
||||
return result; |
|
||||
} |
|
||||
} |
|
||||
Loading…
Reference in new issue