Add WeChat Pay local skills
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
# 开发必要参数说明
|
||||
|
||||
普通商户模式接入微信支付 APIv3 前,需要准备开发必要参数(mchid、appid、商户API证书、微信支付公钥、APIv3密钥等)。
|
||||
|
||||
## APPID 详解与绑定
|
||||
|
||||
### APPID 类型
|
||||
|
||||
APPID 是微信生态中应用的唯一标识,格式都是 `wx` + 一串字符(如 `wxd678efh567hg6787`),根据注册平台不同分为三种类型:
|
||||
|
||||
| APPID 类型 | 注册平台 | 用途 |
|
||||
|-----------|---------|------|
|
||||
| 公众号 AppID | 公众平台(mp.weixin.qq.com) | 服务号/订阅号,用于公众号内网页场景 |
|
||||
| 小程序 AppID | 公众平台(mp.weixin.qq.com) | 微信小程序场景 |
|
||||
| 移动应用 AppID | 开放平台(open.weixin.qq.com) | 原生 APP(iOS/Android/鸿蒙)场景 |
|
||||
|
||||
> **三种 APPID 格式相同但不能混用**。拿小程序 AppID 做 JSAPI 支付会报错,拿公众号 AppID 做 APP 支付也不行。
|
||||
|
||||
### 为什么需要绑定 APPID
|
||||
|
||||
微信支付的所有支付方式都要求商户号与 APPID 建立绑定关系,未绑定时下单接口会报错。
|
||||
|
||||
### 如何查询 APPID
|
||||
|
||||
| APPID 类型 | 查询路径 |
|
||||
|-----------|---------|
|
||||
| 服务号/公众号 | 登录公众平台 → 设置与开发 → 开发接口管理 → 基本配置 → 开发者ID(AppID) |
|
||||
| 小程序 | 登录公众平台 → 开发与服务 → 开发管理 → 开发设置 → AppID(小程序ID) |
|
||||
| 移动应用 | 登录开放平台 → 管理中心 → 移动应用 → 查看 → 详情页面 → APPID |
|
||||
|
||||
### 如何绑定
|
||||
|
||||
**第一步:在商户平台发起绑定申请**
|
||||
|
||||
登录商户平台 → 产品中心 → APPID授权管理 → +关联AppID → 新增授权 → 填写 APPID → 提交
|
||||
|
||||
- 主体一致:直接填写 APPID 提交
|
||||
- 主体不一致:还需填写 APPID 认证主体,并勾选《微信支付联合营运承诺函》
|
||||
|
||||
**第二步:在对应平台确认授权**
|
||||
|
||||
| APPID 类型 | 确认路径 |
|
||||
|-----------|---------|
|
||||
| 服务号/公众号 | 登录公众平台 → 微信支付 → 商户号管理 → 待关联商户号 → 确认 |
|
||||
| 小程序 | 登录公众平台 → 微信支付 → 商户号管理 → 待关联商户号 → 确认 |
|
||||
| 移动应用 | 登录开放平台 → 移动应用 → 详情 → 能力专区 → 微信支付 → 查询详情 → 待关联商户号 → 确认 |
|
||||
|
||||
> 委托第三方创建的小程序,需先设置邮箱密码后登录 PC 端确认。
|
||||
|
||||
**第三步:查看绑定结果**
|
||||
|
||||
登录商户平台 → 产品中心 → APPID账号管理 → 我关联的APPID账号
|
||||
|
||||
### 绑定限制
|
||||
|
||||
| 限制项 | 说明 |
|
||||
|-------|------|
|
||||
| 数量上限 | 一个商户号最多关联 50 个 APPID |
|
||||
| 解绑 | 绑定后不支持解绑,每条关系相互独立 |
|
||||
| 跨主体 | 需补充 APPID 主体信息 |
|
||||
| 特殊费率 | 享有特殊行业费率的商户号,提交后有额外审核(1-3个工作日) |
|
||||
| 费率一致性 | APPID 已绑定其他商户号时,新商户号的费率需与已绑定的一致 |
|
||||
| 风控 | 商户号或 APPID 存在风险时(资料不全、有未处理处罚等),可能增加审核或被驳回 |
|
||||
|
||||
### APPID 相关常见报错
|
||||
|
||||
| 报错信息 | 原因 | 处理方式 |
|
||||
|---------|------|---------|
|
||||
| `appid and mchid not match` | 下单时传入的 appid 与商户号未建立绑定关系 | 按上述流程绑定 |
|
||||
| `appid is invalid` | appid 格式不对,或使用了错误类型的 appid | 检查是否用了正确类型的 APPID(如 JSAPI 需要公众号 AppID,不能用小程序 AppID) |
|
||||
| JSAPI 支付报权限错误 | 商户号绑定的是小程序 APPID,但用 JSAPI 调起 | JSAPI 需要绑定服务号 APPID |
|
||||
|
||||
## 参数与代码示例的对应关系
|
||||
|
||||
示例代码中构造函数所需的参数与上述开发必要参数的对应:
|
||||
|
||||
```
|
||||
mchid → 商户号
|
||||
certificateSerialNo → 商户API证书序列号
|
||||
privateKeyFilePath → 商户API证书私钥文件路径(apiclient_key.pem)
|
||||
wechatPayPublicKeyId → 微信支付公钥ID
|
||||
wechatPayPublicKeyFilePath → 微信支付公钥文件路径(wxp_pub.pem)
|
||||
```
|
||||
@@ -0,0 +1,203 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &DirectApiv3JsapiPrepayRequest{
|
||||
Appid: wxpay_utility.String("wxd678efh567hg6787"),
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
Description: wxpay_utility.String("Image形象店-深圳腾大-QQ公仔"),
|
||||
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
TimeExpire: wxpay_utility.Time(time.Now()),
|
||||
Attach: wxpay_utility.String("自定义数据说明"),
|
||||
NotifyUrl: wxpay_utility.String(" https://www.weixin.qq.com/wxpay/pay.php"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SupportFapiao: wxpay_utility.Bool(false),
|
||||
Amount: &CommonAmountInfo{
|
||||
Total: wxpay_utility.Int64(100),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Payer: &JsapiReqPayerInfo{
|
||||
Openid: wxpay_utility.String("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"),
|
||||
},
|
||||
Detail: &CouponInfo{
|
||||
CostPrice: wxpay_utility.Int64(608800),
|
||||
InvoiceId: wxpay_utility.String("微信123"),
|
||||
GoodsDetail: []GoodsDetail{GoodsDetail{
|
||||
MerchantGoodsId: wxpay_utility.String("1246464644"),
|
||||
WechatpayGoodsId: wxpay_utility.String("1001"),
|
||||
GoodsName: wxpay_utility.String("iPhoneX 256G"),
|
||||
Quantity: wxpay_utility.Int64(1),
|
||||
UnitPrice: wxpay_utility.Int64(528800),
|
||||
}},
|
||||
},
|
||||
SceneInfo: &CommonSceneInfo{
|
||||
PayerClientIp: wxpay_utility.String("14.23.150.211"),
|
||||
DeviceId: wxpay_utility.String("013467007045764"),
|
||||
StoreInfo: &StoreInfo{
|
||||
Id: wxpay_utility.String("0001"),
|
||||
Name: wxpay_utility.String("腾讯大厦分店"),
|
||||
AreaCode: wxpay_utility.String("440305"),
|
||||
Address: wxpay_utility.String("广东省深圳市南山区科技中一道10000号"),
|
||||
},
|
||||
},
|
||||
SettleInfo: &SettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
}
|
||||
|
||||
response, err := JsapiPrepay(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func JsapiPrepay(config *wxpay_utility.MchConfig, request *DirectApiv3JsapiPrepayRequest) (response *DirectApiv3JsapiPrepayResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/pay/transactions/jsapi"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &DirectApiv3JsapiPrepayResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type DirectApiv3JsapiPrepayRequest struct {
|
||||
Appid *string `json:"appid,omitempty"`
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
TimeExpire *time.Time `json:"time_expire,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
NotifyUrl *string `json:"notify_url,omitempty"`
|
||||
GoodsTag *string `json:"goods_tag,omitempty"`
|
||||
SupportFapiao *bool `json:"support_fapiao,omitempty"`
|
||||
Amount *CommonAmountInfo `json:"amount,omitempty"`
|
||||
Payer *JsapiReqPayerInfo `json:"payer,omitempty"`
|
||||
Detail *CouponInfo `json:"detail,omitempty"`
|
||||
SceneInfo *CommonSceneInfo `json:"scene_info,omitempty"`
|
||||
SettleInfo *SettleInfo `json:"settle_info,omitempty"`
|
||||
}
|
||||
|
||||
type DirectApiv3JsapiPrepayResponse struct {
|
||||
PrepayId *string `json:"prepay_id,omitempty"`
|
||||
}
|
||||
|
||||
type CommonAmountInfo struct {
|
||||
Total *int64 `json:"total,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
type JsapiReqPayerInfo struct {
|
||||
Openid *string `json:"openid,omitempty"`
|
||||
}
|
||||
|
||||
type CouponInfo struct {
|
||||
CostPrice *int64 `json:"cost_price,omitempty"`
|
||||
InvoiceId *string `json:"invoice_id,omitempty"`
|
||||
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type CommonSceneInfo struct {
|
||||
PayerClientIp *string `json:"payer_client_ip,omitempty"`
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
StoreInfo *StoreInfo `json:"store_info,omitempty"`
|
||||
}
|
||||
|
||||
type SettleInfo struct {
|
||||
ProfitSharing *bool `json:"profit_sharing,omitempty"`
|
||||
}
|
||||
|
||||
type GoodsDetail struct {
|
||||
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
|
||||
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
|
||||
GoodsName *string `json:"goods_name,omitempty"`
|
||||
Quantity *int64 `json:"quantity,omitempty"`
|
||||
UnitPrice *int64 `json:"unit_price,omitempty"`
|
||||
}
|
||||
|
||||
type StoreInfo struct {
|
||||
Id *string `json:"id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
AreaCode *string `json:"area_code,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
// 申请交易账单API
|
||||
//
|
||||
// 关键注意:
|
||||
// 1. 次日10点后拉取,API仅支持3个月内单日账单,更早的需在商户平台下载。
|
||||
// 2. 返回的是下载链接(download_url),需二次请求下载(gzip压缩CSV)。
|
||||
// 3. 账单金额单位为"元",与下单API的"分"不同,对账时注意转换。
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &GetTradeBillRequest{
|
||||
BillDate: wxpay_utility.String("2019-06-11"),
|
||||
BillType: BILLTYPE_ALL.Ptr(),
|
||||
TarType: TARTYPE_GZIP.Ptr(),
|
||||
}
|
||||
|
||||
response, err := GetTradeBill(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
// GetTradeBill 申请交易账单API
|
||||
func GetTradeBill(config *wxpay_utility.MchConfig, request *GetTradeBillRequest) (response *QueryBillEntity, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "GET"
|
||||
path = "/v3/bill/tradebill"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := reqUrl.Query()
|
||||
if request.BillDate != nil {
|
||||
query.Add("bill_date", *request.BillDate)
|
||||
}
|
||||
if request.BillType != nil {
|
||||
query.Add("bill_type", fmt.Sprintf("%v", *request.BillType))
|
||||
}
|
||||
if request.TarType != nil {
|
||||
query.Add("tar_type", fmt.Sprintf("%v", *request.TarType))
|
||||
}
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &QueryBillEntity{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type GetTradeBillRequest struct {
|
||||
BillDate *string `json:"bill_date,omitempty"`
|
||||
BillType *BillType `json:"bill_type,omitempty"`
|
||||
TarType *TarType `json:"tar_type,omitempty"`
|
||||
}
|
||||
|
||||
func (o *GetTradeBillRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias GetTradeBillRequest
|
||||
a := &struct {
|
||||
BillDate *string `json:"bill_date,omitempty"`
|
||||
BillType *BillType `json:"bill_type,omitempty"`
|
||||
TarType *TarType `json:"tar_type,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
BillDate: nil,
|
||||
BillType: nil,
|
||||
TarType: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type QueryBillEntity struct {
|
||||
HashType *HashType `json:"hash_type,omitempty"`
|
||||
HashValue *string `json:"hash_value,omitempty"`
|
||||
DownloadUrl *string `json:"download_url,omitempty"`
|
||||
}
|
||||
|
||||
type BillType string
|
||||
|
||||
func (e BillType) Ptr() *BillType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
BILLTYPE_ALL BillType = "ALL"
|
||||
BILLTYPE_SUCCESS BillType = "SUCCESS"
|
||||
BILLTYPE_REFUND BillType = "REFUND"
|
||||
)
|
||||
|
||||
type TarType string
|
||||
|
||||
func (e TarType) Ptr() *TarType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
TARTYPE_GZIP TarType = "GZIP"
|
||||
)
|
||||
|
||||
type HashType string
|
||||
|
||||
func (e HashType) Ptr() *HashType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
HASHTYPE_SHA1 HashType = "SHA1"
|
||||
)
|
||||
@@ -0,0 +1,171 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &GetFundFlowBillRequest{
|
||||
BillDate: wxpay_utility.String("2019-06-11"),
|
||||
AccountType: FUNDFLOWBILLACCOUNTTYPE_BASIC.Ptr(),
|
||||
TarType: TARTYPE_GZIP.Ptr(),
|
||||
}
|
||||
|
||||
response, err := GetFundFlowBill(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
// GetFundFlowBill 申请资金账单API
|
||||
func GetFundFlowBill(config *wxpay_utility.MchConfig, request *GetFundFlowBillRequest) (response *QueryBillEntity, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "GET"
|
||||
path = "/v3/bill/fundflowbill"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := reqUrl.Query()
|
||||
if request.BillDate != nil {
|
||||
query.Add("bill_date", *request.BillDate)
|
||||
}
|
||||
if request.AccountType != nil {
|
||||
query.Add("account_type", fmt.Sprintf("%v", *request.AccountType))
|
||||
}
|
||||
if request.TarType != nil {
|
||||
query.Add("tar_type", fmt.Sprintf("%v", *request.TarType))
|
||||
}
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &QueryBillEntity{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type GetFundFlowBillRequest struct {
|
||||
BillDate *string `json:"bill_date,omitempty"`
|
||||
AccountType *FundFlowBillAccountType `json:"account_type,omitempty"`
|
||||
TarType *TarType `json:"tar_type,omitempty"`
|
||||
}
|
||||
|
||||
func (o *GetFundFlowBillRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias GetFundFlowBillRequest
|
||||
a := &struct {
|
||||
BillDate *string `json:"bill_date,omitempty"`
|
||||
AccountType *FundFlowBillAccountType `json:"account_type,omitempty"`
|
||||
TarType *TarType `json:"tar_type,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
BillDate: nil,
|
||||
AccountType: nil,
|
||||
TarType: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type QueryBillEntity struct {
|
||||
HashType *HashType `json:"hash_type,omitempty"`
|
||||
HashValue *string `json:"hash_value,omitempty"`
|
||||
DownloadUrl *string `json:"download_url,omitempty"`
|
||||
}
|
||||
|
||||
type FundFlowBillAccountType string
|
||||
|
||||
func (e FundFlowBillAccountType) Ptr() *FundFlowBillAccountType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
FUNDFLOWBILLACCOUNTTYPE_BASIC FundFlowBillAccountType = "BASIC"
|
||||
FUNDFLOWBILLACCOUNTTYPE_OPERATION FundFlowBillAccountType = "OPERATION"
|
||||
FUNDFLOWBILLACCOUNTTYPE_FEES FundFlowBillAccountType = "FEES"
|
||||
)
|
||||
|
||||
type TarType string
|
||||
|
||||
func (e TarType) Ptr() *TarType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
TARTYPE_GZIP TarType = "GZIP"
|
||||
)
|
||||
|
||||
type HashType string
|
||||
|
||||
func (e HashType) Ptr() *HashType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
HASHTYPE_SHA1 HashType = "SHA1"
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
package wxpay_utility
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const Host = "https://api.mch.weixin.qq.com"
|
||||
|
||||
// SendGet 发送 GET 请求并返回已验签的应答 Body
|
||||
func SendGet(config *MchConfig, uri string) ([]byte, error) {
|
||||
return sendRequest(config, "GET", uri, nil)
|
||||
}
|
||||
|
||||
// SendPost 发送 POST 请求并返回已验签的应答 Body
|
||||
func SendPost(config *MchConfig, uri string, reqBody []byte) ([]byte, error) {
|
||||
return sendRequest(config, "POST", uri, reqBody)
|
||||
}
|
||||
|
||||
func sendRequest(config *MchConfig, method string, uri string, reqBody []byte) ([]byte, error) {
|
||||
var bodyReader io.Reader
|
||||
if reqBody != nil {
|
||||
bodyReader = bytes.NewReader(reqBody)
|
||||
}
|
||||
|
||||
httpRequest, err := http.NewRequest(method, Host+uri, bodyReader)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
|
||||
authorization, err := BuildAuthorization(config.MchId(), config.CertificateSerialNo(),
|
||||
config.PrivateKey(), method, uri, reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
if reqBody != nil {
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
respBody, err := ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
err = ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return respBody, nil
|
||||
}
|
||||
|
||||
return nil, NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
package wxpay_utility
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/tjfoc/gmsm/sm3"
|
||||
)
|
||||
|
||||
type MchConfig struct {
|
||||
mchId string
|
||||
certificateSerialNo string
|
||||
privateKeyFilePath string
|
||||
wechatPayPublicKeyId string
|
||||
wechatPayPublicKeyFilePath string
|
||||
privateKey *rsa.PrivateKey
|
||||
wechatPayPublicKey *rsa.PublicKey
|
||||
}
|
||||
|
||||
func (c *MchConfig) MchId() string {
|
||||
return c.mchId
|
||||
}
|
||||
|
||||
func (c *MchConfig) CertificateSerialNo() string {
|
||||
return c.certificateSerialNo
|
||||
}
|
||||
|
||||
func (c *MchConfig) PrivateKey() *rsa.PrivateKey {
|
||||
return c.privateKey
|
||||
}
|
||||
|
||||
func (c *MchConfig) WechatPayPublicKeyId() string {
|
||||
return c.wechatPayPublicKeyId
|
||||
}
|
||||
|
||||
func (c *MchConfig) WechatPayPublicKey() *rsa.PublicKey {
|
||||
return c.wechatPayPublicKey
|
||||
}
|
||||
|
||||
func CreateMchConfig(
|
||||
mchId string,
|
||||
certificateSerialNo string,
|
||||
privateKeyFilePath string,
|
||||
wechatPayPublicKeyId string,
|
||||
wechatPayPublicKeyFilePath string,
|
||||
) (*MchConfig, error) {
|
||||
mchConfig := &MchConfig{
|
||||
mchId: mchId,
|
||||
certificateSerialNo: certificateSerialNo,
|
||||
privateKeyFilePath: privateKeyFilePath,
|
||||
wechatPayPublicKeyId: wechatPayPublicKeyId,
|
||||
wechatPayPublicKeyFilePath: wechatPayPublicKeyFilePath,
|
||||
}
|
||||
privateKey, err := LoadPrivateKeyWithPath(mchConfig.privateKeyFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mchConfig.privateKey = privateKey
|
||||
wechatPayPublicKey, err := LoadPublicKeyWithPath(mchConfig.wechatPayPublicKeyFilePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mchConfig.wechatPayPublicKey = wechatPayPublicKey
|
||||
return mchConfig, nil
|
||||
}
|
||||
|
||||
func LoadPrivateKey(privateKeyStr string) (privateKey *rsa.PrivateKey, err error) {
|
||||
block, _ := pem.Decode([]byte(privateKeyStr))
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("decode private key err")
|
||||
}
|
||||
if block.Type != "PRIVATE KEY" {
|
||||
return nil, fmt.Errorf("the kind of PEM should be PRVATE KEY")
|
||||
}
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse private key err:%s", err.Error())
|
||||
}
|
||||
privateKey, ok := key.(*rsa.PrivateKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("not a RSA private key")
|
||||
}
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
func LoadPublicKey(publicKeyStr string) (publicKey *rsa.PublicKey, err error) {
|
||||
block, _ := pem.Decode([]byte(publicKeyStr))
|
||||
if block == nil {
|
||||
return nil, errors.New("decode public key error")
|
||||
}
|
||||
if block.Type != "PUBLIC KEY" {
|
||||
return nil, fmt.Errorf("the kind of PEM should be PUBLIC KEY")
|
||||
}
|
||||
key, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse public key err:%s", err.Error())
|
||||
}
|
||||
publicKey, ok := key.(*rsa.PublicKey)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%s is not rsa public key", publicKeyStr)
|
||||
}
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
func LoadPrivateKeyWithPath(path string) (privateKey *rsa.PrivateKey, err error) {
|
||||
privateKeyBytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read private pem file err:%s", err.Error())
|
||||
}
|
||||
return LoadPrivateKey(string(privateKeyBytes))
|
||||
}
|
||||
|
||||
func LoadPublicKeyWithPath(path string) (publicKey *rsa.PublicKey, err error) {
|
||||
publicKeyBytes, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read certificate pem file err:%s", err.Error())
|
||||
}
|
||||
return LoadPublicKey(string(publicKeyBytes))
|
||||
}
|
||||
|
||||
func EncryptOAEPWithPublicKey(message string, publicKey *rsa.PublicKey) (ciphertext string, err error) {
|
||||
if publicKey == nil {
|
||||
return "", fmt.Errorf("you should input *rsa.PublicKey")
|
||||
}
|
||||
ciphertextByte, err := rsa.EncryptOAEP(sha1.New(), rand.Reader, publicKey, []byte(message), nil)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("encrypt message with public key err:%s", err.Error())
|
||||
}
|
||||
ciphertext = base64.StdEncoding.EncodeToString(ciphertextByte)
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func DecryptAES256GCM(aesKey, associatedData, nonce, ciphertext string) (plaintext string, err error) {
|
||||
decodedCiphertext, err := base64.StdEncoding.DecodeString(ciphertext)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
c, err := aes.NewCipher([]byte(aesKey))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(c)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
dataBytes, err := gcm.Open(nil, []byte(nonce), decodedCiphertext, []byte(associatedData))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(dataBytes), nil
|
||||
}
|
||||
|
||||
func SignSHA256WithRSA(source string, privateKey *rsa.PrivateKey) (signature string, err error) {
|
||||
if privateKey == nil {
|
||||
return "", fmt.Errorf("private key should not be nil")
|
||||
}
|
||||
h := crypto.Hash.New(crypto.SHA256)
|
||||
_, err = h.Write([]byte(source))
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
hashed := h.Sum(nil)
|
||||
signatureByte, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.StdEncoding.EncodeToString(signatureByte), nil
|
||||
}
|
||||
|
||||
func VerifySHA256WithRSA(source string, signature string, publicKey *rsa.PublicKey) error {
|
||||
if publicKey == nil {
|
||||
return fmt.Errorf("public key should not be nil")
|
||||
}
|
||||
|
||||
sigBytes, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify failed: signature is not base64 encoded")
|
||||
}
|
||||
hashed := sha256.Sum256([]byte(source))
|
||||
err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], sigBytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify signature with public key error:%s", err.Error())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GenerateNonce() (string, error) {
|
||||
const (
|
||||
NonceSymbols = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
NonceLength = 32
|
||||
)
|
||||
|
||||
bytes := make([]byte, NonceLength)
|
||||
_, err := rand.Read(bytes)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
symbolsByteLength := byte(len(NonceSymbols))
|
||||
for i, b := range bytes {
|
||||
bytes[i] = NonceSymbols[b%symbolsByteLength]
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func BuildAuthorization(
|
||||
mchid string,
|
||||
certificateSerialNo string,
|
||||
privateKey *rsa.PrivateKey,
|
||||
method string,
|
||||
canonicalURL string,
|
||||
body []byte,
|
||||
) (string, error) {
|
||||
const (
|
||||
SignatureMessageFormat = "%s\n%s\n%d\n%s\n%s\n"
|
||||
HeaderAuthorizationFormat = "WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\""
|
||||
)
|
||||
|
||||
nonce, err := GenerateNonce()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
timestamp := time.Now().Unix()
|
||||
message := fmt.Sprintf(SignatureMessageFormat, method, canonicalURL, timestamp, nonce, body)
|
||||
signature, err := SignSHA256WithRSA(message, privateKey)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
authorization := fmt.Sprintf(
|
||||
HeaderAuthorizationFormat,
|
||||
mchid, nonce, timestamp, certificateSerialNo, signature,
|
||||
)
|
||||
return authorization, nil
|
||||
}
|
||||
|
||||
func ExtractResponseBody(response *http.Response) ([]byte, error) {
|
||||
if response.Body == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read response body err:[%s]", err.Error())
|
||||
}
|
||||
response.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
return body, nil
|
||||
}
|
||||
|
||||
const (
|
||||
WechatPayTimestamp = "Wechatpay-Timestamp"
|
||||
WechatPayNonce = "Wechatpay-Nonce"
|
||||
WechatPaySignature = "Wechatpay-Signature"
|
||||
WechatPaySerial = "Wechatpay-Serial"
|
||||
RequestID = "Request-Id"
|
||||
)
|
||||
|
||||
func validateWechatPaySignature(
|
||||
wechatpayPublicKeyId string,
|
||||
wechatpayPublicKey *rsa.PublicKey,
|
||||
headers *http.Header,
|
||||
body []byte,
|
||||
) error {
|
||||
timestampStr := headers.Get(WechatPayTimestamp)
|
||||
serialNo := headers.Get(WechatPaySerial)
|
||||
signature := headers.Get(WechatPaySignature)
|
||||
nonce := headers.Get(WechatPayNonce)
|
||||
|
||||
timestamp, err := strconv.ParseInt(timestampStr, 10, 64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid timestamp: %w", err)
|
||||
}
|
||||
if time.Now().Sub(time.Unix(timestamp, 0)) > 5*time.Minute {
|
||||
return fmt.Errorf("timestamp expired: %d", timestamp)
|
||||
}
|
||||
|
||||
if serialNo != wechatpayPublicKeyId {
|
||||
return fmt.Errorf(
|
||||
"serial-no mismatch: got %s, expected %s",
|
||||
serialNo,
|
||||
wechatpayPublicKeyId,
|
||||
)
|
||||
}
|
||||
|
||||
message := fmt.Sprintf("%s\n%s\n%s\n", timestampStr, nonce, body)
|
||||
if err := VerifySHA256WithRSA(message, signature, wechatpayPublicKey); err != nil {
|
||||
return fmt.Errorf("invalid signature: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func ValidateResponse(
|
||||
wechatpayPublicKeyId string,
|
||||
wechatpayPublicKey *rsa.PublicKey,
|
||||
headers *http.Header,
|
||||
body []byte,
|
||||
) error {
|
||||
if err := validateWechatPaySignature(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); err != nil {
|
||||
return fmt.Errorf("validate response err: %w, RequestID: %s", err, headers.Get(RequestID))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateNotification(
|
||||
wechatpayPublicKeyId string,
|
||||
wechatpayPublicKey *rsa.PublicKey,
|
||||
headers *http.Header,
|
||||
body []byte,
|
||||
) error {
|
||||
if err := validateWechatPaySignature(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); err != nil {
|
||||
return fmt.Errorf("validate notification err: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type Resource struct {
|
||||
Algorithm string `json:"algorithm"`
|
||||
Ciphertext string `json:"ciphertext"`
|
||||
AssociatedData string `json:"associated_data"`
|
||||
Nonce string `json:"nonce"`
|
||||
OriginalType string `json:"original_type"`
|
||||
}
|
||||
|
||||
type Notification struct {
|
||||
ID string `json:"id"`
|
||||
CreateTime *time.Time `json:"create_time"`
|
||||
EventType string `json:"event_type"`
|
||||
ResourceType string `json:"resource_type"`
|
||||
Resource *Resource `json:"resource"`
|
||||
Summary string `json:"summary"`
|
||||
|
||||
Plaintext string
|
||||
}
|
||||
|
||||
func (c *Notification) validate() error {
|
||||
if c.Resource == nil {
|
||||
return errors.New("resource is nil")
|
||||
}
|
||||
|
||||
if c.Resource.Algorithm != "AEAD_AES_256_GCM" {
|
||||
return fmt.Errorf("unsupported algorithm: %s", c.Resource.Algorithm)
|
||||
}
|
||||
|
||||
if c.Resource.Ciphertext == "" {
|
||||
return errors.New("ciphertext is empty")
|
||||
}
|
||||
|
||||
if c.Resource.AssociatedData == "" {
|
||||
return errors.New("associated_data is empty")
|
||||
}
|
||||
|
||||
if c.Resource.Nonce == "" {
|
||||
return errors.New("nonce is empty")
|
||||
}
|
||||
|
||||
if c.Resource.OriginalType == "" {
|
||||
return fmt.Errorf("original_type is empty")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *Notification) decrypt(apiv3Key string) error {
|
||||
if err := c.validate(); err != nil {
|
||||
return fmt.Errorf("notification format err: %w", err)
|
||||
}
|
||||
|
||||
plaintext, err := DecryptAES256GCM(
|
||||
apiv3Key,
|
||||
c.Resource.AssociatedData,
|
||||
c.Resource.Nonce,
|
||||
c.Resource.Ciphertext,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf("notification decrypt err: %w", err)
|
||||
}
|
||||
|
||||
c.Plaintext = plaintext
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseNotification(
|
||||
wechatpayPublicKeyId string,
|
||||
wechatpayPublicKey *rsa.PublicKey,
|
||||
apiv3Key string,
|
||||
headers *http.Header,
|
||||
body []byte,
|
||||
) (*Notification, error) {
|
||||
if err := validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notification := &Notification{}
|
||||
if err := json.Unmarshal(body, notification); err != nil {
|
||||
return nil, fmt.Errorf("parse notification err: %w", err)
|
||||
}
|
||||
|
||||
if err := notification.decrypt(apiv3Key); err != nil {
|
||||
return nil, fmt.Errorf("notification decrypt err: %w", err)
|
||||
}
|
||||
|
||||
return notification, nil
|
||||
}
|
||||
|
||||
type ApiException struct {
|
||||
statusCode int
|
||||
header http.Header
|
||||
body []byte
|
||||
errorCode string
|
||||
errorMessage string
|
||||
}
|
||||
|
||||
func (c *ApiException) Error() string {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.WriteString(fmt.Sprintf("api error:[StatusCode: %d, Body: %s", c.statusCode, string(c.body)))
|
||||
if len(c.header) > 0 {
|
||||
buf.WriteString(" Header: ")
|
||||
for key, value := range c.header {
|
||||
buf.WriteString(fmt.Sprintf("\n - %v=%v", key, value))
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
buf.WriteString("]")
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (c *ApiException) StatusCode() int {
|
||||
return c.statusCode
|
||||
}
|
||||
|
||||
func (c *ApiException) Header() http.Header {
|
||||
return c.header
|
||||
}
|
||||
|
||||
func (c *ApiException) Body() []byte {
|
||||
return c.body
|
||||
}
|
||||
|
||||
func (c *ApiException) ErrorCode() string {
|
||||
return c.errorCode
|
||||
}
|
||||
|
||||
func (c *ApiException) ErrorMessage() string {
|
||||
return c.errorMessage
|
||||
}
|
||||
|
||||
func NewApiException(statusCode int, header http.Header, body []byte) error {
|
||||
ret := &ApiException{
|
||||
statusCode: statusCode,
|
||||
header: header,
|
||||
body: body,
|
||||
}
|
||||
|
||||
bodyObject := map[string]interface{}{}
|
||||
if err := json.Unmarshal(body, &bodyObject); err == nil {
|
||||
if val, ok := bodyObject["code"]; ok {
|
||||
ret.errorCode = val.(string)
|
||||
}
|
||||
if val, ok := bodyObject["message"]; ok {
|
||||
ret.errorMessage = val.(string)
|
||||
}
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
|
||||
func Time(t time.Time) *time.Time {
|
||||
return &t
|
||||
}
|
||||
|
||||
func String(s string) *string {
|
||||
return &s
|
||||
}
|
||||
|
||||
func Bytes(b []byte) *[]byte {
|
||||
return &b
|
||||
}
|
||||
|
||||
func Bool(b bool) *bool {
|
||||
return &b
|
||||
}
|
||||
|
||||
func Float64(f float64) *float64 {
|
||||
return &f
|
||||
}
|
||||
|
||||
func Float32(f float32) *float32 {
|
||||
return &f
|
||||
}
|
||||
|
||||
func Int64(i int64) *int64 {
|
||||
return &i
|
||||
}
|
||||
|
||||
func Int32(i int32) *int32 {
|
||||
return &i
|
||||
}
|
||||
|
||||
func generateHashFromStream(reader io.Reader, hashFunc func() hash.Hash, algorithmName string) (string, error) {
|
||||
hash := hashFunc()
|
||||
if _, err := io.Copy(hash, reader); err != nil {
|
||||
return "", fmt.Errorf("failed to read stream for %s: %w", algorithmName, err)
|
||||
}
|
||||
return fmt.Sprintf("%x", hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func GenerateSHA256FromStream(reader io.Reader) (string, error) {
|
||||
return generateHashFromStream(reader, sha256.New, "SHA256")
|
||||
}
|
||||
|
||||
func GenerateSHA1FromStream(reader io.Reader) (string, error) {
|
||||
return generateHashFromStream(reader, sha1.New, "SHA1")
|
||||
}
|
||||
|
||||
func GenerateSM3FromStream(reader io.Reader) (string, error) {
|
||||
h := sm3.New()
|
||||
if _, err := io.Copy(h, reader); err != nil {
|
||||
return "", fmt.Errorf("failed to read stream for SM3: %w", err)
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &UnionApiv3AppPrepayRequest{
|
||||
CombineAppid: wxpay_utility.String("wxd678efh567hg6787"),
|
||||
CombineOutTradeNo: wxpay_utility.String("20150806125345"),
|
||||
CombineMchid: wxpay_utility.String("1900000109"),
|
||||
SceneInfo: &UnionSceneInfo{
|
||||
DeviceId: wxpay_utility.String("POS1:1"),
|
||||
PayerClientIp: wxpay_utility.String("14.17.22.32"),
|
||||
},
|
||||
SubOrders: []UnionSubOrder{
|
||||
UnionSubOrder{
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
OutTradeNo: wxpay_utility.String("20150806125346"),
|
||||
Amount: &UnionAmountInfo{
|
||||
TotalAmount: wxpay_utility.Int64(10),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Attach: wxpay_utility.String("深圳分店"),
|
||||
Description: wxpay_utility.String("腾讯充值中心-QQ会员充值"),
|
||||
Detail: wxpay_utility.String("买单费用"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SettleInfo: &UnionSettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
},
|
||||
UnionSubOrder{
|
||||
Mchid: wxpay_utility.String("1230000119"),
|
||||
OutTradeNo: wxpay_utility.String("20150806125347"),
|
||||
Amount: &UnionAmountInfo{
|
||||
TotalAmount: wxpay_utility.Int64(10),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Attach: wxpay_utility.String("广州分店"),
|
||||
Description: wxpay_utility.String("腾讯充值中心-微信充值"),
|
||||
Detail: wxpay_utility.String("买单费用"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SettleInfo: &UnionSettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
CombinePayerInfo: &UnionAppPayerInfo{
|
||||
Openid: wxpay_utility.String("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"),
|
||||
},
|
||||
TimeExpire: wxpay_utility.Time(time.Now()),
|
||||
NotifyUrl: wxpay_utility.String("https://yourapp.com/notify"),
|
||||
}
|
||||
|
||||
response, err := UnionAppPrepay(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func UnionAppPrepay(config *wxpay_utility.MchConfig, request *UnionApiv3AppPrepayRequest) (response *UnionApiv3AppPrepayResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/combine-transactions/app"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &UnionApiv3AppPrepayResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type UnionApiv3AppPrepayRequest struct {
|
||||
CombineAppid *string `json:"combine_appid,omitempty"`
|
||||
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
|
||||
CombineMchid *string `json:"combine_mchid,omitempty"`
|
||||
SceneInfo *UnionSceneInfo `json:"scene_info,omitempty"`
|
||||
SubOrders []UnionSubOrder `json:"sub_orders,omitempty"`
|
||||
CombinePayerInfo *UnionAppPayerInfo `json:"combine_payer_info,omitempty"`
|
||||
TimeExpire *time.Time `json:"time_expire,omitempty"`
|
||||
NotifyUrl *string `json:"notify_url,omitempty"`
|
||||
TradeScenario *string `json:"trade_scenario,omitempty"`
|
||||
}
|
||||
|
||||
type UnionApiv3AppPrepayResponse struct {
|
||||
PrepayId *string `json:"prepay_id,omitempty"`
|
||||
}
|
||||
|
||||
type UnionSceneInfo struct {
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
PayerClientIp *string `json:"payer_client_ip,omitempty"`
|
||||
}
|
||||
|
||||
type UnionSubOrder struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
Amount *UnionAmountInfo `json:"amount,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Detail *string `json:"detail,omitempty"`
|
||||
GoodsTag *string `json:"goods_tag,omitempty"`
|
||||
SettleInfo *UnionSettleInfo `json:"settle_info,omitempty"`
|
||||
}
|
||||
|
||||
type UnionAppPayerInfo struct {
|
||||
Openid *string `json:"openid,omitempty"`
|
||||
}
|
||||
|
||||
type UnionAmountInfo struct {
|
||||
TotalAmount *int64 `json:"total_amount,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
type UnionSettleInfo struct {
|
||||
ProfitSharing *bool `json:"profit_sharing,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &UnionCloseRequest{
|
||||
CombineOutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
CombineAppid: wxpay_utility.String("wxd678efh567hg6787"),
|
||||
SubOrders: []UnionCloseSubOrder{UnionCloseSubOrder{
|
||||
Mchid: wxpay_utility.String("1900000109"),
|
||||
OutTradeNo: wxpay_utility.String("20150806125346"),
|
||||
}},
|
||||
}
|
||||
|
||||
err = UnionClose(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Println("请求成功")
|
||||
}
|
||||
|
||||
func UnionClose(config *wxpay_utility.MchConfig, request *UnionCloseRequest) (err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/combine-transactions/out-trade-no/{combine_out_trade_no}/close"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqUrl.Path = strings.Replace(reqUrl.Path, "{combine_out_trade_no}", url.PathEscape(*request.CombineOutTradeNo), -1)
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type UnionCloseRequest struct {
|
||||
CombineAppid *string `json:"combine_appid,omitempty"`
|
||||
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
|
||||
SubOrders []UnionCloseSubOrder `json:"sub_orders,omitempty"`
|
||||
}
|
||||
|
||||
func (o *UnionCloseRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias UnionCloseRequest
|
||||
a := &struct {
|
||||
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
CombineOutTradeNo: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type UnionCloseSubOrder struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &UnionApiv3H5PrepayRequest{
|
||||
CombineAppid: wxpay_utility.String("wxd678efh567hg6787"),
|
||||
CombineOutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
CombineMchid: wxpay_utility.String("1230000109"),
|
||||
SceneInfo: &UnionH5SceneInfo{
|
||||
PayerClientIp: wxpay_utility.String("14.23.150.211"),
|
||||
DeviceId: wxpay_utility.String("013467007045764"),
|
||||
H5Info: &UnionH5Info{
|
||||
Type: wxpay_utility.String("iOS"),
|
||||
AppName: wxpay_utility.String("王者荣耀"),
|
||||
AppUrl: wxpay_utility.String("https://pay.qq.com"),
|
||||
BundleId: wxpay_utility.String("com.tencent.wzryiOS"),
|
||||
PackageName: wxpay_utility.String("com.tencent.tmgp.sgame"),
|
||||
},
|
||||
},
|
||||
SubOrders: []UnionCommonSubOrder{
|
||||
UnionCommonSubOrder{
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
OutTradeNo: wxpay_utility.String("20150806125346"),
|
||||
Amount: &UnionAmountInfo{
|
||||
TotalAmount: wxpay_utility.Int64(10),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Attach: wxpay_utility.String("深圳分店"),
|
||||
Description: wxpay_utility.String("腾讯充值中心-QQ会员充值"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SettleInfo: &UnionSettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
},
|
||||
UnionCommonSubOrder{
|
||||
Mchid: wxpay_utility.String("1230000119"),
|
||||
OutTradeNo: wxpay_utility.String("20150806125347"),
|
||||
Amount: &UnionAmountInfo{
|
||||
TotalAmount: wxpay_utility.Int64(10),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Attach: wxpay_utility.String("广州分店"),
|
||||
Description: wxpay_utility.String("腾讯充值中心-微信充值"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SettleInfo: &UnionSettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
TimeExpire: wxpay_utility.Time(time.Now()),
|
||||
NotifyUrl: wxpay_utility.String("https://yourapp.com/notify"),
|
||||
}
|
||||
|
||||
response, err := UnionH5Prepay(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func UnionH5Prepay(config *wxpay_utility.MchConfig, request *UnionApiv3H5PrepayRequest) (response *UnionApiv3H5PrepayResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/combine-transactions/h5"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &UnionApiv3H5PrepayResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type UnionApiv3H5PrepayRequest struct {
|
||||
CombineAppid *string `json:"combine_appid,omitempty"`
|
||||
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
|
||||
CombineMchid *string `json:"combine_mchid,omitempty"`
|
||||
SceneInfo *UnionH5SceneInfo `json:"scene_info,omitempty"`
|
||||
SubOrders []UnionCommonSubOrder `json:"sub_orders,omitempty"`
|
||||
TimeExpire *time.Time `json:"time_expire,omitempty"`
|
||||
NotifyUrl *string `json:"notify_url,omitempty"`
|
||||
}
|
||||
|
||||
type UnionApiv3H5PrepayResponse struct {
|
||||
H5Url *string `json:"h5_url,omitempty"`
|
||||
}
|
||||
|
||||
type UnionH5SceneInfo struct {
|
||||
PayerClientIp *string `json:"payer_client_ip,omitempty"`
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
H5Info *UnionH5Info `json:"h5_info,omitempty"`
|
||||
}
|
||||
|
||||
type UnionCommonSubOrder struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
Amount *UnionAmountInfo `json:"amount,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
GoodsTag *string `json:"goods_tag,omitempty"`
|
||||
SettleInfo *UnionSettleInfo `json:"settle_info,omitempty"`
|
||||
}
|
||||
|
||||
type UnionH5Info struct {
|
||||
Type *string `json:"type,omitempty"`
|
||||
AppName *string `json:"app_name,omitempty"`
|
||||
AppUrl *string `json:"app_url,omitempty"`
|
||||
BundleId *string `json:"bundle_id,omitempty"`
|
||||
PackageName *string `json:"package_name,omitempty"`
|
||||
}
|
||||
|
||||
type UnionAmountInfo struct {
|
||||
TotalAmount *int64 `json:"total_amount,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
type UnionSettleInfo struct {
|
||||
ProfitSharing *bool `json:"profit_sharing,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &UnionApiv3JsapiPrepayRequest{
|
||||
CombineAppid: wxpay_utility.String("wxd678efh567hg6787"),
|
||||
CombineMchid: wxpay_utility.String("1230000109"),
|
||||
CombineOutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
CombinePayerInfo: &UnionPayerInfo{
|
||||
Openid: wxpay_utility.String("oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"),
|
||||
},
|
||||
SceneInfo: &UnionSceneInfo{
|
||||
DeviceId: wxpay_utility.String("POS1:1"),
|
||||
PayerClientIp: wxpay_utility.String("14.17.22.32"),
|
||||
},
|
||||
SubOrders: []UnionSubOrder{
|
||||
UnionSubOrder{
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
OutTradeNo: wxpay_utility.String("20150806125346"),
|
||||
Amount: &UnionAmountInfo{
|
||||
TotalAmount: wxpay_utility.Int64(10),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Attach: wxpay_utility.String("深圳分店"),
|
||||
Description: wxpay_utility.String("腾讯充值中心-QQ会员充值"),
|
||||
Detail: wxpay_utility.String("买单费用"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SettleInfo: &UnionSettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
},
|
||||
UnionSubOrder{
|
||||
Mchid: wxpay_utility.String("1230000119"),
|
||||
OutTradeNo: wxpay_utility.String("20150806125347"),
|
||||
Amount: &UnionAmountInfo{
|
||||
TotalAmount: wxpay_utility.Int64(10),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Attach: wxpay_utility.String("广州分店"),
|
||||
Description: wxpay_utility.String("腾讯充值中心-微信充值"),
|
||||
Detail: wxpay_utility.String("买单费用"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SettleInfo: &UnionSettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
TimeExpire: wxpay_utility.Time(time.Now()),
|
||||
NotifyUrl: wxpay_utility.String("https://yourapp.com/notify"),
|
||||
}
|
||||
|
||||
response, err := UnionJsapiPrepay(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func UnionJsapiPrepay(config *wxpay_utility.MchConfig, request *UnionApiv3JsapiPrepayRequest) (response *UnionApiv3JsapiPrepayResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/combine-transactions/jsapi"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &UnionApiv3JsapiPrepayResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type UnionApiv3JsapiPrepayRequest struct {
|
||||
CombineAppid *string `json:"combine_appid,omitempty"`
|
||||
CombineMchid *string `json:"combine_mchid,omitempty"`
|
||||
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
|
||||
CombinePayerInfo *UnionPayerInfo `json:"combine_payer_info,omitempty"`
|
||||
SceneInfo *UnionSceneInfo `json:"scene_info,omitempty"`
|
||||
SubOrders []UnionSubOrder `json:"sub_orders,omitempty"`
|
||||
TimeExpire *time.Time `json:"time_expire,omitempty"`
|
||||
NotifyUrl *string `json:"notify_url,omitempty"`
|
||||
}
|
||||
|
||||
type UnionApiv3JsapiPrepayResponse struct {
|
||||
PrepayId *string `json:"prepay_id,omitempty"`
|
||||
}
|
||||
|
||||
type UnionPayerInfo struct {
|
||||
Openid *string `json:"openid,omitempty"`
|
||||
SubOpenid *string `json:"sub_openid,omitempty"`
|
||||
}
|
||||
|
||||
type UnionSceneInfo struct {
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
PayerClientIp *string `json:"payer_client_ip,omitempty"`
|
||||
}
|
||||
|
||||
type UnionSubOrder struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
Amount *UnionAmountInfo `json:"amount,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Detail *string `json:"detail,omitempty"`
|
||||
GoodsTag *string `json:"goods_tag,omitempty"`
|
||||
SettleInfo *UnionSettleInfo `json:"settle_info,omitempty"`
|
||||
}
|
||||
|
||||
type UnionAmountInfo struct {
|
||||
TotalAmount *int64 `json:"total_amount,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
type UnionSettleInfo struct {
|
||||
ProfitSharing *bool `json:"profit_sharing,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &UnionApiv3NativePrepayRequest{
|
||||
CombineAppid: wxpay_utility.String("wxd678efh567hg6787"),
|
||||
CombineOutTradeNo: wxpay_utility.String("20150806125346"),
|
||||
CombineMchid: wxpay_utility.String("1900000109"),
|
||||
SceneInfo: &UnionSceneInfo{
|
||||
DeviceId: wxpay_utility.String("POS1:1"),
|
||||
PayerClientIp: wxpay_utility.String("14.17.22.32"),
|
||||
},
|
||||
SubOrders: []UnionCommonSubOrder{
|
||||
UnionCommonSubOrder{
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
OutTradeNo: wxpay_utility.String("20150806125346"),
|
||||
Amount: &UnionAmountInfo{
|
||||
TotalAmount: wxpay_utility.Int64(10),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Attach: wxpay_utility.String("深圳分店"),
|
||||
Description: wxpay_utility.String("腾讯充值中心-QQ会员充值"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SettleInfo: &UnionSettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
},
|
||||
UnionCommonSubOrder{
|
||||
Mchid: wxpay_utility.String("1230000119"),
|
||||
OutTradeNo: wxpay_utility.String("20150806125347"),
|
||||
Amount: &UnionAmountInfo{
|
||||
TotalAmount: wxpay_utility.Int64(10),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Attach: wxpay_utility.String("广州分店"),
|
||||
Description: wxpay_utility.String("腾讯充值中心-微信充值"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SettleInfo: &UnionSettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
},
|
||||
},
|
||||
TimeExpire: wxpay_utility.Time(time.Now()),
|
||||
NotifyUrl: wxpay_utility.String("https://yourapp.com/notify"),
|
||||
}
|
||||
|
||||
response, err := UnionNativePrepay(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func UnionNativePrepay(config *wxpay_utility.MchConfig, request *UnionApiv3NativePrepayRequest) (response *UnionApiv3NativePrepayResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/combine-transactions/native"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &UnionApiv3NativePrepayResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type UnionApiv3NativePrepayRequest struct {
|
||||
CombineAppid *string `json:"combine_appid,omitempty"`
|
||||
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
|
||||
CombineMchid *string `json:"combine_mchid,omitempty"`
|
||||
SceneInfo *UnionSceneInfo `json:"scene_info,omitempty"`
|
||||
SubOrders []UnionCommonSubOrder `json:"sub_orders,omitempty"`
|
||||
TimeExpire *time.Time `json:"time_expire,omitempty"`
|
||||
NotifyUrl *string `json:"notify_url,omitempty"`
|
||||
}
|
||||
|
||||
type UnionApiv3NativePrepayResponse struct {
|
||||
CodeUrl *string `json:"code_url,omitempty"`
|
||||
}
|
||||
|
||||
type UnionSceneInfo struct {
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
PayerClientIp *string `json:"payer_client_ip,omitempty"`
|
||||
}
|
||||
|
||||
type UnionCommonSubOrder struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
Amount *UnionAmountInfo `json:"amount,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
GoodsTag *string `json:"goods_tag,omitempty"`
|
||||
SettleInfo *UnionSettleInfo `json:"settle_info,omitempty"`
|
||||
}
|
||||
|
||||
type UnionAmountInfo struct {
|
||||
TotalAmount *int64 `json:"total_amount,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
type UnionSettleInfo struct {
|
||||
ProfitSharing *bool `json:"profit_sharing,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &UnionQueryByOutTradeNoRequest{
|
||||
CombineOutTradeNo: wxpay_utility.String("P20150806125346"),
|
||||
}
|
||||
|
||||
response, err := UnionQueryByOutTradeNo(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func UnionQueryByOutTradeNo(config *wxpay_utility.MchConfig, request *UnionQueryByOutTradeNoRequest) (response *UnionApiv3UnionQueryResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "GET"
|
||||
path = "/v3/combine-transactions/out-trade-no/{combine_out_trade_no}"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqUrl.Path = strings.Replace(reqUrl.Path, "{combine_out_trade_no}", url.PathEscape(*request.CombineOutTradeNo), -1)
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &UnionApiv3UnionQueryResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type UnionQueryByOutTradeNoRequest struct {
|
||||
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
|
||||
}
|
||||
|
||||
func (o *UnionQueryByOutTradeNoRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias UnionQueryByOutTradeNoRequest
|
||||
a := &struct {
|
||||
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
CombineOutTradeNo: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type UnionApiv3UnionQueryResponse struct {
|
||||
CombineAppid *string `json:"combine_appid,omitempty"`
|
||||
CombineMchid *string `json:"combine_mchid,omitempty"`
|
||||
CombineOutTradeNo *string `json:"combine_out_trade_no,omitempty"`
|
||||
CombinePayerInfo *UnionCommRespPayerInfo `json:"combine_payer_info,omitempty"`
|
||||
SceneInfo *UnionCommRespSceneInfo `json:"scene_info,omitempty"`
|
||||
SubOrders []UnionSubOrder `json:"sub_orders,omitempty"`
|
||||
}
|
||||
|
||||
type UnionCommRespPayerInfo struct {
|
||||
Openid *string `json:"openid,omitempty"`
|
||||
}
|
||||
|
||||
type UnionCommRespSceneInfo struct {
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
}
|
||||
|
||||
type UnionSubOrder struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
SubMchid *string `json:"sub_mchid,omitempty"`
|
||||
SubAppid *string `json:"sub_appid,omitempty"`
|
||||
SubOpenid *string `json:"sub_openid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
TradeType *string `json:"trade_type,omitempty"`
|
||||
TradeState *string `json:"trade_state,omitempty"`
|
||||
BankType *string `json:"bank_type,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
SuccessTime *string `json:"success_time,omitempty"`
|
||||
Amount *UnionCommRespAmountInfo `json:"amount,omitempty"`
|
||||
PromotionDetail []UnionPromotionDetail `json:"promotion_detail,omitempty"`
|
||||
}
|
||||
|
||||
type UnionCommRespAmountInfo struct {
|
||||
TotalAmount *int64 `json:"total_amount,omitempty"`
|
||||
PayerAmount *int64 `json:"payer_amount,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
PayerCurrency *string `json:"payer_currency,omitempty"`
|
||||
SettlementRate *int64 `json:"settlement_rate,omitempty"`
|
||||
}
|
||||
|
||||
type UnionPromotionDetail struct {
|
||||
CouponId *string `json:"coupon_id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
Type *string `json:"type,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
StockId *string `json:"stock_id,omitempty"`
|
||||
WechatpayContribute *int64 `json:"wechatpay_contribute,omitempty"`
|
||||
MerchantContribute *int64 `json:"merchant_contribute,omitempty"`
|
||||
OtherContribute *int64 `json:"other_contribute,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
GoodsDetail []GoodsDetailInPromotion `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type GoodsDetailInPromotion struct {
|
||||
GoodsId *string `json:"goods_id,omitempty"`
|
||||
Quantity *int64 `json:"quantity,omitempty"`
|
||||
UnitPrice *int64 `json:"unit_price,omitempty"`
|
||||
DiscountAmount *int64 `json:"discount_amount,omitempty"`
|
||||
GoodsRemark *string `json:"goods_remark,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &AddReceiverRequest{
|
||||
Appid: wxpay_utility.String("wx8888888888888888"),
|
||||
Type: RECEIVERTYPE_MERCHANT_ID.Ptr(),
|
||||
Account: wxpay_utility.String("86693852"),
|
||||
Name: wxpay_utility.String("hu89ohu89ohu89o"), /*请传入wxpay_utility.EncryptOAEPWithPublicKey 加密结果*/
|
||||
RelationType: RECEIVERRELATIONTYPE_STORE.Ptr(),
|
||||
CustomRelation: wxpay_utility.String("代理商"),
|
||||
}
|
||||
|
||||
response, err := AddReceiver(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func AddReceiver(config *wxpay_utility.MchConfig, request *AddReceiverRequest) (response *AddReceiverResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/profitsharing/receivers/add"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &AddReceiverResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type AddReceiverRequest struct {
|
||||
Appid *string `json:"appid,omitempty"`
|
||||
Type *ReceiverType `json:"type,omitempty"`
|
||||
Account *string `json:"account,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
RelationType *ReceiverRelationType `json:"relation_type,omitempty"`
|
||||
CustomRelation *string `json:"custom_relation,omitempty"`
|
||||
}
|
||||
|
||||
type AddReceiverResponse struct {
|
||||
Type *ReceiverType `json:"type,omitempty"`
|
||||
Account *string `json:"account,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
RelationType *ReceiverRelationType `json:"relation_type,omitempty"`
|
||||
CustomRelation *string `json:"custom_relation,omitempty"`
|
||||
}
|
||||
|
||||
type ReceiverType string
|
||||
|
||||
func (e ReceiverType) Ptr() *ReceiverType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
|
||||
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
|
||||
)
|
||||
|
||||
type ReceiverRelationType string
|
||||
|
||||
func (e ReceiverRelationType) Ptr() *ReceiverRelationType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
RECEIVERRELATIONTYPE_STORE ReceiverRelationType = "STORE"
|
||||
RECEIVERRELATIONTYPE_STAFF ReceiverRelationType = "STAFF"
|
||||
RECEIVERRELATIONTYPE_STORE_OWNER ReceiverRelationType = "STORE_OWNER"
|
||||
RECEIVERRELATIONTYPE_PARTNER ReceiverRelationType = "PARTNER"
|
||||
RECEIVERRELATIONTYPE_HEADQUARTER ReceiverRelationType = "HEADQUARTER"
|
||||
RECEIVERRELATIONTYPE_BRAND ReceiverRelationType = "BRAND"
|
||||
RECEIVERRELATIONTYPE_DISTRIBUTOR ReceiverRelationType = "DISTRIBUTOR"
|
||||
RECEIVERRELATIONTYPE_USER ReceiverRelationType = "USER"
|
||||
RECEIVERRELATIONTYPE_SUPPLIER ReceiverRelationType = "SUPPLIER"
|
||||
RECEIVERRELATIONTYPE_CUSTOM ReceiverRelationType = "CUSTOM"
|
||||
)
|
||||
|
||||
删除分账接收方
|
||||
@@ -0,0 +1,178 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx",
|
||||
"1DDE55AD98Exxxxxxxxxx",
|
||||
"/path/to/apiclient_key.pem",
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx",
|
||||
"/path/to/wxp_pub.pem",
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &CreateOrderRequest{
|
||||
Appid: wxpay_utility.String("wx8888888888888888"),
|
||||
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
|
||||
OutOrderNo: wxpay_utility.String("P20150806125346"),
|
||||
Receivers: []CreateOrderReceiver{CreateOrderReceiver{
|
||||
Type: wxpay_utility.String("MERCHANT_ID"),
|
||||
Account: wxpay_utility.String("86693852"),
|
||||
Name: wxpay_utility.String("hu89ohu89ohu89o"), /*请传入wxpay_utility.EncryptOAEPWithPublicKey 加密结果*/
|
||||
Amount: wxpay_utility.Int64(888),
|
||||
Description: wxpay_utility.String("分给商户A"),
|
||||
}},
|
||||
UnfreezeUnsplit: wxpay_utility.Bool(true),
|
||||
}
|
||||
|
||||
response, err := CreateOrder(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func CreateOrder(config *wxpay_utility.MchConfig, request *CreateOrderRequest) (response *OrdersEntity, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/profitsharing/orders"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
err = wxpay_utility.ValidateResponse(config.WechatPayPublicKeyId(), config.WechatPayPublicKey(), &httpResponse.Header, respBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &OrdersEntity{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(httpResponse.StatusCode, httpResponse.Header, respBody)
|
||||
}
|
||||
}
|
||||
|
||||
type CreateOrderRequest struct {
|
||||
Appid *string `json:"appid,omitempty"`
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
Receivers []CreateOrderReceiver `json:"receivers,omitempty"`
|
||||
UnfreezeUnsplit *bool `json:"unfreeze_unsplit,omitempty"`
|
||||
}
|
||||
|
||||
type OrdersEntity struct {
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
OrderId *string `json:"order_id,omitempty"`
|
||||
State *OrderStatus `json:"state,omitempty"`
|
||||
Receivers []OrderReceiverDetail `json:"receivers,omitempty"`
|
||||
}
|
||||
|
||||
type CreateOrderReceiver struct {
|
||||
Type *string `json:"type,omitempty"`
|
||||
Account *string `json:"account,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type OrderStatus string
|
||||
|
||||
func (e OrderStatus) Ptr() *OrderStatus { return &e }
|
||||
|
||||
const (
|
||||
ORDERSTATUS_PROCESSING OrderStatus = "PROCESSING"
|
||||
ORDERSTATUS_FINISHED OrderStatus = "FINISHED"
|
||||
)
|
||||
|
||||
type OrderReceiverDetail struct {
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type *ReceiverType `json:"type,omitempty"`
|
||||
Account *string `json:"account,omitempty"`
|
||||
Result *DetailStatus `json:"result,omitempty"`
|
||||
FailReason *DetailFailReason `json:"fail_reason,omitempty"`
|
||||
CreateTime *time.Time `json:"create_time,omitempty"`
|
||||
FinishTime *time.Time `json:"finish_time,omitempty"`
|
||||
DetailId *string `json:"detail_id,omitempty"`
|
||||
}
|
||||
|
||||
type ReceiverType string
|
||||
|
||||
func (e ReceiverType) Ptr() *ReceiverType { return &e }
|
||||
|
||||
const (
|
||||
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
|
||||
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
|
||||
)
|
||||
|
||||
type DetailStatus string
|
||||
|
||||
func (e DetailStatus) Ptr() *DetailStatus { return &e }
|
||||
|
||||
const (
|
||||
DETAILSTATUS_PENDING DetailStatus = "PENDING"
|
||||
DETAILSTATUS_SUCCESS DetailStatus = "SUCCESS"
|
||||
DETAILSTATUS_CLOSED DetailStatus = "CLOSED"
|
||||
)
|
||||
|
||||
type DetailFailReason string
|
||||
|
||||
func (e DetailFailReason) Ptr() *DetailFailReason { return &e }
|
||||
|
||||
const (
|
||||
DETAILFAILREASON_ACCOUNT_ABNORMAL DetailFailReason = "ACCOUNT_ABNORMAL"
|
||||
DETAILFAILREASON_NO_RELATION DetailFailReason = "NO_RELATION"
|
||||
DETAILFAILREASON_RECEIVER_HIGH_RISK DetailFailReason = "RECEIVER_HIGH_RISK"
|
||||
DETAILFAILREASON_RECEIVER_REAL_NAME_NOT_VERIFIED DetailFailReason = "RECEIVER_REAL_NAME_NOT_VERIFIED"
|
||||
DETAILFAILREASON_NO_AUTH DetailFailReason = "NO_AUTH"
|
||||
DETAILFAILREASON_RECEIVER_RECEIPT_LIMIT DetailFailReason = "RECEIVER_RECEIPT_LIMIT"
|
||||
DETAILFAILREASON_PAYER_ACCOUNT_ABNORMAL DetailFailReason = "PAYER_ACCOUNT_ABNORMAL"
|
||||
DETAILFAILREASON_INVALID_REQUEST DetailFailReason = "INVALID_REQUEST"
|
||||
)
|
||||
@@ -0,0 +1,159 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &CreateReturnOrderRequest{
|
||||
OrderId: wxpay_utility.String("3008450740201411110007820472"),
|
||||
OutOrderNo: wxpay_utility.String("P20150806125346"),
|
||||
OutReturnNo: wxpay_utility.String("R20190516001"),
|
||||
ReturnMchid: wxpay_utility.String("86693852"),
|
||||
Amount: wxpay_utility.Int64(10),
|
||||
Description: wxpay_utility.String("用户退款"),
|
||||
}
|
||||
|
||||
response, err := CreateReturnOrder(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func CreateReturnOrder(config *wxpay_utility.MchConfig, request *CreateReturnOrderRequest) (response *ReturnOrdersEntity, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/profitsharing/return-orders"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &ReturnOrdersEntity{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type CreateReturnOrderRequest struct {
|
||||
OrderId *string `json:"order_id,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
OutReturnNo *string `json:"out_return_no,omitempty"`
|
||||
ReturnMchid *string `json:"return_mchid,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type ReturnOrdersEntity struct {
|
||||
OrderId *string `json:"order_id,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
OutReturnNo *string `json:"out_return_no,omitempty"`
|
||||
ReturnId *string `json:"return_id,omitempty"`
|
||||
ReturnMchid *string `json:"return_mchid,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Result *ReturnOrderStatus `json:"result,omitempty"`
|
||||
FailReason *ReturnOrderFailReason `json:"fail_reason,omitempty"`
|
||||
CreateTime *time.Time `json:"create_time,omitempty"`
|
||||
FinishTime *time.Time `json:"finish_time,omitempty"`
|
||||
}
|
||||
|
||||
type ReturnOrderStatus string
|
||||
|
||||
func (e ReturnOrderStatus) Ptr() *ReturnOrderStatus {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
RETURNORDERSTATUS_PROCESSING ReturnOrderStatus = "PROCESSING"
|
||||
RETURNORDERSTATUS_SUCCESS ReturnOrderStatus = "SUCCESS"
|
||||
RETURNORDERSTATUS_FAILED ReturnOrderStatus = "FAILED"
|
||||
)
|
||||
|
||||
type ReturnOrderFailReason string
|
||||
|
||||
func (e ReturnOrderFailReason) Ptr() *ReturnOrderFailReason {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
RETURNORDERFAILREASON_ACCOUNT_ABNORMAL ReturnOrderFailReason = "ACCOUNT_ABNORMAL"
|
||||
RETURNORDERFAILREASON_BALANCE_NOT_ENOUGH ReturnOrderFailReason = "BALANCE_NOT_ENOUGH"
|
||||
RETURNORDERFAILREASON_TIME_OUT_CLOSED ReturnOrderFailReason = "TIME_OUT_CLOSED"
|
||||
RETURNORDERFAILREASON_PAYER_ACCOUNT_ABNORMAL ReturnOrderFailReason = "PAYER_ACCOUNT_ABNORMAL"
|
||||
RETURNORDERFAILREASON_INVALID_REQUEST ReturnOrderFailReason = "INVALID_REQUEST"
|
||||
)
|
||||
|
||||
查询分账回退结果
|
||||
@@ -0,0 +1,128 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &DeleteReceiverRequest{
|
||||
Appid: wxpay_utility.String("wx8888888888888888"),
|
||||
Type: RECEIVERTYPE_MERCHANT_ID.Ptr(),
|
||||
Account: wxpay_utility.String("1900000109"),
|
||||
}
|
||||
|
||||
response, err := DeleteReceiver(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func DeleteReceiver(config *wxpay_utility.MchConfig, request *DeleteReceiverRequest) (response *DeleteReceiverResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/profitsharing/receivers/delete"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &DeleteReceiverResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type DeleteReceiverRequest struct {
|
||||
Appid *string `json:"appid,omitempty"`
|
||||
Type *ReceiverType `json:"type,omitempty"`
|
||||
Account *string `json:"account,omitempty"`
|
||||
}
|
||||
|
||||
type DeleteReceiverResponse struct {
|
||||
Type *ReceiverType `json:"type,omitempty"`
|
||||
Account *string `json:"account,omitempty"`
|
||||
}
|
||||
|
||||
type ReceiverType string
|
||||
|
||||
func (e ReceiverType) Ptr() *ReceiverType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
|
||||
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
|
||||
)
|
||||
|
||||
申请分账账单
|
||||
@@ -0,0 +1,198 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &QueryOrderRequest{
|
||||
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
|
||||
OutOrderNo: wxpay_utility.String("P20150806125346"),
|
||||
}
|
||||
|
||||
response, err := QueryOrder(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func QueryOrder(config *wxpay_utility.MchConfig, request *QueryOrderRequest) (response *OrdersEntity, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "GET"
|
||||
path = "/v3/profitsharing/orders/{out_order_no}"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_order_no}", url.PathEscape(*request.OutOrderNo), -1)
|
||||
query := reqUrl.Query()
|
||||
if request.TransactionId != nil {
|
||||
query.Add("transaction_id", *request.TransactionId)
|
||||
}
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &OrdersEntity{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type QueryOrderRequest struct {
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
}
|
||||
|
||||
func (o *QueryOrderRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias QueryOrderRequest
|
||||
a := &struct {
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
TransactionId: nil,
|
||||
OutOrderNo: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type OrdersEntity struct {
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
OrderId *string `json:"order_id,omitempty"`
|
||||
State *OrderStatus `json:"state,omitempty"`
|
||||
Receivers []OrderReceiverDetail `json:"receivers,omitempty"`
|
||||
}
|
||||
|
||||
type OrderStatus string
|
||||
|
||||
func (e OrderStatus) Ptr() *OrderStatus {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
ORDERSTATUS_PROCESSING OrderStatus = "PROCESSING"
|
||||
ORDERSTATUS_FINISHED OrderStatus = "FINISHED"
|
||||
)
|
||||
|
||||
type OrderReceiverDetail struct {
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type *ReceiverType `json:"type,omitempty"`
|
||||
Account *string `json:"account,omitempty"`
|
||||
Result *DetailStatus `json:"result,omitempty"`
|
||||
FailReason *DetailFailReason `json:"fail_reason,omitempty"`
|
||||
CreateTime *time.Time `json:"create_time,omitempty"`
|
||||
FinishTime *time.Time `json:"finish_time,omitempty"`
|
||||
DetailId *string `json:"detail_id,omitempty"`
|
||||
}
|
||||
|
||||
type ReceiverType string
|
||||
|
||||
func (e ReceiverType) Ptr() *ReceiverType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
|
||||
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
|
||||
)
|
||||
|
||||
type DetailStatus string
|
||||
|
||||
func (e DetailStatus) Ptr() *DetailStatus {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
DETAILSTATUS_PENDING DetailStatus = "PENDING"
|
||||
DETAILSTATUS_SUCCESS DetailStatus = "SUCCESS"
|
||||
DETAILSTATUS_CLOSED DetailStatus = "CLOSED"
|
||||
)
|
||||
|
||||
type DetailFailReason string
|
||||
|
||||
func (e DetailFailReason) Ptr() *DetailFailReason {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
DETAILFAILREASON_ACCOUNT_ABNORMAL DetailFailReason = "ACCOUNT_ABNORMAL"
|
||||
DETAILFAILREASON_NO_RELATION DetailFailReason = "NO_RELATION"
|
||||
DETAILFAILREASON_RECEIVER_HIGH_RISK DetailFailReason = "RECEIVER_HIGH_RISK"
|
||||
DETAILFAILREASON_RECEIVER_REAL_NAME_NOT_VERIFIED DetailFailReason = "RECEIVER_REAL_NAME_NOT_VERIFIED"
|
||||
DETAILFAILREASON_NO_AUTH DetailFailReason = "NO_AUTH"
|
||||
DETAILFAILREASON_RECEIVER_RECEIPT_LIMIT DetailFailReason = "RECEIVER_RECEIPT_LIMIT"
|
||||
DETAILFAILREASON_PAYER_ACCOUNT_ABNORMAL DetailFailReason = "PAYER_ACCOUNT_ABNORMAL"
|
||||
DETAILFAILREASON_INVALID_REQUEST DetailFailReason = "INVALID_REQUEST"
|
||||
)
|
||||
|
||||
请求分账回退
|
||||
@@ -0,0 +1,122 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &QueryOrderAmountRequest{
|
||||
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
|
||||
}
|
||||
|
||||
response, err := QueryOrderAmount(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func QueryOrderAmount(config *wxpay_utility.MchConfig, request *QueryOrderAmountRequest) (response *QueryOrderAmountResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "GET"
|
||||
path = "/v3/profitsharing/transactions/{transaction_id}/amounts"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqUrl.Path = strings.Replace(reqUrl.Path, "{transaction_id}", url.PathEscape(*request.TransactionId), -1)
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &QueryOrderAmountResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type QueryOrderAmountRequest struct {
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
}
|
||||
|
||||
func (o *QueryOrderAmountRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias QueryOrderAmountRequest
|
||||
a := &struct {
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
TransactionId: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type QueryOrderAmountResponse struct {
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
UnsplitAmount *int64 `json:"unsplit_amount,omitempty"`
|
||||
}
|
||||
|
||||
添加分账接收方
|
||||
@@ -0,0 +1,167 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &QueryReturnOrderRequest{
|
||||
OutReturnNo: wxpay_utility.String("R20190516001"),
|
||||
OutOrderNo: wxpay_utility.String("P20190806125346"),
|
||||
}
|
||||
|
||||
response, err := QueryReturnOrder(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func QueryReturnOrder(config *wxpay_utility.MchConfig, request *QueryReturnOrderRequest) (response *ReturnOrdersEntity, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "GET"
|
||||
path = "/v3/profitsharing/return-orders/{out_return_no}"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_return_no}", url.PathEscape(*request.OutReturnNo), -1)
|
||||
query := reqUrl.Query()
|
||||
if request.OutOrderNo != nil {
|
||||
query.Add("out_order_no", *request.OutOrderNo)
|
||||
}
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &ReturnOrdersEntity{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type QueryReturnOrderRequest struct {
|
||||
OutReturnNo *string `json:"out_return_no,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
}
|
||||
|
||||
func (o *QueryReturnOrderRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias QueryReturnOrderRequest
|
||||
a := &struct {
|
||||
OutReturnNo *string `json:"out_return_no,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
OutReturnNo: nil,
|
||||
OutOrderNo: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type ReturnOrdersEntity struct {
|
||||
OrderId *string `json:"order_id,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
OutReturnNo *string `json:"out_return_no,omitempty"`
|
||||
ReturnId *string `json:"return_id,omitempty"`
|
||||
ReturnMchid *string `json:"return_mchid,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Result *ReturnOrderStatus `json:"result,omitempty"`
|
||||
FailReason *ReturnOrderFailReason `json:"fail_reason,omitempty"`
|
||||
CreateTime *time.Time `json:"create_time,omitempty"`
|
||||
FinishTime *time.Time `json:"finish_time,omitempty"`
|
||||
}
|
||||
|
||||
type ReturnOrderStatus string
|
||||
|
||||
func (e ReturnOrderStatus) Ptr() *ReturnOrderStatus {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
RETURNORDERSTATUS_PROCESSING ReturnOrderStatus = "PROCESSING"
|
||||
RETURNORDERSTATUS_SUCCESS ReturnOrderStatus = "SUCCESS"
|
||||
RETURNORDERSTATUS_FAILED ReturnOrderStatus = "FAILED"
|
||||
)
|
||||
|
||||
type ReturnOrderFailReason string
|
||||
|
||||
func (e ReturnOrderFailReason) Ptr() *ReturnOrderFailReason {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
RETURNORDERFAILREASON_ACCOUNT_ABNORMAL ReturnOrderFailReason = "ACCOUNT_ABNORMAL"
|
||||
RETURNORDERFAILREASON_BALANCE_NOT_ENOUGH ReturnOrderFailReason = "BALANCE_NOT_ENOUGH"
|
||||
RETURNORDERFAILREASON_TIME_OUT_CLOSED ReturnOrderFailReason = "TIME_OUT_CLOSED"
|
||||
RETURNORDERFAILREASON_PAYER_ACCOUNT_ABNORMAL ReturnOrderFailReason = "PAYER_ACCOUNT_ABNORMAL"
|
||||
RETURNORDERFAILREASON_INVALID_REQUEST ReturnOrderFailReason = "INVALID_REQUEST"
|
||||
)
|
||||
|
||||
解冻剩余资金
|
||||
@@ -0,0 +1,151 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &SplitBillRequest{
|
||||
BillDate: wxpay_utility.String("2019-06-11"),
|
||||
TarType: SPLITBILLTARTYPE_GZIP.Ptr(),
|
||||
}
|
||||
|
||||
response, err := SplitBill(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func SplitBill(config *wxpay_utility.MchConfig, request *SplitBillRequest) (response *SplitBillResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "GET"
|
||||
path = "/v3/profitsharing/bills"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
query := reqUrl.Query()
|
||||
if request.BillDate != nil {
|
||||
query.Add("bill_date", *request.BillDate)
|
||||
}
|
||||
if request.TarType != nil {
|
||||
query.Add("tar_type", fmt.Sprintf("%v", *request.TarType))
|
||||
}
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &SplitBillResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type SplitBillRequest struct {
|
||||
BillDate *string `json:"bill_date,omitempty"`
|
||||
TarType *SplitBillTarType `json:"tar_type,omitempty"`
|
||||
}
|
||||
|
||||
func (o *SplitBillRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias SplitBillRequest
|
||||
a := &struct {
|
||||
BillDate *string `json:"bill_date,omitempty"`
|
||||
TarType *SplitBillTarType `json:"tar_type,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
BillDate: nil,
|
||||
TarType: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type SplitBillResponse struct {
|
||||
HashType *SplitBillHashType `json:"hash_type,omitempty"`
|
||||
HashValue *string `json:"hash_value,omitempty"`
|
||||
DownloadUrl *string `json:"download_url,omitempty"`
|
||||
}
|
||||
|
||||
type SplitBillTarType string
|
||||
|
||||
func (e SplitBillTarType) Ptr() *SplitBillTarType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
SPLITBILLTARTYPE_GZIP SplitBillTarType = "GZIP"
|
||||
)
|
||||
|
||||
type SplitBillHashType string
|
||||
|
||||
func (e SplitBillHashType) Ptr() *SplitBillHashType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
SPLITBILLHASHTYPE_SHA1 SplitBillHashType = "SHA1"
|
||||
)
|
||||
@@ -0,0 +1,184 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &UnfreezeOrderRequest{
|
||||
TransactionId: wxpay_utility.String("4208450740201411110007820472"),
|
||||
OutOrderNo: wxpay_utility.String("P20150806125346"),
|
||||
Description: wxpay_utility.String("解冻全部剩余资金"),
|
||||
}
|
||||
|
||||
response, err := UnfreezeOrder(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func UnfreezeOrder(config *wxpay_utility.MchConfig, request *UnfreezeOrderRequest) (response *OrdersEntity, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/profitsharing/orders/unfreeze"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &OrdersEntity{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type UnfreezeOrderRequest struct {
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
}
|
||||
|
||||
type OrdersEntity struct {
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutOrderNo *string `json:"out_order_no,omitempty"`
|
||||
OrderId *string `json:"order_id,omitempty"`
|
||||
State *OrderStatus `json:"state,omitempty"`
|
||||
Receivers []OrderReceiverDetail `json:"receivers,omitempty"`
|
||||
}
|
||||
|
||||
type OrderStatus string
|
||||
|
||||
func (e OrderStatus) Ptr() *OrderStatus {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
ORDERSTATUS_PROCESSING OrderStatus = "PROCESSING"
|
||||
ORDERSTATUS_FINISHED OrderStatus = "FINISHED"
|
||||
)
|
||||
|
||||
type OrderReceiverDetail struct {
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Type *ReceiverType `json:"type,omitempty"`
|
||||
Account *string `json:"account,omitempty"`
|
||||
Result *DetailStatus `json:"result,omitempty"`
|
||||
FailReason *DetailFailReason `json:"fail_reason,omitempty"`
|
||||
CreateTime *time.Time `json:"create_time,omitempty"`
|
||||
FinishTime *time.Time `json:"finish_time,omitempty"`
|
||||
DetailId *string `json:"detail_id,omitempty"`
|
||||
}
|
||||
|
||||
type ReceiverType string
|
||||
|
||||
func (e ReceiverType) Ptr() *ReceiverType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
RECEIVERTYPE_MERCHANT_ID ReceiverType = "MERCHANT_ID"
|
||||
RECEIVERTYPE_PERSONAL_OPENID ReceiverType = "PERSONAL_OPENID"
|
||||
)
|
||||
|
||||
type DetailStatus string
|
||||
|
||||
func (e DetailStatus) Ptr() *DetailStatus {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
DETAILSTATUS_PENDING DetailStatus = "PENDING"
|
||||
DETAILSTATUS_SUCCESS DetailStatus = "SUCCESS"
|
||||
DETAILSTATUS_CLOSED DetailStatus = "CLOSED"
|
||||
)
|
||||
|
||||
type DetailFailReason string
|
||||
|
||||
func (e DetailFailReason) Ptr() *DetailFailReason {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
DETAILFAILREASON_ACCOUNT_ABNORMAL DetailFailReason = "ACCOUNT_ABNORMAL"
|
||||
DETAILFAILREASON_NO_RELATION DetailFailReason = "NO_RELATION"
|
||||
DETAILFAILREASON_RECEIVER_HIGH_RISK DetailFailReason = "RECEIVER_HIGH_RISK"
|
||||
DETAILFAILREASON_RECEIVER_REAL_NAME_NOT_VERIFIED DetailFailReason = "RECEIVER_REAL_NAME_NOT_VERIFIED"
|
||||
DETAILFAILREASON_NO_AUTH DetailFailReason = "NO_AUTH"
|
||||
DETAILFAILREASON_RECEIVER_RECEIPT_LIMIT DetailFailReason = "RECEIVER_RECEIPT_LIMIT"
|
||||
DETAILFAILREASON_PAYER_ACCOUNT_ABNORMAL DetailFailReason = "PAYER_ACCOUNT_ABNORMAL"
|
||||
DETAILFAILREASON_INVALID_REQUEST DetailFailReason = "INVALID_REQUEST"
|
||||
)
|
||||
|
||||
查询剩余待分金额
|
||||
@@ -0,0 +1,196 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &CommonPrepayRequest{
|
||||
Appid: wxpay_utility.String("wxd678efh567hg6787"),
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
Description: wxpay_utility.String("Image形象店-深圳腾大-QQ公仔"),
|
||||
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
TimeExpire: wxpay_utility.Time(time.Now()),
|
||||
Attach: wxpay_utility.String("自定义数据说明"),
|
||||
NotifyUrl: wxpay_utility.String(" https://www.weixin.qq.com/wxpay/pay.php"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SupportFapiao: wxpay_utility.Bool(false),
|
||||
Amount: &CommonAmountInfo{
|
||||
Total: wxpay_utility.Int64(100),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Detail: &CouponInfo{
|
||||
CostPrice: wxpay_utility.Int64(608800),
|
||||
InvoiceId: wxpay_utility.String("微信123"),
|
||||
GoodsDetail: []GoodsDetail{GoodsDetail{
|
||||
MerchantGoodsId: wxpay_utility.String("1246464644"),
|
||||
WechatpayGoodsId: wxpay_utility.String("1001"),
|
||||
GoodsName: wxpay_utility.String("iPhoneX 256G"),
|
||||
Quantity: wxpay_utility.Int64(1),
|
||||
UnitPrice: wxpay_utility.Int64(528800),
|
||||
}},
|
||||
},
|
||||
SceneInfo: &CommonSceneInfo{
|
||||
PayerClientIp: wxpay_utility.String("14.23.150.211"),
|
||||
DeviceId: wxpay_utility.String("013467007045764"),
|
||||
StoreInfo: &StoreInfo{
|
||||
Id: wxpay_utility.String("0001"),
|
||||
Name: wxpay_utility.String("腾讯大厦分店"),
|
||||
AreaCode: wxpay_utility.String("440305"),
|
||||
Address: wxpay_utility.String("广东省深圳市南山区科技中一道10000号"),
|
||||
},
|
||||
},
|
||||
SettleInfo: &SettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
}
|
||||
|
||||
response, err := AppPrepay(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
// AppPrepay App下单
|
||||
func AppPrepay(config *wxpay_utility.MchConfig, request *CommonPrepayRequest) (response *DirectApiv3AppPrepayResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/pay/transactions/app"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &DirectApiv3AppPrepayResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type CommonPrepayRequest struct {
|
||||
Appid *string `json:"appid,omitempty"`
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
TimeExpire *time.Time `json:"time_expire,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
NotifyUrl *string `json:"notify_url,omitempty"`
|
||||
GoodsTag *string `json:"goods_tag,omitempty"`
|
||||
SupportFapiao *bool `json:"support_fapiao,omitempty"`
|
||||
Amount *CommonAmountInfo `json:"amount,omitempty"`
|
||||
Detail *CouponInfo `json:"detail,omitempty"`
|
||||
SceneInfo *CommonSceneInfo `json:"scene_info,omitempty"`
|
||||
SettleInfo *SettleInfo `json:"settle_info,omitempty"`
|
||||
}
|
||||
|
||||
type DirectApiv3AppPrepayResponse struct {
|
||||
PrepayId *string `json:"prepay_id,omitempty"`
|
||||
}
|
||||
|
||||
type CommonAmountInfo struct {
|
||||
Total *int64 `json:"total,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
type CouponInfo struct {
|
||||
CostPrice *int64 `json:"cost_price,omitempty"`
|
||||
InvoiceId *string `json:"invoice_id,omitempty"`
|
||||
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type CommonSceneInfo struct {
|
||||
PayerClientIp *string `json:"payer_client_ip,omitempty"`
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
StoreInfo *StoreInfo `json:"store_info,omitempty"`
|
||||
}
|
||||
|
||||
type SettleInfo struct {
|
||||
ProfitSharing *bool `json:"profit_sharing,omitempty"`
|
||||
}
|
||||
|
||||
type GoodsDetail struct {
|
||||
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
|
||||
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
|
||||
GoodsName *string `json:"goods_name,omitempty"`
|
||||
Quantity *int64 `json:"quantity,omitempty"`
|
||||
UnitPrice *int64 `json:"unit_price,omitempty"`
|
||||
}
|
||||
|
||||
type StoreInfo struct {
|
||||
Id *string `json:"id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
AreaCode *string `json:"area_code,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &DirectApiv3H5PrepayRequest{
|
||||
Appid: wxpay_utility.String("wxd678efh567hg6787"),
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
Description: wxpay_utility.String("Image形象店-深圳腾大-QQ公仔"),
|
||||
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
TimeExpire: wxpay_utility.Time(time.Now()),
|
||||
Attach: wxpay_utility.String("自定义数据说明"),
|
||||
NotifyUrl: wxpay_utility.String(" https://www.weixin.qq.com/wxpay/pay.php"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SupportFapiao: wxpay_utility.Bool(false),
|
||||
Amount: &CommonAmountInfo{
|
||||
Total: wxpay_utility.Int64(100),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Detail: &CouponInfo{
|
||||
CostPrice: wxpay_utility.Int64(608800),
|
||||
InvoiceId: wxpay_utility.String("微信123"),
|
||||
GoodsDetail: []GoodsDetail{GoodsDetail{
|
||||
MerchantGoodsId: wxpay_utility.String("1246464644"),
|
||||
WechatpayGoodsId: wxpay_utility.String("1001"),
|
||||
GoodsName: wxpay_utility.String("iPhoneX 256G"),
|
||||
Quantity: wxpay_utility.Int64(1),
|
||||
UnitPrice: wxpay_utility.Int64(528800),
|
||||
}},
|
||||
},
|
||||
SceneInfo: &H5ReqSceneInfo{
|
||||
PayerClientIp: wxpay_utility.String("14.23.150.211"),
|
||||
DeviceId: wxpay_utility.String("013467007045764"),
|
||||
StoreInfo: &StoreInfo{
|
||||
Id: wxpay_utility.String("0001"),
|
||||
Name: wxpay_utility.String("腾讯大厦分店"),
|
||||
AreaCode: wxpay_utility.String("440305"),
|
||||
Address: wxpay_utility.String("广东省深圳市南山区科技中一道10000号"),
|
||||
},
|
||||
H5Info: &H5Info{
|
||||
Type: wxpay_utility.String("iOS"),
|
||||
AppName: wxpay_utility.String("王者荣耀"),
|
||||
AppUrl: wxpay_utility.String("https://pay.qq.com"),
|
||||
BundleId: wxpay_utility.String("com.tencent.wzryiOS"),
|
||||
PackageName: wxpay_utility.String("com.tencent.tmgp.sgame"),
|
||||
},
|
||||
},
|
||||
SettleInfo: &SettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
}
|
||||
|
||||
response, err := H5Prepay(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
// H5Prepay H5下单
|
||||
func H5Prepay(config *wxpay_utility.MchConfig, request *DirectApiv3H5PrepayRequest) (response *DirectApiv3H5PrepayResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/pay/transactions/h5"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &DirectApiv3H5PrepayResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type DirectApiv3H5PrepayRequest struct {
|
||||
Appid *string `json:"appid,omitempty"`
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
TimeExpire *time.Time `json:"time_expire,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
NotifyUrl *string `json:"notify_url,omitempty"`
|
||||
GoodsTag *string `json:"goods_tag,omitempty"`
|
||||
SupportFapiao *bool `json:"support_fapiao,omitempty"`
|
||||
Amount *CommonAmountInfo `json:"amount,omitempty"`
|
||||
Detail *CouponInfo `json:"detail,omitempty"`
|
||||
SceneInfo *H5ReqSceneInfo `json:"scene_info,omitempty"`
|
||||
SettleInfo *SettleInfo `json:"settle_info,omitempty"`
|
||||
}
|
||||
|
||||
type DirectApiv3H5PrepayResponse struct {
|
||||
H5Url *string `json:"h5_url,omitempty"`
|
||||
}
|
||||
|
||||
type CommonAmountInfo struct {
|
||||
Total *int64 `json:"total,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
type CouponInfo struct {
|
||||
CostPrice *int64 `json:"cost_price,omitempty"`
|
||||
InvoiceId *string `json:"invoice_id,omitempty"`
|
||||
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type H5ReqSceneInfo struct {
|
||||
PayerClientIp *string `json:"payer_client_ip,omitempty"`
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
StoreInfo *StoreInfo `json:"store_info,omitempty"`
|
||||
H5Info *H5Info `json:"h5_info,omitempty"`
|
||||
}
|
||||
|
||||
type SettleInfo struct {
|
||||
ProfitSharing *bool `json:"profit_sharing,omitempty"`
|
||||
}
|
||||
|
||||
type GoodsDetail struct {
|
||||
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
|
||||
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
|
||||
GoodsName *string `json:"goods_name,omitempty"`
|
||||
Quantity *int64 `json:"quantity,omitempty"`
|
||||
UnitPrice *int64 `json:"unit_price,omitempty"`
|
||||
}
|
||||
|
||||
type StoreInfo struct {
|
||||
Id *string `json:"id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
AreaCode *string `json:"area_code,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
}
|
||||
|
||||
type H5Info struct {
|
||||
Type *string `json:"type,omitempty"`
|
||||
AppName *string `json:"app_name,omitempty"`
|
||||
AppUrl *string `json:"app_url,omitempty"`
|
||||
BundleId *string `json:"bundle_id,omitempty"`
|
||||
PackageName *string `json:"package_name,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &CommonPrepayRequest{
|
||||
Appid: wxpay_utility.String("wxd678efh567hg6787"),
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
Description: wxpay_utility.String("Image形象店-深圳腾大-QQ公仔"),
|
||||
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
TimeExpire: wxpay_utility.Time(time.Now()),
|
||||
Attach: wxpay_utility.String("自定义数据说明"),
|
||||
NotifyUrl: wxpay_utility.String(" https://www.weixin.qq.com/wxpay/pay.php"),
|
||||
GoodsTag: wxpay_utility.String("WXG"),
|
||||
SupportFapiao: wxpay_utility.Bool(false),
|
||||
Amount: &CommonAmountInfo{
|
||||
Total: wxpay_utility.Int64(100),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
Detail: &CouponInfo{
|
||||
CostPrice: wxpay_utility.Int64(608800),
|
||||
InvoiceId: wxpay_utility.String("微信123"),
|
||||
GoodsDetail: []GoodsDetail{GoodsDetail{
|
||||
MerchantGoodsId: wxpay_utility.String("1246464644"),
|
||||
WechatpayGoodsId: wxpay_utility.String("1001"),
|
||||
GoodsName: wxpay_utility.String("iPhoneX 256G"),
|
||||
Quantity: wxpay_utility.Int64(1),
|
||||
UnitPrice: wxpay_utility.Int64(528800),
|
||||
}},
|
||||
},
|
||||
SceneInfo: &CommonSceneInfo{
|
||||
PayerClientIp: wxpay_utility.String("14.23.150.211"),
|
||||
DeviceId: wxpay_utility.String("013467007045764"),
|
||||
StoreInfo: &StoreInfo{
|
||||
Id: wxpay_utility.String("0001"),
|
||||
Name: wxpay_utility.String("腾讯大厦分店"),
|
||||
AreaCode: wxpay_utility.String("440305"),
|
||||
Address: wxpay_utility.String("广东省深圳市南山区科技中一道10000号"),
|
||||
},
|
||||
},
|
||||
SettleInfo: &SettleInfo{
|
||||
ProfitSharing: wxpay_utility.Bool(false),
|
||||
},
|
||||
}
|
||||
|
||||
response, err := NativePrepay(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
// NativePrepay Native下单
|
||||
func NativePrepay(config *wxpay_utility.MchConfig, request *CommonPrepayRequest) (response *DirectApiv3DirectNativePrepayResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/pay/transactions/native"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &DirectApiv3DirectNativePrepayResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type CommonPrepayRequest struct {
|
||||
Appid *string `json:"appid,omitempty"`
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
TimeExpire *time.Time `json:"time_expire,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
NotifyUrl *string `json:"notify_url,omitempty"`
|
||||
GoodsTag *string `json:"goods_tag,omitempty"`
|
||||
SupportFapiao *bool `json:"support_fapiao,omitempty"`
|
||||
Amount *CommonAmountInfo `json:"amount,omitempty"`
|
||||
Detail *CouponInfo `json:"detail,omitempty"`
|
||||
SceneInfo *CommonSceneInfo `json:"scene_info,omitempty"`
|
||||
SettleInfo *SettleInfo `json:"settle_info,omitempty"`
|
||||
}
|
||||
|
||||
type DirectApiv3DirectNativePrepayResponse struct {
|
||||
CodeUrl *string `json:"code_url,omitempty"`
|
||||
}
|
||||
|
||||
type CommonAmountInfo struct {
|
||||
Total *int64 `json:"total,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
type CouponInfo struct {
|
||||
CostPrice *int64 `json:"cost_price,omitempty"`
|
||||
InvoiceId *string `json:"invoice_id,omitempty"`
|
||||
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type CommonSceneInfo struct {
|
||||
PayerClientIp *string `json:"payer_client_ip,omitempty"`
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
StoreInfo *StoreInfo `json:"store_info,omitempty"`
|
||||
}
|
||||
|
||||
type SettleInfo struct {
|
||||
ProfitSharing *bool `json:"profit_sharing,omitempty"`
|
||||
}
|
||||
|
||||
type GoodsDetail struct {
|
||||
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
|
||||
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
|
||||
GoodsName *string `json:"goods_name,omitempty"`
|
||||
Quantity *int64 `json:"quantity,omitempty"`
|
||||
UnitPrice *int64 `json:"unit_price,omitempty"`
|
||||
}
|
||||
|
||||
type StoreInfo struct {
|
||||
Id *string `json:"id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
AreaCode *string `json:"area_code,omitempty"`
|
||||
Address *string `json:"address,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &QueryByOutTradeNoRequest{
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
}
|
||||
|
||||
response, err := QueryByOutTradeNo(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func QueryByOutTradeNo(config *wxpay_utility.MchConfig, request *QueryByOutTradeNoRequest) (response *DirectApiv3QueryResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "GET"
|
||||
path = "/v3/pay/transactions/out-trade-no/{out_trade_no}"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_trade_no}", url.PathEscape(*request.OutTradeNo), -1)
|
||||
query := reqUrl.Query()
|
||||
if request.Mchid != nil {
|
||||
query.Add("mchid", *request.Mchid)
|
||||
}
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &DirectApiv3QueryResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type QueryByOutTradeNoRequest struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
}
|
||||
|
||||
func (o *QueryByOutTradeNoRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias QueryByOutTradeNoRequest
|
||||
a := &struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
Mchid: nil,
|
||||
OutTradeNo: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type DirectApiv3QueryResponse struct {
|
||||
Appid *string `json:"appid,omitempty"`
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
TradeType *string `json:"trade_type,omitempty"`
|
||||
TradeState *string `json:"trade_state,omitempty"`
|
||||
TradeStateDesc *string `json:"trade_state_desc,omitempty"`
|
||||
BankType *string `json:"bank_type,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
SuccessTime *string `json:"success_time,omitempty"`
|
||||
Payer *CommRespPayerInfo `json:"payer,omitempty"`
|
||||
Amount *CommRespAmountInfo `json:"amount,omitempty"`
|
||||
SceneInfo *CommRespSceneInfo `json:"scene_info,omitempty"`
|
||||
PromotionDetail []PromotionDetail `json:"promotion_detail,omitempty"`
|
||||
}
|
||||
|
||||
type CommRespPayerInfo struct {
|
||||
Openid *string `json:"openid,omitempty"`
|
||||
}
|
||||
|
||||
type CommRespAmountInfo struct {
|
||||
Total *int64 `json:"total,omitempty"`
|
||||
PayerTotal *int64 `json:"payer_total,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
PayerCurrency *string `json:"payer_currency,omitempty"`
|
||||
}
|
||||
|
||||
type CommRespSceneInfo struct {
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
}
|
||||
|
||||
type PromotionDetail struct {
|
||||
CouponId *string `json:"coupon_id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
Type *string `json:"type,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
StockId *string `json:"stock_id,omitempty"`
|
||||
WechatpayContribute *int64 `json:"wechatpay_contribute,omitempty"`
|
||||
MerchantContribute *int64 `json:"merchant_contribute,omitempty"`
|
||||
OtherContribute *int64 `json:"other_contribute,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
GoodsDetail []GoodsDetailInPromotion `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type GoodsDetailInPromotion struct {
|
||||
GoodsId *string `json:"goods_id,omitempty"`
|
||||
Quantity *int64 `json:"quantity,omitempty"`
|
||||
UnitPrice *int64 `json:"unit_price,omitempty"`
|
||||
DiscountAmount *int64 `json:"discount_amount,omitempty"`
|
||||
GoodsRemark *string `json:"goods_remark,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &QueryByWxTradeNoRequest{
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
TransactionId: wxpay_utility.String("1217752501201407033233368018"),
|
||||
}
|
||||
|
||||
response, err := QueryByWxTradeNo(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
func QueryByWxTradeNo(config *wxpay_utility.MchConfig, request *QueryByWxTradeNoRequest) (response *DirectApiv3QueryResponse, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "GET"
|
||||
path = "/v3/pay/transactions/id/{transaction_id}"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqUrl.Path = strings.Replace(reqUrl.Path, "{transaction_id}", url.PathEscape(*request.TransactionId), -1)
|
||||
query := reqUrl.Query()
|
||||
if request.Mchid != nil {
|
||||
query.Add("mchid", *request.Mchid)
|
||||
}
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &DirectApiv3QueryResponse{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type QueryByWxTradeNoRequest struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
}
|
||||
|
||||
func (o *QueryByWxTradeNoRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias QueryByWxTradeNoRequest
|
||||
a := &struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
Mchid: nil,
|
||||
TransactionId: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type DirectApiv3QueryResponse struct {
|
||||
Appid *string `json:"appid,omitempty"`
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
TradeType *string `json:"trade_type,omitempty"`
|
||||
TradeState *string `json:"trade_state,omitempty"`
|
||||
TradeStateDesc *string `json:"trade_state_desc,omitempty"`
|
||||
BankType *string `json:"bank_type,omitempty"`
|
||||
Attach *string `json:"attach,omitempty"`
|
||||
SuccessTime *string `json:"success_time,omitempty"`
|
||||
Payer *CommRespPayerInfo `json:"payer,omitempty"`
|
||||
Amount *CommRespAmountInfo `json:"amount,omitempty"`
|
||||
SceneInfo *CommRespSceneInfo `json:"scene_info,omitempty"`
|
||||
PromotionDetail []PromotionDetail `json:"promotion_detail,omitempty"`
|
||||
}
|
||||
|
||||
type CommRespPayerInfo struct {
|
||||
Openid *string `json:"openid,omitempty"`
|
||||
}
|
||||
|
||||
type CommRespAmountInfo struct {
|
||||
Total *int64 `json:"total,omitempty"`
|
||||
PayerTotal *int64 `json:"payer_total,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
PayerCurrency *string `json:"payer_currency,omitempty"`
|
||||
}
|
||||
|
||||
type CommRespSceneInfo struct {
|
||||
DeviceId *string `json:"device_id,omitempty"`
|
||||
}
|
||||
|
||||
type PromotionDetail struct {
|
||||
CouponId *string `json:"coupon_id,omitempty"`
|
||||
Name *string `json:"name,omitempty"`
|
||||
Scope *string `json:"scope,omitempty"`
|
||||
Type *string `json:"type,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
StockId *string `json:"stock_id,omitempty"`
|
||||
WechatpayContribute *int64 `json:"wechatpay_contribute,omitempty"`
|
||||
MerchantContribute *int64 `json:"merchant_contribute,omitempty"`
|
||||
OtherContribute *int64 `json:"other_contribute,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
GoodsDetail []GoodsDetailInPromotion `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type GoodsDetailInPromotion struct {
|
||||
GoodsId *string `json:"goods_id,omitempty"`
|
||||
Quantity *int64 `json:"quantity,omitempty"`
|
||||
UnitPrice *int64 `json:"unit_price,omitempty"`
|
||||
DiscountAmount *int64 `json:"discount_amount,omitempty"`
|
||||
GoodsRemark *string `json:"goods_remark,omitempty"`
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// 关闭订单
|
||||
//
|
||||
// 未支付状态的订单,可在无需支付时调用此接口关闭订单。常见关单情况包括:
|
||||
// 1. 用户在商户系统提交取消订单请求,商户需执行关单操作。
|
||||
// 2. 订单超时未支付(超出商户系统设定的可支付时间或下单时的time_expire支付截止时间),商户需进行关单处理。
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &CloseOrderRequest{
|
||||
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
Mchid: wxpay_utility.String("1230000109"),
|
||||
}
|
||||
|
||||
err = CloseOrder(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Println("请求成功")
|
||||
}
|
||||
|
||||
// CloseOrder 关闭订单
|
||||
//
|
||||
// 未支付状态的订单,可在无需支付时调用此接口关闭订单。常见关单情况包括:
|
||||
// 1. 用户在商户系统提交取消订单请求,商户需执行关单操作。
|
||||
// 2. 订单超时未支付(超出商户系统设定的可支付时间或下单时的time_expire支付截止时间),商户需进行关单处理。
|
||||
func CloseOrder(config *wxpay_utility.MchConfig, request *CloseOrderRequest) (err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/pay/transactions/out-trade-no/{out_trade_no}/close"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_trade_no}", url.PathEscape(*request.OutTradeNo), -1)
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
} else {
|
||||
return wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type CloseOrderRequest struct {
|
||||
Mchid *string `json:"mchid,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
}
|
||||
|
||||
func (o *CloseOrderRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias CloseOrderRequest
|
||||
a := &struct {
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
OutTradeNo: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
@@ -0,0 +1,269 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &CreateAbnormalRefundRequest{
|
||||
RefundId: wxpay_utility.String("50000000382019052709732678859"),
|
||||
OutRefundNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
Type: ABNORMALRECEIVETYPE_MERCHANT_BANK_CARD.Ptr(),
|
||||
BankType: wxpay_utility.String("ICBC_DEBIT"),
|
||||
BankAccount: wxpay_utility.String("d+xT+MQCvrLHUVDWv/8MR/dB7TkXLVfSrUxMPZy6jWWYzpRrEEaYQE8ZRGYoeorwC+w=="), /*请传入wxpay_utility.EncryptOAEPWithPublicKey 加密结果*/
|
||||
RealName: wxpay_utility.String("UPgQcZSdq3zOayJwZ5XLrHY2dZU1W2Cd"), /*请传入wxpay_utility.EncryptOAEPWithPublicKey 加密结果*/
|
||||
}
|
||||
|
||||
response, err := CreateAbnormalRefund(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
// CreateAbnormalRefund 发起异常退款
|
||||
func CreateAbnormalRefund(config *wxpay_utility.MchConfig, request *CreateAbnormalRefundRequest) (response *Refund, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqUrl.Path = strings.Replace(reqUrl.Path, "{refund_id}", url.PathEscape(*request.RefundId), -1)
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &Refund{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type CreateAbnormalRefundRequest struct {
|
||||
RefundId *string `json:"refund_id,omitempty"`
|
||||
OutRefundNo *string `json:"out_refund_no,omitempty"`
|
||||
Type *AbnormalReceiveType `json:"type,omitempty"`
|
||||
BankType *string `json:"bank_type,omitempty"`
|
||||
BankAccount *string `json:"bank_account,omitempty"`
|
||||
RealName *string `json:"real_name,omitempty"`
|
||||
}
|
||||
|
||||
func (o *CreateAbnormalRefundRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias CreateAbnormalRefundRequest
|
||||
a := &struct {
|
||||
RefundId *string `json:"refund_id,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
RefundId: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type Refund struct {
|
||||
RefundId *string `json:"refund_id,omitempty"`
|
||||
OutRefundNo *string `json:"out_refund_no,omitempty"`
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
Channel *Channel `json:"channel,omitempty"`
|
||||
UserReceivedAccount *string `json:"user_received_account,omitempty"`
|
||||
SuccessTime *time.Time `json:"success_time,omitempty"`
|
||||
CreateTime *time.Time `json:"create_time,omitempty"`
|
||||
Status *Status `json:"status,omitempty"`
|
||||
FundsAccount *FundsAccount `json:"funds_account,omitempty"`
|
||||
Amount *Amount `json:"amount,omitempty"`
|
||||
PromotionDetail []Promotion `json:"promotion_detail,omitempty"`
|
||||
}
|
||||
|
||||
type AbnormalReceiveType string
|
||||
|
||||
func (e AbnormalReceiveType) Ptr() *AbnormalReceiveType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
ABNORMALRECEIVETYPE_USER_BANK_CARD AbnormalReceiveType = "USER_BANK_CARD"
|
||||
ABNORMALRECEIVETYPE_MERCHANT_BANK_CARD AbnormalReceiveType = "MERCHANT_BANK_CARD"
|
||||
)
|
||||
|
||||
type Channel string
|
||||
|
||||
func (e Channel) Ptr() *Channel {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
CHANNEL_ORIGINAL Channel = "ORIGINAL"
|
||||
CHANNEL_BALANCE Channel = "BALANCE"
|
||||
CHANNEL_OTHER_BALANCE Channel = "OTHER_BALANCE"
|
||||
CHANNEL_OTHER_BANKCARD Channel = "OTHER_BANKCARD"
|
||||
)
|
||||
|
||||
type Status string
|
||||
|
||||
func (e Status) Ptr() *Status {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
STATUS_SUCCESS Status = "SUCCESS"
|
||||
STATUS_CLOSED Status = "CLOSED"
|
||||
STATUS_PROCESSING Status = "PROCESSING"
|
||||
STATUS_ABNORMAL Status = "ABNORMAL"
|
||||
)
|
||||
|
||||
type FundsAccount string
|
||||
|
||||
func (e FundsAccount) Ptr() *FundsAccount {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
FUNDSACCOUNT_UNSETTLED FundsAccount = "UNSETTLED"
|
||||
FUNDSACCOUNT_AVAILABLE FundsAccount = "AVAILABLE"
|
||||
FUNDSACCOUNT_UNAVAILABLE FundsAccount = "UNAVAILABLE"
|
||||
FUNDSACCOUNT_OPERATION FundsAccount = "OPERATION"
|
||||
FUNDSACCOUNT_BASIC FundsAccount = "BASIC"
|
||||
FUNDSACCOUNT_ECNY_BASIC FundsAccount = "ECNY_BASIC"
|
||||
)
|
||||
|
||||
type Amount struct {
|
||||
Total *int64 `json:"total,omitempty"`
|
||||
Refund *int64 `json:"refund,omitempty"`
|
||||
From []FundsFromItem `json:"from,omitempty"`
|
||||
PayerTotal *int64 `json:"payer_total,omitempty"`
|
||||
PayerRefund *int64 `json:"payer_refund,omitempty"`
|
||||
SettlementRefund *int64 `json:"settlement_refund,omitempty"`
|
||||
SettlementTotal *int64 `json:"settlement_total,omitempty"`
|
||||
DiscountRefund *int64 `json:"discount_refund,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
RefundFee *int64 `json:"refund_fee,omitempty"`
|
||||
}
|
||||
|
||||
type Promotion struct {
|
||||
PromotionId *string `json:"promotion_id,omitempty"`
|
||||
Scope *PromotionScope `json:"scope,omitempty"`
|
||||
Type *PromotionType `json:"type,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
RefundAmount *int64 `json:"refund_amount,omitempty"`
|
||||
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type FundsFromItem struct {
|
||||
Account *Account `json:"account,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
}
|
||||
|
||||
type PromotionScope string
|
||||
|
||||
func (e PromotionScope) Ptr() *PromotionScope {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
PROMOTIONSCOPE_GLOBAL PromotionScope = "GLOBAL"
|
||||
PROMOTIONSCOPE_SINGLE PromotionScope = "SINGLE"
|
||||
)
|
||||
|
||||
type PromotionType string
|
||||
|
||||
func (e PromotionType) Ptr() *PromotionType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
PROMOTIONTYPE_CASH PromotionType = "CASH"
|
||||
PROMOTIONTYPE_NOCASH PromotionType = "NOCASH"
|
||||
)
|
||||
|
||||
type GoodsDetail struct {
|
||||
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
|
||||
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
|
||||
GoodsName *string `json:"goods_name,omitempty"`
|
||||
UnitPrice *int64 `json:"unit_price,omitempty"`
|
||||
RefundAmount *int64 `json:"refund_amount,omitempty"`
|
||||
RefundQuantity *int64 `json:"refund_quantity,omitempty"`
|
||||
}
|
||||
|
||||
type Account string
|
||||
|
||||
func (e Account) Ptr() *Account {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
ACCOUNT_AVAILABLE Account = "AVAILABLE"
|
||||
ACCOUNT_UNAVAILABLE Account = "UNAVAILABLE"
|
||||
)
|
||||
@@ -0,0 +1,300 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 退款申请
|
||||
//
|
||||
// 支付成功后1年内,可通过此接口将款项全部或部分原路退还给用户(也可在商户平台手动操作)。
|
||||
//
|
||||
// 关键注意:
|
||||
// 1. 一笔订单最多50次部分退款,重试必须用原 out_refund_no,否则会重复退款。
|
||||
// 2. 接口返回成功仅表示受理成功,实际结果以退款回调通知或查询退款接口为准。
|
||||
// 3. 原路退还:银行卡1-3个工作日到账,零钱即时到账。
|
||||
// 4. 有代金券的订单部分退款时,退给用户 = 退款金额 × (实付 ÷ 总额),四舍五入。
|
||||
// 5. 有分账的订单,需确保可用余额充足;部分分账未解冻时需先调"完结分账"。
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &CreateRequest{
|
||||
TransactionId: wxpay_utility.String("1217752501201407033233368018"),
|
||||
OutTradeNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
OutRefundNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
Reason: wxpay_utility.String("商品已售完"),
|
||||
NotifyUrl: wxpay_utility.String("https://weixin.qq.com"),
|
||||
FundsAccount: REQFUNDSACCOUNT_AVAILABLE.Ptr(),
|
||||
Amount: &AmountReq{
|
||||
Refund: wxpay_utility.Int64(888),
|
||||
From: []FundsFromItem{FundsFromItem{
|
||||
Account: ACCOUNT_AVAILABLE.Ptr(),
|
||||
Amount: wxpay_utility.Int64(444),
|
||||
}},
|
||||
Total: wxpay_utility.Int64(888),
|
||||
Currency: wxpay_utility.String("CNY"),
|
||||
},
|
||||
GoodsDetail: []GoodsDetail{GoodsDetail{
|
||||
MerchantGoodsId: wxpay_utility.String("1217752501201407033233368018"),
|
||||
WechatpayGoodsId: wxpay_utility.String("1001"),
|
||||
GoodsName: wxpay_utility.String("iPhone6s 16G"),
|
||||
UnitPrice: wxpay_utility.Int64(528800),
|
||||
RefundAmount: wxpay_utility.Int64(528800),
|
||||
RefundQuantity: wxpay_utility.Int64(1),
|
||||
}},
|
||||
}
|
||||
|
||||
response, err := CreateRefund(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
// CreateRefund 退款申请
|
||||
//
|
||||
// 在交易完成后的一年内(以支付成功时间为起点+365天计算),若因用户或商户方面导致需进行订单退款,
|
||||
// 商户可通过此接口将支付金额的全部或部分原路退还至用户。
|
||||
//
|
||||
// 注意:
|
||||
// 1. 一笔订单最多支持50次部分退款(若需多次部分退款,请更换商户退款单号并间隔1分钟后再次调用)。
|
||||
// 2. 在申请退款失败后进行重试时,请务必使用原商户退款单号,以避免因重复退款而导致的资金损失。
|
||||
// 3. 同一商户号下,此接口调用成功的频率限制为150QPS,而调用失败报错时的频率限制为6QPS。
|
||||
// 4. 申请退款接口返回成功仅表示退款单已受理成功,具体的退款结果需依据退款结果通知及查询退款的返回信息为准。
|
||||
// 5. 若一个月前的订单申请退款时返回报错"频率限制,1个月之前的订单请降低申请频率再重试",请调整退款时间,再使用原参数进行重试。
|
||||
func CreateRefund(config *wxpay_utility.MchConfig, request *CreateRequest) (response *Refund, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "POST"
|
||||
path = "/v3/refund/domestic/refunds"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqBody, err := json.Marshal(request)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
httpRequest.Header.Set("Content-Type", "application/json")
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), reqBody)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &Refund{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type CreateRequest struct {
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
OutRefundNo *string `json:"out_refund_no,omitempty"`
|
||||
Reason *string `json:"reason,omitempty"`
|
||||
NotifyUrl *string `json:"notify_url,omitempty"`
|
||||
FundsAccount *ReqFundsAccount `json:"funds_account,omitempty"`
|
||||
Amount *AmountReq `json:"amount,omitempty"`
|
||||
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type Refund struct {
|
||||
RefundId *string `json:"refund_id,omitempty"`
|
||||
OutRefundNo *string `json:"out_refund_no,omitempty"`
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
Channel *Channel `json:"channel,omitempty"`
|
||||
UserReceivedAccount *string `json:"user_received_account,omitempty"`
|
||||
SuccessTime *time.Time `json:"success_time,omitempty"`
|
||||
CreateTime *time.Time `json:"create_time,omitempty"`
|
||||
Status *Status `json:"status,omitempty"`
|
||||
FundsAccount *FundsAccount `json:"funds_account,omitempty"`
|
||||
Amount *Amount `json:"amount,omitempty"`
|
||||
PromotionDetail []Promotion `json:"promotion_detail,omitempty"`
|
||||
}
|
||||
|
||||
type ReqFundsAccount string
|
||||
|
||||
func (e ReqFundsAccount) Ptr() *ReqFundsAccount {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
REQFUNDSACCOUNT_AVAILABLE ReqFundsAccount = "AVAILABLE"
|
||||
REQFUNDSACCOUNT_UNSETTLED ReqFundsAccount = "UNSETTLED"
|
||||
)
|
||||
|
||||
type AmountReq struct {
|
||||
Refund *int64 `json:"refund,omitempty"`
|
||||
From []FundsFromItem `json:"from,omitempty"`
|
||||
Total *int64 `json:"total,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
}
|
||||
|
||||
type GoodsDetail struct {
|
||||
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
|
||||
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
|
||||
GoodsName *string `json:"goods_name,omitempty"`
|
||||
UnitPrice *int64 `json:"unit_price,omitempty"`
|
||||
RefundAmount *int64 `json:"refund_amount,omitempty"`
|
||||
RefundQuantity *int64 `json:"refund_quantity,omitempty"`
|
||||
}
|
||||
|
||||
type Channel string
|
||||
|
||||
func (e Channel) Ptr() *Channel {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
CHANNEL_ORIGINAL Channel = "ORIGINAL"
|
||||
CHANNEL_BALANCE Channel = "BALANCE"
|
||||
CHANNEL_OTHER_BALANCE Channel = "OTHER_BALANCE"
|
||||
CHANNEL_OTHER_BANKCARD Channel = "OTHER_BANKCARD"
|
||||
)
|
||||
|
||||
type Status string
|
||||
|
||||
func (e Status) Ptr() *Status {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
STATUS_SUCCESS Status = "SUCCESS"
|
||||
STATUS_CLOSED Status = "CLOSED"
|
||||
STATUS_PROCESSING Status = "PROCESSING"
|
||||
STATUS_ABNORMAL Status = "ABNORMAL"
|
||||
)
|
||||
|
||||
type FundsAccount string
|
||||
|
||||
func (e FundsAccount) Ptr() *FundsAccount {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
FUNDSACCOUNT_UNSETTLED FundsAccount = "UNSETTLED"
|
||||
FUNDSACCOUNT_AVAILABLE FundsAccount = "AVAILABLE"
|
||||
FUNDSACCOUNT_UNAVAILABLE FundsAccount = "UNAVAILABLE"
|
||||
FUNDSACCOUNT_OPERATION FundsAccount = "OPERATION"
|
||||
FUNDSACCOUNT_BASIC FundsAccount = "BASIC"
|
||||
FUNDSACCOUNT_ECNY_BASIC FundsAccount = "ECNY_BASIC"
|
||||
)
|
||||
|
||||
type Amount struct {
|
||||
Total *int64 `json:"total,omitempty"`
|
||||
Refund *int64 `json:"refund,omitempty"`
|
||||
From []FundsFromItem `json:"from,omitempty"`
|
||||
PayerTotal *int64 `json:"payer_total,omitempty"`
|
||||
PayerRefund *int64 `json:"payer_refund,omitempty"`
|
||||
SettlementRefund *int64 `json:"settlement_refund,omitempty"`
|
||||
SettlementTotal *int64 `json:"settlement_total,omitempty"`
|
||||
DiscountRefund *int64 `json:"discount_refund,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
RefundFee *int64 `json:"refund_fee,omitempty"`
|
||||
}
|
||||
|
||||
type Promotion struct {
|
||||
PromotionId *string `json:"promotion_id,omitempty"`
|
||||
Scope *PromotionScope `json:"scope,omitempty"`
|
||||
Type *PromotionType `json:"type,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
RefundAmount *int64 `json:"refund_amount,omitempty"`
|
||||
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type FundsFromItem struct {
|
||||
Account *Account `json:"account,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
}
|
||||
|
||||
type PromotionScope string
|
||||
|
||||
func (e PromotionScope) Ptr() *PromotionScope {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
PROMOTIONSCOPE_GLOBAL PromotionScope = "GLOBAL"
|
||||
PROMOTIONSCOPE_SINGLE PromotionScope = "SINGLE"
|
||||
)
|
||||
|
||||
type PromotionType string
|
||||
|
||||
func (e PromotionType) Ptr() *PromotionType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
PROMOTIONTYPE_CASH PromotionType = "CASH"
|
||||
PROMOTIONTYPE_NOCASH PromotionType = "NOCASH"
|
||||
)
|
||||
|
||||
type Account string
|
||||
|
||||
func (e Account) Ptr() *Account {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
ACCOUNT_AVAILABLE Account = "AVAILABLE"
|
||||
ACCOUNT_UNAVAILABLE Account = "UNAVAILABLE"
|
||||
)
|
||||
@@ -0,0 +1,251 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"demo/wxpay_utility" // 引用微信支付工具库,参考 https://pay.weixin.qq.com/doc/v3/merchant/4015119334
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
config, err := wxpay_utility.CreateMchConfig(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem", // 微信支付公钥文件路径,本地文件路径
|
||||
)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
request := &QueryByOutRefundNoRequest{
|
||||
OutRefundNo: wxpay_utility.String("1217752501201407033233368018"),
|
||||
}
|
||||
|
||||
response, err := QueryByOutRefundNo(config, request)
|
||||
if err != nil {
|
||||
fmt.Printf("请求失败: %+v\n", err)
|
||||
// TODO: 请求失败,根据状态码执行不同的处理
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
fmt.Printf("请求成功: %+v\n", response)
|
||||
}
|
||||
|
||||
// QueryByOutRefundNo 查询单笔退款(通过商户退款单号)
|
||||
//
|
||||
// 提交退款申请后,推荐每间隔1分钟调用该接口查询一次退款状态,若超过5分钟仍是退款处理中状态,
|
||||
// 建议开始逐步衰减查询频率(比如之后间隔5分钟、10分钟、20分钟、30分钟……查询一次)。
|
||||
//
|
||||
// 退款有一定延时,零钱支付的订单退款一般5分钟内到账,银行卡支付的订单退款一般1-3个工作日到账。
|
||||
//
|
||||
// 同一商户号查询退款频率限制为300qps,如返回FREQUENCY_LIMITED频率限制报错可间隔1分钟再重试查询。
|
||||
func QueryByOutRefundNo(config *wxpay_utility.MchConfig, request *QueryByOutRefundNoRequest) (response *Refund, err error) {
|
||||
const (
|
||||
host = "https://api.mch.weixin.qq.com"
|
||||
method = "GET"
|
||||
path = "/v3/refund/domestic/refunds/{out_refund_no}"
|
||||
)
|
||||
|
||||
reqUrl, err := url.Parse(fmt.Sprintf("%s%s", host, path))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
reqUrl.Path = strings.Replace(reqUrl.Path, "{out_refund_no}", url.PathEscape(*request.OutRefundNo), -1)
|
||||
query := reqUrl.Query()
|
||||
reqUrl.RawQuery = query.Encode()
|
||||
httpRequest, err := http.NewRequest(method, reqUrl.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Accept", "application/json")
|
||||
httpRequest.Header.Set("Wechatpay-Serial", config.WechatPayPublicKeyId())
|
||||
authorization, err := wxpay_utility.BuildAuthorization(config.MchId(), config.CertificateSerialNo(), config.PrivateKey(), method, reqUrl.RequestURI(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
httpRequest.Header.Set("Authorization", authorization)
|
||||
|
||||
client := &http.Client{}
|
||||
httpResponse, err := client.Do(httpRequest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
respBody, err := wxpay_utility.ExtractResponseBody(httpResponse)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if httpResponse.StatusCode >= 200 && httpResponse.StatusCode < 300 {
|
||||
// 2XX 成功,验证应答签名
|
||||
err = wxpay_utility.ValidateResponse(
|
||||
config.WechatPayPublicKeyId(),
|
||||
config.WechatPayPublicKey(),
|
||||
&httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
response := &Refund{}
|
||||
if err := json.Unmarshal(respBody, response); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return response, nil
|
||||
} else {
|
||||
return nil, wxpay_utility.NewApiException(
|
||||
httpResponse.StatusCode,
|
||||
httpResponse.Header,
|
||||
respBody,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
type QueryByOutRefundNoRequest struct {
|
||||
OutRefundNo *string `json:"out_refund_no,omitempty"`
|
||||
}
|
||||
|
||||
func (o *QueryByOutRefundNoRequest) MarshalJSON() ([]byte, error) {
|
||||
type Alias QueryByOutRefundNoRequest
|
||||
a := &struct {
|
||||
OutRefundNo *string `json:"out_refund_no,omitempty"`
|
||||
*Alias
|
||||
}{
|
||||
// 序列化时移除非 Body 字段
|
||||
OutRefundNo: nil,
|
||||
Alias: (*Alias)(o),
|
||||
}
|
||||
return json.Marshal(a)
|
||||
}
|
||||
|
||||
type Refund struct {
|
||||
RefundId *string `json:"refund_id,omitempty"`
|
||||
OutRefundNo *string `json:"out_refund_no,omitempty"`
|
||||
TransactionId *string `json:"transaction_id,omitempty"`
|
||||
OutTradeNo *string `json:"out_trade_no,omitempty"`
|
||||
Channel *Channel `json:"channel,omitempty"`
|
||||
UserReceivedAccount *string `json:"user_received_account,omitempty"`
|
||||
SuccessTime *time.Time `json:"success_time,omitempty"`
|
||||
CreateTime *time.Time `json:"create_time,omitempty"`
|
||||
Status *Status `json:"status,omitempty"`
|
||||
FundsAccount *FundsAccount `json:"funds_account,omitempty"`
|
||||
Amount *Amount `json:"amount,omitempty"`
|
||||
PromotionDetail []Promotion `json:"promotion_detail,omitempty"`
|
||||
}
|
||||
|
||||
type Channel string
|
||||
|
||||
func (e Channel) Ptr() *Channel {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
CHANNEL_ORIGINAL Channel = "ORIGINAL"
|
||||
CHANNEL_BALANCE Channel = "BALANCE"
|
||||
CHANNEL_OTHER_BALANCE Channel = "OTHER_BALANCE"
|
||||
CHANNEL_OTHER_BANKCARD Channel = "OTHER_BANKCARD"
|
||||
)
|
||||
|
||||
type Status string
|
||||
|
||||
func (e Status) Ptr() *Status {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
STATUS_SUCCESS Status = "SUCCESS"
|
||||
STATUS_CLOSED Status = "CLOSED"
|
||||
STATUS_PROCESSING Status = "PROCESSING"
|
||||
STATUS_ABNORMAL Status = "ABNORMAL"
|
||||
)
|
||||
|
||||
type FundsAccount string
|
||||
|
||||
func (e FundsAccount) Ptr() *FundsAccount {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
FUNDSACCOUNT_UNSETTLED FundsAccount = "UNSETTLED"
|
||||
FUNDSACCOUNT_AVAILABLE FundsAccount = "AVAILABLE"
|
||||
FUNDSACCOUNT_UNAVAILABLE FundsAccount = "UNAVAILABLE"
|
||||
FUNDSACCOUNT_OPERATION FundsAccount = "OPERATION"
|
||||
FUNDSACCOUNT_BASIC FundsAccount = "BASIC"
|
||||
FUNDSACCOUNT_ECNY_BASIC FundsAccount = "ECNY_BASIC"
|
||||
)
|
||||
|
||||
type Amount struct {
|
||||
Total *int64 `json:"total,omitempty"`
|
||||
Refund *int64 `json:"refund,omitempty"`
|
||||
From []FundsFromItem `json:"from,omitempty"`
|
||||
PayerTotal *int64 `json:"payer_total,omitempty"`
|
||||
PayerRefund *int64 `json:"payer_refund,omitempty"`
|
||||
SettlementRefund *int64 `json:"settlement_refund,omitempty"`
|
||||
SettlementTotal *int64 `json:"settlement_total,omitempty"`
|
||||
DiscountRefund *int64 `json:"discount_refund,omitempty"`
|
||||
Currency *string `json:"currency,omitempty"`
|
||||
RefundFee *int64 `json:"refund_fee,omitempty"`
|
||||
}
|
||||
|
||||
type Promotion struct {
|
||||
PromotionId *string `json:"promotion_id,omitempty"`
|
||||
Scope *PromotionScope `json:"scope,omitempty"`
|
||||
Type *PromotionType `json:"type,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
RefundAmount *int64 `json:"refund_amount,omitempty"`
|
||||
GoodsDetail []GoodsDetail `json:"goods_detail,omitempty"`
|
||||
}
|
||||
|
||||
type FundsFromItem struct {
|
||||
Account *Account `json:"account,omitempty"`
|
||||
Amount *int64 `json:"amount,omitempty"`
|
||||
}
|
||||
|
||||
type PromotionScope string
|
||||
|
||||
func (e PromotionScope) Ptr() *PromotionScope {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
PROMOTIONSCOPE_GLOBAL PromotionScope = "GLOBAL"
|
||||
PROMOTIONSCOPE_SINGLE PromotionScope = "SINGLE"
|
||||
)
|
||||
|
||||
type PromotionType string
|
||||
|
||||
func (e PromotionType) Ptr() *PromotionType {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
PROMOTIONTYPE_CASH PromotionType = "CASH"
|
||||
PROMOTIONTYPE_NOCASH PromotionType = "NOCASH"
|
||||
)
|
||||
|
||||
type GoodsDetail struct {
|
||||
MerchantGoodsId *string `json:"merchant_goods_id,omitempty"`
|
||||
WechatpayGoodsId *string `json:"wechatpay_goods_id,omitempty"`
|
||||
GoodsName *string `json:"goods_name,omitempty"`
|
||||
UnitPrice *int64 `json:"unit_price,omitempty"`
|
||||
RefundAmount *int64 `json:"refund_amount,omitempty"`
|
||||
RefundQuantity *int64 `json:"refund_quantity,omitempty"`
|
||||
}
|
||||
|
||||
type Account string
|
||||
|
||||
func (e Account) Ptr() *Account {
|
||||
return &e
|
||||
}
|
||||
|
||||
const (
|
||||
ACCOUNT_AVAILABLE Account = "AVAILABLE"
|
||||
ACCOUNT_UNAVAILABLE Account = "UNAVAILABLE"
|
||||
)
|
||||
@@ -0,0 +1,39 @@
|
||||
# JSAPI调起支付
|
||||
|
||||
## 说明
|
||||
|
||||
商户通过JSAPI/小程序下单接口获取到发起支付的必要参数 `prepay_id` 后,再通过微信浏览器内置对象方法(WeixinJSBridge)调起微信支付收银台。
|
||||
|
||||
## 示例代码
|
||||
|
||||
```javascript
|
||||
function onBridgeReady() {
|
||||
WeixinJSBridge.invoke('getBrandWCPayRequest', {
|
||||
"appId": "wx2421b1c4370ec43b", //公众号ID,由商户传入
|
||||
"timeStamp": "1395712654", //时间戳,自1970年以来的秒数
|
||||
"nonceStr": "e61463f8efa94090b1f366cccfbbb444", //随机串
|
||||
"package": "prepay_id=wx21201855730335ac86f8c43d1889123400",
|
||||
"signType": "RSA", //微信签名方式
|
||||
"paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg=="
|
||||
},
|
||||
function(res) {
|
||||
if (res.err_msg == "get_brand_wcpay_request:ok") {
|
||||
// 使用以上方式判断前端返回,微信团队郑重提示:
|
||||
// res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠,
|
||||
// 商户需进一步调用后端查单确认支付结果。
|
||||
}
|
||||
});
|
||||
}
|
||||
if (typeof WeixinJSBridge == "undefined") {
|
||||
if (document.addEventListener) {
|
||||
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
|
||||
} else if (document.attachEvent) {
|
||||
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
|
||||
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
|
||||
}
|
||||
} else {
|
||||
onBridgeReady();
|
||||
}
|
||||
```
|
||||
|
||||
> **重要**:`res.err_msg` 将在用户支付成功后返回 ok,但并不保证它绝对可靠,商户需进一步调用后端查单确认支付结果。
|
||||
@@ -0,0 +1,254 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* JSAPI下单
|
||||
*/
|
||||
public class JsapiPrepay {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/pay/transactions/jsapi";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
JsapiPrepay client = new JsapiPrepay(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
DirectAPIv3JsapiPrepayRequest request = new DirectAPIv3JsapiPrepayRequest();
|
||||
request.appid = "wxd678efh567hg6787";
|
||||
request.mchid = "1230000109";
|
||||
request.description = "Image形象店-深圳腾大-QQ公仔";
|
||||
request.outTradeNo = "1217752501201407033233368018";
|
||||
request.timeExpire = "2018-06-08T10:34:56+08:00";
|
||||
request.attach = "自定义数据说明";
|
||||
request.notifyUrl = " https://www.weixin.qq.com/wxpay/pay.php";
|
||||
request.goodsTag = "WXG";
|
||||
request.supportFapiao = false;
|
||||
request.amount = new CommonAmountInfo();
|
||||
request.amount.total = 100L;
|
||||
request.amount.currency = "CNY";
|
||||
request.payer = new JsapiReqPayerInfo();
|
||||
request.payer.openid = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o";
|
||||
request.detail = new CouponInfo();
|
||||
request.detail.costPrice = 608800L;
|
||||
request.detail.invoiceId = "微信123";
|
||||
request.detail.goodsDetail = new ArrayList<>();
|
||||
{
|
||||
GoodsDetail goodsDetailItem = new GoodsDetail();
|
||||
goodsDetailItem.merchantGoodsId = "1246464644";
|
||||
goodsDetailItem.wechatpayGoodsId = "1001";
|
||||
goodsDetailItem.goodsName = "iPhoneX 256G";
|
||||
goodsDetailItem.quantity = 1L;
|
||||
goodsDetailItem.unitPrice = 528800L;
|
||||
request.detail.goodsDetail.add(goodsDetailItem);
|
||||
};
|
||||
request.sceneInfo = new CommonSceneInfo();
|
||||
request.sceneInfo.payerClientIp = "14.23.150.211";
|
||||
request.sceneInfo.deviceId = "013467007045764";
|
||||
request.sceneInfo.storeInfo = new StoreInfo();
|
||||
request.sceneInfo.storeInfo.id = "0001";
|
||||
request.sceneInfo.storeInfo.name = "腾讯大厦分店";
|
||||
request.sceneInfo.storeInfo.areaCode = "440305";
|
||||
request.sceneInfo.storeInfo.address = "广东省深圳市南山区科技中一道10000号";
|
||||
request.settleInfo = new SettleInfo();
|
||||
request.settleInfo.profitSharing = false;
|
||||
try {
|
||||
DirectAPIv3JsapiPrepayResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public DirectAPIv3JsapiPrepayResponse run(DirectAPIv3JsapiPrepayRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, DirectAPIv3JsapiPrepayResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public JsapiPrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class DirectAPIv3JsapiPrepayRequest {
|
||||
@SerializedName("appid")
|
||||
public String appid;
|
||||
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("time_expire")
|
||||
public String timeExpire;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("notify_url")
|
||||
public String notifyUrl;
|
||||
|
||||
@SerializedName("goods_tag")
|
||||
public String goodsTag;
|
||||
|
||||
@SerializedName("support_fapiao")
|
||||
public Boolean supportFapiao;
|
||||
|
||||
@SerializedName("amount")
|
||||
public CommonAmountInfo amount;
|
||||
|
||||
@SerializedName("payer")
|
||||
public JsapiReqPayerInfo payer;
|
||||
|
||||
@SerializedName("detail")
|
||||
public CouponInfo detail;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public CommonSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("settle_info")
|
||||
public SettleInfo settleInfo;
|
||||
}
|
||||
|
||||
public static class DirectAPIv3JsapiPrepayResponse {
|
||||
@SerializedName("prepay_id")
|
||||
public String prepayId;
|
||||
}
|
||||
|
||||
public static class CommonAmountInfo {
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
}
|
||||
|
||||
public static class JsapiReqPayerInfo {
|
||||
@SerializedName("openid")
|
||||
public String openid;
|
||||
}
|
||||
|
||||
public static class CouponInfo {
|
||||
@SerializedName("cost_price")
|
||||
public Long costPrice;
|
||||
|
||||
@SerializedName("invoice_id")
|
||||
public String invoiceId;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetail> goodsDetail;
|
||||
}
|
||||
|
||||
public static class CommonSceneInfo {
|
||||
@SerializedName("payer_client_ip")
|
||||
public String payerClientIp;
|
||||
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
|
||||
@SerializedName("store_info")
|
||||
public StoreInfo storeInfo;
|
||||
}
|
||||
|
||||
public static class SettleInfo {
|
||||
@SerializedName("profit_sharing")
|
||||
public Boolean profitSharing;
|
||||
}
|
||||
|
||||
public static class GoodsDetail {
|
||||
@SerializedName("merchant_goods_id")
|
||||
public String merchantGoodsId;
|
||||
|
||||
@SerializedName("wechatpay_goods_id")
|
||||
public String wechatpayGoodsId;
|
||||
|
||||
@SerializedName("goods_name")
|
||||
public String goodsName;
|
||||
|
||||
@SerializedName("quantity")
|
||||
public Long quantity;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
}
|
||||
|
||||
public static class StoreInfo {
|
||||
@SerializedName("id")
|
||||
public String id;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("area_code")
|
||||
public String areaCode;
|
||||
|
||||
@SerializedName("address")
|
||||
public String address;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# 退款结果回调通知
|
||||
|
||||
> 参考官方文档:https://pay.weixin.qq.com/doc/v3/merchant/4012791865
|
||||
|
||||
## 回调描述
|
||||
|
||||
用户支付完成后,商户可根据需求发起退款。当退款单状态发生变更时(变更为退款成功/退款关闭/退款异常),微信支付会通过POST的请求方式,向商户预先设置的退款回调地址(申请退款传入的 notify_url)发送回调通知,让商户知晓退款单的处理结果。
|
||||
|
||||
> **注意**:商户侧对微信支付回调IP有防火墙策略限制的,需要对微信回调IP段开通白名单,详情参考回调处理逻辑注意事项。
|
||||
|
||||
## 回调报文格式
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "EV-2018022511223320873",
|
||||
"create_time": "2018-06-08T10:34:56+08:00",
|
||||
"resource_type": "encrypt-resource",
|
||||
"event_type": "REFUND.SUCCESS",
|
||||
"summary": "退款成功",
|
||||
"resource": {
|
||||
"algorithm": "AEAD_AES_256_GCM",
|
||||
"original_type": "refund",
|
||||
"ciphertext": "...",
|
||||
"nonce": "...",
|
||||
"associated_data": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 关键字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `event_type` | 退款事件类型:`REFUND.SUCCESS`(退款成功)/ `REFUND.ABNORMAL`(退款异常)/ `REFUND.CLOSED`(退款关闭) |
|
||||
| `resource.original_type` | 固定为 `refund` |
|
||||
| `resource.algorithm` | 加密算法,固定为 `AEAD_AES_256_GCM` |
|
||||
| `resource.ciphertext` | 密文,需使用商户APIv3密钥解密后得到退款详情 |
|
||||
| `resource.associated_data` | 附加数据,解密时作为 AAD 参数 |
|
||||
| `resource.nonce` | 随机串,解密时作为 Nonce 参数 |
|
||||
|
||||
## 解密后的退款详情(resource.ciphertext 解密结果)
|
||||
|
||||
```json
|
||||
{
|
||||
"mchid": "1900000100",
|
||||
"transaction_id": "1008450740201411110005820873",
|
||||
"out_trade_no": "20150806125346",
|
||||
"refund_id": "50200207182018070300011301001",
|
||||
"out_refund_no": "7752501201407033233368018",
|
||||
"refund_status": "SUCCESS",
|
||||
"success_time": "2018-06-08T10:34:56+08:00",
|
||||
"user_received_account": "招商银行信用卡0403",
|
||||
"amount": {
|
||||
"total": 999,
|
||||
"refund": 999,
|
||||
"payer_total": 999,
|
||||
"payer_refund": 999
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `refund_status` | 退款状态:`SUCCESS`(成功)/ `CLOSED`(关闭)/ `PROCESSING`(处理中)/ `ABNORMAL`(异常) |
|
||||
| `user_received_account` | 退款入账账户,如"招商银行信用卡0403"、"支付用户零钱"等 |
|
||||
| `amount.total` | 原订单金额(分) |
|
||||
| `amount.refund` | 退款金额(分) |
|
||||
| `amount.payer_total` | 用户实际支付金额(分) |
|
||||
| `amount.payer_refund` | 用户实际收到的退款金额(分) |
|
||||
|
||||
## 处理要求
|
||||
|
||||
1. 接收到回调后,先验证请求签名(使用微信支付公钥验签)
|
||||
2. 使用商户APIv3密钥 + AEAD_AES_256_GCM 解密 `resource.ciphertext` 得到退款明文
|
||||
3. 验签通过:返回 HTTP 200 或 204,无需返回应答报文
|
||||
4. 验签不通过:返回 HTTP 4XX/5XX + `{"code": "FAIL", "message": "失败"}`
|
||||
5. 应答后再异步处理业务逻辑,避免超时
|
||||
|
||||
## 重试机制
|
||||
|
||||
若商户应答失败或超时(5秒),微信支付按以下频次重试:
|
||||
15s / 15s / 30s / 3m / 10m / 20m / 30m / 30m / 30m / 60m / 3h / 3h / 3h / 6h / 6h(最多15次)
|
||||
|
||||
> **重要**:商户系统不能仅依赖回调通知获取结果,需结合查询退款接口使用,避免遗漏或延迟问题。收到重复通知时请做好幂等处理并持续应答 200。
|
||||
@@ -0,0 +1,154 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 申请交易账单API
|
||||
*
|
||||
* 关键注意:
|
||||
* 1. 次日10点后拉取,API仅支持3个月内单日账单,更早的需在商户平台下载。
|
||||
* 2. 返回的是下载链接(download_url),需二次请求下载(gzip压缩CSV)。
|
||||
* 3. 账单金额单位为"元",与下单API的"分"不同,对账时注意转换。
|
||||
*/
|
||||
public class GetTradeBill {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "GET";
|
||||
private static String PATH = "/v3/bill/tradebill";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
GetTradeBill client = new GetTradeBill(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
GetTradeBillRequest request = new GetTradeBillRequest();
|
||||
request.billDate = "2019-06-11";
|
||||
request.billType = BillType.ALL;
|
||||
request.tarType = TarType.GZIP;
|
||||
try {
|
||||
QueryBillEntity response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public QueryBillEntity run(GetTradeBillRequest request) {
|
||||
String uri = PATH;
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("bill_date", request.billDate);
|
||||
args.put("bill_type", request.billType);
|
||||
args.put("tar_type", request.tarType);
|
||||
String queryString = WXPayUtility.urlEncode(args);
|
||||
if (!queryString.isEmpty()) {
|
||||
uri = uri + "?" + queryString;
|
||||
}
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, QueryBillEntity.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public GetTradeBill(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class GetTradeBillRequest {
|
||||
@SerializedName("bill_date")
|
||||
@Expose(serialize = false)
|
||||
public String billDate;
|
||||
|
||||
@SerializedName("bill_type")
|
||||
@Expose(serialize = false)
|
||||
public BillType billType;
|
||||
|
||||
@SerializedName("tar_type")
|
||||
@Expose(serialize = false)
|
||||
public TarType tarType;
|
||||
}
|
||||
|
||||
public static class QueryBillEntity {
|
||||
@SerializedName("hash_type")
|
||||
public HashType hashType;
|
||||
|
||||
@SerializedName("hash_value")
|
||||
public String hashValue;
|
||||
|
||||
@SerializedName("download_url")
|
||||
public String downloadUrl;
|
||||
}
|
||||
|
||||
public enum BillType {
|
||||
@SerializedName("ALL")
|
||||
ALL,
|
||||
@SerializedName("SUCCESS")
|
||||
SUCCESS,
|
||||
@SerializedName("REFUND")
|
||||
REFUND
|
||||
}
|
||||
|
||||
public enum TarType {
|
||||
@SerializedName("GZIP")
|
||||
GZIP
|
||||
}
|
||||
|
||||
public enum HashType {
|
||||
@SerializedName("SHA1")
|
||||
SHA1
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 申请资金账单API
|
||||
*/
|
||||
public class GetFundFlowBill {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "GET";
|
||||
private static String PATH = "/v3/bill/fundflowbill";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
GetFundFlowBill client = new GetFundFlowBill(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
GetFundFlowBillRequest request = new GetFundFlowBillRequest();
|
||||
request.billDate = "2019-06-11";
|
||||
request.accountType = FundFlowBillAccountType.BASIC;
|
||||
request.tarType = TarType.GZIP;
|
||||
try {
|
||||
QueryBillEntity response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public QueryBillEntity run(GetFundFlowBillRequest request) {
|
||||
String uri = PATH;
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("bill_date", request.billDate);
|
||||
args.put("account_type", request.accountType);
|
||||
args.put("tar_type", request.tarType);
|
||||
String queryString = WXPayUtility.urlEncode(args);
|
||||
if (!queryString.isEmpty()) {
|
||||
uri = uri + "?" + queryString;
|
||||
}
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, QueryBillEntity.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public GetFundFlowBill(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class GetFundFlowBillRequest {
|
||||
@SerializedName("bill_date")
|
||||
@Expose(serialize = false)
|
||||
public String billDate;
|
||||
|
||||
@SerializedName("account_type")
|
||||
@Expose(serialize = false)
|
||||
public FundFlowBillAccountType accountType;
|
||||
|
||||
@SerializedName("tar_type")
|
||||
@Expose(serialize = false)
|
||||
public TarType tarType;
|
||||
}
|
||||
|
||||
public static class QueryBillEntity {
|
||||
@SerializedName("hash_type")
|
||||
public HashType hashType;
|
||||
|
||||
@SerializedName("hash_value")
|
||||
public String hashValue;
|
||||
|
||||
@SerializedName("download_url")
|
||||
public String downloadUrl;
|
||||
}
|
||||
|
||||
public enum FundFlowBillAccountType {
|
||||
@SerializedName("BASIC")
|
||||
BASIC,
|
||||
@SerializedName("OPERATION")
|
||||
OPERATION,
|
||||
@SerializedName("FEES")
|
||||
FEES
|
||||
}
|
||||
|
||||
public enum TarType {
|
||||
@SerializedName("GZIP")
|
||||
GZIP
|
||||
}
|
||||
|
||||
public enum HashType {
|
||||
@SerializedName("SHA1")
|
||||
SHA1
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库 参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import okhttp3.Response;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import org.apache.commons.codec.digest.DigestUtils;
|
||||
import java.util.zip.GZIPInputStream;
|
||||
|
||||
/**
|
||||
* 下载账单
|
||||
*
|
||||
* 当商户调用申请交易账单/申请资金账单接口,获取到下载账单链接download_url后,
|
||||
* 需按照V3接口规则生成签名,然后请求下载账单链接download_url获取对应的账单文件。
|
||||
*/
|
||||
public class DownloadBill {
|
||||
private static String METHOD = "GET";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
DownloadBill client = new DownloadBill(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
DownloadBillRequest request = new DownloadBillRequest();
|
||||
request.downloadUrl = "https://api.mch.weixin.qq.com/v3/billdownload/file?token=xxx";
|
||||
request.localFilePath = "downloaded_bill.csv";
|
||||
request.expectedHashType = HashType.SHA1;
|
||||
request.expectedHashValue = "79bb0f45fc4c42234a918000b2668d689e2bde04";
|
||||
request.tarType = TarType.GZIP;
|
||||
try {
|
||||
client.run(request);
|
||||
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println("File downloaded successfully! Local file path: " + request.localFilePath);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void run(DownloadBillRequest request) {
|
||||
Request.Builder reqBuilder = new Request.Builder().url(request.downloadUrl);
|
||||
|
||||
String uri = getPathQueryFromUrl(request.downloadUrl);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization",
|
||||
WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
if (httpResponse.code() < 200 || httpResponse.code() > 300) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
|
||||
// 2XX 成功,流式下载文件
|
||||
ResponseBody body = httpResponse.body();
|
||||
if (body == null) {
|
||||
throw new IOException("Response body is null");
|
||||
}
|
||||
// 读取流
|
||||
try (InputStream inputStream = (request.tarType == DownloadBill.TarType.GZIP)
|
||||
? new GZIPInputStream(body.byteStream())
|
||||
: body.byteStream();
|
||||
FileOutputStream outputStream = new FileOutputStream(request.localFilePath)) {
|
||||
|
||||
byte[] buffer = new byte[8096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
outputStream.write(buffer, 0, bytesRead);
|
||||
}
|
||||
outputStream.flush();
|
||||
}
|
||||
// 下载成功后校验文件SHA1
|
||||
if (request.expectedHashType == HashType.SHA1) {
|
||||
String sha1 = DigestUtils.sha1Hex(new FileInputStream(request.localFilePath));
|
||||
if (!sha1.equals(request.expectedHashValue)) {
|
||||
throw new IOException("SHA1 checksum mismatch");
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getPathQueryFromUrl(String url) {
|
||||
try {
|
||||
URI uri = new URI(url);
|
||||
String path = uri.getRawPath(); // /v3/billdownload/file
|
||||
String query = uri.getRawQuery(); // token=xxx&tartype=gzip
|
||||
return (query == null || query.isEmpty()) ? path : path + "?" + query;
|
||||
} catch (URISyntaxException e) {
|
||||
e.printStackTrace();
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public DownloadBill(String mchid, String certificateSerialNo, String privateKeyFilePath,
|
||||
String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public enum HashType {
|
||||
@SerializedName("SHA1")
|
||||
SHA1
|
||||
}
|
||||
|
||||
public static class DownloadBillRequest {
|
||||
@SerializedName("download_url")
|
||||
@Expose(serialize = false)
|
||||
public String downloadUrl;
|
||||
|
||||
@SerializedName("local_file_path")
|
||||
@Expose(serialize = false)
|
||||
public String localFilePath;
|
||||
|
||||
@SerializedName("expected_hash_type")
|
||||
@Expose(serialize = false)
|
||||
public HashType expectedHashType;
|
||||
|
||||
@SerializedName("expected_hash_value")
|
||||
@Expose(serialize = false)
|
||||
public String expectedHashValue;
|
||||
|
||||
@SerializedName("tar_type")
|
||||
@Expose(serialize = false)
|
||||
public TarType tarType;
|
||||
}
|
||||
|
||||
public enum TarType {
|
||||
@SerializedName("GZIP")
|
||||
GZIP
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package com.java.utils;
|
||||
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
|
||||
/**
|
||||
* 微信支付 HTTP 客户端,封装了请求签名、发送、应答验签的完整流程。
|
||||
* 依赖 WXPayUtility 提供的签名、验签、序列化等基础能力。
|
||||
*/
|
||||
public class WXPayClient {
|
||||
private static final String HOST = "https://api.mch.weixin.qq.com";
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public WXPayClient(String mchid, String certificateSerialNo, String privateKeyFilePath,
|
||||
String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 GET 请求,返回已验签的应答 Body
|
||||
*/
|
||||
public String sendGet(String uri) {
|
||||
return sendRequest("GET", uri, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送 POST 请求,返回已验签的应答 Body
|
||||
*/
|
||||
public String sendPost(String uri, String reqBody) {
|
||||
return sendRequest("POST", uri, reqBody);
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用公钥加密敏感信息
|
||||
*/
|
||||
public String encrypt(String plainText) {
|
||||
return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText);
|
||||
}
|
||||
|
||||
private String sendRequest(String method, String uri, String reqBody) {
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(
|
||||
mchid, certificateSerialNo, privateKey, method, uri, reqBody));
|
||||
|
||||
if (reqBody != null) {
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody body = RequestBody.create(
|
||||
MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(method, body);
|
||||
} else {
|
||||
reqBuilder.method(method, null);
|
||||
}
|
||||
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(reqBuilder.build()).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
WXPayUtility.validateResponse(wechatPayPublicKeyId, wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
return respBody;
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,700 @@
|
||||
package com.java.utils;
|
||||
|
||||
import com.google.gson.ExclusionStrategy;
|
||||
import com.google.gson.FieldAttributes;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.GsonBuilder;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Response;
|
||||
import okio.BufferedSource;
|
||||
|
||||
import javax.crypto.BadPaddingException;
|
||||
import javax.crypto.Cipher;
|
||||
import javax.crypto.IllegalBlockSizeException;
|
||||
import javax.crypto.NoSuchPaddingException;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.InvalidAlgorithmParameterException;
|
||||
import java.security.InvalidKeyException;
|
||||
import java.security.KeyFactory;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.security.SecureRandom;
|
||||
import java.security.Signature;
|
||||
import java.security.SignatureException;
|
||||
import java.security.spec.InvalidKeySpecException;
|
||||
import java.security.spec.PKCS8EncodedKeySpec;
|
||||
import java.security.spec.X509EncodedKeySpec;
|
||||
import java.time.DateTimeException;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.security.MessageDigest;
|
||||
import java.io.InputStream;
|
||||
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||
import java.security.Security;
|
||||
|
||||
public class WXPayUtility {
|
||||
private static final Gson gson = new GsonBuilder()
|
||||
.disableHtmlEscaping()
|
||||
.addSerializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
|
||||
final Expose expose = fieldAttributes.getAnnotation(Expose.class);
|
||||
return expose != null && !expose.serialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> aClass) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.addDeserializationExclusionStrategy(new ExclusionStrategy() {
|
||||
@Override
|
||||
public boolean shouldSkipField(FieldAttributes fieldAttributes) {
|
||||
final Expose expose = fieldAttributes.getAnnotation(Expose.class);
|
||||
return expose != null && !expose.deserialize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldSkipClass(Class<?> aClass) {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.create();
|
||||
private static final char[] SYMBOLS =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ".toCharArray();
|
||||
private static final SecureRandom random = new SecureRandom();
|
||||
|
||||
public static String toJson(Object object) {
|
||||
return gson.toJson(object);
|
||||
}
|
||||
|
||||
public static <T> T fromJson(String json, Class<T> classOfT) throws JsonSyntaxException {
|
||||
return gson.fromJson(json, classOfT);
|
||||
}
|
||||
|
||||
private static String readKeyStringFromPath(String keyPath) {
|
||||
try {
|
||||
return new String(Files.readAllBytes(Paths.get(keyPath)), StandardCharsets.UTF_8);
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static PrivateKey loadPrivateKeyFromString(String keyString) {
|
||||
try {
|
||||
keyString = keyString.replace("-----BEGIN PRIVATE KEY-----", "")
|
||||
.replace("-----END PRIVATE KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
return KeyFactory.getInstance("RSA").generatePrivate(
|
||||
new PKCS8EncodedKeySpec(Base64.getDecoder().decode(keyString)));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static PrivateKey loadPrivateKeyFromPath(String keyPath) {
|
||||
return loadPrivateKeyFromString(readKeyStringFromPath(keyPath));
|
||||
}
|
||||
|
||||
public static PublicKey loadPublicKeyFromString(String keyString) {
|
||||
try {
|
||||
keyString = keyString.replace("-----BEGIN PUBLIC KEY-----", "")
|
||||
.replace("-----END PUBLIC KEY-----", "")
|
||||
.replaceAll("\\s+", "");
|
||||
return KeyFactory.getInstance("RSA").generatePublic(
|
||||
new X509EncodedKeySpec(Base64.getDecoder().decode(keyString)));
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException(e);
|
||||
} catch (InvalidKeySpecException e) {
|
||||
throw new IllegalArgumentException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static PublicKey loadPublicKeyFromPath(String keyPath) {
|
||||
return loadPublicKeyFromString(readKeyStringFromPath(keyPath));
|
||||
}
|
||||
|
||||
public static String createNonce(int length) {
|
||||
char[] buf = new char[length];
|
||||
for (int i = 0; i < length; ++i) {
|
||||
buf[i] = SYMBOLS[random.nextInt(SYMBOLS.length)];
|
||||
}
|
||||
return new String(buf);
|
||||
}
|
||||
|
||||
public static String encrypt(PublicKey publicKey, String plaintext) {
|
||||
final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
return Base64.getEncoder().encodeToString(cipher.doFinal(plaintext.getBytes(StandardCharsets.UTF_8)));
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("RSA encryption using an illegal publicKey", e);
|
||||
} catch (BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new IllegalArgumentException("Plaintext is too long", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String rsaOaepDecrypt(PrivateKey privateKey, String ciphertext) {
|
||||
final String transformation = "RSA/ECB/OAEPWithSHA-1AndMGF1Padding";
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
cipher.init(Cipher.DECRYPT_MODE, privateKey);
|
||||
byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(ciphertext));
|
||||
return new String(decryptedBytes, StandardCharsets.UTF_8);
|
||||
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
|
||||
throw new IllegalArgumentException("The current Java environment does not support " + transformation, e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("RSA decryption using an illegal privateKey", e);
|
||||
} catch (BadPaddingException | IllegalBlockSizeException e) {
|
||||
throw new IllegalArgumentException("Ciphertext decryption failed", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String aesAeadDecrypt(byte[] key, byte[] associatedData, byte[] nonce,
|
||||
byte[] ciphertext) {
|
||||
final String transformation = "AES/GCM/NoPadding";
|
||||
final String algorithm = "AES";
|
||||
final int tagLengthBit = 128;
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(transformation);
|
||||
cipher.init(
|
||||
Cipher.DECRYPT_MODE,
|
||||
new SecretKeySpec(key, algorithm),
|
||||
new GCMParameterSpec(tagLengthBit, nonce));
|
||||
if (associatedData != null) {
|
||||
cipher.updateAAD(associatedData);
|
||||
}
|
||||
return new String(cipher.doFinal(ciphertext), StandardCharsets.UTF_8);
|
||||
} catch (InvalidKeyException
|
||||
| InvalidAlgorithmParameterException
|
||||
| BadPaddingException
|
||||
| IllegalBlockSizeException
|
||||
| NoSuchAlgorithmException
|
||||
| NoSuchPaddingException e) {
|
||||
throw new IllegalArgumentException(String.format("AesAeadDecrypt with %s Failed",
|
||||
transformation), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String sign(String message, String algorithm, PrivateKey privateKey) {
|
||||
byte[] sign;
|
||||
try {
|
||||
Signature signature = Signature.getInstance(algorithm);
|
||||
signature.initSign(privateKey);
|
||||
signature.update(message.getBytes(StandardCharsets.UTF_8));
|
||||
sign = signature.sign();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException("The current Java environment does not support " + algorithm, e);
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException(algorithm + " signature uses an illegal privateKey.", e);
|
||||
} catch (SignatureException e) {
|
||||
throw new RuntimeException("An error occurred during the sign process.", e);
|
||||
}
|
||||
return Base64.getEncoder().encodeToString(sign);
|
||||
}
|
||||
|
||||
public static boolean verify(String message, String signature, String algorithm,
|
||||
PublicKey publicKey) {
|
||||
try {
|
||||
Signature sign = Signature.getInstance(algorithm);
|
||||
sign.initVerify(publicKey);
|
||||
sign.update(message.getBytes(StandardCharsets.UTF_8));
|
||||
return sign.verify(Base64.getDecoder().decode(signature));
|
||||
} catch (SignatureException e) {
|
||||
return false;
|
||||
} catch (InvalidKeyException e) {
|
||||
throw new IllegalArgumentException("verify uses an illegal publickey.", e);
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException("The current Java environment does not support" + algorithm, e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String buildAuthorization(String mchid, String certificateSerialNo,
|
||||
PrivateKey privateKey,
|
||||
String method, String uri, String body) {
|
||||
String nonce = createNonce(32);
|
||||
long timestamp = Instant.now().getEpochSecond();
|
||||
|
||||
String message = String.format("%s\n%s\n%d\n%s\n%s\n", method, uri, timestamp, nonce,
|
||||
body == null ? "" : body);
|
||||
|
||||
String signature = sign(message, "SHA256withRSA", privateKey);
|
||||
|
||||
return String.format(
|
||||
"WECHATPAY2-SHA256-RSA2048 mchid=\"%s\",nonce_str=\"%s\",signature=\"%s\"," +
|
||||
"timestamp=\"%d\",serial_no=\"%s\"",
|
||||
mchid, nonce, signature, timestamp, certificateSerialNo);
|
||||
}
|
||||
|
||||
private static String calculateHash(InputStream inputStream, String algorithm) {
|
||||
try {
|
||||
MessageDigest digest = MessageDigest.getInstance(algorithm);
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, bytesRead);
|
||||
}
|
||||
byte[] hashBytes = digest.digest();
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (NoSuchAlgorithmException e) {
|
||||
throw new UnsupportedOperationException(algorithm + " algorithm not available", e);
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Error reading from input stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String sha256(InputStream inputStream) {
|
||||
return calculateHash(inputStream, "SHA-256");
|
||||
}
|
||||
|
||||
public static String sha1(InputStream inputStream) {
|
||||
return calculateHash(inputStream, "SHA-1");
|
||||
}
|
||||
|
||||
public static String sm3(InputStream inputStream) {
|
||||
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||
Security.addProvider(new BouncyCastleProvider());
|
||||
}
|
||||
|
||||
try {
|
||||
SM3Digest digest = new SM3Digest();
|
||||
byte[] buffer = new byte[8192];
|
||||
int bytesRead;
|
||||
while ((bytesRead = inputStream.read(buffer)) != -1) {
|
||||
digest.update(buffer, 0, bytesRead);
|
||||
}
|
||||
byte[] hashBytes = new byte[digest.getDigestSize()];
|
||||
digest.doFinal(hashBytes, 0);
|
||||
|
||||
StringBuilder hexString = new StringBuilder();
|
||||
for (byte b : hashBytes) {
|
||||
String hex = Integer.toHexString(0xff & b);
|
||||
if (hex.length() == 1) {
|
||||
hexString.append('0');
|
||||
}
|
||||
hexString.append(hex);
|
||||
}
|
||||
return hexString.toString();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Error reading from input stream", e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String urlEncode(String content) {
|
||||
try {
|
||||
return URLEncoder.encode(content, StandardCharsets.UTF_8.name());
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public static String urlEncode(Map<String, Object> params) {
|
||||
if (params == null || params.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Entry<String, Object> entry : params.entrySet()) {
|
||||
if (entry.getValue() == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
String key = entry.getKey();
|
||||
Object value = entry.getValue();
|
||||
if (value instanceof List) {
|
||||
List<?> list = (List<?>) entry.getValue();
|
||||
for (Object temp : list) {
|
||||
appendParam(result, key, temp);
|
||||
}
|
||||
} else {
|
||||
appendParam(result, key, value);
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
private static void appendParam(StringBuilder result, String key, Object value) {
|
||||
if (result.length() > 0) {
|
||||
result.append("&");
|
||||
}
|
||||
|
||||
String valueString;
|
||||
if (value instanceof String || value instanceof Number ||
|
||||
value instanceof Boolean || value instanceof Enum) {
|
||||
valueString = value.toString();
|
||||
} else {
|
||||
valueString = toJson(value);
|
||||
}
|
||||
|
||||
result.append(key)
|
||||
.append("=")
|
||||
.append(urlEncode(valueString));
|
||||
}
|
||||
|
||||
public static String extractBody(Response response) {
|
||||
if (response.body() == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
BufferedSource source = response.body().source();
|
||||
return source.readUtf8();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException(String.format("An error occurred during reading response body. " +
|
||||
"Status: %d", response.code()), e);
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateResponse(String wechatpayPublicKeyId, PublicKey wechatpayPublicKey,
|
||||
Headers headers,
|
||||
String body) {
|
||||
String timestamp = headers.get("Wechatpay-Timestamp");
|
||||
String requestId = headers.get("Request-ID");
|
||||
try {
|
||||
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
|
||||
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate response failed, timestamp[%s] is expired, request-id[%s]",
|
||||
timestamp, requestId));
|
||||
}
|
||||
} catch (DateTimeException | NumberFormatException e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate response failed, timestamp[%s] is invalid, request-id[%s]",
|
||||
timestamp, requestId));
|
||||
}
|
||||
String serialNumber = headers.get("Wechatpay-Serial");
|
||||
if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate response failed, Invalid Wechatpay-Serial, Local: %s, Remote: " +
|
||||
"%s", wechatpayPublicKeyId, serialNumber));
|
||||
}
|
||||
|
||||
String signature = headers.get("Wechatpay-Signature");
|
||||
String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
|
||||
body == null ? "" : body);
|
||||
|
||||
boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
|
||||
if (!success) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate response failed,the WechatPay signature is incorrect.%n"
|
||||
+ "Request-ID[%s]\tresponseHeader[%s]\tresponseBody[%.1024s]",
|
||||
headers.get("Request-ID"), headers, body));
|
||||
}
|
||||
}
|
||||
|
||||
public static void validateNotification(String wechatpayPublicKeyId,
|
||||
PublicKey wechatpayPublicKey, Headers headers,
|
||||
String body) {
|
||||
String timestamp = headers.get("Wechatpay-Timestamp");
|
||||
try {
|
||||
Instant responseTime = Instant.ofEpochSecond(Long.parseLong(timestamp));
|
||||
if (Duration.between(responseTime, Instant.now()).abs().toMinutes() >= 5) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate notification failed, timestamp[%s] is expired", timestamp));
|
||||
}
|
||||
} catch (DateTimeException | NumberFormatException e) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate notification failed, timestamp[%s] is invalid", timestamp));
|
||||
}
|
||||
String serialNumber = headers.get("Wechatpay-Serial");
|
||||
if (!Objects.equals(serialNumber, wechatpayPublicKeyId)) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate notification failed, Invalid Wechatpay-Serial, Local: %s, " +
|
||||
"Remote: %s",
|
||||
wechatpayPublicKeyId,
|
||||
serialNumber));
|
||||
}
|
||||
|
||||
String signature = headers.get("Wechatpay-Signature");
|
||||
String message = String.format("%s\n%s\n%s\n", timestamp, headers.get("Wechatpay-Nonce"),
|
||||
body == null ? "" : body);
|
||||
|
||||
boolean success = verify(message, signature, "SHA256withRSA", wechatpayPublicKey);
|
||||
if (!success) {
|
||||
throw new IllegalArgumentException(
|
||||
String.format("Validate notification failed, WechatPay signature is incorrect.\n"
|
||||
+ "responseHeader[%s]\tresponseBody[%.1024s]",
|
||||
headers, body));
|
||||
}
|
||||
}
|
||||
|
||||
public static Notification parseNotification(String apiv3Key, String wechatpayPublicKeyId,
|
||||
PublicKey wechatpayPublicKey, Headers headers,
|
||||
String body) {
|
||||
validateNotification(wechatpayPublicKeyId, wechatpayPublicKey, headers, body);
|
||||
Notification notification = gson.fromJson(body, Notification.class);
|
||||
notification.decrypt(apiv3Key);
|
||||
return notification;
|
||||
}
|
||||
|
||||
public static class ApiException extends RuntimeException {
|
||||
private static final long serialVersionUID = 2261086748874802175L;
|
||||
|
||||
private final int statusCode;
|
||||
private final String body;
|
||||
private final Headers headers;
|
||||
private final String errorCode;
|
||||
private final String errorMessage;
|
||||
|
||||
public ApiException(int statusCode, String body, Headers headers) {
|
||||
super(String.format("微信支付API访问失败,StatusCode: [%s], Body: [%s], Headers: [%s]", statusCode,
|
||||
body, headers));
|
||||
this.statusCode = statusCode;
|
||||
this.body = body;
|
||||
this.headers = headers;
|
||||
|
||||
if (body != null && !body.isEmpty()) {
|
||||
JsonElement code;
|
||||
JsonElement message;
|
||||
|
||||
try {
|
||||
JsonObject jsonObject = gson.fromJson(body, JsonObject.class);
|
||||
code = jsonObject.get("code");
|
||||
message = jsonObject.get("message");
|
||||
} catch (JsonSyntaxException ignored) {
|
||||
code = null;
|
||||
message = null;
|
||||
}
|
||||
this.errorCode = code == null ? null : code.getAsString();
|
||||
this.errorMessage = message == null ? null : message.getAsString();
|
||||
} else {
|
||||
this.errorCode = null;
|
||||
this.errorMessage = null;
|
||||
}
|
||||
}
|
||||
|
||||
public int getStatusCode() {
|
||||
return statusCode;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public Headers getHeaders() {
|
||||
return headers;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
|
||||
public String getErrorMessage() {
|
||||
return errorMessage;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Notification {
|
||||
@SerializedName("id")
|
||||
private String id;
|
||||
@SerializedName("create_time")
|
||||
private String createTime;
|
||||
@SerializedName("event_type")
|
||||
private String eventType;
|
||||
@SerializedName("resource_type")
|
||||
private String resourceType;
|
||||
@SerializedName("summary")
|
||||
private String summary;
|
||||
@SerializedName("resource")
|
||||
private Resource resource;
|
||||
private String plaintext;
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public String getEventType() {
|
||||
return eventType;
|
||||
}
|
||||
|
||||
public String getResourceType() {
|
||||
return resourceType;
|
||||
}
|
||||
|
||||
public String getSummary() {
|
||||
return summary;
|
||||
}
|
||||
|
||||
public Resource getResource() {
|
||||
return resource;
|
||||
}
|
||||
|
||||
public String getPlaintext() {
|
||||
return plaintext;
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
if (resource == null) {
|
||||
throw new IllegalArgumentException("Missing required field `resource` in notification");
|
||||
}
|
||||
resource.validate();
|
||||
}
|
||||
|
||||
private void decrypt(String apiv3Key) {
|
||||
validate();
|
||||
|
||||
plaintext = aesAeadDecrypt(
|
||||
apiv3Key.getBytes(StandardCharsets.UTF_8),
|
||||
resource.associatedData.getBytes(StandardCharsets.UTF_8),
|
||||
resource.nonce.getBytes(StandardCharsets.UTF_8),
|
||||
Base64.getDecoder().decode(resource.ciphertext)
|
||||
);
|
||||
}
|
||||
|
||||
public static class Resource {
|
||||
@SerializedName("algorithm")
|
||||
private String algorithm;
|
||||
|
||||
@SerializedName("ciphertext")
|
||||
private String ciphertext;
|
||||
|
||||
@SerializedName("associated_data")
|
||||
private String associatedData;
|
||||
|
||||
@SerializedName("nonce")
|
||||
private String nonce;
|
||||
|
||||
@SerializedName("original_type")
|
||||
private String originalType;
|
||||
|
||||
public String getAlgorithm() {
|
||||
return algorithm;
|
||||
}
|
||||
|
||||
public String getCiphertext() {
|
||||
return ciphertext;
|
||||
}
|
||||
|
||||
public String getAssociatedData() {
|
||||
return associatedData;
|
||||
}
|
||||
|
||||
public String getNonce() {
|
||||
return nonce;
|
||||
}
|
||||
|
||||
public String getOriginalType() {
|
||||
return originalType;
|
||||
}
|
||||
|
||||
private void validate() {
|
||||
if (algorithm == null || algorithm.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field `algorithm` in Notification" +
|
||||
".Resource");
|
||||
}
|
||||
if (!Objects.equals(algorithm, "AEAD_AES_256_GCM")) {
|
||||
throw new IllegalArgumentException(String.format("Unsupported `algorithm`[%s] in " +
|
||||
"Notification.Resource", algorithm));
|
||||
}
|
||||
|
||||
if (ciphertext == null || ciphertext.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field `ciphertext` in Notification" +
|
||||
".Resource");
|
||||
}
|
||||
|
||||
if (associatedData == null || associatedData.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field `associatedData` in " +
|
||||
"Notification.Resource");
|
||||
}
|
||||
|
||||
if (nonce == null || nonce.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field `nonce` in Notification" +
|
||||
".Resource");
|
||||
}
|
||||
|
||||
if (originalType == null || originalType.isEmpty()) {
|
||||
throw new IllegalArgumentException("Missing required field `originalType` in " +
|
||||
"Notification.Resource");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static String getContentTypeByFileName(String fileName) {
|
||||
if (fileName == null || fileName.isEmpty()) {
|
||||
return "application/octet-stream";
|
||||
}
|
||||
|
||||
String extension = "";
|
||||
int lastDotIndex = fileName.lastIndexOf('.');
|
||||
if (lastDotIndex > 0 && lastDotIndex < fileName.length() - 1) {
|
||||
extension = fileName.substring(lastDotIndex + 1).toLowerCase();
|
||||
}
|
||||
|
||||
Map<String, String> contentTypeMap = new HashMap<>();
|
||||
contentTypeMap.put("png", "image/png");
|
||||
contentTypeMap.put("jpg", "image/jpeg");
|
||||
contentTypeMap.put("jpeg", "image/jpeg");
|
||||
contentTypeMap.put("gif", "image/gif");
|
||||
contentTypeMap.put("bmp", "image/bmp");
|
||||
contentTypeMap.put("webp", "image/webp");
|
||||
contentTypeMap.put("svg", "image/svg+xml");
|
||||
contentTypeMap.put("ico", "image/x-icon");
|
||||
contentTypeMap.put("pdf", "application/pdf");
|
||||
contentTypeMap.put("doc", "application/msword");
|
||||
contentTypeMap.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||
contentTypeMap.put("xls", "application/vnd.ms-excel");
|
||||
contentTypeMap.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
|
||||
contentTypeMap.put("ppt", "application/vnd.ms-powerpoint");
|
||||
contentTypeMap.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation");
|
||||
contentTypeMap.put("txt", "text/plain");
|
||||
contentTypeMap.put("html", "text/html");
|
||||
contentTypeMap.put("css", "text/css");
|
||||
contentTypeMap.put("js", "application/javascript");
|
||||
contentTypeMap.put("json", "application/json");
|
||||
contentTypeMap.put("xml", "application/xml");
|
||||
contentTypeMap.put("csv", "text/csv");
|
||||
contentTypeMap.put("mp3", "audio/mpeg");
|
||||
contentTypeMap.put("wav", "audio/wav");
|
||||
contentTypeMap.put("mp4", "video/mp4");
|
||||
contentTypeMap.put("avi", "video/x-msvideo");
|
||||
contentTypeMap.put("mov", "video/quicktime");
|
||||
contentTypeMap.put("zip", "application/zip");
|
||||
contentTypeMap.put("rar", "application/x-rar-compressed");
|
||||
contentTypeMap.put("7z", "application/x-7z-compressed");
|
||||
|
||||
return contentTypeMap.getOrDefault(extension, "application/octet-stream");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 合单下单-APP
|
||||
*/
|
||||
public class UnionAppPrepay {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/combine-transactions/app";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
UnionAppPrepay client = new UnionAppPrepay(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
UnionAPIv3AppPrepayRequest request = new UnionAPIv3AppPrepayRequest();
|
||||
request.combineAppid = "wxd678efh567hg6787";
|
||||
request.combineOutTradeNo = "20150806125345";
|
||||
request.combineMchid = "1900000109";
|
||||
request.sceneInfo = new UnionSceneInfo();
|
||||
request.sceneInfo.deviceId = "POS1:1";
|
||||
request.sceneInfo.payerClientIp = "14.17.22.32";
|
||||
request.subOrders = new ArrayList<>();
|
||||
{
|
||||
UnionSubOrder subOrdersItem0 = new UnionSubOrder();
|
||||
subOrdersItem0.mchid = "1230000109";
|
||||
subOrdersItem0.outTradeNo = "20150806125346";
|
||||
subOrdersItem0.amount = new UnionAmountInfo();
|
||||
subOrdersItem0.amount.totalAmount = 10L;
|
||||
subOrdersItem0.amount.currency = "CNY";
|
||||
subOrdersItem0.attach = "深圳分店";
|
||||
subOrdersItem0.description = "腾讯充值中心-QQ会员充值";
|
||||
subOrdersItem0.detail = "买单费用";
|
||||
subOrdersItem0.goodsTag = "WXG";
|
||||
subOrdersItem0.settleInfo = new UnionSettleInfo();
|
||||
subOrdersItem0.settleInfo.profitSharing = false;
|
||||
request.subOrders.add(subOrdersItem0);
|
||||
UnionSubOrder subOrdersItem1 = new UnionSubOrder();
|
||||
subOrdersItem1.mchid = "1230000119";
|
||||
subOrdersItem1.outTradeNo = "20150806125347";
|
||||
subOrdersItem1.amount = new UnionAmountInfo();
|
||||
subOrdersItem1.amount.totalAmount = 10L;
|
||||
subOrdersItem1.amount.currency = "CNY";
|
||||
subOrdersItem1.attach = "广州分店";
|
||||
subOrdersItem1.description = "腾讯充值中心-微信充值";
|
||||
subOrdersItem1.detail = "买单费用";
|
||||
subOrdersItem1.goodsTag = "WXG";
|
||||
subOrdersItem1.settleInfo = new UnionSettleInfo();
|
||||
subOrdersItem1.settleInfo.profitSharing = false;
|
||||
request.subOrders.add(subOrdersItem1);
|
||||
};
|
||||
request.combinePayerInfo = new UnionAppPayerInfo();
|
||||
request.combinePayerInfo.openid = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o";
|
||||
request.timeExpire = "2000-01-01T00:00:00+08:00";
|
||||
request.notifyUrl = "https://yourapp.com/notify";
|
||||
try {
|
||||
UnionAPIv3AppPrepayResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public UnionAPIv3AppPrepayResponse run(UnionAPIv3AppPrepayRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, UnionAPIv3AppPrepayResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public UnionAppPrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class UnionAPIv3AppPrepayRequest {
|
||||
@SerializedName("combine_appid")
|
||||
public String combineAppid;
|
||||
|
||||
@SerializedName("combine_out_trade_no")
|
||||
public String combineOutTradeNo;
|
||||
|
||||
@SerializedName("combine_mchid")
|
||||
public String combineMchid;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public UnionSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("sub_orders")
|
||||
public List<UnionSubOrder> subOrders = new ArrayList<UnionSubOrder>();
|
||||
|
||||
@SerializedName("combine_payer_info")
|
||||
public UnionAppPayerInfo combinePayerInfo;
|
||||
|
||||
@SerializedName("time_expire")
|
||||
public String timeExpire;
|
||||
|
||||
@SerializedName("notify_url")
|
||||
public String notifyUrl;
|
||||
|
||||
@SerializedName("trade_scenario")
|
||||
public String tradeScenario;
|
||||
}
|
||||
|
||||
public static class UnionAPIv3AppPrepayResponse {
|
||||
@SerializedName("prepay_id")
|
||||
public String prepayId;
|
||||
}
|
||||
|
||||
public static class UnionSceneInfo {
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
|
||||
@SerializedName("payer_client_ip")
|
||||
public String payerClientIp;
|
||||
}
|
||||
|
||||
public static class UnionSubOrder {
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("amount")
|
||||
public UnionAmountInfo amount;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("detail")
|
||||
public String detail;
|
||||
|
||||
@SerializedName("goods_tag")
|
||||
public String goodsTag;
|
||||
|
||||
@SerializedName("settle_info")
|
||||
public UnionSettleInfo settleInfo;
|
||||
}
|
||||
|
||||
public static class UnionAppPayerInfo {
|
||||
@SerializedName("openid")
|
||||
public String openid;
|
||||
}
|
||||
|
||||
public static class UnionAmountInfo {
|
||||
@SerializedName("total_amount")
|
||||
public Long totalAmount;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
}
|
||||
|
||||
public static class UnionSettleInfo {
|
||||
@SerializedName("profit_sharing")
|
||||
public Boolean profitSharing;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 合单关单
|
||||
*/
|
||||
public class UnionClose {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/combine-transactions/out-trade-no/{combine_out_trade_no}/close";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
UnionClose client = new UnionClose(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
UnionCloseRequest request = new UnionCloseRequest();
|
||||
request.combineOutTradeNo = "1217752501201407033233368018";
|
||||
request.combineAppid = "wxd678efh567hg6787";
|
||||
request.subOrders = new ArrayList<>();
|
||||
{
|
||||
UnionCloseSubOrder subOrdersItem = new UnionCloseSubOrder();
|
||||
subOrdersItem.mchid = "1900000109";
|
||||
subOrdersItem.outTradeNo = "20150806125346";
|
||||
request.subOrders.add(subOrdersItem);
|
||||
};
|
||||
try {
|
||||
client.run(request);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void run(UnionCloseRequest request) {
|
||||
String uri = PATH;
|
||||
uri = uri.replace("{combine_out_trade_no}", WXPayUtility.urlEncode(request.combineOutTradeNo));
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
return;
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public UnionClose(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class UnionCloseRequest {
|
||||
@SerializedName("combine_appid")
|
||||
public String combineAppid;
|
||||
|
||||
@SerializedName("combine_out_trade_no")
|
||||
@Expose(serialize = false)
|
||||
public String combineOutTradeNo;
|
||||
|
||||
@SerializedName("sub_orders")
|
||||
public List<UnionCloseSubOrder> subOrders = new ArrayList<UnionCloseSubOrder>();
|
||||
}
|
||||
|
||||
public static class UnionCloseSubOrder {
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,230 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 合单下单-H5
|
||||
*/
|
||||
public class UnionH5Prepay {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/combine-transactions/h5";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
UnionH5Prepay client = new UnionH5Prepay(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
UnionAPIv3H5PrepayRequest request = new UnionAPIv3H5PrepayRequest();
|
||||
request.combineAppid = "wxd678efh567hg6787";
|
||||
request.combineOutTradeNo = "1217752501201407033233368018";
|
||||
request.combineMchid = "1230000109";
|
||||
request.sceneInfo = new UnionH5SceneInfo();
|
||||
request.sceneInfo.payerClientIp = "14.23.150.211";
|
||||
request.sceneInfo.deviceId = "013467007045764";
|
||||
request.sceneInfo.h5Info = new UnionH5Info();
|
||||
request.sceneInfo.h5Info.type = "iOS";
|
||||
request.sceneInfo.h5Info.appName = "王者荣耀";
|
||||
request.sceneInfo.h5Info.appUrl = "https://pay.qq.com";
|
||||
request.sceneInfo.h5Info.bundleId = "com.tencent.wzryiOS";
|
||||
request.sceneInfo.h5Info.packageName = "com.tencent.tmgp.sgame";
|
||||
request.subOrders = new ArrayList<>();
|
||||
{
|
||||
UnionCommonSubOrder subOrdersItem0 = new UnionCommonSubOrder();
|
||||
subOrdersItem0.mchid = "1230000109";
|
||||
subOrdersItem0.outTradeNo = "20150806125346";
|
||||
subOrdersItem0.amount = new UnionAmountInfo();
|
||||
subOrdersItem0.amount.totalAmount = 10L;
|
||||
subOrdersItem0.amount.currency = "CNY";
|
||||
subOrdersItem0.attach = "深圳分店";
|
||||
subOrdersItem0.description = "腾讯充值中心-QQ会员充值";
|
||||
subOrdersItem0.goodsTag = "WXG";
|
||||
subOrdersItem0.settleInfo = new UnionSettleInfo();
|
||||
subOrdersItem0.settleInfo.profitSharing = false;
|
||||
request.subOrders.add(subOrdersItem0);
|
||||
UnionCommonSubOrder subOrdersItem1 = new UnionCommonSubOrder();
|
||||
subOrdersItem1.mchid = "1230000119";
|
||||
subOrdersItem1.outTradeNo = "20150806125347";
|
||||
subOrdersItem1.amount = new UnionAmountInfo();
|
||||
subOrdersItem1.amount.totalAmount = 10L;
|
||||
subOrdersItem1.amount.currency = "CNY";
|
||||
subOrdersItem1.attach = "广州分店";
|
||||
subOrdersItem1.description = "腾讯充值中心-微信充值";
|
||||
subOrdersItem1.goodsTag = "WXG";
|
||||
subOrdersItem1.settleInfo = new UnionSettleInfo();
|
||||
subOrdersItem1.settleInfo.profitSharing = false;
|
||||
request.subOrders.add(subOrdersItem1);
|
||||
};
|
||||
request.timeExpire = "2018-06-08T10:34:56+08:00";
|
||||
request.notifyUrl = "https://yourapp.com/notify";
|
||||
try {
|
||||
UnionAPIv3H5PrepayResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public UnionAPIv3H5PrepayResponse run(UnionAPIv3H5PrepayRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, UnionAPIv3H5PrepayResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public UnionH5Prepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class UnionAPIv3H5PrepayRequest {
|
||||
@SerializedName("combine_appid")
|
||||
public String combineAppid;
|
||||
|
||||
@SerializedName("combine_out_trade_no")
|
||||
public String combineOutTradeNo;
|
||||
|
||||
@SerializedName("combine_mchid")
|
||||
public String combineMchid;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public UnionH5SceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("sub_orders")
|
||||
public List<UnionCommonSubOrder> subOrders = new ArrayList<UnionCommonSubOrder>();
|
||||
|
||||
@SerializedName("time_expire")
|
||||
public String timeExpire;
|
||||
|
||||
@SerializedName("notify_url")
|
||||
public String notifyUrl;
|
||||
}
|
||||
|
||||
public static class UnionAPIv3H5PrepayResponse {
|
||||
@SerializedName("h5_url")
|
||||
public String h5Url;
|
||||
}
|
||||
|
||||
public static class UnionH5SceneInfo {
|
||||
@SerializedName("payer_client_ip")
|
||||
public String payerClientIp;
|
||||
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
|
||||
@SerializedName("h5_info")
|
||||
public UnionH5Info h5Info;
|
||||
}
|
||||
|
||||
public static class UnionCommonSubOrder {
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("amount")
|
||||
public UnionAmountInfo amount;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("goods_tag")
|
||||
public String goodsTag;
|
||||
|
||||
@SerializedName("settle_info")
|
||||
public UnionSettleInfo settleInfo;
|
||||
}
|
||||
|
||||
public static class UnionH5Info {
|
||||
@SerializedName("type")
|
||||
public String type;
|
||||
|
||||
@SerializedName("app_name")
|
||||
public String appName;
|
||||
|
||||
@SerializedName("app_url")
|
||||
public String appUrl;
|
||||
|
||||
@SerializedName("bundle_id")
|
||||
public String bundleId;
|
||||
|
||||
@SerializedName("package_name")
|
||||
public String packageName;
|
||||
}
|
||||
|
||||
public static class UnionAmountInfo {
|
||||
@SerializedName("total_amount")
|
||||
public Long totalAmount;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
}
|
||||
|
||||
public static class UnionSettleInfo {
|
||||
@SerializedName("profit_sharing")
|
||||
public Boolean profitSharing;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 合单下单-JSAPI
|
||||
*/
|
||||
public class UnionJsapiPrepay {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/combine-transactions/jsapi";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
UnionJsapiPrepay client = new UnionJsapiPrepay(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
UnionAPIv3JsapiPrepayRequest request = new UnionAPIv3JsapiPrepayRequest();
|
||||
request.combineAppid = "wxd678efh567hg6787";
|
||||
request.combineMchid = "1230000109";
|
||||
request.combineOutTradeNo = "1217752501201407033233368018";
|
||||
request.combinePayerInfo = new UnionPayerInfo();
|
||||
request.combinePayerInfo.openid = "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o";
|
||||
request.sceneInfo = new UnionSceneInfo();
|
||||
request.sceneInfo.deviceId = "POS1:1";
|
||||
request.sceneInfo.payerClientIp = "14.17.22.32";
|
||||
request.subOrders = new ArrayList<>();
|
||||
{
|
||||
UnionSubOrder subOrdersItem0 = new UnionSubOrder();
|
||||
subOrdersItem0.mchid = "1230000109";
|
||||
subOrdersItem0.outTradeNo = "20150806125346";
|
||||
subOrdersItem0.amount = new UnionAmountInfo();
|
||||
subOrdersItem0.amount.totalAmount = 10L;
|
||||
subOrdersItem0.amount.currency = "CNY";
|
||||
subOrdersItem0.attach = "深圳分店";
|
||||
subOrdersItem0.description = "腾讯充值中心-QQ会员充值";
|
||||
subOrdersItem0.detail = "买单费用";
|
||||
subOrdersItem0.goodsTag = "WXG";
|
||||
subOrdersItem0.settleInfo = new UnionSettleInfo();
|
||||
subOrdersItem0.settleInfo.profitSharing = false;
|
||||
request.subOrders.add(subOrdersItem0);
|
||||
UnionSubOrder subOrdersItem1 = new UnionSubOrder();
|
||||
subOrdersItem1.mchid = "1230000119";
|
||||
subOrdersItem1.outTradeNo = "20150806125347";
|
||||
subOrdersItem1.amount = new UnionAmountInfo();
|
||||
subOrdersItem1.amount.totalAmount = 10L;
|
||||
subOrdersItem1.amount.currency = "CNY";
|
||||
subOrdersItem1.attach = "广州分店";
|
||||
subOrdersItem1.description = "腾讯充值中心-微信充值";
|
||||
subOrdersItem1.detail = "买单费用";
|
||||
subOrdersItem1.goodsTag = "WXG";
|
||||
subOrdersItem1.settleInfo = new UnionSettleInfo();
|
||||
subOrdersItem1.settleInfo.profitSharing = false;
|
||||
request.subOrders.add(subOrdersItem1);
|
||||
};
|
||||
request.timeExpire = "2018-06-08T10:34:56+08:00";
|
||||
request.notifyUrl = "https://yourapp.com/notify";
|
||||
try {
|
||||
UnionAPIv3JsapiPrepayResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public UnionAPIv3JsapiPrepayResponse run(UnionAPIv3JsapiPrepayRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, UnionAPIv3JsapiPrepayResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public UnionJsapiPrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class UnionAPIv3JsapiPrepayRequest {
|
||||
@SerializedName("combine_appid")
|
||||
public String combineAppid;
|
||||
|
||||
@SerializedName("combine_mchid")
|
||||
public String combineMchid;
|
||||
|
||||
@SerializedName("combine_out_trade_no")
|
||||
public String combineOutTradeNo;
|
||||
|
||||
@SerializedName("combine_payer_info")
|
||||
public UnionPayerInfo combinePayerInfo;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public UnionSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("sub_orders")
|
||||
public List<UnionSubOrder> subOrders = new ArrayList<UnionSubOrder>();
|
||||
|
||||
@SerializedName("time_expire")
|
||||
public String timeExpire;
|
||||
|
||||
@SerializedName("notify_url")
|
||||
public String notifyUrl;
|
||||
}
|
||||
|
||||
public static class UnionAPIv3JsapiPrepayResponse {
|
||||
@SerializedName("prepay_id")
|
||||
public String prepayId;
|
||||
}
|
||||
|
||||
public static class UnionPayerInfo {
|
||||
@SerializedName("openid")
|
||||
public String openid;
|
||||
|
||||
@SerializedName("sub_openid")
|
||||
public String subOpenid;
|
||||
}
|
||||
|
||||
public static class UnionSceneInfo {
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
|
||||
@SerializedName("payer_client_ip")
|
||||
public String payerClientIp;
|
||||
}
|
||||
|
||||
public static class UnionSubOrder {
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("amount")
|
||||
public UnionAmountInfo amount;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("detail")
|
||||
public String detail;
|
||||
|
||||
@SerializedName("goods_tag")
|
||||
public String goodsTag;
|
||||
|
||||
@SerializedName("settle_info")
|
||||
public UnionSettleInfo settleInfo;
|
||||
}
|
||||
|
||||
public static class UnionAmountInfo {
|
||||
@SerializedName("total_amount")
|
||||
public Long totalAmount;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
}
|
||||
|
||||
public static class UnionSettleInfo {
|
||||
@SerializedName("profit_sharing")
|
||||
public Boolean profitSharing;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,204 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 合单下单-NATIVE
|
||||
*/
|
||||
public class UnionNativePrepay {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/combine-transactions/native";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
UnionNativePrepay client = new UnionNativePrepay(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
UnionAPIv3NativePrepayRequest request = new UnionAPIv3NativePrepayRequest();
|
||||
request.combineAppid = "wxd678efh567hg6787";
|
||||
request.combineOutTradeNo = "20150806125346";
|
||||
request.combineMchid = "1900000109";
|
||||
request.sceneInfo = new UnionSceneInfo();
|
||||
request.sceneInfo.deviceId = "POS1:1";
|
||||
request.sceneInfo.payerClientIp = "14.17.22.32";
|
||||
request.subOrders = new ArrayList<>();
|
||||
{
|
||||
UnionCommonSubOrder subOrdersItem0 = new UnionCommonSubOrder();
|
||||
subOrdersItem0.mchid = "1230000109";
|
||||
subOrdersItem0.outTradeNo = "20150806125346";
|
||||
subOrdersItem0.amount = new UnionAmountInfo();
|
||||
subOrdersItem0.amount.totalAmount = 10L;
|
||||
subOrdersItem0.amount.currency = "CNY";
|
||||
subOrdersItem0.attach = "深圳分店";
|
||||
subOrdersItem0.description = "腾讯充值中心-QQ会员充值";
|
||||
subOrdersItem0.goodsTag = "WXG";
|
||||
subOrdersItem0.settleInfo = new UnionSettleInfo();
|
||||
subOrdersItem0.settleInfo.profitSharing = false;
|
||||
request.subOrders.add(subOrdersItem0);
|
||||
UnionCommonSubOrder subOrdersItem1 = new UnionCommonSubOrder();
|
||||
subOrdersItem1.mchid = "1230000119";
|
||||
subOrdersItem1.outTradeNo = "20150806125347";
|
||||
subOrdersItem1.amount = new UnionAmountInfo();
|
||||
subOrdersItem1.amount.totalAmount = 10L;
|
||||
subOrdersItem1.amount.currency = "CNY";
|
||||
subOrdersItem1.attach = "广州分店";
|
||||
subOrdersItem1.description = "腾讯充值中心-微信充值";
|
||||
subOrdersItem1.goodsTag = "WXG";
|
||||
subOrdersItem1.settleInfo = new UnionSettleInfo();
|
||||
subOrdersItem1.settleInfo.profitSharing = false;
|
||||
request.subOrders.add(subOrdersItem1);
|
||||
};
|
||||
request.timeExpire = "2000-01-01T00:00:00+08:00";
|
||||
request.notifyUrl = "https://yourapp.com/notify";
|
||||
try {
|
||||
UnionAPIv3NativePrepayResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public UnionAPIv3NativePrepayResponse run(UnionAPIv3NativePrepayRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, UnionAPIv3NativePrepayResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public UnionNativePrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class UnionAPIv3NativePrepayRequest {
|
||||
@SerializedName("combine_appid")
|
||||
public String combineAppid;
|
||||
|
||||
@SerializedName("combine_out_trade_no")
|
||||
public String combineOutTradeNo;
|
||||
|
||||
@SerializedName("combine_mchid")
|
||||
public String combineMchid;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public UnionSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("sub_orders")
|
||||
public List<UnionCommonSubOrder> subOrders = new ArrayList<UnionCommonSubOrder>();
|
||||
|
||||
@SerializedName("time_expire")
|
||||
public String timeExpire;
|
||||
|
||||
@SerializedName("notify_url")
|
||||
public String notifyUrl;
|
||||
}
|
||||
|
||||
public static class UnionAPIv3NativePrepayResponse {
|
||||
@SerializedName("code_url")
|
||||
public String codeUrl;
|
||||
}
|
||||
|
||||
public static class UnionSceneInfo {
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
|
||||
@SerializedName("payer_client_ip")
|
||||
public String payerClientIp;
|
||||
}
|
||||
|
||||
public static class UnionCommonSubOrder {
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("amount")
|
||||
public UnionAmountInfo amount;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("goods_tag")
|
||||
public String goodsTag;
|
||||
|
||||
@SerializedName("settle_info")
|
||||
public UnionSettleInfo settleInfo;
|
||||
}
|
||||
|
||||
public static class UnionAmountInfo {
|
||||
@SerializedName("total_amount")
|
||||
public Long totalAmount;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
}
|
||||
|
||||
public static class UnionSettleInfo {
|
||||
@SerializedName("profit_sharing")
|
||||
public Boolean profitSharing;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 商户订单号查询订单
|
||||
*/
|
||||
public class UnionQueryByOutTradeNo {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "GET";
|
||||
private static String PATH = "/v3/combine-transactions/out-trade-no/{combine_out_trade_no}";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
UnionQueryByOutTradeNo client = new UnionQueryByOutTradeNo(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
UnionQueryByOutTradeNoRequest request = new UnionQueryByOutTradeNoRequest();
|
||||
request.combineOutTradeNo = "P20150806125346";
|
||||
try {
|
||||
UnionAPIv3UnionQueryResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public UnionAPIv3UnionQueryResponse run(UnionQueryByOutTradeNoRequest request) {
|
||||
String uri = PATH;
|
||||
uri = uri.replace("{combine_out_trade_no}", WXPayUtility.urlEncode(request.combineOutTradeNo));
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, UnionAPIv3UnionQueryResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public UnionQueryByOutTradeNo(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class UnionQueryByOutTradeNoRequest {
|
||||
@SerializedName("combine_out_trade_no")
|
||||
@Expose(serialize = false)
|
||||
public String combineOutTradeNo;
|
||||
}
|
||||
|
||||
public static class UnionAPIv3UnionQueryResponse {
|
||||
@SerializedName("combine_appid")
|
||||
public String combineAppid;
|
||||
|
||||
@SerializedName("combine_mchid")
|
||||
public String combineMchid;
|
||||
|
||||
@SerializedName("combine_out_trade_no")
|
||||
public String combineOutTradeNo;
|
||||
|
||||
@SerializedName("combine_payer_info")
|
||||
public UnionCommRespPayerInfo combinePayerInfo;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public UnionCommRespSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("sub_orders")
|
||||
public List<UnionSubOrder> subOrders;
|
||||
}
|
||||
|
||||
public static class UnionCommRespPayerInfo {
|
||||
@SerializedName("openid")
|
||||
public String openid;
|
||||
}
|
||||
|
||||
public static class UnionCommRespSceneInfo {
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
}
|
||||
|
||||
public static class UnionSubOrder {
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("sub_mchid")
|
||||
public String subMchid;
|
||||
|
||||
@SerializedName("sub_appid")
|
||||
public String subAppid;
|
||||
|
||||
@SerializedName("sub_openid")
|
||||
public String subOpenid;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("trade_type")
|
||||
public String tradeType;
|
||||
|
||||
@SerializedName("trade_state")
|
||||
public String tradeState;
|
||||
|
||||
@SerializedName("bank_type")
|
||||
public String bankType;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("success_time")
|
||||
public String successTime;
|
||||
|
||||
@SerializedName("amount")
|
||||
public UnionCommRespAmountInfo amount;
|
||||
|
||||
@SerializedName("promotion_detail")
|
||||
public List<UnionPromotionDetail> promotionDetail;
|
||||
}
|
||||
|
||||
public static class UnionCommRespAmountInfo {
|
||||
@SerializedName("total_amount")
|
||||
public Long totalAmount;
|
||||
|
||||
@SerializedName("payer_amount")
|
||||
public Long payerAmount;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
|
||||
@SerializedName("payer_currency")
|
||||
public String payerCurrency;
|
||||
|
||||
@SerializedName("settlement_rate")
|
||||
public Long settlementRate;
|
||||
}
|
||||
|
||||
public static class UnionPromotionDetail {
|
||||
@SerializedName("coupon_id")
|
||||
public String couponId;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("type")
|
||||
public String type;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("stock_id")
|
||||
public String stockId;
|
||||
|
||||
@SerializedName("wechatpay_contribute")
|
||||
public Long wechatpayContribute;
|
||||
|
||||
@SerializedName("merchant_contribute")
|
||||
public Long merchantContribute;
|
||||
|
||||
@SerializedName("other_contribute")
|
||||
public Long otherContribute;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetailInPromotion> goodsDetail;
|
||||
}
|
||||
|
||||
public static class GoodsDetailInPromotion {
|
||||
@SerializedName("goods_id")
|
||||
public String goodsId;
|
||||
|
||||
@SerializedName("quantity")
|
||||
public Long quantity;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
|
||||
@SerializedName("discount_amount")
|
||||
public Long discountAmount;
|
||||
|
||||
@SerializedName("goods_remark")
|
||||
public String goodsRemark;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
# 合单订单支付成功回调通知
|
||||
|
||||
## 回调描述
|
||||
|
||||
用户使用合单支付(APP合单支付/H5合单支付/JSAPI合单支付/Native合单支付/小程序合单支付)功能,当用户成功支付订单后,微信支付会通过POST的请求方式,向商户预先设置的回调地址(APP合单支付/H5合单支付/JSAPI合单支付/Native合单支付/小程序合单支付下单接口传入的notify_url)发送回调通知,让商户知晓用户已完成支付。
|
||||
|
||||
> **注意**:商户侧对微信支付回调IP有防火墙策略限制的,需要对微信回调IP段开通白名单,否则会导致收不到回调(微信支付回调被商户防火墙拦截),详情参考回调处理逻辑注意事项。
|
||||
|
||||
## 回调报文格式
|
||||
|
||||
微信支付会通过POST的方式向回调地址发送回调报文,回调通知的请求主体中会包含JSON格式的通知参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "EV-2018022511223320873",
|
||||
"create_time": "2015-05-20T13:29:35+08:00",
|
||||
"resource_type": "encrypt-resource",
|
||||
"event_type": "TRANSACTION.SUCCESS",
|
||||
"summary": "支付成功",
|
||||
"resource": {
|
||||
"original_type": "transaction",
|
||||
"algorithm": "AEAD_AES_256_GCM",
|
||||
"ciphertext": "",
|
||||
"associated_data": "",
|
||||
"nonce": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 关键字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `event_type` | 事件类型,支付成功为 `TRANSACTION.SUCCESS` |
|
||||
| `resource.algorithm` | 加密算法,固定为 `AEAD_AES_256_GCM` |
|
||||
| `resource.ciphertext` | 密文,需使用商户APIv3密钥解密后得到合单订单详情 |
|
||||
| `resource.associated_data` | 附加数据,解密时作为 AAD 参数 |
|
||||
| `resource.nonce` | 随机串,解密时作为 Nonce 参数 |
|
||||
|
||||
## 解密后的合单订单明文结构
|
||||
|
||||
> 与基础支付回调的关键区别:解密后返回的是 `sub_orders[]` 数组(含各子单状态),而非单个订单。
|
||||
|
||||
```json
|
||||
{
|
||||
"combine_appid": "wxd678efh567hg6787",
|
||||
"combine_out_trade_no": "20150806125346",
|
||||
"combine_mchid": "1900000109",
|
||||
"scene_info": {
|
||||
"device_id": "POS1:1"
|
||||
},
|
||||
"sub_orders": [
|
||||
{
|
||||
"mchid": "1900000109",
|
||||
"trade_type": "JSAPI",
|
||||
"trade_state": "SUCCESS",
|
||||
"bank_type": "CMC",
|
||||
"attach": "深圳分店",
|
||||
"amount": {
|
||||
"total_amount": 10,
|
||||
"currency": "CNY",
|
||||
"payer_amount": 10,
|
||||
"payer_currency": "CNY"
|
||||
},
|
||||
"success_time": "2015-05-20T13:29:35.120+08:00",
|
||||
"transaction_id": "1009660380201506130728806387",
|
||||
"out_trade_no": "20150806125346"
|
||||
},
|
||||
{
|
||||
"mchid": "1900000179",
|
||||
"trade_type": "JSAPI",
|
||||
"trade_state": "SUCCESS",
|
||||
"bank_type": "CMC",
|
||||
"attach": "广州分店",
|
||||
"amount": {
|
||||
"total_amount": 10,
|
||||
"currency": "CNY",
|
||||
"payer_amount": 10,
|
||||
"payer_currency": "CNY"
|
||||
},
|
||||
"success_time": "2015-05-20T13:29:35.120+08:00",
|
||||
"transaction_id": "1009660380201506130728452147",
|
||||
"out_trade_no": "20150806124855"
|
||||
}
|
||||
],
|
||||
"combine_payer_info": {
|
||||
"openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 解密后关键字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `combine_appid` | 合单发起方的 APPID |
|
||||
| `combine_mchid` | 合单发起方的商户号 |
|
||||
| `combine_out_trade_no` | 合单商户订单号 |
|
||||
| `sub_orders[].mchid` | 子单商户号 |
|
||||
| `sub_orders[].trade_type` | 交易类型:`JSAPI`、`NATIVE`、`APP`、`MWEB` |
|
||||
| `sub_orders[].trade_state` | 交易状态:`SUCCESS`(支付成功)、`NOTPAY`(未支付)、`CLOSED`(已关闭) |
|
||||
| `sub_orders[].transaction_id` | 子单微信支付订单号(退款时需用此字段) |
|
||||
| `sub_orders[].out_trade_no` | 子单商户订单号 |
|
||||
| `sub_orders[].amount.total_amount` | 子单金额,单位:分 |
|
||||
| `sub_orders[].amount.payer_amount` | 用户实付金额(= total_amount - 代金券优惠) |
|
||||
| `sub_orders[].success_time` | 支付完成时间,rfc3339 格式 |
|
||||
| `sub_orders[].bank_type` | 银行类型,非银行卡支付统一返回 `OTHERS` |
|
||||
| `combine_payer_info.openid` | 实际支付用户在 `combine_appid` 下的 openid |
|
||||
|
||||
## 处理要求
|
||||
|
||||
1. 接收到回调后,先验证请求签名(使用微信支付公钥验签),验签机制与基础支付相同
|
||||
2. 使用商户APIv3密钥 + AEAD_AES_256_GCM 解密 `resource.ciphertext` 得到合单订单明文
|
||||
3. 验签通过后返回 HTTP 200 或 204(无应答报文),表示确认收到
|
||||
4. 验签不通过返回 HTTP 5XX 或 4XX + `{"code": "FAIL", "message": "失败原因"}`
|
||||
5. 若返回非200或超时(5秒),微信支付按策略重试(15s/15s/30s/3m/10m/20m/30m/30m/30m/60m/3h/3h/3h/6h/6h),最多15次
|
||||
6. 不可仅依赖回调获取支付结果,需结合**查询合单订单接口**使用
|
||||
7. 收到重复回调通知时需做好幂等处理,持续应答 200
|
||||
@@ -0,0 +1,174 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 添加分账接收方
|
||||
*/
|
||||
public class AddReceiver {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/profitsharing/receivers/add";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
AddReceiver client = new AddReceiver(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
AddReceiverRequest request = new AddReceiverRequest();
|
||||
request.appid = "wx8888888888888888";
|
||||
request.type = ReceiverType.MERCHANT_ID;
|
||||
request.account = "86693852";
|
||||
request.name = client.encrypt("name");
|
||||
request.relationType = ReceiverRelationType.STORE;
|
||||
request.customRelation = "代理商";
|
||||
try {
|
||||
AddReceiverResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public AddReceiverResponse run(AddReceiverRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, AddReceiverResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public AddReceiver(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public String encrypt(String plainText) {
|
||||
return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText);
|
||||
}
|
||||
|
||||
public static class AddReceiverRequest {
|
||||
@SerializedName("appid")
|
||||
public String appid;
|
||||
|
||||
@SerializedName("type")
|
||||
public ReceiverType type;
|
||||
|
||||
@SerializedName("account")
|
||||
public String account;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("relation_type")
|
||||
public ReceiverRelationType relationType;
|
||||
|
||||
@SerializedName("custom_relation")
|
||||
public String customRelation;
|
||||
}
|
||||
|
||||
public static class AddReceiverResponse {
|
||||
@SerializedName("type")
|
||||
public ReceiverType type;
|
||||
|
||||
@SerializedName("account")
|
||||
public String account;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("relation_type")
|
||||
public ReceiverRelationType relationType;
|
||||
|
||||
@SerializedName("custom_relation")
|
||||
public String customRelation;
|
||||
}
|
||||
|
||||
public enum ReceiverType {
|
||||
@SerializedName("MERCHANT_ID")
|
||||
MERCHANT_ID,
|
||||
@SerializedName("PERSONAL_OPENID")
|
||||
PERSONAL_OPENID
|
||||
}
|
||||
|
||||
public enum ReceiverRelationType {
|
||||
@SerializedName("STORE")
|
||||
STORE,
|
||||
@SerializedName("STAFF")
|
||||
STAFF,
|
||||
@SerializedName("STORE_OWNER")
|
||||
STORE_OWNER,
|
||||
@SerializedName("PARTNER")
|
||||
PARTNER,
|
||||
@SerializedName("HEADQUARTER")
|
||||
HEADQUARTER,
|
||||
@SerializedName("BRAND")
|
||||
BRAND,
|
||||
@SerializedName("DISTRIBUTOR")
|
||||
DISTRIBUTOR,
|
||||
@SerializedName("USER")
|
||||
USER,
|
||||
@SerializedName("SUPPLIER")
|
||||
SUPPLIER,
|
||||
@SerializedName("CUSTOM")
|
||||
CUSTOM
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 请求分账
|
||||
*/
|
||||
public class CreateOrder {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/profitsharing/orders";
|
||||
|
||||
public static void main(String[] args) {
|
||||
CreateOrder client = new CreateOrder(
|
||||
"19xxxxxxxx",
|
||||
"1DDE55AD98Exxxxxxxxxx",
|
||||
"/path/to/apiclient_key.pem",
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx",
|
||||
"/path/to/wxp_pub.pem"
|
||||
);
|
||||
|
||||
CreateOrderRequest request = new CreateOrderRequest();
|
||||
request.appid = "wx8888888888888888";
|
||||
request.transactionId = "4208450740201411110007820472";
|
||||
request.outOrderNo = "P20150806125346";
|
||||
request.receivers = new ArrayList<>();
|
||||
{
|
||||
CreateOrderReceiver receiversItem = new CreateOrderReceiver();
|
||||
receiversItem.type = "MERCHANT_ID";
|
||||
receiversItem.account = "86693852";
|
||||
receiversItem.name = client.encrypt("name");
|
||||
receiversItem.amount = 888L;
|
||||
receiversItem.description = "分给商户A";
|
||||
request.receivers.add(receiversItem);
|
||||
};
|
||||
request.unfreezeUnsplit = true;
|
||||
try {
|
||||
OrdersEntity response = client.run(request);
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public OrdersEntity run(CreateOrderRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
return WXPayUtility.fromJson(respBody, OrdersEntity.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public CreateOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public String encrypt(String plainText) {
|
||||
return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText);
|
||||
}
|
||||
|
||||
public static class CreateOrderRequest {
|
||||
@SerializedName("appid")
|
||||
public String appid;
|
||||
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("out_order_no")
|
||||
public String outOrderNo;
|
||||
|
||||
@SerializedName("receivers")
|
||||
public List<CreateOrderReceiver> receivers = new ArrayList<CreateOrderReceiver>();
|
||||
|
||||
@SerializedName("unfreeze_unsplit")
|
||||
public Boolean unfreezeUnsplit;
|
||||
}
|
||||
|
||||
public static class OrdersEntity {
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("out_order_no")
|
||||
public String outOrderNo;
|
||||
|
||||
@SerializedName("order_id")
|
||||
public String orderId;
|
||||
|
||||
@SerializedName("state")
|
||||
public OrderStatus state;
|
||||
|
||||
@SerializedName("receivers")
|
||||
public List<OrderReceiverDetail> receivers = new ArrayList<OrderReceiverDetail>();
|
||||
}
|
||||
|
||||
public static class CreateOrderReceiver {
|
||||
@SerializedName("type")
|
||||
public String type;
|
||||
|
||||
@SerializedName("account")
|
||||
public String account;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
}
|
||||
|
||||
public enum OrderStatus {
|
||||
@SerializedName("PROCESSING") PROCESSING,
|
||||
@SerializedName("FINISHED") FINISHED
|
||||
}
|
||||
|
||||
public static class OrderReceiverDetail {
|
||||
@SerializedName("amount") public Long amount;
|
||||
@SerializedName("description") public String description;
|
||||
@SerializedName("type") public ReceiverType type;
|
||||
@SerializedName("account") public String account;
|
||||
@SerializedName("result") public DetailStatus result;
|
||||
@SerializedName("fail_reason") public DetailFailReason failReason;
|
||||
@SerializedName("create_time") public String createTime;
|
||||
@SerializedName("finish_time") public String finishTime;
|
||||
@SerializedName("detail_id") public String detailId;
|
||||
}
|
||||
|
||||
public enum ReceiverType {
|
||||
@SerializedName("MERCHANT_ID") MERCHANT_ID,
|
||||
@SerializedName("PERSONAL_OPENID") PERSONAL_OPENID
|
||||
}
|
||||
|
||||
public enum DetailStatus {
|
||||
@SerializedName("PENDING") PENDING,
|
||||
@SerializedName("SUCCESS") SUCCESS,
|
||||
@SerializedName("CLOSED") CLOSED
|
||||
}
|
||||
|
||||
public enum DetailFailReason {
|
||||
@SerializedName("ACCOUNT_ABNORMAL") ACCOUNT_ABNORMAL,
|
||||
@SerializedName("NO_RELATION") NO_RELATION,
|
||||
@SerializedName("RECEIVER_HIGH_RISK") RECEIVER_HIGH_RISK,
|
||||
@SerializedName("RECEIVER_REAL_NAME_NOT_VERIFIED") RECEIVER_REAL_NAME_NOT_VERIFIED,
|
||||
@SerializedName("NO_AUTH") NO_AUTH,
|
||||
@SerializedName("RECEIVER_RECEIPT_LIMIT") RECEIVER_RECEIPT_LIMIT,
|
||||
@SerializedName("PAYER_ACCOUNT_ABNORMAL") PAYER_ACCOUNT_ABNORMAL,
|
||||
@SerializedName("INVALID_REQUEST") INVALID_REQUEST
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 请求分账回退
|
||||
*/
|
||||
public class CreateReturnOrder {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/profitsharing/return-orders";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
CreateReturnOrder client = new CreateReturnOrder(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
CreateReturnOrderRequest request = new CreateReturnOrderRequest();
|
||||
request.orderId = "3008450740201411110007820472";
|
||||
request.outOrderNo = "P20150806125346";
|
||||
request.outReturnNo = "R20190516001";
|
||||
request.returnMchid = "86693852";
|
||||
request.amount = 10L;
|
||||
request.description = "用户退款";
|
||||
try {
|
||||
ReturnOrdersEntity response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public ReturnOrdersEntity run(CreateReturnOrderRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, ReturnOrdersEntity.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public CreateReturnOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class CreateReturnOrderRequest {
|
||||
@SerializedName("order_id")
|
||||
public String orderId;
|
||||
|
||||
@SerializedName("out_order_no")
|
||||
public String outOrderNo;
|
||||
|
||||
@SerializedName("out_return_no")
|
||||
public String outReturnNo;
|
||||
|
||||
@SerializedName("return_mchid")
|
||||
public String returnMchid;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
}
|
||||
|
||||
public static class ReturnOrdersEntity {
|
||||
@SerializedName("order_id")
|
||||
public String orderId;
|
||||
|
||||
@SerializedName("out_order_no")
|
||||
public String outOrderNo;
|
||||
|
||||
@SerializedName("out_return_no")
|
||||
public String outReturnNo;
|
||||
|
||||
@SerializedName("return_id")
|
||||
public String returnId;
|
||||
|
||||
@SerializedName("return_mchid")
|
||||
public String returnMchid;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("result")
|
||||
public ReturnOrderStatus result;
|
||||
|
||||
@SerializedName("fail_reason")
|
||||
public ReturnOrderFailReason failReason;
|
||||
|
||||
@SerializedName("create_time")
|
||||
public String createTime;
|
||||
|
||||
@SerializedName("finish_time")
|
||||
public String finishTime;
|
||||
}
|
||||
|
||||
public enum ReturnOrderStatus {
|
||||
@SerializedName("PROCESSING")
|
||||
PROCESSING,
|
||||
@SerializedName("SUCCESS")
|
||||
SUCCESS,
|
||||
@SerializedName("FAILED")
|
||||
FAILED
|
||||
}
|
||||
|
||||
public enum ReturnOrderFailReason {
|
||||
@SerializedName("ACCOUNT_ABNORMAL")
|
||||
ACCOUNT_ABNORMAL,
|
||||
@SerializedName("BALANCE_NOT_ENOUGH")
|
||||
BALANCE_NOT_ENOUGH,
|
||||
@SerializedName("TIME_OUT_CLOSED")
|
||||
TIME_OUT_CLOSED,
|
||||
@SerializedName("PAYER_ACCOUNT_ABNORMAL")
|
||||
PAYER_ACCOUNT_ABNORMAL,
|
||||
@SerializedName("INVALID_REQUEST")
|
||||
INVALID_REQUEST
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 删除分账接收方
|
||||
*/
|
||||
public class DeleteReceiver {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/profitsharing/receivers/delete";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
DeleteReceiver client = new DeleteReceiver(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
DeleteReceiverRequest request = new DeleteReceiverRequest();
|
||||
request.appid = "wx8888888888888888";
|
||||
request.type = ReceiverType.MERCHANT_ID;
|
||||
request.account = "1900000109";
|
||||
try {
|
||||
DeleteReceiverResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public DeleteReceiverResponse run(DeleteReceiverRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, DeleteReceiverResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public DeleteReceiver(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class DeleteReceiverRequest {
|
||||
@SerializedName("appid")
|
||||
public String appid;
|
||||
|
||||
@SerializedName("type")
|
||||
public ReceiverType type;
|
||||
|
||||
@SerializedName("account")
|
||||
public String account;
|
||||
}
|
||||
|
||||
public static class DeleteReceiverResponse {
|
||||
@SerializedName("type")
|
||||
public ReceiverType type;
|
||||
|
||||
@SerializedName("account")
|
||||
public String account;
|
||||
}
|
||||
|
||||
public enum ReceiverType {
|
||||
@SerializedName("MERCHANT_ID")
|
||||
MERCHANT_ID,
|
||||
@SerializedName("PERSONAL_OPENID")
|
||||
PERSONAL_OPENID
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 查询分账结果
|
||||
*/
|
||||
public class QueryOrder {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "GET";
|
||||
private static String PATH = "/v3/profitsharing/orders/{out_order_no}";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
QueryOrder client = new QueryOrder(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
QueryOrderRequest request = new QueryOrderRequest();
|
||||
request.outOrderNo = "P20150806125346";
|
||||
request.transactionId = "4208450740201411110007820472";
|
||||
try {
|
||||
OrdersEntity response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public OrdersEntity run(QueryOrderRequest request) {
|
||||
String uri = PATH;
|
||||
uri = uri.replace("{out_order_no}", WXPayUtility.urlEncode(request.outOrderNo));
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("transaction_id", request.transactionId);
|
||||
String queryString = WXPayUtility.urlEncode(args);
|
||||
if (!queryString.isEmpty()) {
|
||||
uri = uri + "?" + queryString;
|
||||
}
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, OrdersEntity.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public QueryOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class QueryOrderRequest {
|
||||
@SerializedName("transaction_id")
|
||||
@Expose(serialize = false)
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("out_order_no")
|
||||
@Expose(serialize = false)
|
||||
public String outOrderNo;
|
||||
}
|
||||
|
||||
public static class OrdersEntity {
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("out_order_no")
|
||||
public String outOrderNo;
|
||||
|
||||
@SerializedName("order_id")
|
||||
public String orderId;
|
||||
|
||||
@SerializedName("state")
|
||||
public OrderStatus state;
|
||||
|
||||
@SerializedName("receivers")
|
||||
public List<OrderReceiverDetail> receivers = new ArrayList<OrderReceiverDetail>();
|
||||
}
|
||||
|
||||
public enum OrderStatus {
|
||||
@SerializedName("PROCESSING")
|
||||
PROCESSING,
|
||||
@SerializedName("FINISHED")
|
||||
FINISHED
|
||||
}
|
||||
|
||||
public static class OrderReceiverDetail {
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("type")
|
||||
public ReceiverType type;
|
||||
|
||||
@SerializedName("account")
|
||||
public String account;
|
||||
|
||||
@SerializedName("result")
|
||||
public DetailStatus result;
|
||||
|
||||
@SerializedName("fail_reason")
|
||||
public DetailFailReason failReason;
|
||||
|
||||
@SerializedName("create_time")
|
||||
public String createTime;
|
||||
|
||||
@SerializedName("finish_time")
|
||||
public String finishTime;
|
||||
|
||||
@SerializedName("detail_id")
|
||||
public String detailId;
|
||||
}
|
||||
|
||||
public enum ReceiverType {
|
||||
@SerializedName("MERCHANT_ID")
|
||||
MERCHANT_ID,
|
||||
@SerializedName("PERSONAL_OPENID")
|
||||
PERSONAL_OPENID
|
||||
}
|
||||
|
||||
public enum DetailStatus {
|
||||
@SerializedName("PENDING")
|
||||
PENDING,
|
||||
@SerializedName("SUCCESS")
|
||||
SUCCESS,
|
||||
@SerializedName("CLOSED")
|
||||
CLOSED
|
||||
}
|
||||
|
||||
public enum DetailFailReason {
|
||||
@SerializedName("ACCOUNT_ABNORMAL")
|
||||
ACCOUNT_ABNORMAL,
|
||||
@SerializedName("NO_RELATION")
|
||||
NO_RELATION,
|
||||
@SerializedName("RECEIVER_HIGH_RISK")
|
||||
RECEIVER_HIGH_RISK,
|
||||
@SerializedName("RECEIVER_REAL_NAME_NOT_VERIFIED")
|
||||
RECEIVER_REAL_NAME_NOT_VERIFIED,
|
||||
@SerializedName("NO_AUTH")
|
||||
NO_AUTH,
|
||||
@SerializedName("RECEIVER_RECEIPT_LIMIT")
|
||||
RECEIVER_RECEIPT_LIMIT,
|
||||
@SerializedName("PAYER_ACCOUNT_ABNORMAL")
|
||||
PAYER_ACCOUNT_ABNORMAL,
|
||||
@SerializedName("INVALID_REQUEST")
|
||||
INVALID_REQUEST
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 查询剩余待分金额
|
||||
*/
|
||||
public class QueryOrderAmount {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "GET";
|
||||
private static String PATH = "/v3/profitsharing/transactions/{transaction_id}/amounts";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
QueryOrderAmount client = new QueryOrderAmount(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
QueryOrderAmountRequest request = new QueryOrderAmountRequest();
|
||||
request.transactionId = "4208450740201411110007820472";
|
||||
try {
|
||||
QueryOrderAmountResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public QueryOrderAmountResponse run(QueryOrderAmountRequest request) {
|
||||
String uri = PATH;
|
||||
uri = uri.replace("{transaction_id}", WXPayUtility.urlEncode(request.transactionId));
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, QueryOrderAmountResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public QueryOrderAmount(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class QueryOrderAmountRequest {
|
||||
@SerializedName("transaction_id")
|
||||
@Expose(serialize = false)
|
||||
public String transactionId;
|
||||
}
|
||||
|
||||
public static class QueryOrderAmountResponse {
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("unsplit_amount")
|
||||
public Long unsplitAmount;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 查询分账回退结果
|
||||
*/
|
||||
public class QueryReturnOrder {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "GET";
|
||||
private static String PATH = "/v3/profitsharing/return-orders/{out_return_no}";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
QueryReturnOrder client = new QueryReturnOrder(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
QueryReturnOrderRequest request = new QueryReturnOrderRequest();
|
||||
request.outReturnNo = "R20190516001";
|
||||
request.outOrderNo = "P20190806125346";
|
||||
try {
|
||||
ReturnOrdersEntity response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public ReturnOrdersEntity run(QueryReturnOrderRequest request) {
|
||||
String uri = PATH;
|
||||
uri = uri.replace("{out_return_no}", WXPayUtility.urlEncode(request.outReturnNo));
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("out_order_no", request.outOrderNo);
|
||||
String queryString = WXPayUtility.urlEncode(args);
|
||||
if (!queryString.isEmpty()) {
|
||||
uri = uri + "?" + queryString;
|
||||
}
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, ReturnOrdersEntity.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public QueryReturnOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class QueryReturnOrderRequest {
|
||||
@SerializedName("out_return_no")
|
||||
@Expose(serialize = false)
|
||||
public String outReturnNo;
|
||||
|
||||
@SerializedName("out_order_no")
|
||||
@Expose(serialize = false)
|
||||
public String outOrderNo;
|
||||
}
|
||||
|
||||
public static class ReturnOrdersEntity {
|
||||
@SerializedName("order_id")
|
||||
public String orderId;
|
||||
|
||||
@SerializedName("out_order_no")
|
||||
public String outOrderNo;
|
||||
|
||||
@SerializedName("out_return_no")
|
||||
public String outReturnNo;
|
||||
|
||||
@SerializedName("return_id")
|
||||
public String returnId;
|
||||
|
||||
@SerializedName("return_mchid")
|
||||
public String returnMchid;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("result")
|
||||
public ReturnOrderStatus result;
|
||||
|
||||
@SerializedName("fail_reason")
|
||||
public ReturnOrderFailReason failReason;
|
||||
|
||||
@SerializedName("create_time")
|
||||
public String createTime;
|
||||
|
||||
@SerializedName("finish_time")
|
||||
public String finishTime;
|
||||
}
|
||||
|
||||
public enum ReturnOrderStatus {
|
||||
@SerializedName("PROCESSING")
|
||||
PROCESSING,
|
||||
@SerializedName("SUCCESS")
|
||||
SUCCESS,
|
||||
@SerializedName("FAILED")
|
||||
FAILED
|
||||
}
|
||||
|
||||
public enum ReturnOrderFailReason {
|
||||
@SerializedName("ACCOUNT_ABNORMAL")
|
||||
ACCOUNT_ABNORMAL,
|
||||
@SerializedName("BALANCE_NOT_ENOUGH")
|
||||
BALANCE_NOT_ENOUGH,
|
||||
@SerializedName("TIME_OUT_CLOSED")
|
||||
TIME_OUT_CLOSED,
|
||||
@SerializedName("PAYER_ACCOUNT_ABNORMAL")
|
||||
PAYER_ACCOUNT_ABNORMAL,
|
||||
@SerializedName("INVALID_REQUEST")
|
||||
INVALID_REQUEST
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 申请分账账单
|
||||
*/
|
||||
public class SplitBill {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "GET";
|
||||
private static String PATH = "/v3/profitsharing/bills";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
SplitBill client = new SplitBill(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
SplitBillRequest request = new SplitBillRequest();
|
||||
request.billDate = "2019-06-11";
|
||||
request.tarType = SplitBillTarType.GZIP;
|
||||
try {
|
||||
SplitBillResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public SplitBillResponse run(SplitBillRequest request) {
|
||||
String uri = PATH;
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("bill_date", request.billDate);
|
||||
args.put("tar_type", request.tarType);
|
||||
String queryString = WXPayUtility.urlEncode(args);
|
||||
if (!queryString.isEmpty()) {
|
||||
uri = uri + "?" + queryString;
|
||||
}
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, SplitBillResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public SplitBill(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class SplitBillRequest {
|
||||
@SerializedName("bill_date")
|
||||
@Expose(serialize = false)
|
||||
public String billDate;
|
||||
|
||||
@SerializedName("tar_type")
|
||||
@Expose(serialize = false)
|
||||
public SplitBillTarType tarType;
|
||||
}
|
||||
|
||||
public static class SplitBillResponse {
|
||||
@SerializedName("hash_type")
|
||||
public SplitBillHashType hashType;
|
||||
|
||||
@SerializedName("hash_value")
|
||||
public String hashValue;
|
||||
|
||||
@SerializedName("download_url")
|
||||
public String downloadUrl;
|
||||
}
|
||||
|
||||
public enum SplitBillTarType {
|
||||
@SerializedName("GZIP")
|
||||
GZIP
|
||||
}
|
||||
|
||||
public enum SplitBillHashType {
|
||||
@SerializedName("SHA1")
|
||||
SHA1
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 解冻剩余资金
|
||||
*/
|
||||
public class UnfreezeOrder {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/profitsharing/orders/unfreeze";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
UnfreezeOrder client = new UnfreezeOrder(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
UnfreezeOrderRequest request = new UnfreezeOrderRequest();
|
||||
request.transactionId = "4208450740201411110007820472";
|
||||
request.outOrderNo = "P20150806125346";
|
||||
request.description = "解冻全部剩余资金";
|
||||
try {
|
||||
OrdersEntity response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public OrdersEntity run(UnfreezeOrderRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, OrdersEntity.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public UnfreezeOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class UnfreezeOrderRequest {
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("out_order_no")
|
||||
public String outOrderNo;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
}
|
||||
|
||||
public static class OrdersEntity {
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("out_order_no")
|
||||
public String outOrderNo;
|
||||
|
||||
@SerializedName("order_id")
|
||||
public String orderId;
|
||||
|
||||
@SerializedName("state")
|
||||
public OrderStatus state;
|
||||
|
||||
@SerializedName("receivers")
|
||||
public List<OrderReceiverDetail> receivers = new ArrayList<OrderReceiverDetail>();
|
||||
}
|
||||
|
||||
public enum OrderStatus {
|
||||
@SerializedName("PROCESSING")
|
||||
PROCESSING,
|
||||
@SerializedName("FINISHED")
|
||||
FINISHED
|
||||
}
|
||||
|
||||
public static class OrderReceiverDetail {
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("type")
|
||||
public ReceiverType type;
|
||||
|
||||
@SerializedName("account")
|
||||
public String account;
|
||||
|
||||
@SerializedName("result")
|
||||
public DetailStatus result;
|
||||
|
||||
@SerializedName("fail_reason")
|
||||
public DetailFailReason failReason;
|
||||
|
||||
@SerializedName("create_time")
|
||||
public String createTime;
|
||||
|
||||
@SerializedName("finish_time")
|
||||
public String finishTime;
|
||||
|
||||
@SerializedName("detail_id")
|
||||
public String detailId;
|
||||
}
|
||||
|
||||
public enum ReceiverType {
|
||||
@SerializedName("MERCHANT_ID")
|
||||
MERCHANT_ID,
|
||||
@SerializedName("PERSONAL_OPENID")
|
||||
PERSONAL_OPENID
|
||||
}
|
||||
|
||||
public enum DetailStatus {
|
||||
@SerializedName("PENDING")
|
||||
PENDING,
|
||||
@SerializedName("SUCCESS")
|
||||
SUCCESS,
|
||||
@SerializedName("CLOSED")
|
||||
CLOSED
|
||||
}
|
||||
|
||||
public enum DetailFailReason {
|
||||
@SerializedName("ACCOUNT_ABNORMAL")
|
||||
ACCOUNT_ABNORMAL,
|
||||
@SerializedName("NO_RELATION")
|
||||
NO_RELATION,
|
||||
@SerializedName("RECEIVER_HIGH_RISK")
|
||||
RECEIVER_HIGH_RISK,
|
||||
@SerializedName("RECEIVER_REAL_NAME_NOT_VERIFIED")
|
||||
RECEIVER_REAL_NAME_NOT_VERIFIED,
|
||||
@SerializedName("NO_AUTH")
|
||||
NO_AUTH,
|
||||
@SerializedName("RECEIVER_RECEIPT_LIMIT")
|
||||
RECEIVER_RECEIPT_LIMIT,
|
||||
@SerializedName("PAYER_ACCOUNT_ABNORMAL")
|
||||
PAYER_ACCOUNT_ABNORMAL,
|
||||
@SerializedName("INVALID_REQUEST")
|
||||
INVALID_REQUEST
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
# APP调起支付
|
||||
|
||||
> 参考官方文档:https://pay.weixin.qq.com/doc/v3/merchant/4013070351
|
||||
|
||||
## 说明
|
||||
|
||||
商户通过APP下单接口获取到发起支付的必要参数 `prepay_id` 后,商户APP再通过 OpenSDK 的 `sendReq` 方法拉起微信支付。
|
||||
|
||||
**注意**:需严格遵循 OpenSDK 接入指引([Android](https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/Android.html) / [iOS](https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/iOS.html) / [鸿蒙](https://developers.weixin.qq.com/doc/oplatform/Mobile_App/Access_Guide/ohos.html))接入SDK以及配置开发环境。
|
||||
|
||||
## 请求参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `appId` | 是 | 下单时传入的应用ID |
|
||||
| `partnerId` | 是 | 下单时传入的商户号 |
|
||||
| `prepayId` | 是 | APP下单接口返回的 prepay_id,有效期2小时 |
|
||||
| `packageValue` | 是 | 固定值 `Sign=WXPay`(iOS 中字段名为 `package`) |
|
||||
| `nonceStr` | 是 | 随机字符串,不长于32位 |
|
||||
| `timeStamp` | 是 | Unix 秒级时间戳 |
|
||||
| `sign` | 是 | 使用 appId、timeStamp、nonceStr、prepayId + 商户API证书私钥生成的 RSA 签名 |
|
||||
|
||||
## iOS 示例代码
|
||||
|
||||
```objectivec
|
||||
PayReq *request = [[[PayReq alloc] init] autorelease];
|
||||
request.appId = "wxd930ea5d5a258f4f";
|
||||
request.partnerId = "1900000109";
|
||||
request.prepayId= "1101000000140415649af9fc314aa427",;
|
||||
request.package = "Sign=WXPay";
|
||||
request.nonceStr= "1101000000140429eb40476f8896f4c9";
|
||||
request.timeStamp= "1398746574";
|
||||
request.sign= "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==";
|
||||
[WXApi sendReq:request];
|
||||
```
|
||||
|
||||
## Android 示例代码
|
||||
|
||||
```java
|
||||
IWXAPI api;
|
||||
PayReq request = new PayReq();
|
||||
request.appId = "wxd930ea5d5a258f4f";
|
||||
request.partnerId = "1900000109";
|
||||
request.prepayId= "1101000000140415649af9fc314aa427",;
|
||||
request.packageValue = "Sign=WXPay";
|
||||
request.nonceStr= "1101000000140429eb40476f8896f4c9";
|
||||
request.timeStamp= "1398746574";
|
||||
request.sign= "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==";
|
||||
api.sendReq(request);
|
||||
```
|
||||
|
||||
## 鸿蒙示例代码
|
||||
|
||||
```typescript
|
||||
IWXAPI api;
|
||||
let req = new wxopensdk.PayReq
|
||||
req.appId = 'wxd930ea5d5a258f4f'
|
||||
req.partnerId = '1900000109'
|
||||
req.prepayId = '1101000000140415649af9fc314aa427'
|
||||
req.packageValue = 'Sign=WXPay'
|
||||
req.nonceStr = '1101000000140429eb40476f8896f4c9'
|
||||
req.timeStamp = '1398746574'
|
||||
req.sign = 'oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg=='
|
||||
api.sendReq(context: common.UIAbilityContext, req)
|
||||
```
|
||||
|
||||
## 回调 errCode 说明
|
||||
|
||||
| errCode | 描述 | 商户APP处理方案 |
|
||||
|---------|------|---------------|
|
||||
| 0 | 成功 | 调用后端查单接口,订单已支付则展示支付成功页面 |
|
||||
| -1 | 错误 | 可能原因:签名错误、未注册AppID、AppID不匹配等 |
|
||||
| -2 | 取消支付 | 用户取消支付返回App,商户自行处理展示 |
|
||||
|
||||
> **重要**:前端回调不保证绝对可靠,不可只依赖前端回调判断订单支付状态,订单状态需以后端查询订单和支付成功回调通知为准。
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* App下单
|
||||
*/
|
||||
public class AppPrepay {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/pay/transactions/app";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
AppPrepay client = new AppPrepay(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
CommonPrepayRequest request = new CommonPrepayRequest();
|
||||
request.appid = "wxd678efh567hg6787";
|
||||
request.mchid = "1230000109";
|
||||
request.description = "Image形象店-深圳腾大-QQ公仔";
|
||||
request.outTradeNo = "1217752501201407033233368018";
|
||||
request.timeExpire = "2018-06-08T10:34:56+08:00";
|
||||
request.attach = "自定义数据说明";
|
||||
request.notifyUrl = " https://www.weixin.qq.com/wxpay/pay.php";
|
||||
request.goodsTag = "WXG";
|
||||
request.supportFapiao = false;
|
||||
request.amount = new CommonAmountInfo();
|
||||
request.amount.total = 100L;
|
||||
request.amount.currency = "CNY";
|
||||
request.detail = new CouponInfo();
|
||||
request.detail.costPrice = 608800L;
|
||||
request.detail.invoiceId = "微信123";
|
||||
request.detail.goodsDetail = new ArrayList<>();
|
||||
{
|
||||
GoodsDetail goodsDetailItem = new GoodsDetail();
|
||||
goodsDetailItem.merchantGoodsId = "1246464644";
|
||||
goodsDetailItem.wechatpayGoodsId = "1001";
|
||||
goodsDetailItem.goodsName = "iPhoneX 256G";
|
||||
goodsDetailItem.quantity = 1L;
|
||||
goodsDetailItem.unitPrice = 528800L;
|
||||
request.detail.goodsDetail.add(goodsDetailItem);
|
||||
};
|
||||
request.sceneInfo = new CommonSceneInfo();
|
||||
request.sceneInfo.payerClientIp = "14.23.150.211";
|
||||
request.sceneInfo.deviceId = "013467007045764";
|
||||
request.sceneInfo.storeInfo = new StoreInfo();
|
||||
request.sceneInfo.storeInfo.id = "0001";
|
||||
request.sceneInfo.storeInfo.name = "腾讯大厦分店";
|
||||
request.sceneInfo.storeInfo.areaCode = "440305";
|
||||
request.sceneInfo.storeInfo.address = "广东省深圳市南山区科技中一道10000号";
|
||||
request.settleInfo = new SettleInfo();
|
||||
request.settleInfo.profitSharing = false;
|
||||
try {
|
||||
DirectAPIv3AppPrepayResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public DirectAPIv3AppPrepayResponse run(CommonPrepayRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, DirectAPIv3AppPrepayResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public AppPrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class CommonPrepayRequest {
|
||||
@SerializedName("appid")
|
||||
public String appid;
|
||||
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("time_expire")
|
||||
public String timeExpire;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("notify_url")
|
||||
public String notifyUrl;
|
||||
|
||||
@SerializedName("goods_tag")
|
||||
public String goodsTag;
|
||||
|
||||
@SerializedName("support_fapiao")
|
||||
public Boolean supportFapiao;
|
||||
|
||||
@SerializedName("amount")
|
||||
public CommonAmountInfo amount;
|
||||
|
||||
@SerializedName("detail")
|
||||
public CouponInfo detail;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public CommonSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("settle_info")
|
||||
public SettleInfo settleInfo;
|
||||
}
|
||||
|
||||
public static class DirectAPIv3AppPrepayResponse {
|
||||
@SerializedName("prepay_id")
|
||||
public String prepayId;
|
||||
}
|
||||
|
||||
public static class CommonAmountInfo {
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
}
|
||||
|
||||
public static class CouponInfo {
|
||||
@SerializedName("cost_price")
|
||||
public Long costPrice;
|
||||
|
||||
@SerializedName("invoice_id")
|
||||
public String invoiceId;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetail> goodsDetail;
|
||||
}
|
||||
|
||||
public static class CommonSceneInfo {
|
||||
@SerializedName("payer_client_ip")
|
||||
public String payerClientIp;
|
||||
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
|
||||
@SerializedName("store_info")
|
||||
public StoreInfo storeInfo;
|
||||
}
|
||||
|
||||
public static class SettleInfo {
|
||||
@SerializedName("profit_sharing")
|
||||
public Boolean profitSharing;
|
||||
}
|
||||
|
||||
public static class GoodsDetail {
|
||||
@SerializedName("merchant_goods_id")
|
||||
public String merchantGoodsId;
|
||||
|
||||
@SerializedName("wechatpay_goods_id")
|
||||
public String wechatpayGoodsId;
|
||||
|
||||
@SerializedName("goods_name")
|
||||
public String goodsName;
|
||||
|
||||
@SerializedName("quantity")
|
||||
public Long quantity;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
}
|
||||
|
||||
public static class StoreInfo {
|
||||
@SerializedName("id")
|
||||
public String id;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("area_code")
|
||||
public String areaCode;
|
||||
|
||||
@SerializedName("address")
|
||||
public String address;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
# H5调起支付
|
||||
|
||||
> 参考官方文档:https://pay.weixin.qq.com/doc/v3/merchant/4012791835
|
||||
|
||||
## 调起支付步骤
|
||||
|
||||
1. 商户通过H5下单接口获取到发起支付的必要参数 `h5_url`
|
||||
2. 商户在配置了H5支付域名的网页中跳转 `h5_url`,调起微信支付收银台中间页
|
||||
3. 微信支付收银台中间页会进行H5权限的校验和安全性检查,校验通过后用户可正常支付
|
||||
|
||||
## 支付后返回指定页面
|
||||
|
||||
用户支付完成后默认返回发起支付的页面。如需返回指定页面,可在 `h5_url` 后拼接 `redirect_url` 参数:
|
||||
|
||||
```
|
||||
h5_url=https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx20161110163838f231619da20804912345&package=1037687096&redirect_url=https%3A%2F%2Fwww.wechatpay.com.cn
|
||||
```
|
||||
|
||||
**注意**:
|
||||
- `redirect_url` 的域名必须为商户配置的H5支付域名
|
||||
- 需对 `redirect_url` 进行 urlencode 处理
|
||||
|
||||
## 返回商户页面后查单
|
||||
|
||||
用户点击"取消支付"或支付成功后点击"完成"时,会返回商户支付页面(或指定的 redirect_url 页面)。建议:
|
||||
|
||||
在回跳页面中增设"确认支付情况"按钮,用户点击后触发查单操作,确保用户能及时了解订单状态。
|
||||
|
||||
> **重要**:H5支付不像JSAPI/APP有前端回调,商户必须通过后端查单接口或支付成功回调通知来确认订单状态。
|
||||
@@ -0,0 +1,270 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* H5下单
|
||||
*/
|
||||
public class H5Prepay {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/pay/transactions/h5";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
H5Prepay client = new H5Prepay(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
DirectAPIv3H5PrepayRequest request = new DirectAPIv3H5PrepayRequest();
|
||||
request.appid = "wxd678efh567hg6787";
|
||||
request.mchid = "1230000109";
|
||||
request.description = "Image形象店-深圳腾大-QQ公仔";
|
||||
request.outTradeNo = "1217752501201407033233368018";
|
||||
request.timeExpire = "2018-06-08T10:34:56+08:00";
|
||||
request.attach = "自定义数据说明";
|
||||
request.notifyUrl = " https://www.weixin.qq.com/wxpay/pay.php";
|
||||
request.goodsTag = "WXG";
|
||||
request.supportFapiao = false;
|
||||
request.amount = new CommonAmountInfo();
|
||||
request.amount.total = 100L;
|
||||
request.amount.currency = "CNY";
|
||||
request.detail = new CouponInfo();
|
||||
request.detail.costPrice = 608800L;
|
||||
request.detail.invoiceId = "微信123";
|
||||
request.detail.goodsDetail = new ArrayList<>();
|
||||
{
|
||||
GoodsDetail goodsDetailItem = new GoodsDetail();
|
||||
goodsDetailItem.merchantGoodsId = "1246464644";
|
||||
goodsDetailItem.wechatpayGoodsId = "1001";
|
||||
goodsDetailItem.goodsName = "iPhoneX 256G";
|
||||
goodsDetailItem.quantity = 1L;
|
||||
goodsDetailItem.unitPrice = 528800L;
|
||||
request.detail.goodsDetail.add(goodsDetailItem);
|
||||
};
|
||||
request.sceneInfo = new H5ReqSceneInfo();
|
||||
request.sceneInfo.payerClientIp = "14.23.150.211";
|
||||
request.sceneInfo.deviceId = "013467007045764";
|
||||
request.sceneInfo.storeInfo = new StoreInfo();
|
||||
request.sceneInfo.storeInfo.id = "0001";
|
||||
request.sceneInfo.storeInfo.name = "腾讯大厦分店";
|
||||
request.sceneInfo.storeInfo.areaCode = "440305";
|
||||
request.sceneInfo.storeInfo.address = "广东省深圳市南山区科技中一道10000号";
|
||||
request.sceneInfo.h5Info = new H5Info();
|
||||
request.sceneInfo.h5Info.type = "iOS";
|
||||
request.sceneInfo.h5Info.appName = "王者荣耀";
|
||||
request.sceneInfo.h5Info.appUrl = "https://pay.qq.com";
|
||||
request.sceneInfo.h5Info.bundleId = "com.tencent.wzryiOS";
|
||||
request.sceneInfo.h5Info.packageName = "com.tencent.tmgp.sgame";
|
||||
request.settleInfo = new SettleInfo();
|
||||
request.settleInfo.profitSharing = false;
|
||||
try {
|
||||
DirectAPIv3H5PrepayResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public DirectAPIv3H5PrepayResponse run(DirectAPIv3H5PrepayRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, DirectAPIv3H5PrepayResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public H5Prepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class DirectAPIv3H5PrepayRequest {
|
||||
@SerializedName("appid")
|
||||
public String appid;
|
||||
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("time_expire")
|
||||
public String timeExpire;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("notify_url")
|
||||
public String notifyUrl;
|
||||
|
||||
@SerializedName("goods_tag")
|
||||
public String goodsTag;
|
||||
|
||||
@SerializedName("support_fapiao")
|
||||
public Boolean supportFapiao;
|
||||
|
||||
@SerializedName("amount")
|
||||
public CommonAmountInfo amount;
|
||||
|
||||
@SerializedName("detail")
|
||||
public CouponInfo detail;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public H5ReqSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("settle_info")
|
||||
public SettleInfo settleInfo;
|
||||
}
|
||||
|
||||
public static class DirectAPIv3H5PrepayResponse {
|
||||
@SerializedName("h5_url")
|
||||
public String h5Url;
|
||||
}
|
||||
|
||||
public static class CommonAmountInfo {
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
}
|
||||
|
||||
public static class CouponInfo {
|
||||
@SerializedName("cost_price")
|
||||
public Long costPrice;
|
||||
|
||||
@SerializedName("invoice_id")
|
||||
public String invoiceId;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetail> goodsDetail;
|
||||
}
|
||||
|
||||
public static class H5ReqSceneInfo {
|
||||
@SerializedName("payer_client_ip")
|
||||
public String payerClientIp;
|
||||
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
|
||||
@SerializedName("store_info")
|
||||
public StoreInfo storeInfo;
|
||||
|
||||
@SerializedName("h5_info")
|
||||
public H5Info h5Info;
|
||||
}
|
||||
|
||||
public static class SettleInfo {
|
||||
@SerializedName("profit_sharing")
|
||||
public Boolean profitSharing;
|
||||
}
|
||||
|
||||
public static class GoodsDetail {
|
||||
@SerializedName("merchant_goods_id")
|
||||
public String merchantGoodsId;
|
||||
|
||||
@SerializedName("wechatpay_goods_id")
|
||||
public String wechatpayGoodsId;
|
||||
|
||||
@SerializedName("goods_name")
|
||||
public String goodsName;
|
||||
|
||||
@SerializedName("quantity")
|
||||
public Long quantity;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
}
|
||||
|
||||
public static class StoreInfo {
|
||||
@SerializedName("id")
|
||||
public String id;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("area_code")
|
||||
public String areaCode;
|
||||
|
||||
@SerializedName("address")
|
||||
public String address;
|
||||
}
|
||||
|
||||
public static class H5Info {
|
||||
@SerializedName("type")
|
||||
public String type;
|
||||
|
||||
@SerializedName("app_name")
|
||||
public String appName;
|
||||
|
||||
@SerializedName("app_url")
|
||||
public String appUrl;
|
||||
|
||||
@SerializedName("bundle_id")
|
||||
public String bundleId;
|
||||
|
||||
@SerializedName("package_name")
|
||||
public String packageName;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
# Native调起支付
|
||||
|
||||
> 参考官方文档:https://pay.weixin.qq.com/doc/v3/merchant/4012791878
|
||||
|
||||
## 调起支付步骤
|
||||
|
||||
1. 通过 Native下单接口获取到发起支付的必要参数 `code_url`
|
||||
2. 将 `code_url` 链接转换为二维码图片后,展示给用户
|
||||
3. 用户打开微信"扫一扫"功能,扫描二维码,进行 Native 支付
|
||||
|
||||
## 示例
|
||||
|
||||
将 `weixin://pay.weixin.qq.com/bizpayurl/up?pr=NwY5Mz9&groupid=00` 生成二维码,展示给用户扫码即可。
|
||||
|
||||
## 注意事项
|
||||
|
||||
- Native 支付的 `code_url` 有效期为2小时,超时需重新调用下单接口获取新的 `code_url`
|
||||
- 二维码仅支持微信"扫一扫"功能扫描,不支持长按识别或相册识别
|
||||
- 商户需通过后端查单接口或支付成功回调通知来确认订单状态,不能仅依赖用户告知
|
||||
@@ -0,0 +1,244 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Native下单
|
||||
*/
|
||||
public class NativePrepay {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/pay/transactions/native";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
NativePrepay client = new NativePrepay(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
CommonPrepayRequest request = new CommonPrepayRequest();
|
||||
request.appid = "wxd678efh567hg6787";
|
||||
request.mchid = "1230000109";
|
||||
request.description = "Image形象店-深圳腾大-QQ公仔";
|
||||
request.outTradeNo = "1217752501201407033233368018";
|
||||
request.timeExpire = "2018-06-08T10:34:56+08:00";
|
||||
request.attach = "自定义数据说明";
|
||||
request.notifyUrl = " https://www.weixin.qq.com/wxpay/pay.php";
|
||||
request.goodsTag = "WXG";
|
||||
request.supportFapiao = false;
|
||||
request.amount = new CommonAmountInfo();
|
||||
request.amount.total = 100L;
|
||||
request.amount.currency = "CNY";
|
||||
request.detail = new CouponInfo();
|
||||
request.detail.costPrice = 608800L;
|
||||
request.detail.invoiceId = "微信123";
|
||||
request.detail.goodsDetail = new ArrayList<>();
|
||||
{
|
||||
GoodsDetail goodsDetailItem = new GoodsDetail();
|
||||
goodsDetailItem.merchantGoodsId = "1246464644";
|
||||
goodsDetailItem.wechatpayGoodsId = "1001";
|
||||
goodsDetailItem.goodsName = "iPhoneX 256G";
|
||||
goodsDetailItem.quantity = 1L;
|
||||
goodsDetailItem.unitPrice = 528800L;
|
||||
request.detail.goodsDetail.add(goodsDetailItem);
|
||||
};
|
||||
request.sceneInfo = new CommonSceneInfo();
|
||||
request.sceneInfo.payerClientIp = "14.23.150.211";
|
||||
request.sceneInfo.deviceId = "013467007045764";
|
||||
request.sceneInfo.storeInfo = new StoreInfo();
|
||||
request.sceneInfo.storeInfo.id = "0001";
|
||||
request.sceneInfo.storeInfo.name = "腾讯大厦分店";
|
||||
request.sceneInfo.storeInfo.areaCode = "440305";
|
||||
request.sceneInfo.storeInfo.address = "广东省深圳市南山区科技中一道10000号";
|
||||
request.settleInfo = new SettleInfo();
|
||||
request.settleInfo.profitSharing = false;
|
||||
try {
|
||||
DirectAPIv3DirectNativePrepayResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public DirectAPIv3DirectNativePrepayResponse run(CommonPrepayRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, DirectAPIv3DirectNativePrepayResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public NativePrepay(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class CommonPrepayRequest {
|
||||
@SerializedName("appid")
|
||||
public String appid;
|
||||
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("description")
|
||||
public String description;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("time_expire")
|
||||
public String timeExpire;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("notify_url")
|
||||
public String notifyUrl;
|
||||
|
||||
@SerializedName("goods_tag")
|
||||
public String goodsTag;
|
||||
|
||||
@SerializedName("support_fapiao")
|
||||
public Boolean supportFapiao;
|
||||
|
||||
@SerializedName("amount")
|
||||
public CommonAmountInfo amount;
|
||||
|
||||
@SerializedName("detail")
|
||||
public CouponInfo detail;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public CommonSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("settle_info")
|
||||
public SettleInfo settleInfo;
|
||||
}
|
||||
|
||||
public static class DirectAPIv3DirectNativePrepayResponse {
|
||||
@SerializedName("code_url")
|
||||
public String codeUrl;
|
||||
}
|
||||
|
||||
public static class CommonAmountInfo {
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
}
|
||||
|
||||
public static class CouponInfo {
|
||||
@SerializedName("cost_price")
|
||||
public Long costPrice;
|
||||
|
||||
@SerializedName("invoice_id")
|
||||
public String invoiceId;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetail> goodsDetail;
|
||||
}
|
||||
|
||||
public static class CommonSceneInfo {
|
||||
@SerializedName("payer_client_ip")
|
||||
public String payerClientIp;
|
||||
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
|
||||
@SerializedName("store_info")
|
||||
public StoreInfo storeInfo;
|
||||
}
|
||||
|
||||
public static class SettleInfo {
|
||||
@SerializedName("profit_sharing")
|
||||
public Boolean profitSharing;
|
||||
}
|
||||
|
||||
public static class GoodsDetail {
|
||||
@SerializedName("merchant_goods_id")
|
||||
public String merchantGoodsId;
|
||||
|
||||
@SerializedName("wechatpay_goods_id")
|
||||
public String wechatpayGoodsId;
|
||||
|
||||
@SerializedName("goods_name")
|
||||
public String goodsName;
|
||||
|
||||
@SerializedName("quantity")
|
||||
public Long quantity;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
}
|
||||
|
||||
public static class StoreInfo {
|
||||
@SerializedName("id")
|
||||
public String id;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("area_code")
|
||||
public String areaCode;
|
||||
|
||||
@SerializedName("address")
|
||||
public String address;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# 小程序调起支付
|
||||
|
||||
## 说明
|
||||
|
||||
商户通过JSAPI/小程序下单接口获取到发起支付的必要参数 `prepay_id` 后,在小程序中通过 `wx.requestPayment` 调起微信支付收银台。
|
||||
|
||||
小程序和JSAPI共用同一个下单接口(`/v3/pay/transactions/jsapi`),区别在于调起支付的前端方式不同:小程序用 `wx.requestPayment`,JSAPI 用 `WeixinJSBridge`。
|
||||
|
||||
## 示例代码
|
||||
|
||||
```javascript
|
||||
wx.requestPayment(
|
||||
{
|
||||
"timeStamp": "1414561699",
|
||||
"nonceStr": "5K8264ILTKCH16CQ2502SI8ZNMTM67VS",
|
||||
"package": "prepay_id=wx201410272009395522657a690389285100",
|
||||
"signType": "RSA",
|
||||
"paySign": "oR9d8PuhnIc+YZ8cBHFCwfgpaK9gd7vaRvkYD7rthRAZ\/X+QBhcCYL21N7cHCTUxbQ+EAt6Uy+lwSN22f5YZvI45MLko8Pfso0jm46v5hqcVwrk6uddkGuT+Cdvu4WBqDzaDjnNa5UK3GfE1Wfl2gHxIIY5lLdUgWFts17D4WuolLLkiFZV+JSHMvH7eaLdT9N5GBovBwu5yYKUR7skR8Fu+LozcSqQixnlEZUfyE55feLOQTUYzLmR9pNtPbPsu6WVhbNHMS3Ss2+AehHvz+n64GDmXxbX++IOBvm2olHu3PsOUGRwhudhVf7UcGcunXt8cqNjKNqZLhLw4jq\/xDg==",
|
||||
"success":function(res){},
|
||||
"fail":function(res){},
|
||||
"complete":function(res){}
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
> **重要**:前端回调不保证绝对可靠,商户需通过后端查单接口或支付成功回调通知来确认订单状态。
|
||||
@@ -0,0 +1,233 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 微信支付商户订单号查询订单
|
||||
*/
|
||||
public class QueryByOutTradeNo {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "GET";
|
||||
private static String PATH = "/v3/pay/transactions/out-trade-no/{out_trade_no}";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
QueryByOutTradeNo client = new QueryByOutTradeNo(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
QueryByOutTradeNoRequest request = new QueryByOutTradeNoRequest();
|
||||
request.outTradeNo = "1217752501201407033233368018";
|
||||
request.mchid = "1230000109";
|
||||
try {
|
||||
DirectAPIv3QueryResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public DirectAPIv3QueryResponse run(QueryByOutTradeNoRequest request) {
|
||||
String uri = PATH;
|
||||
uri = uri.replace("{out_trade_no}", WXPayUtility.urlEncode(request.outTradeNo));
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("mchid", request.mchid);
|
||||
String queryString = WXPayUtility.urlEncode(args);
|
||||
if (!queryString.isEmpty()) {
|
||||
uri = uri + "?" + queryString;
|
||||
}
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, DirectAPIv3QueryResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public QueryByOutTradeNo(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class QueryByOutTradeNoRequest {
|
||||
@SerializedName("mchid")
|
||||
@Expose(serialize = false)
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
@Expose(serialize = false)
|
||||
public String outTradeNo;
|
||||
}
|
||||
|
||||
public static class DirectAPIv3QueryResponse {
|
||||
@SerializedName("appid")
|
||||
public String appid;
|
||||
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("trade_type")
|
||||
public String tradeType;
|
||||
|
||||
@SerializedName("trade_state")
|
||||
public String tradeState;
|
||||
|
||||
@SerializedName("trade_state_desc")
|
||||
public String tradeStateDesc;
|
||||
|
||||
@SerializedName("bank_type")
|
||||
public String bankType;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("success_time")
|
||||
public String successTime;
|
||||
|
||||
@SerializedName("payer")
|
||||
public CommRespPayerInfo payer;
|
||||
|
||||
@SerializedName("amount")
|
||||
public CommRespAmountInfo amount;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public CommRespSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("promotion_detail")
|
||||
public List<PromotionDetail> promotionDetail;
|
||||
}
|
||||
|
||||
public static class CommRespPayerInfo {
|
||||
@SerializedName("openid")
|
||||
public String openid;
|
||||
}
|
||||
|
||||
public static class CommRespAmountInfo {
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("payer_total")
|
||||
public Long payerTotal;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
|
||||
@SerializedName("payer_currency")
|
||||
public String payerCurrency;
|
||||
}
|
||||
|
||||
public static class CommRespSceneInfo {
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
}
|
||||
|
||||
public static class PromotionDetail {
|
||||
@SerializedName("coupon_id")
|
||||
public String couponId;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("type")
|
||||
public String type;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("stock_id")
|
||||
public String stockId;
|
||||
|
||||
@SerializedName("wechatpay_contribute")
|
||||
public Long wechatpayContribute;
|
||||
|
||||
@SerializedName("merchant_contribute")
|
||||
public Long merchantContribute;
|
||||
|
||||
@SerializedName("other_contribute")
|
||||
public Long otherContribute;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetailInPromotion> goodsDetail;
|
||||
}
|
||||
|
||||
public static class GoodsDetailInPromotion {
|
||||
@SerializedName("goods_id")
|
||||
public String goodsId;
|
||||
|
||||
@SerializedName("quantity")
|
||||
public Long quantity;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
|
||||
@SerializedName("discount_amount")
|
||||
public Long discountAmount;
|
||||
|
||||
@SerializedName("goods_remark")
|
||||
public String goodsRemark;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,233 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 微信支付订单号查询订单
|
||||
*/
|
||||
public class QueryByWxTradeNo {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "GET";
|
||||
private static String PATH = "/v3/pay/transactions/id/{transaction_id}";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
QueryByWxTradeNo client = new QueryByWxTradeNo(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
QueryByWxTradeNoRequest request = new QueryByWxTradeNoRequest();
|
||||
request.transactionId = "1217752501201407033233368018";
|
||||
request.mchid = "1230000109";
|
||||
try {
|
||||
DirectAPIv3QueryResponse response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public DirectAPIv3QueryResponse run(QueryByWxTradeNoRequest request) {
|
||||
String uri = PATH;
|
||||
uri = uri.replace("{transaction_id}", WXPayUtility.urlEncode(request.transactionId));
|
||||
Map<String, Object> args = new HashMap<>();
|
||||
args.put("mchid", request.mchid);
|
||||
String queryString = WXPayUtility.urlEncode(args);
|
||||
if (!queryString.isEmpty()) {
|
||||
uri = uri + "?" + queryString;
|
||||
}
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, DirectAPIv3QueryResponse.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public QueryByWxTradeNo(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class QueryByWxTradeNoRequest {
|
||||
@SerializedName("mchid")
|
||||
@Expose(serialize = false)
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("transaction_id")
|
||||
@Expose(serialize = false)
|
||||
public String transactionId;
|
||||
}
|
||||
|
||||
public static class DirectAPIv3QueryResponse {
|
||||
@SerializedName("appid")
|
||||
public String appid;
|
||||
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("trade_type")
|
||||
public String tradeType;
|
||||
|
||||
@SerializedName("trade_state")
|
||||
public String tradeState;
|
||||
|
||||
@SerializedName("trade_state_desc")
|
||||
public String tradeStateDesc;
|
||||
|
||||
@SerializedName("bank_type")
|
||||
public String bankType;
|
||||
|
||||
@SerializedName("attach")
|
||||
public String attach;
|
||||
|
||||
@SerializedName("success_time")
|
||||
public String successTime;
|
||||
|
||||
@SerializedName("payer")
|
||||
public CommRespPayerInfo payer;
|
||||
|
||||
@SerializedName("amount")
|
||||
public CommRespAmountInfo amount;
|
||||
|
||||
@SerializedName("scene_info")
|
||||
public CommRespSceneInfo sceneInfo;
|
||||
|
||||
@SerializedName("promotion_detail")
|
||||
public List<PromotionDetail> promotionDetail;
|
||||
}
|
||||
|
||||
public static class CommRespPayerInfo {
|
||||
@SerializedName("openid")
|
||||
public String openid;
|
||||
}
|
||||
|
||||
public static class CommRespAmountInfo {
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("payer_total")
|
||||
public Long payerTotal;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
|
||||
@SerializedName("payer_currency")
|
||||
public String payerCurrency;
|
||||
}
|
||||
|
||||
public static class CommRespSceneInfo {
|
||||
@SerializedName("device_id")
|
||||
public String deviceId;
|
||||
}
|
||||
|
||||
public static class PromotionDetail {
|
||||
@SerializedName("coupon_id")
|
||||
public String couponId;
|
||||
|
||||
@SerializedName("name")
|
||||
public String name;
|
||||
|
||||
@SerializedName("scope")
|
||||
public String scope;
|
||||
|
||||
@SerializedName("type")
|
||||
public String type;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("stock_id")
|
||||
public String stockId;
|
||||
|
||||
@SerializedName("wechatpay_contribute")
|
||||
public Long wechatpayContribute;
|
||||
|
||||
@SerializedName("merchant_contribute")
|
||||
public Long merchantContribute;
|
||||
|
||||
@SerializedName("other_contribute")
|
||||
public Long otherContribute;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetailInPromotion> goodsDetail;
|
||||
}
|
||||
|
||||
public static class GoodsDetailInPromotion {
|
||||
@SerializedName("goods_id")
|
||||
public String goodsId;
|
||||
|
||||
@SerializedName("quantity")
|
||||
public Long quantity;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
|
||||
@SerializedName("discount_amount")
|
||||
public Long discountAmount;
|
||||
|
||||
@SerializedName("goods_remark")
|
||||
public String goodsRemark;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 关闭订单
|
||||
*
|
||||
* 未支付状态的订单,可在无需支付时调用此接口关闭订单。常见关单情况包括:
|
||||
* 1. 用户在商户系统提交取消订单请求,商户需执行关单操作。
|
||||
* 2. 订单超时未支付(超出商户系统设定的可支付时间或下单时的time_expire支付截止时间),商户需进行关单处理。
|
||||
*/
|
||||
public class CloseOrder {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/pay/transactions/out-trade-no/{out_trade_no}/close";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
CloseOrder client = new CloseOrder(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
CloseOrderRequest request = new CloseOrderRequest();
|
||||
request.outTradeNo = "1217752501201407033233368018";
|
||||
request.mchid = "1230000109";
|
||||
try {
|
||||
client.run(request);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public void run(CloseOrderRequest request) {
|
||||
String uri = PATH;
|
||||
uri = uri.replace("{out_trade_no}", WXPayUtility.urlEncode(request.outTradeNo));
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
return;
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public CloseOrder(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class CloseOrderRequest {
|
||||
@SerializedName("mchid")
|
||||
public String mchid;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
@Expose(serialize = false)
|
||||
public String outTradeNo;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 发起异常退款
|
||||
*/
|
||||
public class CreateAbnormalRefund {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
CreateAbnormalRefund client = new CreateAbnormalRefund(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
CreateAbnormalRefundRequest request = new CreateAbnormalRefundRequest();
|
||||
request.refundId = "50000000382019052709732678859";
|
||||
request.outRefundNo = "1217752501201407033233368018";
|
||||
request.type = AbnormalReceiveType.MERCHANT_BANK_CARD;
|
||||
request.bankType = "ICBC_DEBIT";
|
||||
request.bankAccount = client.encrypt("bank_account");
|
||||
request.realName = client.encrypt("real_name");
|
||||
try {
|
||||
Refund response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public Refund run(CreateAbnormalRefundRequest request) {
|
||||
String uri = PATH;
|
||||
uri = uri.replace("{refund_id}", WXPayUtility.urlEncode(request.refundId));
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, Refund.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public CreateAbnormalRefund(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public String encrypt(String plainText) {
|
||||
return WXPayUtility.encrypt(this.wechatPayPublicKey, plainText);
|
||||
}
|
||||
|
||||
public static class CreateAbnormalRefundRequest {
|
||||
@SerializedName("refund_id")
|
||||
@Expose(serialize = false)
|
||||
public String refundId;
|
||||
|
||||
@SerializedName("out_refund_no")
|
||||
public String outRefundNo;
|
||||
|
||||
@SerializedName("type")
|
||||
public AbnormalReceiveType type;
|
||||
|
||||
@SerializedName("bank_type")
|
||||
public String bankType;
|
||||
|
||||
@SerializedName("bank_account")
|
||||
public String bankAccount;
|
||||
|
||||
@SerializedName("real_name")
|
||||
public String realName;
|
||||
}
|
||||
|
||||
public static class Refund {
|
||||
@SerializedName("refund_id")
|
||||
public String refundId;
|
||||
|
||||
@SerializedName("out_refund_no")
|
||||
public String outRefundNo;
|
||||
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("channel")
|
||||
public Channel channel;
|
||||
|
||||
@SerializedName("user_received_account")
|
||||
public String userReceivedAccount;
|
||||
|
||||
@SerializedName("success_time")
|
||||
public String successTime;
|
||||
|
||||
@SerializedName("create_time")
|
||||
public String createTime;
|
||||
|
||||
@SerializedName("status")
|
||||
public Status status;
|
||||
|
||||
@SerializedName("funds_account")
|
||||
public FundsAccount fundsAccount;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Amount amount;
|
||||
|
||||
@SerializedName("promotion_detail")
|
||||
public List<Promotion> promotionDetail;
|
||||
|
||||
}
|
||||
|
||||
public enum AbnormalReceiveType {
|
||||
@SerializedName("USER_BANK_CARD")
|
||||
USER_BANK_CARD,
|
||||
@SerializedName("MERCHANT_BANK_CARD")
|
||||
MERCHANT_BANK_CARD
|
||||
}
|
||||
|
||||
public enum Channel {
|
||||
@SerializedName("ORIGINAL")
|
||||
ORIGINAL,
|
||||
@SerializedName("BALANCE")
|
||||
BALANCE,
|
||||
@SerializedName("OTHER_BALANCE")
|
||||
OTHER_BALANCE,
|
||||
@SerializedName("OTHER_BANKCARD")
|
||||
OTHER_BANKCARD
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
@SerializedName("SUCCESS")
|
||||
SUCCESS,
|
||||
@SerializedName("CLOSED")
|
||||
CLOSED,
|
||||
@SerializedName("PROCESSING")
|
||||
PROCESSING,
|
||||
@SerializedName("ABNORMAL")
|
||||
ABNORMAL
|
||||
}
|
||||
|
||||
public enum FundsAccount {
|
||||
@SerializedName("UNSETTLED")
|
||||
UNSETTLED,
|
||||
@SerializedName("AVAILABLE")
|
||||
AVAILABLE,
|
||||
@SerializedName("UNAVAILABLE")
|
||||
UNAVAILABLE,
|
||||
@SerializedName("OPERATION")
|
||||
OPERATION,
|
||||
@SerializedName("BASIC")
|
||||
BASIC,
|
||||
@SerializedName("ECNY_BASIC")
|
||||
ECNY_BASIC
|
||||
}
|
||||
|
||||
public static class Amount {
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("refund")
|
||||
public Long refund;
|
||||
|
||||
@SerializedName("from")
|
||||
public List<FundsFromItem> from;
|
||||
|
||||
@SerializedName("payer_total")
|
||||
public Long payerTotal;
|
||||
|
||||
@SerializedName("payer_refund")
|
||||
public Long payerRefund;
|
||||
|
||||
@SerializedName("settlement_refund")
|
||||
public Long settlementRefund;
|
||||
|
||||
@SerializedName("settlement_total")
|
||||
public Long settlementTotal;
|
||||
|
||||
@SerializedName("discount_refund")
|
||||
public Long discountRefund;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
|
||||
@SerializedName("refund_fee")
|
||||
public Long refundFee;
|
||||
}
|
||||
|
||||
public static class Promotion {
|
||||
@SerializedName("promotion_id")
|
||||
public String promotionId;
|
||||
|
||||
@SerializedName("scope")
|
||||
public PromotionScope scope;
|
||||
|
||||
@SerializedName("type")
|
||||
public PromotionType type;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("refund_amount")
|
||||
public Long refundAmount;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetail> goodsDetail;
|
||||
}
|
||||
|
||||
|
||||
public static class FundsFromItem {
|
||||
@SerializedName("account")
|
||||
public Account account;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
}
|
||||
|
||||
public enum PromotionScope {
|
||||
@SerializedName("GLOBAL")
|
||||
GLOBAL,
|
||||
@SerializedName("SINGLE")
|
||||
SINGLE
|
||||
}
|
||||
|
||||
public enum PromotionType {
|
||||
@SerializedName("CASH")
|
||||
CASH,
|
||||
@SerializedName("NOCASH")
|
||||
NOCASH
|
||||
}
|
||||
|
||||
public static class GoodsDetail {
|
||||
@SerializedName("merchant_goods_id")
|
||||
public String merchantGoodsId;
|
||||
|
||||
@SerializedName("wechatpay_goods_id")
|
||||
public String wechatpayGoodsId;
|
||||
|
||||
@SerializedName("goods_name")
|
||||
public String goodsName;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
|
||||
@SerializedName("refund_amount")
|
||||
public Long refundAmount;
|
||||
|
||||
@SerializedName("refund_quantity")
|
||||
public Long refundQuantity;
|
||||
}
|
||||
|
||||
public enum Account {
|
||||
@SerializedName("AVAILABLE")
|
||||
AVAILABLE,
|
||||
@SerializedName("UNAVAILABLE")
|
||||
UNAVAILABLE
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,360 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 退款申请
|
||||
*
|
||||
* 支付成功后1年内,可通过此接口将款项全部或部分原路退还给用户(也可在商户平台手动操作)。
|
||||
*
|
||||
* 关键注意:
|
||||
* 1. 一笔订单最多50次部分退款,重试必须用原 out_refund_no,否则会重复退款。
|
||||
* 2. 接口返回成功仅表示受理成功,实际结果以退款回调通知或查询退款接口为准。
|
||||
* 3. 原路退还:银行卡1-3个工作日到账,零钱即时到账。
|
||||
* 4. 有代金券的订单部分退款时,退给用户 = 退款金额 × (实付 ÷ 总额),四舍五入。
|
||||
* 5. 有分账的订单,需确保可用余额充足;部分分账未解冻时需先调"完结分账"。
|
||||
*/
|
||||
public class CreateRefund {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "POST";
|
||||
private static String PATH = "/v3/refund/domestic/refunds";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
CreateRefund client = new CreateRefund(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
CreateRequest request = new CreateRequest();
|
||||
request.transactionId = "1217752501201407033233368018";
|
||||
request.outTradeNo = "1217752501201407033233368018";
|
||||
request.outRefundNo = "1217752501201407033233368018";
|
||||
request.reason = "商品已售完";
|
||||
request.notifyUrl = "https://weixin.qq.com";
|
||||
request.fundsAccount = ReqFundsAccount.AVAILABLE;
|
||||
request.amount = new AmountReq();
|
||||
request.amount.refund = 888L;
|
||||
request.amount.from = new ArrayList<>();
|
||||
{
|
||||
FundsFromItem fromItem = new FundsFromItem();
|
||||
fromItem.account = Account.AVAILABLE;
|
||||
fromItem.amount = 444L;
|
||||
request.amount.from.add(fromItem);
|
||||
};
|
||||
request.amount.total = 888L;
|
||||
request.amount.currency = "CNY";
|
||||
request.goodsDetail = new ArrayList<>();
|
||||
{
|
||||
GoodsDetail goodsDetailItem = new GoodsDetail();
|
||||
goodsDetailItem.merchantGoodsId = "1217752501201407033233368018";
|
||||
goodsDetailItem.wechatpayGoodsId = "1001";
|
||||
goodsDetailItem.goodsName = "iPhone6s 16G";
|
||||
goodsDetailItem.unitPrice = 528800L;
|
||||
goodsDetailItem.refundAmount = 528800L;
|
||||
goodsDetailItem.refundQuantity = 1L;
|
||||
request.goodsDetail.add(goodsDetailItem);
|
||||
};
|
||||
try {
|
||||
Refund response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public Refund run(CreateRequest request) {
|
||||
String uri = PATH;
|
||||
String reqBody = WXPayUtility.toJson(request);
|
||||
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo,privateKey, METHOD, uri, reqBody));
|
||||
reqBuilder.addHeader("Content-Type", "application/json");
|
||||
RequestBody requestBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"), reqBody);
|
||||
reqBuilder.method(METHOD, requestBody);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, Refund.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public CreateRefund(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class CreateRequest {
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("out_refund_no")
|
||||
public String outRefundNo;
|
||||
|
||||
@SerializedName("reason")
|
||||
public String reason;
|
||||
|
||||
@SerializedName("notify_url")
|
||||
public String notifyUrl;
|
||||
|
||||
@SerializedName("funds_account")
|
||||
public ReqFundsAccount fundsAccount;
|
||||
|
||||
@SerializedName("amount")
|
||||
public AmountReq amount;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetail> goodsDetail;
|
||||
|
||||
}
|
||||
|
||||
public static class Refund {
|
||||
@SerializedName("refund_id")
|
||||
public String refundId;
|
||||
|
||||
@SerializedName("out_refund_no")
|
||||
public String outRefundNo;
|
||||
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("channel")
|
||||
public Channel channel;
|
||||
|
||||
@SerializedName("user_received_account")
|
||||
public String userReceivedAccount;
|
||||
|
||||
@SerializedName("success_time")
|
||||
public String successTime;
|
||||
|
||||
@SerializedName("create_time")
|
||||
public String createTime;
|
||||
|
||||
@SerializedName("status")
|
||||
public Status status;
|
||||
|
||||
@SerializedName("funds_account")
|
||||
public FundsAccount fundsAccount;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Amount amount;
|
||||
|
||||
@SerializedName("promotion_detail")
|
||||
public List<Promotion> promotionDetail;
|
||||
|
||||
}
|
||||
|
||||
public enum ReqFundsAccount {
|
||||
@SerializedName("AVAILABLE")
|
||||
AVAILABLE,
|
||||
@SerializedName("UNSETTLED")
|
||||
UNSETTLED
|
||||
}
|
||||
|
||||
public static class AmountReq {
|
||||
@SerializedName("refund")
|
||||
public Long refund;
|
||||
|
||||
@SerializedName("from")
|
||||
public List<FundsFromItem> from;
|
||||
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
}
|
||||
|
||||
public static class GoodsDetail {
|
||||
@SerializedName("merchant_goods_id")
|
||||
public String merchantGoodsId;
|
||||
|
||||
@SerializedName("wechatpay_goods_id")
|
||||
public String wechatpayGoodsId;
|
||||
|
||||
@SerializedName("goods_name")
|
||||
public String goodsName;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
|
||||
@SerializedName("refund_amount")
|
||||
public Long refundAmount;
|
||||
|
||||
@SerializedName("refund_quantity")
|
||||
public Long refundQuantity;
|
||||
}
|
||||
|
||||
|
||||
public enum Channel {
|
||||
@SerializedName("ORIGINAL")
|
||||
ORIGINAL,
|
||||
@SerializedName("BALANCE")
|
||||
BALANCE,
|
||||
@SerializedName("OTHER_BALANCE")
|
||||
OTHER_BALANCE,
|
||||
@SerializedName("OTHER_BANKCARD")
|
||||
OTHER_BANKCARD
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
@SerializedName("SUCCESS")
|
||||
SUCCESS,
|
||||
@SerializedName("CLOSED")
|
||||
CLOSED,
|
||||
@SerializedName("PROCESSING")
|
||||
PROCESSING,
|
||||
@SerializedName("ABNORMAL")
|
||||
ABNORMAL
|
||||
}
|
||||
|
||||
public enum FundsAccount {
|
||||
@SerializedName("UNSETTLED")
|
||||
UNSETTLED,
|
||||
@SerializedName("AVAILABLE")
|
||||
AVAILABLE,
|
||||
@SerializedName("UNAVAILABLE")
|
||||
UNAVAILABLE,
|
||||
@SerializedName("OPERATION")
|
||||
OPERATION,
|
||||
@SerializedName("BASIC")
|
||||
BASIC,
|
||||
@SerializedName("ECNY_BASIC")
|
||||
ECNY_BASIC
|
||||
}
|
||||
|
||||
public static class Amount {
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("refund")
|
||||
public Long refund;
|
||||
|
||||
@SerializedName("from")
|
||||
public List<FundsFromItem> from;
|
||||
|
||||
@SerializedName("payer_total")
|
||||
public Long payerTotal;
|
||||
|
||||
@SerializedName("payer_refund")
|
||||
public Long payerRefund;
|
||||
|
||||
@SerializedName("settlement_refund")
|
||||
public Long settlementRefund;
|
||||
|
||||
@SerializedName("settlement_total")
|
||||
public Long settlementTotal;
|
||||
|
||||
@SerializedName("discount_refund")
|
||||
public Long discountRefund;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
|
||||
@SerializedName("refund_fee")
|
||||
public Long refundFee;
|
||||
}
|
||||
|
||||
public static class Promotion {
|
||||
@SerializedName("promotion_id")
|
||||
public String promotionId;
|
||||
|
||||
@SerializedName("scope")
|
||||
public PromotionScope scope;
|
||||
|
||||
@SerializedName("type")
|
||||
public PromotionType type;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("refund_amount")
|
||||
public Long refundAmount;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetail> goodsDetail;
|
||||
}
|
||||
|
||||
public static class FundsFromItem {
|
||||
@SerializedName("account")
|
||||
public Account account;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
}
|
||||
|
||||
public enum PromotionScope {
|
||||
@SerializedName("GLOBAL")
|
||||
GLOBAL,
|
||||
@SerializedName("SINGLE")
|
||||
SINGLE
|
||||
}
|
||||
|
||||
public enum PromotionType {
|
||||
@SerializedName("CASH")
|
||||
CASH,
|
||||
@SerializedName("NOCASH")
|
||||
NOCASH
|
||||
}
|
||||
|
||||
public enum Account {
|
||||
@SerializedName("AVAILABLE")
|
||||
AVAILABLE,
|
||||
@SerializedName("UNAVAILABLE")
|
||||
UNAVAILABLE
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,286 @@
|
||||
package com.java.demo;
|
||||
|
||||
import com.java.utils.WXPayUtility; // 引用微信支付工具库,参考:https://pay.weixin.qq.com/doc/v3/merchant/4014931831
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import com.google.gson.annotations.Expose;
|
||||
import okhttp3.MediaType;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.UncheckedIOException;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 查询单笔退款(通过商户退款单号)
|
||||
*
|
||||
* 提交退款申请后,推荐每间隔1分钟调用该接口查询一次退款状态,若超过5分钟仍是退款处理中状态,
|
||||
* 建议开始逐步衰减查询频率(比如之后间隔5分钟、10分钟、20分钟、30分钟……查询一次)。
|
||||
*
|
||||
* 退款有一定延时,零钱支付的订单退款一般5分钟内到账,银行卡支付的订单退款一般1-3个工作日到账。
|
||||
*
|
||||
* 同一商户号查询退款频率限制为300qps,如返回FREQUENCY_LIMITED频率限制报错可间隔1分钟再重试查询。
|
||||
*/
|
||||
public class QueryByOutRefundNo {
|
||||
private static String HOST = "https://api.mch.weixin.qq.com";
|
||||
private static String METHOD = "GET";
|
||||
private static String PATH = "/v3/refund/domestic/refunds/{out_refund_no}";
|
||||
|
||||
public static void main(String[] args) {
|
||||
// TODO: 请准备商户开发必要参数,参考:https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
QueryByOutRefundNo client = new QueryByOutRefundNo(
|
||||
"19xxxxxxxx", // 商户号,是由微信支付系统生成并分配给每个商户的唯一标识符,商户号获取方式参考 https://pay.weixin.qq.com/doc/v3/merchant/4013070756
|
||||
"1DDE55AD98Exxxxxxxxxx", // 商户API证书序列号,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013053053
|
||||
"/path/to/apiclient_key.pem", // 商户API证书私钥文件路径,本地文件路径
|
||||
"PUB_KEY_ID_xxxxxxxxxxxxx", // 微信支付公钥ID,如何获取请参考 https://pay.weixin.qq.com/doc/v3/merchant/4013038816
|
||||
"/path/to/wxp_pub.pem" // 微信支付公钥文件路径,本地文件路径
|
||||
);
|
||||
|
||||
QueryByOutRefundNoRequest request = new QueryByOutRefundNoRequest();
|
||||
request.outRefundNo = "1217752501201407033233368018";
|
||||
try {
|
||||
Refund response = client.run(request);
|
||||
// TODO: 请求成功,继续业务逻辑
|
||||
System.out.println(response);
|
||||
} catch (WXPayUtility.ApiException e) {
|
||||
// TODO: 请求失败,根据状态码执行不同的逻辑
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public Refund run(QueryByOutRefundNoRequest request) {
|
||||
String uri = PATH;
|
||||
uri = uri.replace("{out_refund_no}", WXPayUtility.urlEncode(request.outRefundNo));
|
||||
Request.Builder reqBuilder = new Request.Builder().url(HOST + uri);
|
||||
reqBuilder.addHeader("Accept", "application/json");
|
||||
reqBuilder.addHeader("Wechatpay-Serial", wechatPayPublicKeyId);
|
||||
reqBuilder.addHeader("Authorization", WXPayUtility.buildAuthorization(mchid, certificateSerialNo, privateKey, METHOD, uri, null));
|
||||
reqBuilder.method(METHOD, null);
|
||||
Request httpRequest = reqBuilder.build();
|
||||
|
||||
// 发送HTTP请求
|
||||
OkHttpClient client = new OkHttpClient.Builder().build();
|
||||
try (Response httpResponse = client.newCall(httpRequest).execute()) {
|
||||
String respBody = WXPayUtility.extractBody(httpResponse);
|
||||
if (httpResponse.code() >= 200 && httpResponse.code() < 300) {
|
||||
// 2XX 成功,验证应答签名
|
||||
WXPayUtility.validateResponse(this.wechatPayPublicKeyId, this.wechatPayPublicKey,
|
||||
httpResponse.headers(), respBody);
|
||||
|
||||
// 从HTTP应答报文构建返回数据
|
||||
return WXPayUtility.fromJson(respBody, Refund.class);
|
||||
} else {
|
||||
throw new WXPayUtility.ApiException(httpResponse.code(), respBody, httpResponse.headers());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new UncheckedIOException("Sending request to " + uri + " failed.", e);
|
||||
}
|
||||
}
|
||||
|
||||
private final String mchid;
|
||||
private final String certificateSerialNo;
|
||||
private final PrivateKey privateKey;
|
||||
private final String wechatPayPublicKeyId;
|
||||
private final PublicKey wechatPayPublicKey;
|
||||
|
||||
public QueryByOutRefundNo(String mchid, String certificateSerialNo, String privateKeyFilePath, String wechatPayPublicKeyId, String wechatPayPublicKeyFilePath) {
|
||||
this.mchid = mchid;
|
||||
this.certificateSerialNo = certificateSerialNo;
|
||||
this.privateKey = WXPayUtility.loadPrivateKeyFromPath(privateKeyFilePath);
|
||||
this.wechatPayPublicKeyId = wechatPayPublicKeyId;
|
||||
this.wechatPayPublicKey = WXPayUtility.loadPublicKeyFromPath(wechatPayPublicKeyFilePath);
|
||||
}
|
||||
|
||||
public static class QueryByOutRefundNoRequest {
|
||||
@SerializedName("out_refund_no")
|
||||
@Expose(serialize = false)
|
||||
public String outRefundNo;
|
||||
|
||||
}
|
||||
|
||||
public static class Refund {
|
||||
@SerializedName("refund_id")
|
||||
public String refundId;
|
||||
|
||||
@SerializedName("out_refund_no")
|
||||
public String outRefundNo;
|
||||
|
||||
@SerializedName("transaction_id")
|
||||
public String transactionId;
|
||||
|
||||
@SerializedName("out_trade_no")
|
||||
public String outTradeNo;
|
||||
|
||||
@SerializedName("channel")
|
||||
public Channel channel;
|
||||
|
||||
@SerializedName("user_received_account")
|
||||
public String userReceivedAccount;
|
||||
|
||||
@SerializedName("success_time")
|
||||
public String successTime;
|
||||
|
||||
@SerializedName("create_time")
|
||||
public String createTime;
|
||||
|
||||
@SerializedName("status")
|
||||
public Status status;
|
||||
|
||||
@SerializedName("funds_account")
|
||||
public FundsAccount fundsAccount;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Amount amount;
|
||||
|
||||
@SerializedName("promotion_detail")
|
||||
public List<Promotion> promotionDetail;
|
||||
|
||||
}
|
||||
|
||||
public enum Channel {
|
||||
@SerializedName("ORIGINAL")
|
||||
ORIGINAL,
|
||||
@SerializedName("BALANCE")
|
||||
BALANCE,
|
||||
@SerializedName("OTHER_BALANCE")
|
||||
OTHER_BALANCE,
|
||||
@SerializedName("OTHER_BANKCARD")
|
||||
OTHER_BANKCARD
|
||||
}
|
||||
|
||||
public enum Status {
|
||||
@SerializedName("SUCCESS")
|
||||
SUCCESS,
|
||||
@SerializedName("CLOSED")
|
||||
CLOSED,
|
||||
@SerializedName("PROCESSING")
|
||||
PROCESSING,
|
||||
@SerializedName("ABNORMAL")
|
||||
ABNORMAL
|
||||
}
|
||||
|
||||
public enum FundsAccount {
|
||||
@SerializedName("UNSETTLED")
|
||||
UNSETTLED,
|
||||
@SerializedName("AVAILABLE")
|
||||
AVAILABLE,
|
||||
@SerializedName("UNAVAILABLE")
|
||||
UNAVAILABLE,
|
||||
@SerializedName("OPERATION")
|
||||
OPERATION,
|
||||
@SerializedName("BASIC")
|
||||
BASIC,
|
||||
@SerializedName("ECNY_BASIC")
|
||||
ECNY_BASIC
|
||||
}
|
||||
|
||||
public static class Amount {
|
||||
@SerializedName("total")
|
||||
public Long total;
|
||||
|
||||
@SerializedName("refund")
|
||||
public Long refund;
|
||||
|
||||
@SerializedName("from")
|
||||
public List<FundsFromItem> from;
|
||||
|
||||
@SerializedName("payer_total")
|
||||
public Long payerTotal;
|
||||
|
||||
@SerializedName("payer_refund")
|
||||
public Long payerRefund;
|
||||
|
||||
@SerializedName("settlement_refund")
|
||||
public Long settlementRefund;
|
||||
|
||||
@SerializedName("settlement_total")
|
||||
public Long settlementTotal;
|
||||
|
||||
@SerializedName("discount_refund")
|
||||
public Long discountRefund;
|
||||
|
||||
@SerializedName("currency")
|
||||
public String currency;
|
||||
|
||||
@SerializedName("refund_fee")
|
||||
public Long refundFee;
|
||||
}
|
||||
|
||||
public static class Promotion {
|
||||
@SerializedName("promotion_id")
|
||||
public String promotionId;
|
||||
|
||||
@SerializedName("scope")
|
||||
public PromotionScope scope;
|
||||
|
||||
@SerializedName("type")
|
||||
public PromotionType type;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
|
||||
@SerializedName("refund_amount")
|
||||
public Long refundAmount;
|
||||
|
||||
@SerializedName("goods_detail")
|
||||
public List<GoodsDetail> goodsDetail;
|
||||
}
|
||||
|
||||
public static class FundsFromItem {
|
||||
@SerializedName("account")
|
||||
public Account account;
|
||||
|
||||
@SerializedName("amount")
|
||||
public Long amount;
|
||||
}
|
||||
|
||||
public enum PromotionScope {
|
||||
@SerializedName("GLOBAL")
|
||||
GLOBAL,
|
||||
@SerializedName("SINGLE")
|
||||
SINGLE
|
||||
}
|
||||
|
||||
public enum PromotionType {
|
||||
@SerializedName("CASH")
|
||||
CASH,
|
||||
@SerializedName("NOCASH")
|
||||
NOCASH
|
||||
}
|
||||
|
||||
public static class GoodsDetail {
|
||||
@SerializedName("merchant_goods_id")
|
||||
public String merchantGoodsId;
|
||||
|
||||
@SerializedName("wechatpay_goods_id")
|
||||
public String wechatpayGoodsId;
|
||||
|
||||
@SerializedName("goods_name")
|
||||
public String goodsName;
|
||||
|
||||
@SerializedName("unit_price")
|
||||
public Long unitPrice;
|
||||
|
||||
@SerializedName("refund_amount")
|
||||
public Long refundAmount;
|
||||
|
||||
@SerializedName("refund_quantity")
|
||||
public Long refundQuantity;
|
||||
}
|
||||
|
||||
public enum Account {
|
||||
@SerializedName("AVAILABLE")
|
||||
AVAILABLE,
|
||||
@SerializedName("UNAVAILABLE")
|
||||
UNAVAILABLE
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
# 支付成功回调通知
|
||||
|
||||
## 回调描述
|
||||
|
||||
用户使用普通支付(APP支付/H5支付/JSAPI支付/Native支付/小程序支付)功能,当用户成功支付订单后,微信支付会通过POST的请求方式,向商户预先设置的回调地址(APP支付/H5支付/JSAPI支付/Native支付/小程序支付下单接口传入的notify_url)发送回调通知,让商户知晓用户已完成支付。
|
||||
|
||||
> **注意**:商户侧对微信支付回调IP有防火墙策略限制的,需要对微信回调IP段开通白名单,否则会导致收不到回调(微信支付回调被商户防火墙拦截),详情参考回调处理逻辑注意事项。
|
||||
|
||||
## 回调报文格式
|
||||
|
||||
微信支付会通过POST的方式向回调地址发送回调报文,回调通知的请求主体中会包含JSON格式的通知参数:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "EV-2018022511223320873",
|
||||
"create_time": "2015-05-20T13:29:35+08:00",
|
||||
"resource_type": "encrypt-resource",
|
||||
"event_type": "TRANSACTION.SUCCESS",
|
||||
"summary": "支付成功",
|
||||
"resource": {
|
||||
"original_type": "transaction",
|
||||
"algorithm": "AEAD_AES_256_GCM",
|
||||
"ciphertext": "",
|
||||
"associated_data": "",
|
||||
"nonce": ""
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 关键字段说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| `event_type` | 事件类型,支付成功为 `TRANSACTION.SUCCESS` |
|
||||
| `resource.algorithm` | 加密算法,固定为 `AEAD_AES_256_GCM` |
|
||||
| `resource.ciphertext` | 密文,需使用商户APIv3密钥解密后得到支付订单详情 |
|
||||
| `resource.associated_data` | 附加数据,解密时作为 AAD 参数 |
|
||||
| `resource.nonce` | 随机串,解密时作为 Nonce 参数 |
|
||||
|
||||
## 处理要求
|
||||
|
||||
1. 接收到回调后,先验证请求签名(使用微信支付公钥验签)
|
||||
2. 使用商户APIv3密钥 + AEAD_AES_256_GCM 解密 `resource.ciphertext` 得到订单明文
|
||||
3. 处理完成后返回 HTTP 200 + `{"code": "SUCCESS", "message": "成功"}` 表示确认收到
|
||||
4. 若返回非200或超时,微信支付会按策略重试通知
|
||||
@@ -0,0 +1,104 @@
|
||||
# 基础支付 & 合单支付示例代码接口索引
|
||||
|
||||
> 根据用户确认的开发语言,加载对应语言目录下的文件。Java 和 Go 目录结构一致。
|
||||
|
||||
## 下单(需确认支付方式)
|
||||
|
||||
| 支付方式 | 接口 | Java | Go |
|
||||
|---------|------|------|-----|
|
||||
| JSAPI/小程序 | POST /v3/pay/transactions/jsapi | `Java/1-JSAPI支付/JsapiPrepay.java` | `Go/1-JSAPI支付/JsapiPrepay.go` |
|
||||
| APP | POST /v3/pay/transactions/app | `Java/2-APP支付/AppPrepay.java` | `Go/2-APP支付/AppPrepay.go` |
|
||||
| H5 | POST /v3/pay/transactions/h5 | `Java/3-H5支付/H5Prepay.java` | `Go/3-H5支付/H5Prepay.go` |
|
||||
| Native | POST /v3/pay/transactions/native | `Java/4-Native支付/NativePrepay.java` | `Go/4-Native支付/NativePrepay.go` |
|
||||
|
||||
## 调起支付(需确认支付方式,前端/客户端集成参考)
|
||||
|
||||
| 支付方式 | 调起方式 | Java |
|
||||
|---------|---------|------|
|
||||
| JSAPI | WeixinJSBridge | `Java/1-JSAPI支付/JsapiInvoke.md` |
|
||||
| 小程序 | wx.requestPayment | `Java/5-小程序支付/MiniProgramInvoke.md` |
|
||||
| APP | OpenSDK | `Java/2-APP支付/AppInvoke.md` |
|
||||
| H5 | 跳转h5_url | `Java/3-H5支付/H5Invoke.md` |
|
||||
| Native | code_url转二维码 | `Java/4-Native支付/NativeInvoke.md` |
|
||||
|
||||
## 通用接口(无需确认支付方式,各支付方式完全相同)
|
||||
|
||||
| 业务 | 接口 | Java | Go |
|
||||
|------|------|------|-----|
|
||||
| 微信订单号查单 | GET /v3/pay/transactions/id/{transaction_id} | `Java/6-订单查询/QueryByWxTradeNo.java` | `Go/6-订单查询/QueryByWxTradeNo.go` |
|
||||
| 商户订单号查单 | GET /v3/pay/transactions/out-trade-no/{out_trade_no} | `Java/6-订单查询/QueryByOutTradeNo.java` | `Go/6-订单查询/QueryByOutTradeNo.go` |
|
||||
| 关闭订单 | POST /v3/pay/transactions/out-trade-no/{out_trade_no}/close | `Java/7-关闭订单/CloseOrder.java` | `Go/7-关闭订单/CloseOrder.go` |
|
||||
| 退款申请 | POST /v3/refund/domestic/refunds | `Java/8-订单退款/CreateRefund.java` | `Go/8-订单退款/CreateRefund.go` |
|
||||
| 查询退款 | GET /v3/refund/domestic/refunds/{out_refund_no} | `Java/8-订单退款/QueryByOutRefundNo.java` | `Go/8-订单退款/QueryByOutRefundNo.go` |
|
||||
| 异常退款 | POST /v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund | `Java/8-订单退款/CreateAbnormalRefund.java` | `Go/8-订单退款/CreateAbnormalRefund.go` |
|
||||
| 支付回调通知 | 回调报文格式与处理要求 | `Java/9-支付回调通知/支付成功回调通知说明.md` | — |
|
||||
| 退款回调通知 | 回调报文格式与处理要求 | `Java/10-退款结果回调通知/退款结果回调通知说明.md` | — |
|
||||
| 申请交易账单 | GET /v3/bill/tradebill | `Java/11-申请交易账单/GetTradeBill.java` | `Go/11-申请交易账单/GetTradeBill.go` |
|
||||
| 申请资金账单 | GET /v3/bill/fundflowbill | `Java/12-申请资金账单/GetFundFlowBill.java` | `Go/12-申请资金账单/GetFundFlowBill.go` |
|
||||
| 下载账单 | GET download_url | `Java/13-下载账单/DownloadBill.java` | — |
|
||||
|
||||
---
|
||||
|
||||
## 合单支付下单(需确认支付方式)
|
||||
|
||||
> 合单支付不是独立的支付方式,而是将多个子订单合并为一笔支付的模式。下单 API 路径为 `/v3/combine-transactions/{type}`,与基础支付不同。
|
||||
|
||||
| 支付方式 | 接口 | Java | Go |
|
||||
|---------|------|------|-----|
|
||||
| JSAPI/小程序 | POST /v3/combine-transactions/jsapi | `Java/15-合单支付/UnionJsapiPrepay.java` | `Go/15-合单支付/UnionJsapiPrepay.go` |
|
||||
| APP | POST /v3/combine-transactions/app | `Java/15-合单支付/UnionAppPrepay.java` | `Go/15-合单支付/UnionAppPrepay.go` |
|
||||
| H5 | POST /v3/combine-transactions/h5 | `Java/15-合单支付/UnionH5Prepay.java` | `Go/15-合单支付/UnionH5Prepay.go` |
|
||||
| Native | POST /v3/combine-transactions/native | `Java/15-合单支付/UnionNativePrepay.java` | `Go/15-合单支付/UnionNativePrepay.go` |
|
||||
|
||||
> 合单支付的**调起支付**与基础支付完全一致(返回的 `prepay_id` / `h5_url` / `code_url` 用法相同),直接复用上方「调起支付」中的文件即可。
|
||||
|
||||
## 合单支付专用接口(与基础支付不同,必须使用合单专用接口)
|
||||
|
||||
> 合单订单的查单和关单**不能**使用基础支付的查单/关单接口,必须使用以下合单专用接口。
|
||||
|
||||
| 业务 | 接口 | Java | Go |
|
||||
|------|------|------|-----|
|
||||
| 查询合单订单 | GET /v3/combine-transactions/out-trade-no/{combine_out_trade_no} | `Java/15-合单支付/UnionQueryByOutTradeNo.java` | `Go/15-合单支付/UnionQueryByOutTradeNo.go` |
|
||||
| 关闭合单订单 | POST /v3/combine-transactions/out-trade-no/{combine_out_trade_no}/close | `Java/15-合单支付/UnionClose.java` | `Go/15-合单支付/UnionClose.go` |
|
||||
| 合单支付回调通知 | 回调报文格式与处理要求 | `Java/15-合单支付/合单支付成功回调通知说明.md` | — |
|
||||
|
||||
## 合单支付复用基础支付的接口
|
||||
|
||||
> 以下接口合单支付与基础支付完全相同,按**子单维度**操作。退款时使用子单的 `transaction_id`,账单以子单维度记录在各子单商户账单内。
|
||||
|
||||
| 业务 | 接口 | 参考文件 |
|
||||
|------|------|---------|
|
||||
| 退款申请 | POST /v3/refund/domestic/refunds | `Java/8-订单退款/CreateRefund.java`、`Go/8-订单退款/CreateRefund.go` |
|
||||
| 查询退款 | GET /v3/refund/domestic/refunds/{out_refund_no} | `Java/8-订单退款/QueryByOutRefundNo.java`、`Go/8-订单退款/QueryByOutRefundNo.go` |
|
||||
| 异常退款 | POST /v3/refund/domestic/refunds/{refund_id}/apply-abnormal-refund | `Java/8-订单退款/CreateAbnormalRefund.java`、`Go/8-订单退款/CreateAbnormalRefund.go` |
|
||||
| 退款回调通知 | 回调报文格式与处理要求 | `Java/10-退款结果回调通知/退款结果回调通知说明.md` |
|
||||
| 申请交易账单 | GET /v3/bill/tradebill | `Java/11-申请交易账单/GetTradeBill.java`、`Go/11-申请交易账单/GetTradeBill.go` |
|
||||
| 申请资金账单 | GET /v3/bill/fundflowbill | `Java/12-申请资金账单/GetFundFlowBill.java`、`Go/12-申请资金账单/GetFundFlowBill.go` |
|
||||
| 下载账单 | GET download_url | `Java/13-下载账单/DownloadBill.java` |
|
||||
|
||||
## 16. 分账
|
||||
|
||||
| 接口 | Java | Go |
|
||||
|------|------|------|
|
||||
| 请求分账 | `Java/16-分账/CreateOrder.java` | `Go/16-分账/CreateOrder.go` |
|
||||
| 查询分账结果 | `Java/16-分账/QueryOrder.java` | `Go/16-分账/QueryOrder.go` |
|
||||
| 请求分账回退 | `Java/16-分账/CreateReturnOrder.java` | `Go/16-分账/CreateReturnOrder.go` |
|
||||
| 查询分账回退结果 | `Java/16-分账/QueryReturnOrder.java` | `Go/16-分账/QueryReturnOrder.go` |
|
||||
| 解冻剩余资金 | `Java/16-分账/UnfreezeOrder.java` | `Go/16-分账/UnfreezeOrder.go` |
|
||||
| 查询剩余待分金额 | `Java/16-分账/QueryOrderAmount.java` | `Go/16-分账/QueryOrderAmount.go` |
|
||||
| 添加分账接收方 | `Java/16-分账/AddReceiver.java` | `Go/16-分账/AddReceiver.go` |
|
||||
| 删除分账接收方 | `Java/16-分账/DeleteReceiver.java` | `Go/16-分账/DeleteReceiver.go` |
|
||||
| 申请分账账单 | `Java/16-分账/SplitBill.java` | `Go/16-分账/SplitBill.go` |
|
||||
|
||||
> 分账规则、API 列表见 `3-商户与服务商通用/接入指南/分账接入指南.md`。
|
||||
|
||||
## SDK工具类(所有接口的公共依赖)
|
||||
|
||||
> 所有示例代码都依赖此工具类,提供签名、验签、加解密、HTTP请求等基础能力。提醒用户需一并集成。
|
||||
|
||||
| 语言 | 文件 |
|
||||
|------|------|
|
||||
| Java | `Java/14-SDK工具类/WXPayUtility.java` — 签名、验签、加解密等基础能力 |
|
||||
| Java | `Java/14-SDK工具类/WXPayClient.java` — HTTP 客户端,封装请求签名→发送→验签流程 |
|
||||
| Go | `Go/14-SDK工具类/wxpay_utility.go` — 签名、验签、加解密等基础能力 |
|
||||
| Go | `Go/14-SDK工具类/wxpay_client.go` — HTTP 客户端,封装请求签名→发送→验签流程 |
|
||||
Reference in New Issue
Block a user