feat(1.0.3):微信支付对接成功,接口上线
parent
fc961c7817
commit
a925ad776d
@ -0,0 +1 @@
|
||||
dbfX2r5hEeBPV3qp
|
Binary file not shown.
@ -0,0 +1,8 @@
|
||||
package wechat
|
||||
|
||||
type WxJsConfigRes struct {
|
||||
AppId string `json:"appId"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
NonceStr string `json:"nonceStr"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"cls/internal/infrastructure/wechat"
|
||||
"cls/pkg/logger"
|
||||
)
|
||||
|
||||
type WechatService struct {
|
||||
log logger.Logger
|
||||
ws *wechat.WechatService
|
||||
}
|
||||
|
||||
func NewWechatService(log logger.New, ws *wechat.WechatService) *WechatService {
|
||||
return &WechatService{
|
||||
log: log("cls:service:wechatService"),
|
||||
ws: ws,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WechatService) GetOpenId(code string) (string, error) {
|
||||
openid, err := w.ws.GetOpenID(code)
|
||||
if err != nil {
|
||||
w.log.Error(err)
|
||||
|
||||
}
|
||||
return openid, err
|
||||
}
|
||||
|
||||
func (w *WechatService) GetWxJsConfig() (*WxJsConfigRes, error) {
|
||||
return nil, nil
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package price
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestGetFinalAmount(t *testing.T) {
|
||||
p := &Price{Amount: 11, Discount: 0.7}
|
||||
v := p.GetFinalAmount()
|
||||
t.Log(v)
|
||||
}
|
@ -1,167 +0,0 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// APIClient 微信支付API客户端
|
||||
type APIClient struct {
|
||||
config Config
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
// NewAPIClient 创建微信支付API客户端
|
||||
func NewAPIClient(config Config) *APIClient {
|
||||
return &APIClient{
|
||||
config: config,
|
||||
client: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// UnifiedOrderRequest 统一下单请求参数
|
||||
type UnifiedOrderRequest struct {
|
||||
AppID string `xml:"appid"` // 公众号ID
|
||||
MchID string `xml:"mch_id"` // 商户号
|
||||
NonceStr string `xml:"nonce_str"` // 随机字符串
|
||||
Body string `xml:"body"` // 商品描述
|
||||
OutTradeNo string `xml:"out_trade_no"` // 商户订单号
|
||||
TotalFee string `xml:"total_fee"` // 订单金额
|
||||
SpbillCreateIP string `xml:"spbill_create_ip"` // 终端IP
|
||||
NotifyURL string `xml:"notify_url"` // 通知地址
|
||||
TradeType string `xml:"trade_type"` // 交易类型
|
||||
Sign string `xml:"sign"` // 签名
|
||||
}
|
||||
|
||||
// UnifiedOrderResponse 统一下单响应参数
|
||||
type UnifiedOrderResponse struct {
|
||||
ReturnCode string `xml:"return_code"` // 返回状态码
|
||||
ReturnMsg string `xml:"return_msg"` // 返回信息
|
||||
ResultCode string `xml:"result_code"` // 业务结果
|
||||
ErrCode string `xml:"err_code"` // 错误代码
|
||||
ErrCodeDes string `xml:"err_code_des"` // 错误代码描述
|
||||
PrepayID string `xml:"prepay_id"` // 预支付交易会话标识
|
||||
TradeType string `xml:"trade_type"` // 交易类型
|
||||
CodeURL string `xml:"code_url"` // 二维码链接
|
||||
Sign string `xml:"sign"` // 签名
|
||||
}
|
||||
|
||||
// UnifiedOrder 统一下单
|
||||
func (c *APIClient) UnifiedOrder(req *UnifiedOrderRequest) (*UnifiedOrderResponse, error) {
|
||||
// 设置公共参数
|
||||
req.AppID = c.config.AppID
|
||||
req.MchID = c.config.MchID
|
||||
req.NonceStr = generateNonceStr()
|
||||
req.SpbillCreateIP = "127.0.0.1" // 客户端IP
|
||||
req.NotifyURL = c.config.NotifyURL
|
||||
req.TradeType = "NATIVE" // 生成二维码支付链接
|
||||
|
||||
// 生成签名
|
||||
req.Sign = c.generateSign(req)
|
||||
|
||||
// 构建XML请求
|
||||
xmlData := buildXML(req)
|
||||
|
||||
// 发送请求
|
||||
resp, err := c.client.Post("https://api.mch.weixin.qq.com/pay/unifiedorder", "application/xml", strings.NewReader(xmlData))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("请求微信支付接口失败: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 读取响应
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析响应
|
||||
var result UnifiedOrderResponse
|
||||
if err := xml.Unmarshal(body, &result); err != nil {
|
||||
return nil, fmt.Errorf("解析响应失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if !c.verifySign(&result) {
|
||||
return nil, fmt.Errorf("签名验证失败")
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
// generateSign 生成签名
|
||||
func (c *APIClient) generateSign(v interface{}) string {
|
||||
// 将结构体转换为map
|
||||
params := make(map[string]string)
|
||||
val := reflect.ValueOf(v).Elem()
|
||||
typ := val.Type()
|
||||
for i := 0; i < val.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
tag := field.Tag.Get("xml")
|
||||
if tag == "" || tag == "sign" {
|
||||
continue
|
||||
}
|
||||
params[tag] = fmt.Sprintf("%v", val.Field(i).Interface())
|
||||
}
|
||||
|
||||
// 按键排序
|
||||
var keys []string
|
||||
for k := range params {
|
||||
if k != "sign" && params[k] != "" {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// 拼接字符串
|
||||
var builder strings.Builder
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
builder.WriteString("&")
|
||||
}
|
||||
builder.WriteString(k)
|
||||
builder.WriteString("=")
|
||||
builder.WriteString(params[k])
|
||||
}
|
||||
builder.WriteString("&key=")
|
||||
builder.WriteString(c.config.APIKey)
|
||||
|
||||
// MD5加密
|
||||
h := md5.New()
|
||||
h.Write([]byte(builder.String()))
|
||||
return strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
|
||||
}
|
||||
|
||||
// verifySign 验证签名
|
||||
func (c *APIClient) verifySign(v interface{}) bool {
|
||||
// TODO: 实现签名验证逻辑
|
||||
return true
|
||||
}
|
||||
|
||||
// generateNonceStr 生成随机字符串
|
||||
func generateNonceStr() string {
|
||||
const length = 32
|
||||
bytes := make([]byte, length)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// buildXML 构建XML请求
|
||||
func buildXML(v interface{}) string {
|
||||
data, err := xml.Marshal(v)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(data)
|
||||
}
|
@ -1,138 +0,0 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"cls/internal/domain/payment"
|
||||
"cls/pkg/logger"
|
||||
"crypto/md5"
|
||||
"encoding/hex"
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config 微信支付配置
|
||||
type Config struct {
|
||||
AppID string // 公众号ID
|
||||
MchID string // 商户号
|
||||
APIKey string // API密钥
|
||||
NotifyURL string // 支付回调通知地址
|
||||
}
|
||||
|
||||
// Service 微信支付服务
|
||||
type Service struct {
|
||||
config Config
|
||||
client *APIClient
|
||||
log logger.Logger
|
||||
}
|
||||
|
||||
// NewService 创建微信支付服务
|
||||
func NewService(config Config, log logger.New) *Service {
|
||||
return &Service{
|
||||
config: config,
|
||||
client: NewAPIClient(config),
|
||||
log: log("cls:payment:wechat"),
|
||||
}
|
||||
}
|
||||
|
||||
// CreatePayment 创建支付订单
|
||||
func (s *Service) CreatePayment(order *payment.Payment) (map[string]interface{}, error) {
|
||||
// 构建统一下单请求
|
||||
req := &UnifiedOrderRequest{
|
||||
Body: order.Description,
|
||||
OutTradeNo: order.OrderNo,
|
||||
TotalFee: fmt.Sprintf("%d", order.Amount),
|
||||
}
|
||||
|
||||
// 调用统一下单接口
|
||||
resp, err := s.client.UnifiedOrder(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建支付订单失败: %v", err)
|
||||
}
|
||||
|
||||
// 检查响应结果
|
||||
if resp.ReturnCode != "SUCCESS" {
|
||||
return nil, fmt.Errorf("微信支付接口返回错误: %s", resp.ReturnMsg)
|
||||
}
|
||||
|
||||
if resp.ResultCode != "SUCCESS" {
|
||||
return nil, fmt.Errorf("微信支付业务处理失败: %s", resp.ErrCodeDes)
|
||||
}
|
||||
|
||||
// 返回支付二维码链接
|
||||
return map[string]interface{}{
|
||||
"code_url": resp.CodeURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VerifyNotify 验证支付回调通知
|
||||
func (s *Service) VerifyNotify(notifyData string) (map[string]string, error) {
|
||||
// 解析XML数据
|
||||
var notify struct {
|
||||
ReturnCode string `xml:"return_code"`
|
||||
ReturnMsg string `xml:"return_msg"`
|
||||
ResultCode string `xml:"result_code"`
|
||||
ErrCode string `xml:"err_code"`
|
||||
ErrCodeDes string `xml:"err_code_des"`
|
||||
OutTradeNo string `xml:"out_trade_no"`
|
||||
TotalFee string `xml:"total_fee"`
|
||||
TransactionID string `xml:"transaction_id"`
|
||||
Sign string `xml:"sign"`
|
||||
}
|
||||
|
||||
if err := xml.Unmarshal([]byte(notifyData), ¬ify); err != nil {
|
||||
return nil, fmt.Errorf("解析通知数据失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证签名
|
||||
if !s.client.verifySign(¬ify) {
|
||||
return nil, fmt.Errorf("签名验证失败")
|
||||
}
|
||||
|
||||
// 检查支付结果
|
||||
if notify.ReturnCode != "SUCCESS" || notify.ResultCode != "SUCCESS" {
|
||||
return nil, fmt.Errorf("支付失败: %s", notify.ErrCodeDes)
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"order_no": notify.OutTradeNo,
|
||||
"amount": notify.TotalFee,
|
||||
"transaction_id": notify.TransactionID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateSign 生成签名
|
||||
func (s *Service) generateSign(params map[string]string) string {
|
||||
// 按键排序
|
||||
var keys []string
|
||||
for k := range params {
|
||||
if k != "sign" && params[k] != "" {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
// 拼接字符串
|
||||
var builder strings.Builder
|
||||
for i, k := range keys {
|
||||
if i > 0 {
|
||||
builder.WriteString("&")
|
||||
}
|
||||
builder.WriteString(k)
|
||||
builder.WriteString("=")
|
||||
builder.WriteString(params[k])
|
||||
}
|
||||
builder.WriteString("&key=")
|
||||
builder.WriteString(s.config.APIKey)
|
||||
|
||||
// MD5加密
|
||||
h := md5.New()
|
||||
h.Write([]byte(builder.String()))
|
||||
return strings.ToUpper(hex.EncodeToString(h.Sum(nil)))
|
||||
}
|
||||
|
||||
// verifySign 验证签名
|
||||
func (s *Service) verifySign(notifyData, sign string) bool {
|
||||
// TODO: 实现签名验证逻辑
|
||||
return true
|
||||
}
|
@ -0,0 +1,110 @@
|
||||
package wechat
|
||||
|
||||
import (
|
||||
"crypto/sha1"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/pkg/errors"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type WechatService struct {
|
||||
AppId string
|
||||
AppSecret string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func NewWechatService(appId, appSecret string) *WechatService {
|
||||
return &WechatService{appId, appSecret, nil}
|
||||
}
|
||||
|
||||
func (ws *WechatService) GetOpenID(code string) (string, error) {
|
||||
url := fmt.Sprintf(
|
||||
"https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code",
|
||||
ws.AppId,
|
||||
ws.AppSecret,
|
||||
code,
|
||||
)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var res struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
OpenID string `json:"openid"`
|
||||
Scope string `json:"scope"`
|
||||
ErrCode int `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return "", errors.New("Failed to decode response")
|
||||
}
|
||||
fmt.Printf("%+v\n", res)
|
||||
if res.ErrCode != 0 {
|
||||
return "", errors.New(fmt.Sprintf("WeChat error: %s", res.ErrMsg))
|
||||
}
|
||||
|
||||
// 🎯 获取到 openid
|
||||
return res.OpenID, nil
|
||||
}
|
||||
|
||||
func (ws *WechatService) GetAccessToken() (string, error) {
|
||||
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s", ws.AppId, ws.AppSecret)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 获取 access_token
|
||||
accessToken, ok := result["access_token"].(string)
|
||||
if !ok {
|
||||
return "", errors.New("获取 access_token 失败")
|
||||
}
|
||||
|
||||
return accessToken, nil
|
||||
}
|
||||
|
||||
func (ws *WechatService) GetJsapiTicket(accessToken string) (string, error) {
|
||||
url := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=%s&type=jsapi", accessToken)
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result map[string]interface{}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 获取 jsapi_ticket
|
||||
jsapiTicket, ok := result["ticket"].(string)
|
||||
if !ok {
|
||||
return "", errors.New("获取 jsapi_ticket 失败")
|
||||
}
|
||||
|
||||
return jsapiTicket, nil
|
||||
}
|
||||
|
||||
func (ws *WechatService) SignWxConfig(signStr string) (string, error) {
|
||||
h := sha1.New()
|
||||
_, err := io.WriteString(h, signStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
signature := hex.EncodeToString(h.Sum(nil))
|
||||
return signature, nil
|
||||
}
|
@ -1 +1 @@
|
||||
|
||||
package http
|
||||
|
@ -0,0 +1,30 @@
|
||||
import {Injectable} from '@angular/core';
|
||||
import {HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest} from '@angular/common/http';
|
||||
import {Observable, throwError} from 'rxjs';
|
||||
import {catchError} from 'rxjs/operators';
|
||||
|
||||
@Injectable()
|
||||
export class ErrorInterceptor implements HttpInterceptor {
|
||||
|
||||
constructor() {
|
||||
}
|
||||
|
||||
intercept(
|
||||
request: HttpRequest<any>,
|
||||
next: HttpHandler
|
||||
): Observable<HttpEvent<any>> {
|
||||
return next.handle(request)
|
||||
.pipe(
|
||||
catchError((error: HttpErrorResponse) => {
|
||||
if (!(error.error instanceof ErrorEvent)) {
|
||||
if (error.status === 401) {
|
||||
console.log("401了")
|
||||
localStorage.removeItem("token")
|
||||
localStorage.removeItem("giftCount")
|
||||
}
|
||||
}
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@ -1,61 +0,0 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { environment } from 'src/environments/environment';
|
||||
import { Observable, from } from 'rxjs';
|
||||
import { map, tap, mergeMap } from 'rxjs/operators';
|
||||
import {Payment, WechatPayParams} from "../shared/model/order";
|
||||
|
||||
|
||||
|
||||
declare const WeixinJSBridge: any;
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class PaymentService {
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
createPayment(request: PaymentRequest): Observable<Payment> {
|
||||
return this.http.post<Payment>('', request);
|
||||
}
|
||||
|
||||
getPayment(orderNo: string): Observable<Payment> {
|
||||
return this.http.get<Payment>(`${orderNo}`);
|
||||
}
|
||||
|
||||
getPaymentStatus(orderNo: string): Observable<{ status: string }> {
|
||||
return this.http.get<{ status: string }>(`/${orderNo}/status`);
|
||||
}
|
||||
|
||||
getWechatPayParams(orderNo: string): Observable<WechatPayParams> {
|
||||
return this.http.get<WechatPayParams>(`/${orderNo}/wechat-pay`);
|
||||
}
|
||||
|
||||
// 唤起微信支付
|
||||
callWechatPay(orderNo: string): Observable<void> {
|
||||
return this.getWechatPayParams(orderNo).pipe(
|
||||
map(params => {
|
||||
if (typeof WeixinJSBridge === 'undefined') {
|
||||
throw new Error('请在微信浏览器中打开');
|
||||
}
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
WeixinJSBridge.invoke('getBrandWCPayRequest', params, (res: any) => {
|
||||
if (res.err_msg === 'get_brand_wcpay_request:ok') {
|
||||
console.log('支付成功');
|
||||
resolve();
|
||||
} else {
|
||||
console.error('支付失败:', res.err_msg);
|
||||
reject(new Error(res.err_msg));
|
||||
}
|
||||
});
|
||||
});
|
||||
}),
|
||||
mergeMap(promise => from(promise)),
|
||||
tap({
|
||||
error: (error) => console.error('微信支付失败:', error)
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue