diff --git a/CHANGELOG.md b/CHANGELOG.md index 51386adb..cb8fb785 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ -## v1.0.20 / 2021-08-16 +## v1.0.21 / 2021-08-18 + +- 增加微信支付V3版 + +- ## v1.0.20 / 2021-08-16 - 增加电影票服务 \ No newline at end of file diff --git a/library.go b/library.go index c69014d9..f397e79a 100644 --- a/library.go +++ b/library.go @@ -1,5 +1,5 @@ package go_library func Version() string { - return "v1.0.20" + return "v1.0.21" } diff --git a/library_test.go b/library_test.go index a7fc96ff..ba3e34d6 100644 --- a/library_test.go +++ b/library_test.go @@ -5,6 +5,6 @@ import ( "testing" ) -func TestName(t *testing.T) { +func TestVersion(t *testing.T) { fmt.Println(Version()) } diff --git a/service/wechat/pay/v3/app.go b/service/wechat/pay/v3/app.go new file mode 100644 index 00000000..e5a146cc --- /dev/null +++ b/service/wechat/pay/v3/app.go @@ -0,0 +1,73 @@ +package v3 + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" +) + +type App struct { + AppId string // 小程序或者公众号的appid + AppSecret string + MchId string // 微信支付的商户id + AesKey string + ApiV3 string + PrivateSerialNo string // 私钥证书号 + MchPrivateKey string // 路径 apiclient_key.pem +} + +// ErrResp 错误返回 +type ErrResp struct { + Code string `json:"code"` + Message string `json:"message"` + Detail struct { + Field string `json:"field,omitempty"` + Value interface{} `json:"value"` + Issue string `json:"issue,omitempty"` + Location string `json:"location"` + } `json:"detail"` +} + +func (app *App) request(url string, params map[string]interface{}) (resp []byte, result ErrResp, err error) { + + canonicalURL := fmt.Sprintf("%s/%s", WechatPayAPIServer, url) + method := "POST" + authorization, _ := app.authorization(method, params, canonicalURL) + + marshal, _ := json.Marshal(params) + + var req *http.Request + req, err = http.NewRequest(method, canonicalURL, bytes.NewReader(marshal)) + if err != nil { + return + } + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + req.Header.Add("Accept-Language", "zh-CN") + req.Header.Add("Authorization", "WECHATPAY2-SHA256-RSA2048 "+authorization) + httpClient := &http.Client{} + var response *http.Response + response, err = httpClient.Do(req) + + if err != nil { + return nil, result, err + } + + // 处理成功 + defer response.Body.Close() + resp, err = ioutil.ReadAll(response.Body) + + // 检查错误 + if err = json.Unmarshal(resp, &result); err != nil { + return nil, result, err + } + + // 检查请求错误 + if response.StatusCode == 200 { + return resp, result, err + } + + return nil, result, err +} diff --git a/service/wechat/pay/v3/config.go b/service/wechat/pay/v3/config.go new file mode 100644 index 00000000..25897d46 --- /dev/null +++ b/service/wechat/pay/v3/config.go @@ -0,0 +1,7 @@ +package v3 + +// 微信支付 API 地址 +const ( + WechatPayAPIServer = "https://api.mch.weixin.qq.com/v3" // 微信支付 API 地址 + WechatPayAPIServerBackup = "https://api2.mch.weixin.qq.com/v3" // 微信支付 API 备份地址 +) diff --git a/service/wechat/pay/v3/pay_transactions_jsapi.go b/service/wechat/pay/v3/pay_transactions_jsapi.go new file mode 100644 index 00000000..4fe0a464 --- /dev/null +++ b/service/wechat/pay/v3/pay_transactions_jsapi.go @@ -0,0 +1,95 @@ +package v3 + +import ( + "encoding/json" + "time" +) + +type PayTransactionsJsapi struct { + Appid string `json:"appid"` //【是】应用ID + Mchid string `json:"mchid"` //【是】直连商户号 + Description string `json:"description"` //【是】商品描述 + OutTradeNo string `json:"out_trade_no"` //【是】商户订单号 + TimeExpire time.Time `json:"time_expire,omitempty"` //【否】交易结束时间 + Attach string `json:"attach,omitempty"` //【否】附加数据 + NotifyUrl string `json:"notify_url"` //【是】通知地址 + GoodsTag string `json:"goods_tag,omitempty"` //【否】订单优惠标记 + Amount *PayTransactionsJsapiAmount `json:"amount"` //【是】订单金额 + Payer *PayTransactionsJsapiPayer `json:"payer"` //【是】支付者 + Detail *PayTransactionsJsapiDetail `json:"detail,omitempty"` //【否】优惠功能 + SceneInfo *PayTransactionsJsapiSceneInfo `json:"scene_info,omitempty"` //【否】场景信息 + SettleInfo *PayTransactionsJsapiSettleInfo `json:"settle_info,omitempty"` //【否】结算信息 +} + +// PayTransactionsJsapiAmount 订单金额 +type PayTransactionsJsapiAmount struct { + Total int `json:"total"` //【是】总金额 + Currency string `json:"currency,omitempty"` //【否】货币类型 +} + +// PayTransactionsJsapiPayer 支付者 +type PayTransactionsJsapiPayer struct { + Openid string `json:"openid"` //【是】用户标识 +} + +// PayTransactionsJsapiDetail 优惠功能 +type PayTransactionsJsapiDetail struct { + CostPrice int `json:"cost_price,omitempty"` //【否】订单原价 + InvoiceId string `json:"invoice_id,omitempty"` //【否】商品小票ID + GoodsDetail []PayTransactionsJsapiDetailGoodsDetail `json:"goods_detail,omitempty"` //【否】单品列表 +} + +// PayTransactionsJsapiDetailGoodsDetail 单品列表 +type PayTransactionsJsapiDetailGoodsDetail struct { + MerchantGoodsId string `json:"merchant_goods_id"` //【是】商户侧商品编码 + WechatpayGoodsId string `json:"wechatpay_goods_id,omitempty"` //【否】微信侧商品编码 + GoodsName string `json:"goods_name,omitempty"` //【否】商品名称 + Quantity int `json:"quantity"` //【是】商品数量 + UnitPrice int `json:"unit_price"` //【是】商品单价 +} + +// PayTransactionsJsapiSceneInfo 场景信息 +type PayTransactionsJsapiSceneInfo struct { + PayerClientIp string `json:"payer_client_ip"` //【是】用户终端IP + DeviceId string `json:"device_id,omitempty"` //【否】商户端设备号 + StoreInfo *PayTransactionsJsapiSceneInfoStoreInfo `json:"store_info,omitempty"` //【否】商户门店信息 +} + +// PayTransactionsJsapiSceneInfoStoreInfo 商户门店信息 +type PayTransactionsJsapiSceneInfoStoreInfo struct { + Id string `json:"id"` //【是】门店编号 + Name string `json:"name,omitempty"` //【否】门店名称 + AreaCode string `json:"area_code,omitempty"` //【否】地区编码 + Address string `json:"address,omitempty"` //【否】详细地址 +} + +// PayTransactionsJsapiSettleInfo 结算信息 +type PayTransactionsJsapiSettleInfo struct { + ProfitSharing bool `json:"profit_sharing,omitempty"` //【否】是否指定分账 +} + +type PayTransactionsJsapiResult struct { + PrepayId string `json:"prepay_id"` +} + +// PayTransactionsJsapi 小程序 JSAPI下单 https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_5_1.shtml +func (app *App) PayTransactionsJsapi(param PayTransactionsJsapi) (resp PayTransactionsJsapiResult, result ErrResp, err error) { + // api params + params := map[string]interface{}{} + b, _ := json.Marshal(¶m) + var m map[string]interface{} + _ = json.Unmarshal(b, &m) + for k, v := range m { + params[k] = v + } + + body, result, err := app.request("pay/transactions/jsapi", params) + + if err != nil { + return + } + if err = json.Unmarshal(body, &resp); err != nil { + return + } + return +} diff --git a/service/wechat/pay/v3/sign.go b/service/wechat/pay/v3/sign.go new file mode 100644 index 00000000..c45f3488 --- /dev/null +++ b/service/wechat/pay/v3/sign.go @@ -0,0 +1,121 @@ +package v3 + +import ( + "crypto" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "gitee.com/dtapps/go-library/utils/random" + "io/ioutil" + "net/url" + "os" + "time" +) + +// 对消息的散列值进行数字签名 +func (app *App) signPKCS1v15(msg string, privateKey []byte) ([]byte, error) { + + block, _ := pem.Decode(privateKey) + if block == nil { + return nil, errors.New("private key decode error") + } + pri, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, errors.New("parse private key error") + } + key, ok := pri.(*rsa.PrivateKey) + if ok == false { + return nil, errors.New("private key format error") + } + sign, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA256, app.haSha256(msg)) + if err != nil { + return nil, errors.New("sign error") + } + return sign, nil +} + +// base编码 +func (app *App) base64EncodeStr(src []byte) string { + return base64.StdEncoding.EncodeToString(src) +} + +// sha256加密 +func (app *App) haSha256(str string) []byte { + h := sha256.New() + h.Write([]byte(str)) + return h.Sum(nil) +} + +// 生成身份认证信息 +func (app *App) authorization(method string, paramMap map[string]interface{}, rawUrl string) (token string, err error) { + var body string + if len(paramMap) != 0 { + paramJsonBytes, err := json.Marshal(paramMap) + if err != nil { + return token, err + } + body = string(paramJsonBytes) + } + urlPart, err := url.Parse(rawUrl) + if err != nil { + return token, err + } + canonicalUrl := urlPart.RequestURI() + timestamp := time.Now().Unix() + nonce := random.Alphanumeric(32) + message := fmt.Sprintf("%s\n%s\n%d\n%s\n%s\n", method, canonicalUrl, timestamp, nonce, body) + open, err := os.Open(app.MchPrivateKey) // 商户私有证书路径或者从数据库读取 + if err != nil { + return token, err + } + defer open.Close() + privateKey, err := ioutil.ReadAll(open) + + if err != nil { + return token, err + } + + signBytes, err := app.signPKCS1v15(message, privateKey) + + if err != nil { + return token, err + } + + sign := app.base64EncodeStr(signBytes) + + token = fmt.Sprintf("mchid=\"%s\",nonce_str=\"%s\",timestamp=\"%d\",serial_no=\"%s\",signature=\"%s\"", + app.MchId, nonce, timestamp, app.PrivateSerialNo, sign) + return token, nil +} + +// 报文解密 +func (app *App) decryptGCM(aesKey, nonceV, ciphertextV, additionalDataV string) ([]byte, error) { + key := []byte(aesKey) + nonce := []byte(nonceV) + additionalData := []byte(additionalDataV) + ciphertext, err := base64.StdEncoding.DecodeString(ciphertextV) + if err != nil { + return nil, err + } + block, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + aesGCM, err := cipher.NewGCM(block) + if err != nil { + return nil, err + } + plaintext, err := aesGCM.Open(nil, nonce, ciphertext, additionalData) + if err != nil { + return nil, err + } + return plaintext, err +} diff --git a/utils/daes/daes.go b/utils/aes/aes.go similarity index 99% rename from utils/daes/daes.go rename to utils/aes/aes.go index ee78de3e..492c4e1e 100644 --- a/utils/daes/daes.go +++ b/utils/aes/aes.go @@ -1,4 +1,4 @@ -package daes +package aes import ( "bytes" diff --git a/utils/duuid/duuid_test.go b/utils/duuid/duuid_test.go deleted file mode 100644 index 0d769fe9..00000000 --- a/utils/duuid/duuid_test.go +++ /dev/null @@ -1,11 +0,0 @@ -package duuid_test - -import ( - "fmt" - "gitee.com/dtapps/go-library/utils/duuid" - "testing" -) - -func TestName(t *testing.T) { - fmt.Println(duuid.GenUUID()) -} diff --git a/utils/random/random.go b/utils/random/random.go new file mode 100644 index 00000000..98502a09 --- /dev/null +++ b/utils/random/random.go @@ -0,0 +1,45 @@ +package random + +import ( + "math/rand" + "time" +) + +const numbers string = "0123456789" +const letters string = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" +const specials = "~!@#$%^*()_+-=[]{}|;:,./<>?" +const alphanumerics string = letters + numbers +const ascii string = alphanumerics + specials + +func random(n int, chars string) string { + if n <= 0 { + return "" + } + r := rand.New(rand.NewSource(time.Now().UnixNano())) + bytes := make([]byte, n, n) + l := len(chars) + for i := 0; i < n; i++ { + bytes[i] = chars[r.Intn(l)] + } + return string(bytes) +} + +// Alphanumeric 随机字母数字 +func Alphanumeric(n int) string { + return random(n, alphanumerics) +} + +// Alphabetic 随机字母 +func Alphabetic(n int) string { + return random(n, letters) +} + +// Numeric 随机数字 +func Numeric(n int) string { + return random(n, numbers) +} + +// Ascii 随机ASCII +func Ascii(n int) string { + return random(n, ascii) +} diff --git a/utils/random/random_test.go b/utils/random/random_test.go new file mode 100644 index 00000000..bb9ab076 --- /dev/null +++ b/utils/random/random_test.go @@ -0,0 +1,22 @@ +package random + +import ( + "fmt" + "testing" +) + +func TestAlphanumeric(t *testing.T) { + fmt.Println(Alphanumeric(10)) +} + +func TestAlphabetic(t *testing.T) { + fmt.Println(Alphabetic(10)) +} + +func TestNumeric(t *testing.T) { + fmt.Println(Numeric(10)) +} + +func TestAscii(t *testing.T) { + fmt.Println(Ascii(10)) +} diff --git a/utils/dssh/dssh.go b/utils/ssh/ssh.go similarity index 99% rename from utils/dssh/dssh.go rename to utils/ssh/ssh.go index 92067796..c8fb1166 100644 --- a/utils/dssh/dssh.go +++ b/utils/ssh/ssh.go @@ -1,4 +1,4 @@ -package dssh +package ssh import ( "fmt" diff --git a/utils/dssh/dssh_test.go b/utils/ssh/ssh_test.go similarity index 90% rename from utils/dssh/dssh_test.go rename to utils/ssh/ssh_test.go index 6878ac6b..53764359 100644 --- a/utils/dssh/dssh_test.go +++ b/utils/ssh/ssh_test.go @@ -1,4 +1,4 @@ -package dssh +package ssh import ( "testing" diff --git a/utils/dstring/string.go b/utils/string/string.go similarity index 95% rename from utils/dstring/string.go rename to utils/string/string.go index 2313a4bc..1d360357 100644 --- a/utils/dstring/string.go +++ b/utils/string/string.go @@ -1,4 +1,4 @@ -package dstring +package string import ( "crypto/hmac" diff --git a/utils/dtime/dtime.go b/utils/time/time.go similarity index 98% rename from utils/dtime/dtime.go rename to utils/time/time.go index 69579748..fb59e120 100644 --- a/utils/dtime/dtime.go +++ b/utils/time/time.go @@ -1,4 +1,4 @@ -package dtime +package time import "time" diff --git a/utils/dtime/dtime_test.go b/utils/time/time_test.go similarity index 94% rename from utils/dtime/dtime_test.go rename to utils/time/time_test.go index 82bef4fe..52e9d146 100644 --- a/utils/dtime/dtime_test.go +++ b/utils/time/time_test.go @@ -1,4 +1,4 @@ -package dtime +package time import ( "fmt" diff --git a/utils/durl/durl.go b/utils/url/url.go similarity index 98% rename from utils/durl/durl.go rename to utils/url/url.go index eb0ffdcb..a099e7be 100644 --- a/utils/durl/durl.go +++ b/utils/url/url.go @@ -1,4 +1,4 @@ -package durl +package url import ( "io" diff --git a/utils/durl/durl_test.go b/utils/url/url_test.go similarity index 96% rename from utils/durl/durl_test.go rename to utils/url/url_test.go index 0d37560b..9cc8bbf3 100644 --- a/utils/durl/durl_test.go +++ b/utils/url/url_test.go @@ -1,4 +1,4 @@ -package durl +package url import ( "fmt" diff --git a/utils/duuid/duuid.go b/utils/uuid/uuid.go similarity index 90% rename from utils/duuid/duuid.go rename to utils/uuid/uuid.go index 515fca1f..b1c431fa 100644 --- a/utils/duuid/duuid.go +++ b/utils/uuid/uuid.go @@ -1,4 +1,4 @@ -package duuid +package uuid import ( "github.com/google/uuid" diff --git a/utils/uuid/uuid_test.go b/utils/uuid/uuid_test.go new file mode 100644 index 00000000..76f056f0 --- /dev/null +++ b/utils/uuid/uuid_test.go @@ -0,0 +1,11 @@ +package uuid_test + +import ( + "fmt" + "gitee.com/dtapps/go-library/utils/uuid" + "testing" +) + +func TestName(t *testing.T) { + fmt.Println(uuid.GenUUID()) +}