diff --git a/gather-app/pom.xml b/gather-app/pom.xml
index 48af3f2..4752621 100644
--- a/gather-app/pom.xml
+++ b/gather-app/pom.xml
@@ -35,5 +35,17 @@
compile
+
+
+ com.github.binarywang
+ weixin-java-pay
+ 4.5.0
+
+
+
+ com.alipay.sdk
+ alipay-sdk-java
+ 4.40.523.ALL
+
diff --git a/gather-app/src/main/java/com/ruoyi/config/AlipayConfig.java b/gather-app/src/main/java/com/ruoyi/config/AlipayConfig.java
new file mode 100644
index 0000000..5e8ec73
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/config/AlipayConfig.java
@@ -0,0 +1,23 @@
+package com.ruoyi.config;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class AlipayConfig {
+
+ @Value("${alipay.appId}")
+ public String appId;
+
+ @Value("${alipay.privateKey}")
+ public String privateKey;
+
+ @Value("${alipay.publicKey}")
+ public String alipayPublicKey;
+
+ @Value("${alipay.notifyUrl}")
+ public String notifyUrl;
+
+ @Value("${alipay.gateway}")
+ public String gateway;
+}
diff --git a/gather-app/src/main/java/com/ruoyi/config/RestTemplateConfig.java b/gather-app/src/main/java/com/ruoyi/config/RestTemplateConfig.java
new file mode 100644
index 0000000..9c2daca
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/config/RestTemplateConfig.java
@@ -0,0 +1,26 @@
+package com.ruoyi.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.client.ClientHttpRequestFactory;
+import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.web.client.RestTemplate;
+
+@Configuration
+public class RestTemplateConfig {
+
+ @Bean
+ public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
+ return new RestTemplate(factory);
+ }
+
+ @Bean
+ public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
+ SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
+ // 设置连接超时时间(毫秒)
+ factory.setConnectTimeout(15000);
+ // 设置读取超时时间(毫秒)
+ factory.setReadTimeout(15000);
+ return factory;
+ }
+}
diff --git a/gather-app/src/main/java/com/ruoyi/config/WxPayConfiguration.java b/gather-app/src/main/java/com/ruoyi/config/WxPayConfiguration.java
new file mode 100644
index 0000000..520f42a
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/config/WxPayConfiguration.java
@@ -0,0 +1,44 @@
+package com.ruoyi.config;
+
+import com.github.binarywang.wxpay.config.WxPayConfig;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.github.binarywang.wxpay.service.impl.WxPayServiceImpl;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class WxPayConfiguration {
+
+ @Value("${wx.pay.appId}")
+ private String appId;
+
+ @Value("${wx.pay.mchId}")
+ private String mchId;
+
+ @Value("${wx.pay.mchKey}")
+ private String mchKey;
+
+ @Value("${wx.pay.keyPath}")
+ private String keyPath;
+
+ @Value("${wx.pay.notifyUrl}")
+ private String notifyUrl;
+
+ @Bean
+ public WxPayConfig wxPayConfig() {
+ WxPayConfig payConfig = new WxPayConfig();
+ payConfig.setAppId(appId);
+ payConfig.setMchId(mchId);
+ payConfig.setMchKey(mchKey);
+ payConfig.setKeyPath(keyPath);
+ return payConfig;
+ }
+
+ @Bean
+ public WxPayService wxPayService(WxPayConfig wxPayConfig) {
+ WxPayService wxPayService = new WxPayServiceImpl();
+ wxPayService.setConfig(wxPayConfig);
+ return wxPayService;
+ }
+}
\ No newline at end of file
diff --git a/gather-app/src/main/java/com/ruoyi/database/controller/AlipayNotifyController.java b/gather-app/src/main/java/com/ruoyi/database/controller/AlipayNotifyController.java
new file mode 100644
index 0000000..a171581
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/controller/AlipayNotifyController.java
@@ -0,0 +1,84 @@
+package com.ruoyi.database.controller;
+
+import com.alipay.api.internal.util.AlipaySignature;
+import com.ruoyi.config.AlipayConfig;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/alipay")
+public class AlipayNotifyController {
+
+ private final AlipayConfig alipayConfig;
+ // 假设你有一个处理订单业务的服务
+ // private final OrderService orderService;
+
+ public AlipayNotifyController(AlipayConfig alipayConfig) {
+ this.alipayConfig = alipayConfig;
+ }
+
+ @PostMapping("/notify")
+ public String handleNotify(HttpServletRequest request) {
+ Map params = new HashMap<>();
+ // 1. 将异步通知的参数转换成一个Map
+ Map requestParams = request.getParameterMap();
+ for (String name : requestParams.keySet()) {
+ String[] values = requestParams.get(name);
+ String valueStr = "";
+ for (int i = 0; i < values.length; i++) {
+ valueStr = (i == values.length - 1) ? valueStr + values[i] : valueStr + values[i] + ",";
+ }
+ params.put(name, valueStr);
+ }
+
+ try {
+ // 2. 验证签名(非常重要!确保请求来自支付宝)
+ boolean signVerified = AlipaySignature.rsaCheckV1(
+ params,
+ alipayConfig.alipayPublicKey,
+ "UTF-8",
+ "RSA2"
+ );
+
+ if (!signVerified) {
+ // 签名验证失败,记录日志并拒绝
+ System.err.println("支付宝异步通知签名验证失败");
+ return "failure";
+ }
+
+ // 3. 签名验证通过,处理业务逻辑
+ String tradeStatus = params.get("trade_status");
+ String outTradeNo = params.get("out_trade_no"); // 你的商户订单号
+ String tradeNo = params.get("trade_no"); // 支付宝交易号
+
+ // 4. 判断交易状态
+ if ("TRADE_SUCCESS".equals(tradeStatus) || "TRADE_FINISH".equals(tradeStatus)) {
+ // 支付成功
+
+ // 4.1 幂等性处理:查询本地订单是否已处理,避免重复通知导致业务重复执行[citation:5]
+ // if (orderService.isOrderPaid(outTradeNo)) {
+ // return "success";
+ // }
+
+ // 4.2 校验金额(重要!防止金额被篡改)[citation:5]
+ // String totalAmount = params.get("total_amount");
+ // if (!orderService.verifyOrderAmount(outTradeNo, totalAmount)) {
+ // return "failure";
+ // }
+
+ // 4.3 更新本地订单状态为已支付
+ // orderService.updateOrderToPaid(outTradeNo, tradeNo);
+ System.out.println("订单支付成功: " + outTradeNo + ", 支付宝交易号: " + tradeNo);
+ }
+
+ // 5. 处理成功,返回 'success' 字符串给支付宝,支付宝将停止重复通知[citation:5]
+ return "success";
+ } catch (Exception e) {
+ System.err.println("处理支付宝异步通知异常: " + e.getMessage());
+ return "failure";
+ }
+ }
+}
\ No newline at end of file
diff --git a/gather-app/src/main/java/com/ruoyi/database/controller/PaymentController.java b/gather-app/src/main/java/com/ruoyi/database/controller/PaymentController.java
new file mode 100644
index 0000000..54976c4
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/controller/PaymentController.java
@@ -0,0 +1,98 @@
+package com.ruoyi.database.controller;
+
+import com.ruoyi.database.domain.Order;
+import com.ruoyi.database.service.OrderService;
+import com.ruoyi.database.service.WechatMiniProgramPayService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+import javax.servlet.http.HttpServletRequest;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/payment")
+public class PaymentController {
+
+ @Autowired
+ private WechatMiniProgramPayService wechatPayService;
+
+ @Autowired
+ private OrderService orderService;
+
+ /**
+ * 创建支付订单
+ */
+ @PostMapping("/create")
+ public Map createPayment(@RequestParam String openid,
+ @RequestParam String orderId,
+ @RequestParam Integer amount,
+ @RequestParam String description,
+ HttpServletRequest request) {
+ try {
+ System.out.println("收到创建支付订单请求: openid=" + openid +
+ ", orderId=" + orderId + ", amount=" + amount);
+
+ // 1. 创建业务订单
+ orderService.createOrder(orderId, amount, description, openid);
+
+ // 2. 获取客户端IP
+ String clientIp = wechatPayService.getClientIpAddress(request);
+
+ // 3. 创建支付订单
+ Map result = wechatPayService.createJsapiOrder(
+ openid, orderId, amount, description, clientIp);
+
+ return result;
+
+ } catch (Exception e) {
+ System.err.println("创建支付订单异常: " + e.getMessage());
+ e.printStackTrace();
+
+ Map result = new HashMap<>();
+ result.put("success", false);
+ result.put("message", "系统异常: " + e.getMessage());
+ return result;
+ }
+ }
+
+ /**
+ * 查询订单状态
+ */
+ @GetMapping("/status/{orderId}")
+ public Map getOrderStatus(@PathVariable String orderId) {
+ try {
+ System.out.println("查询订单状态: " + orderId);
+
+ Order order = orderService.getOrder(orderId);
+ Map result = new HashMap<>();
+
+ if (order != null) {
+ result.put("success", true);
+ result.put("orderId", order.getOrderId());
+ result.put("status", order.getStatus().name());
+ result.put("amount", order.getAmount());
+ result.put("description", order.getDescription());
+ result.put("createTime", order.getCreateTime());
+ if (order.getPayTime() != null) {
+ result.put("payTime", order.getPayTime());
+ }
+ if (order.getTransactionId() != null) {
+ result.put("transactionId", order.getTransactionId());
+ }
+ } else {
+ result.put("success", false);
+ result.put("message", "订单不存在");
+ }
+
+ return result;
+
+ } catch (Exception e) {
+ System.err.println("查询订单状态异常: " + e.getMessage());
+
+ Map result = new HashMap<>();
+ result.put("success", false);
+ result.put("message", "查询失败: " + e.getMessage());
+ return result;
+ }
+ }
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/controller/WxPayNotifyController.java b/gather-app/src/main/java/com/ruoyi/database/controller/WxPayNotifyController.java
new file mode 100644
index 0000000..e973452
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/controller/WxPayNotifyController.java
@@ -0,0 +1,76 @@
+package com.ruoyi.database.controller;
+
+import com.github.binarywang.wxpay.bean.notify.WxPayNotifyResponse;
+import com.github.binarywang.wxpay.bean.notify.WxPayOrderNotifyResult;
+import com.github.binarywang.wxpay.service.WxPayService;
+import com.ruoyi.database.service.OrderService;
+import com.ruoyi.database.service.WechatMiniProgramPayService;
+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;
+
+@RestController
+@RequestMapping("/api/wxpay")
+public class WxPayNotifyController {
+
+ @Autowired
+ private WechatMiniProgramPayService wechatPayService;
+
+ @Autowired
+ private OrderService orderService;
+
+ /**
+ * 微信支付结果通知回调
+ * 这个接口URL就是在创建订单时设置的 notifyUrl
+ */
+ @PostMapping("/notify")
+ public String handleNotify(@RequestBody String xmlData) {
+ try {
+ System.out.println("========== 收到支付通知 ==========");
+ System.out.println("通知数据: " + xmlData);
+
+ // 1. 获取 wxPayService
+ WxPayService wxPayService = wechatPayService.getWxPayService();
+
+ // 2. 使用SDK解析并验证通知数据(SDK会自动验证签名)
+ WxPayOrderNotifyResult notifyResult = wxPayService.parseOrderNotifyResult(xmlData);
+
+ // 3. 获取关键业务参数
+ String orderId = notifyResult.getOutTradeNo();
+ String transactionId = notifyResult.getTransactionId();
+ Integer totalFee = notifyResult.getTotalFee();
+ String timeEnd = notifyResult.getTimeEnd();
+
+ System.out.println("解析通知数据: orderId=" + orderId +
+ ", transactionId=" + transactionId + ", totalFee=" + totalFee);
+
+ // 4. 幂等性处理:检查订单是否已处理
+ if (orderService.isOrderPaid(orderId)) {
+ System.out.println("订单已处理,直接返回成功");
+ return WxPayNotifyResponse.success("OK");
+ }
+
+ // 5. 校验订单金额(重要!防止资金损失)
+ if (!orderService.verifyOrderAmount(orderId, totalFee)) {
+ System.out.println("订单金额校验失败");
+ return WxPayNotifyResponse.fail("订单金额不一致");
+ }
+
+ // 6. 处理业务逻辑(更新订单状态等)
+ orderService.updateOrderToPaid(orderId, transactionId);
+
+ System.out.println("支付通知处理成功");
+ System.out.println("========== 支付通知处理完成 ==========");
+
+ // 7. 返回成功响应(必须返回success)
+ return WxPayNotifyResponse.success("OK");
+
+ } catch (Exception e) {
+ System.err.println("支付通知处理失败: " + e.getMessage());
+ e.printStackTrace();
+ return WxPayNotifyResponse.fail("处理失败");
+ }
+ }
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/domain/LoginRequest.java b/gather-app/src/main/java/com/ruoyi/database/domain/LoginRequest.java
new file mode 100644
index 0000000..4c1e54d
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/domain/LoginRequest.java
@@ -0,0 +1,24 @@
+package com.ruoyi.database.domain;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import javax.validation.constraints.NotBlank;
+
+@Data
+public class LoginRequest {
+
+ /**
+ * 登录方式: wechat / alipay
+ */
+ @ApiModelProperty("登录方式: wechat 或 alipay")
+ @NotBlank(message = "登录方式不能为空")
+ private String loginType;
+
+ /**
+ * 微信或支付宝的授权code
+ */
+ @ApiModelProperty("微信或支付宝的授权code")
+ @NotBlank(message = "授权code不能为空")
+ private String code;
+
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/domain/Order.java b/gather-app/src/main/java/com/ruoyi/database/domain/Order.java
new file mode 100644
index 0000000..37cfa4f
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/domain/Order.java
@@ -0,0 +1,49 @@
+package com.ruoyi.database.domain;
+
+
+import com.ruoyi.database.enums.OrderStatus;
+import lombok.Data;
+import java.time.LocalDateTime;
+
+@Data
+public class Order {
+ /**
+ * 商户订单号
+ */
+ private String orderId;
+
+ /**
+ * 订单金额(单位:分)
+ */
+ private Integer amount;
+
+ /**
+ * 订单描述
+ */
+ private String description;
+
+ /**
+ * 用户openid
+ */
+ private String openid;
+
+ /**
+ * 订单状态
+ */
+ private OrderStatus status;
+
+ /**
+ * 创建时间
+ */
+ private LocalDateTime createTime;
+
+ /**
+ * 支付时间
+ */
+ private LocalDateTime payTime;
+
+ /**
+ * 微信支付订单号
+ */
+ private String transactionId;
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/enums/LoginType.java b/gather-app/src/main/java/com/ruoyi/database/enums/LoginType.java
new file mode 100644
index 0000000..5a8c954
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/enums/LoginType.java
@@ -0,0 +1,30 @@
+package com.ruoyi.database.enums;
+
+
+import lombok.Getter;
+
+@Getter
+public enum LoginType {
+ WECHAT("wechat", "微信登录"),
+ ALIPAY("alipay", "支付宝登录");
+
+ private final String code;
+ private final String description;
+
+ LoginType(String code, String description) {
+ this.code = code;
+ this.description = description;
+ }
+
+ /**
+ * 根据code获取枚举
+ */
+ public static LoginType getByCode(String code) {
+ for (LoginType type : values()) {
+ if (type.getCode().equals(code)) {
+ return type;
+ }
+ }
+ throw new IllegalArgumentException("不支持的登录方式: " + code);
+ }
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/enums/OrderStatus.java b/gather-app/src/main/java/com/ruoyi/database/enums/OrderStatus.java
new file mode 100644
index 0000000..35e11a3
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/enums/OrderStatus.java
@@ -0,0 +1,11 @@
+package com.ruoyi.database.enums;
+
+/**
+ * 订单状态枚举
+ */
+public enum OrderStatus {
+ PENDING, // 待支付
+ PAID, // 已支付
+ CANCELLED, // 已取消
+ REFUNDED // 已退款
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/exception/BusinessException.java b/gather-app/src/main/java/com/ruoyi/database/exception/BusinessException.java
new file mode 100644
index 0000000..694b10e
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/exception/BusinessException.java
@@ -0,0 +1,23 @@
+package com.ruoyi.database.exception;
+
+
+import lombok.Getter;
+
+@Getter
+public class BusinessException extends RuntimeException {
+
+ private final String code;
+ private final String message;
+
+ public BusinessException(String code, String message) {
+ super(message);
+ this.code = code;
+ this.message = message;
+ }
+
+ public BusinessException(String message) {
+ super(message);
+ this.code = "500";
+ this.message = message;
+ }
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/service/AlipayPhoneService.java b/gather-app/src/main/java/com/ruoyi/database/service/AlipayPhoneService.java
new file mode 100644
index 0000000..35bb0ee
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/service/AlipayPhoneService.java
@@ -0,0 +1,316 @@
+package com.ruoyi.database.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.database.exception.BusinessException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import java.net.URLEncoder;
+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.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+public class AlipayPhoneService {
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Value("${alipay.appId}")
+ private String appId;
+
+ @Value("${alipay.privateKey}")
+ private String privateKey;
+
+ @Value("${alipay.gateway}")
+ private String gateway;
+
+ /**
+ * 通过授权码获取用户手机号
+ */
+ public String getPhoneNumber(String authCode) {
+ try {
+ // 1. 构建请求参数
+ Map params = buildBaseParams();
+ params.put("method", "alipay.system.oauth.token");
+ params.put("grant_type", "authorization_code");
+ params.put("code", authCode);
+
+ log.info("请求参数构建完成: {}", params);
+
+ // 2. 生成签名
+ String sign = generateSign(params);
+ params.put("sign", sign);
+
+ log.info("签名生成完成");
+
+ // 3. 调用支付宝接口 - 使用修复后的方法
+ String responseBody = callAlipayApiWithCharset(params);
+
+ log.info("支付宝手机号接口响应: {}", responseBody);
+
+ // 4. 解析响应
+ Map responseMap = objectMapper.readValue(responseBody, Map.class);
+ Map tokenResponse = (Map) responseMap.get("alipay_system_oauth_token_response");
+
+ if (tokenResponse == null) {
+ // 检查错误响应
+ Map errorResponse = (Map) responseMap.get("error_response");
+ if (errorResponse != null) {
+ String subCode = (String) errorResponse.get("sub_code");
+ String subMsg = (String) errorResponse.get("sub_msg");
+ throw new BusinessException("ALIPAY_PHONE_ERROR",
+ String.format("支付宝接口错误[%s]: %s", subCode, subMsg));
+ }
+ throw new BusinessException("ALIPAY_PHONE_ERROR", "支付宝接口响应格式错误");
+ }
+
+ // 5. 提取手机号
+ String mobile = (String) tokenResponse.get("mobile");
+ if (mobile == null || mobile.trim().isEmpty()) {
+ throw new BusinessException("ALIPAY_PHONE_ERROR", "获取手机号失败: 手机号为空");
+ }
+
+ // 6. 验证手机号格式
+ if (!validatePhoneNumber(mobile)) {
+ log.warn("获取到的手机号格式可能不正确: {}", mobile);
+ }
+
+ log.info("成功获取用户手机号: {}", mobile);
+ return mobile;
+
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("获取支付宝手机号异常", e);
+ throw new BusinessException("ALIPAY_PHONE_ERROR", "获取支付宝手机号异常: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 修复后的API调用方法 - 正确处理charset参数
+ */
+ private String callAlipayApiWithCharset(Map params) {
+ try {
+ // 1. 将charset参数提取到URL查询字符串中
+ String charset = params.get("charset");
+ params.remove("charset"); // 从业务参数中移除
+
+ // 2. 构建表单数据
+ String formData = params.entrySet().stream()
+ .map(entry -> {
+ try {
+ // 使用指定的charset进行编码
+ String encodedValue = URLEncoder.encode(entry.getValue(), charset);
+ return entry.getKey() + "=" + encodedValue;
+ } catch (Exception e) {
+ log.error("参数编码失败: key={}, value={}", entry.getKey(), entry.getValue(), e);
+ return entry.getKey() + "=" + entry.getValue();
+ }
+ })
+ .collect(Collectors.joining("&"));
+
+ // 3. 构建完整的URL,包含charset参数
+ String url = gateway + "?charset=" + charset;
+
+ log.debug("支付宝API请求URL: {}", url);
+ log.debug("支付宝API请求表单数据: {}", formData);
+
+ // 4. 设置请求头
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+ headers.add("User-Agent", "Alipay Client");
+
+ HttpEntity request = new HttpEntity<>(formData, headers);
+
+ // 5. 调用接口
+ ResponseEntity response = restTemplate.postForEntity(url, request, String.class);
+ return response.getBody();
+
+ } catch (Exception e) {
+ log.error("调用支付宝API异常", e);
+ throw new BusinessException("ALIPAY_API_ERROR", "调用支付宝API异常: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 修复签名生成方法
+ */
+ private String generateSign(Map params) {
+ try {
+ // 1. 参数排序
+ List keys = new ArrayList<>(params.keySet());
+ Collections.sort(keys);
+
+ // 2. 拼接待签名字符串(排除sign参数)
+ String signContent = keys.stream()
+ .filter(key -> !"sign".equals(key) && params.get(key) != null && !params.get(key).isEmpty())
+ .map(key -> {
+ try {
+ // 使用UTF-8编码进行签名(即使charset是其他值)
+ String value = params.get(key);
+ return key + "=" + value;
+ } catch (Exception e) {
+ return key + "=" + params.get(key);
+ }
+ })
+ .collect(Collectors.joining("&"));
+
+ log.debug("待签名字符串: {}", signContent);
+
+ // 3. 使用RSA2签名
+ Signature signature = Signature.getInstance("SHA256WithRSA");
+ signature.initSign(getPrivateKey());
+ signature.update(signContent.getBytes(StandardCharsets.UTF_8)); // 使用UTF-8编码签名
+ byte[] signed = signature.sign();
+
+ // 4. Base64编码
+ String sign = Base64.getEncoder().encodeToString(signed);
+ log.debug("生成签名: {}", sign);
+
+ return sign;
+
+ } catch (Exception e) {
+ log.error("生成支付宝签名异常", e);
+ throw new BusinessException("ALIPAY_SIGN_ERROR", "生成签名失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 构建基础参数 - 确保charset正确
+ */
+ private Map buildBaseParams() {
+ Map params = new HashMap<>();
+ params.put("app_id", appId);
+ params.put("charset", "UTF-8"); // 明确指定UTF-8
+ params.put("sign_type", "RSA2");
+ params.put("timestamp", getCurrentTimestamp());
+ params.put("version", "1.0");
+ params.put("format", "json");
+ return params;
+ }
+
+
+
+ /**
+ * 获取当前时间戳
+ */
+ private String getCurrentTimestamp() {
+ return java.time.LocalDateTime.now().format(
+ java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
+ );
+ }
+
+
+
+ /**
+ * 获取私钥
+ */
+ private PrivateKey getPrivateKey() {
+ try {
+ String privateKeyContent = privateKey
+ .replace("-----BEGIN PRIVATE KEY-----", "")
+ .replace("-----END PRIVATE KEY-----", "")
+ .replaceAll("\\s", "");
+
+ byte[] keyBytes = Base64.getDecoder().decode(privateKeyContent);
+ PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(keyBytes);
+ KeyFactory keyFactory = KeyFactory.getInstance("RSA");
+ return keyFactory.generatePrivate(keySpec);
+ } catch (Exception e) {
+ throw new BusinessException("ALIPAY_KEY_ERROR", "加载私钥失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 调用支付宝API
+ */
+ private String callAlipayApi(Map params) {
+ try {
+ // 构建表单数据
+ String formData = params.entrySet().stream()
+ .map(entry -> {
+ try {
+ return entry.getKey() + "=" + URLEncoder.encode(entry.getValue(), StandardCharsets.UTF_8.name());
+ } catch (Exception e) {
+ return entry.getKey() + "=" + entry.getValue();
+ }
+ })
+ .collect(Collectors.joining("&"));
+
+ log.debug("支付宝API请求表单数据: {}", formData);
+
+ // 设置请求头
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
+ headers.add("User-Agent", "Alipay Client");
+
+ HttpEntity request = new HttpEntity<>(formData, headers);
+
+ // 调用接口
+ ResponseEntity response = restTemplate.postForEntity(gateway, request, String.class);
+ return response.getBody();
+
+ } catch (Exception e) {
+ log.error("调用支付宝API异常", e);
+ throw new BusinessException("ALIPAY_API_ERROR", "调用支付宝API异常: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 验证支付宝响应签名(简化版,实际需要完整实现)
+ */
+ private boolean verifySign(Map responseMap) {
+ try {
+ // 这里需要实现完整的支付宝响应签名验证
+ // 包括提取sign、排序参数、使用支付宝公钥验证等
+ // 生产环境必须实现此方法
+
+ log.warn("支付宝响应签名验证需要完整实现");
+ return true; // 测试环境暂时返回true
+
+ } catch (Exception e) {
+ log.error("验证支付宝签名异常", e);
+ return false;
+ }
+ }
+
+ /**
+ * 验证手机号格式
+ */
+ public boolean validatePhoneNumber(String phoneNumber) {
+ if (phoneNumber == null || phoneNumber.trim().isEmpty()) {
+ return false;
+ }
+ // 简单的手机号格式验证
+ return phoneNumber.matches("^1[3-9]\\d{9}$");
+ }
+
+ /**
+ * 获取支付宝用户ID (如果需要)
+ */
+ public String getAlipayUserId(String authCode) {
+ try {
+ String phoneNumber = getPhoneNumber(authCode);
+
+ // 在实际业务中,你可能需要根据手机号关联用户ID
+ // 这里返回手机号作为示例,实际应该返回支付宝用户ID
+ return phoneNumber;
+
+ } catch (Exception e) {
+ log.error("获取支付宝用户ID异常", e);
+ throw new BusinessException("ALIPAY_USER_ID_ERROR", "获取支付宝用户ID异常: " + e.getMessage());
+ }
+ }
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/service/AlipayService.java b/gather-app/src/main/java/com/ruoyi/database/service/AlipayService.java
new file mode 100644
index 0000000..a262ee8
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/service/AlipayService.java
@@ -0,0 +1,73 @@
+package com.ruoyi.database.service;
+
+import com.alipay.api.AlipayClient;
+import com.alipay.api.DefaultAlipayClient;
+import com.alipay.api.request.AlipayTradeCreateRequest;
+import com.alipay.api.response.AlipayTradeCreateResponse;
+import com.ruoyi.config.AlipayConfig;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AlipayService {
+
+ private final AlipayConfig alipayConfig;
+
+ public AlipayService(AlipayConfig alipayConfig) {
+ this.alipayConfig = alipayConfig;
+ }
+
+ /**
+ * 创建支付宝小程序支付订单
+ *
+ * @param orderId 商户订单号
+ * @param amount 支付金额(单位:元)
+ * @param subject 订单标题
+ * @param buyerId 支付宝用户ID(相当于OpenID)
+ * @return 支付订单字符串,用于小程序前端调起支付
+ */
+ public String createOrder(String orderId, String amount, String subject, String buyerId) {
+ try {
+ // 1. 创建AlipayClient实例
+ AlipayClient alipayClient = new DefaultAlipayClient(
+ alipayConfig.gateway,
+ alipayConfig.appId,
+ alipayConfig.privateKey,
+ "json",
+ "UTF-8",
+ alipayConfig.alipayPublicKey,
+ "RSA2"
+ );
+
+ // 2. 创建API请求对象,设置请求参数
+ AlipayTradeCreateRequest request = new AlipayTradeCreateRequest();
+ request.setNotifyUrl(alipayConfig.notifyUrl);
+
+ // 3. 组装业务参数
+ String bizContent = "{" +
+ "\"out_trade_no\":\"" + orderId + "\"," +
+ "\"total_amount\":\"" + amount + "\"," +
+ "\"subject\":\"" + subject + "\"," +
+ "\"buyer_id\":\"" + buyerId + "\"," +
+ "\"product_code\":\"JSAPI_PAY\"" + // 小程序场景固定值
+ "}";
+ request.setBizContent(bizContent);
+
+ // 4. 调用支付宝接口
+ AlipayTradeCreateResponse response = alipayClient.execute(request);
+
+ // 5. 处理响应
+ if (response.isSuccess()) {
+ System.out.println("支付宝预下单成功,交易号: " + response.getTradeNo());
+ // 返回给前端的订单字符串,用于调起支付
+ return response.getOutTradeNo();
+ // 注意:在实际开发中,你可能需要返回一个包含更多信息的对象或重新构造一个支付参数字符串。
+ // 具体返回格式需与前端调用 my.tradePay 时所需的参数保持一致。
+ } else {
+ System.err.println("支付宝预下单失败: " + response.getMsg() + " - " + response.getSubMsg());
+ throw new RuntimeException("支付宝预下单失败");
+ }
+ } catch (Exception e) {
+ throw new RuntimeException("调用支付宝接口异常", e);
+ }
+ }
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/service/OrderService.java b/gather-app/src/main/java/com/ruoyi/database/service/OrderService.java
new file mode 100644
index 0000000..898b9fa
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/service/OrderService.java
@@ -0,0 +1,88 @@
+package com.ruoyi.database.service;
+
+import com.ruoyi.database.domain.Order;
+import com.ruoyi.database.enums.OrderStatus;
+import org.springframework.stereotype.Service;
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Service
+public class OrderService {
+
+ /**
+ * 使用内存Map存储订单,生产环境请用数据库
+ */
+ private static final Map orderCache = new ConcurrentHashMap<>();
+
+ /**
+ * 创建订单
+ */
+ public Order createOrder(String orderId, Integer amount, String description, String openid) {
+ Order order = new Order();
+ order.setOrderId(orderId);
+ order.setAmount(amount);
+ order.setDescription(description);
+ order.setOpenid(openid);
+ order.setStatus(OrderStatus.PENDING);
+ order.setCreateTime(LocalDateTime.now());
+
+ orderCache.put(orderId, order);
+
+ System.out.println("创建订单成功: " + orderId + ", 金额: " + amount + "分");
+ return order;
+ }
+
+ /**
+ * 检查订单是否已支付(幂等性处理)
+ */
+ public boolean isOrderPaid(String orderId) {
+ Order order = orderCache.get(orderId);
+ boolean isPaid = order != null && order.getStatus() == OrderStatus.PAID;
+ if (isPaid) {
+ System.out.println("订单已支付: " + orderId);
+ }
+ return isPaid;
+ }
+
+ /**
+ * 验证订单金额
+ */
+ public boolean verifyOrderAmount(String orderId, Integer totalFee) {
+ Order order = orderCache.get(orderId);
+ boolean isValid = order != null && order.getAmount().equals(totalFee);
+ if (!isValid) {
+ System.out.println("订单金额验证失败: " + orderId +
+ ", 期望: " + (order != null ? order.getAmount() : "null") +
+ ", 实际: " + totalFee);
+ }
+ return isValid;
+ }
+
+ /**
+ * 更新订单状态为已支付
+ */
+ public void updateOrderToPaid(String orderId, String transactionId) {
+ Order order = orderCache.get(orderId);
+ if (order != null) {
+ order.setStatus(OrderStatus.PAID);
+ order.setTransactionId(transactionId);
+ order.setPayTime(LocalDateTime.now());
+ orderCache.put(orderId, order);
+
+ System.out.println("更新订单状态为已支付: " + orderId +
+ ", 微信订单号: " + transactionId);
+
+ // 这里可以添加其他业务逻辑,如发送消息、更新库存等
+ } else {
+ System.out.println("订单不存在: " + orderId);
+ }
+ }
+
+ /**
+ * 获取订单信息
+ */
+ public Order getOrder(String orderId) {
+ return orderCache.get(orderId);
+ }
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/service/WechatMiniProgramPayService.java b/gather-app/src/main/java/com/ruoyi/database/service/WechatMiniProgramPayService.java
new file mode 100644
index 0000000..18bd917
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/service/WechatMiniProgramPayService.java
@@ -0,0 +1,178 @@
+package com.ruoyi.database.service;
+
+import com.github.binarywang.wxpay.bean.request.WxPayUnifiedOrderRequest;
+import com.github.binarywang.wxpay.bean.result.WxPayUnifiedOrderResult;
+import com.github.binarywang.wxpay.service.WxPayService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import javax.servlet.http.HttpServletRequest;
+import java.security.MessageDigest;
+import java.util.*;
+
+@Service
+public class WechatMiniProgramPayService {
+
+ @Autowired
+ private WxPayService wxPayService;
+
+ @Value("${wx.pay.notifyUrl}")
+ private String notifyUrl;
+
+ /**
+ * 创建小程序支付订单
+ */
+ public Map createJsapiOrder(String openid, String orderId, Integer amount,
+ String description, String clientIp) {
+ try {
+ System.out.println("开始创建支付订单: " + orderId + ", openid: " + openid + ", 金额: " + amount + "分");
+
+ // 构建支付请求
+ WxPayUnifiedOrderRequest request = WxPayUnifiedOrderRequest.newBuilder()
+ .outTradeNo(orderId) // 商户订单号
+ .totalFee(amount) // 金额(分)
+ .body(description) // 商品描述
+ .tradeType("JSAPI") // 交易类型:小程序支付
+ .openid(openid) // 用户openid
+ .spbillCreateIp(clientIp) // 终端IP
+ .notifyUrl(notifyUrl) // 支付结果通知地址
+ .build();
+
+ System.out.println("调用微信支付统一下单API...");
+ // 调用微信支付API
+ WxPayUnifiedOrderResult result = wxPayService.unifiedOrder(request);
+
+ // 生成小程序支付参数
+ Map paymentParams = createPaymentParams(result);
+
+ System.out.println("支付订单创建成功: " + orderId + ", prepay_id: " + result.getPrepayId());
+
+ // 返回结果
+ Map response = new HashMap<>();
+ response.put("success", true);
+ response.put("paymentParams", paymentParams);
+ response.put("prepayId", result.getPrepayId());
+ return response;
+
+ } catch (Exception e) {
+ System.err.println("创建支付订单失败: " + orderId + ", 错误: " + e.getMessage());
+ e.printStackTrace();
+
+ Map response = new HashMap<>();
+ response.put("success", false);
+ response.put("message", "创建支付订单失败: " + e.getMessage());
+ return response;
+ }
+ }
+
+ /**
+ * 生成小程序支付参数
+ */
+ private Map createPaymentParams(WxPayUnifiedOrderResult result) {
+ Map paymentParams = new HashMap<>();
+
+ // 小程序支付必需的参数
+ paymentParams.put("appId", result.getAppid());
+ paymentParams.put("timeStamp", String.valueOf(System.currentTimeMillis() / 1000));
+ paymentParams.put("nonceStr", result.getNonceStr());
+ paymentParams.put("package", "prepay_id=" + result.getPrepayId());
+ paymentParams.put("signType", "MD5");
+
+ // 生成签名
+ String sign = generateSign(paymentParams);
+ paymentParams.put("paySign", sign);
+
+ System.out.println("生成支付参数: " + paymentParams);
+ return paymentParams;
+ }
+
+ /**
+ * 生成支付签名
+ */
+ private String generateSign(Map params) {
+ try {
+ // 按照参数名ASCII字典序排序
+ List keys = new ArrayList<>(params.keySet());
+ Collections.sort(keys);
+
+ // 拼接字符串
+ StringBuilder sb = new StringBuilder();
+ for (String key : keys) {
+ if ("sign".equals(key) || key.isEmpty() || params.get(key) == null) {
+ continue;
+ }
+ sb.append(key).append("=").append(params.get(key)).append("&");
+ }
+
+ // 加上API密钥
+ sb.append("key=").append(wxPayService.getConfig().getMchKey());
+
+ String signStr = sb.toString();
+ System.out.println("签名原串: " + signStr);
+
+ // MD5加密并转大写
+ String sign = md5(signStr).toUpperCase();
+ System.out.println("生成签名: " + sign);
+
+ return sign;
+ } catch (Exception e) {
+ throw new RuntimeException("生成签名失败", e);
+ }
+ }
+
+ /**
+ * MD5加密
+ */
+ private String md5(String data) throws Exception {
+ MessageDigest md = MessageDigest.getInstance("MD5");
+ byte[] array = md.digest(data.getBytes("UTF-8"));
+ StringBuilder sb = new StringBuilder();
+ for (byte item : array) {
+ sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
+ }
+ return sb.toString();
+ }
+
+ /**
+ * 获取客户端IP地址
+ */
+ public String getClientIpAddress(HttpServletRequest request) {
+ String ip = request.getHeader("X-Forwarded-For");
+
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("WL-Proxy-Client-IP");
+ }
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("HTTP_CLIENT_IP");
+ }
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getHeader("HTTP_X_FORWARDED_FOR");
+ }
+ if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
+ ip = request.getRemoteAddr();
+ }
+
+ // 如果是多级代理,取第一个IP
+ if (ip != null && ip.contains(",")) {
+ ip = ip.substring(0, ip.indexOf(",")).trim();
+ }
+
+ // 本地开发环境处理
+ if ("0:0:0:0:0:0:0:1".equals(ip) || "127.0.0.1".equals(ip)) {
+ ip = "123.123.123.123"; // 替换为你的服务器公网IP
+ }
+
+ System.out.println("获取到客户端IP: " + ip);
+ return ip;
+ }
+
+ /**
+ * 获取WxPayService实例
+ */
+ public WxPayService getWxPayService() {
+ return wxPayService;
+ }
+}
diff --git a/gather-app/src/main/java/com/ruoyi/database/service/WechatPhoneService.java b/gather-app/src/main/java/com/ruoyi/database/service/WechatPhoneService.java
new file mode 100644
index 0000000..0b78561
--- /dev/null
+++ b/gather-app/src/main/java/com/ruoyi/database/service/WechatPhoneService.java
@@ -0,0 +1,191 @@
+package com.ruoyi.database.service;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.ruoyi.database.exception.BusinessException;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.*;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+import java.util.HashMap;
+import java.util.Map;
+
+@Slf4j
+@Service
+public class WechatPhoneService {
+
+ @Autowired
+ private RestTemplate restTemplate;
+
+ @Autowired
+ private ObjectMapper objectMapper;
+
+ @Value("${wechat.appId}")
+ private String appId;
+
+ @Value("${wechat.appSecret}")
+ private String appSecret;
+
+ @Value("${wechat.getPhoneNumberUrl}")
+ private String getPhoneNumberUrl;
+
+ @Value("${wechat.accessTokenUrl}")
+ private String accessTokenUrl;
+
+ /**
+ * 获取微信访问令牌
+ */
+ public String getAccessToken() {
+ try {
+ String url = String.format("%s?grant_type=client_credential&appid=%s&secret=%s",
+ accessTokenUrl, appId, appSecret);
+
+ log.info("请求微信access_token, URL: {}", url);
+
+ ResponseEntity response = restTemplate.getForEntity(url, String.class);
+ String responseBody = response.getBody();
+
+ log.info("微信access_token响应: {}", responseBody);
+
+ // 解析响应
+ Map result = objectMapper.readValue(responseBody, Map.class);
+ String accessToken = (String) result.get("access_token");
+ Integer errcode = (Integer) result.get("errcode");
+
+ if (accessToken != null) {
+ log.info("成功获取微信access_token");
+ return accessToken;
+ } else if (errcode != null) {
+ String errmsg = (String) result.get("errmsg");
+ log.error("获取微信access_token失败: {} - {}", errcode, errmsg);
+ throw new BusinessException("WECHAT_ACCESS_TOKEN_ERROR",
+ String.format("获取微信访问令牌失败[%s]: %s", errcode, errmsg));
+ } else {
+ throw new BusinessException("WECHAT_ACCESS_TOKEN_ERROR", "获取微信访问令牌失败: 响应格式错误");
+ }
+
+ } catch (BusinessException e) {
+ throw e;
+ } catch (Exception e) {
+ log.error("获取微信access_token异常", e);
+ throw new BusinessException("WECHAT_ACCESS_TOKEN_ERROR", "获取微信访问令牌异常: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 通过code获取用户手机号
+ */
+ public String getPhoneNumber(String code) {
+ try {
+ // 1. 获取access_token
+ String accessToken = getAccessToken();
+
+ // 2. 构建请求参数
+ Map requestBody = new HashMap<>();
+ requestBody.put("code", code);
+
+ String url = getPhoneNumberUrl + "?access_token=" + accessToken;
+
+ log.info("请求微信手机号接口, URL: {}", url);
+ log.debug("请求参数: code={}", code);
+
+ // 3. 设置请求头
+ HttpHeaders headers = new HttpHeaders();
+ headers.setContentType(MediaType.APPLICATION_JSON);
+ HttpEntity