Default run in sandbox mode. you should change api package's const value of isSandbox to false if you change to run on prod mode.
[追加]:被沙箱模式坑死了,总是提示 requestPayment:fail:调用支付JSAPI缺少参数:total_fee;取消 sandbox,直接使用生产模式,未做任何修改却成功了。
[追加]:06/19 第三次修改,callback 时,校验字段补全。
use case:
func (s *WechatPaymentService) PaymentNoticeCallback() {
outputXML, req, err := miniprg.PaymentNoticeCallback(s.Ctx.Input.RequestBody)
s.Ctx.Output.Header("Content-Type", "application/xml; charset=utf-8")
s.Ctx.Output.Body(outputXML)
if err != nil {
beego.Error(err.Error())
return
}
// update transaction status
beego.Debug("Start to update the transaction status after the Wechat payment system return success notice.")
beego.Debug(req.TransactionID)
}
func (s *WechatPaymentService) UnifyOrder() {
currentUser := s.GetCurrentUser()
v := struct {
InvoiceCode string `json:"InvoiceCode"`
}{}
if err := json.Unmarshal(s.Ctx.Input.RequestBody, &v); err != nil {
beego.Notice("Request data parse error: %s", err.Error())
s.ReplyErrCode(errcode.ErrReqPrs)
return
}
appID := beego.AppConfig.String("wechat::appid")
// create unified payment order
order := miniprg.UnifyOrderRequest{}
order.AppID = appID
order.MerchantID = miniprg.GetMerchantID()
order.NotifyURL = miniprg.GetPaymentNotifyURL()
order.TransactionType = "JSAPI"
order.SignatureType = "MD5"
order.OpenID = currentUser.OID
order.ItemDescription = "payment test"
order.RandomString = miniprg.GetRandomStr(32)
order.MerchantTradeNo = v.InvoiceCode
order.TerminalIP = s.Ctx.Input.IP()
order.TotalFee = 102
// send unified payment order request to the Wechat payment system
res, err := miniprg.UnifyOrder(&order)
if err != nil {
beego.Error(err.Error())
return
}
// calculate mini-program payment signature
d := struct {
AppID string `xml:"appId" json:"-"`
TimeStamp string `xml:"timeStamp" json:"timeStamp"`
RandomString string `xml:"nonceStr" json:"nonceStr"`
Package string `xml:"package" json:"package"`
SignType string `xml:"signType" json:"signType"`
Signature string `xml:"paySign" json:"paySign"`
}{}
d.AppID = appID
d.TimeStamp = strconv.FormatInt(time.Now().Unix(), 10)
d.RandomString = miniprg.GetRandomStr(32)
d.Package = fmt.Sprintf("prepay_id=%s", res.PrepayID)
d.SignType = "MD5"
sign, err := miniprg.CalculateSignature(&d, d.SignType, "paySign")
if err != nil {
beego.Error("Failed to calculate wx.requestPayment(Object object) payment signature: %s", err.Error())
return
}
d.Signature = sign
// TODO: create payment record
// reply payment data
s.Reply(d)
}
api package:
const isSandbox = true
var (
paymentAPIKey string
unifyOrderURL string
merchantID = "11111111111"
realPaymentAPIKey = "zzzzzzzzzzzzzzz"
notifyURL = "https://testdomain/pay/paycallback"
)
// UnifyOrderRequest unified order api request data struct
type UnifyOrderRequest struct {
XMLName xml.Name `xml:"xml"`
AppID string `xml:"appid"`
OpenID string `xml:"openid"`
ItemDescription string `xml:"body"`
MerchantID string `xml:"mch_id"`
RandomString string `xml:"nonce_str"`
NotifyURL string `xml:"notify_url"`
TransactionType string `xml:"trade_type"`
TerminalIP string `xml:"spbill_create_ip"`
TotalFee int `xml:"total_fee"`
MerchantTradeNo string `xml:"out_trade_no"`
SignatureType string `xml:"sign_type"`
Signature string `xml:"sign"`
}
// UnifyOrderResponse unified order api response data struct
type UnifyOrderResponse struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
AppID string `xml:"appid"`
MerchantID string `xml:"mch_id"`
RandomString string `xml:"nonce_str"`
Signature string `xml:"sign"`
ServiceResult string `xml:"result_code"`
PrepayID string `xml:"prepay_id"`
TransactionType string `xml:"trade_type"`
}
// PaymentNoticeCallbackRequest data struct of transaction result which from the Wechat payment system after payment completed
type PaymentNoticeCallbackRequest struct {
AppID string `xml:"appid"`
Attach string `xml:"attach"`
BankType string `xml:"bank_type"`
CashFee int `xml:"cash_fee"`
CashFeeType string `xml:"cash_fee_type"`
CouponCount int `xml:"coupon_count"`
CouponFee string `xml:"coupon_fee"`
CouponFees string `xml:"coupon_fee_0"`
CouponIDs string `xml:"coupon_id_0"`
CouponTypes string `xml:"coupon_type_0"`
DeviceInfo string `xml:"device_info"`
ErrorCode string `xml:"err_code"`
ErrorCodeDescription string `xml:"err_code_des"`
FeeType string `xml:"fee_type"`
IsSubscribe string `xml:"is_subscribe"`
MerchantID string `xml:"mch_id"`
Nonce string `xml:"nonce_str"`
OpenID string `xml:"openid"`
OutTradeNo string `xml:"out_trade_no"`
ResultCode string `xml:"result_code"`
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
SettlementTotalFee string `xml:"settlement_total_fee"`
SignatureType string `xml:"sign_type"`
Signature string `xml:"sign"`
TimeEnd string `xml:"time_end"`
TotalFee int `xml:"total_fee"`
TradeType string `xml:"trade_type"`
TransactionID string `xml:"transaction_id"`
}
// CDATA xml character data
type CDATA string
// MarshalXML xml character data marshal method
func (c CDATA) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeElement(struct {
string `xml:",cdata"`
}{string(c)}, start)
}
func init() {
// prod mode
if !isSandbox {
unifyOrderURL = "https://api.mch.weixin.qq.com/pay/unifiedorder"
paymentAPIKey = realPaymentAPIKey
return
}
// test mode
o := struct {
XMLName xml.Name `xml:"xml"`
MerchantID string `xml:"mch_id"`
RandomString string `xml:"nonce_str"`
Signature string `xml:"sign"`
}{}
o.MerchantID = merchantID
o.RandomString = GetRandomStr(32)
paymentAPIKey = realPaymentAPIKey
if sign, err := CalculateSignature(&o, "MD5", "xml", "sign"); err != nil {
panic(fmt.Sprintf("Init sandbox signKey error: %s", err.Error()))
} else {
o.Signature = sign
}
r := struct {
ReturnCode string `xml:"return_code"`
ReturnMsg string `xml:"return_msg"`
MerchantID string `xml:"mch_id"`
SandboxAPIKey string `xml:"sandbox_signkey"`
}{}
if err := xmlRequest("https://api.mch.weixin.qq.com/sandboxnew/pay/getsignkey", o, &r); err != nil {
panic(fmt.Sprintf("Init sandbox signKey error: %s", err.Error()))
}
unifyOrderURL = "https://api.mch.weixin.qq.com/sandboxnew/pay/unifiedorder"
paymentAPIKey = r.SandboxAPIKey
}
// GetMerchantID return merchantID string
func GetMerchantID() string {
return merchantID
}
// GetPaymentNotifyURL return payment callback url
func GetPaymentNotifyURL() string {
return notifyURL
}
// GetRandomStr return random string
func GetRandomStr(l int) string {
bs := bytes.Buffer{}
for {
v := rand.Int63()
bs.WriteString(strconv.FormatInt(v, 16))
if bs.Len() >= l {
break
}
}
return string(bs.Next(l))
}
// xmlRequest post data and verify return code be "SUCCESS"
func xmlRequest(url string, input, output interface{}) error {
// data to xml
sb, err := xml.Marshal(input)
if err != nil {
return fmt.Errorf("Failed to convert data to xml: %s", err.Error())
}
// println("request xml content: ", string(sb))
// send http request
req, err := http.NewRequest("POST", url, bytes.NewReader(sb))
if err != nil {
return fmt.Errorf("Failed to create http request: %s", err.Error())
}
req.Header.Set("Accept", "application/xml")
req.Header.Set("Content-Type", "application/xml;charset=utf-8")
c := http.Client{}
res, err := c.Do(req)
if err != nil {
return fmt.Errorf("Failed to send http request: %s", err.Error())
}
defer res.Body.Close()
// verify return message
sb, err = ioutil.ReadAll(res.Body)
if err != nil {
return fmt.Errorf("Failed to read http response: %s", err.Error())
}
// println("response xml content: ", string(sb))
err = xml.Unmarshal(sb, output)
if err != nil {
return fmt.Errorf("Failed to convert xml response to output data: %s", err.Error())
}
// sb, _ = json.Marshal(output)
// println("response data content: ", string(sb))
fields := reflect.ValueOf(output).Elem()
returnCode := fields.FieldByName("ReturnCode")
returnMsg := fields.FieldByName("ReturnMsg")
if returnCode.Kind() == reflect.String && returnMsg.Kind() == reflect.String {
if returnCode.Interface() != "SUCCESS" {
return fmt.Errorf("The Wechat Payment system response an error: %v", returnMsg.Interface())
}
} else {
return errors.New("return code and message not found in output data")
}
return nil
}
// CalculateSignature keys is xml node tag, not data struct field name
// hasType set to "MD5" if pass a empty string
func CalculateSignature(order interface{}, hashType string, excludeKeys ...string) (sign string, err error) {
if hashType != "MD5" && hashType != "" {
return "", errors.New("Only support MD5 hash type")
}
// recover reflect error
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("Failed to calculate signature: %v", r)
}
}()
fields := reflect.ValueOf(order).Elem()
types := fields.Type()
// step 1, sort keys asc
var ks []string
ns := make(map[string]string)
for i := 0; i < types.NumField(); i++ {
f := types.Field(i)
t := f.Tag.Get("xml")
if func(t string, a []string) bool {
for _, v := range a {
if v == t {
return true
}
}
return false
}(t, excludeKeys) {
continue
}
// add xml keys
ks = append(ks, t)
// add xml key and field name map
ns[t] = f.Name
}
sort.Strings(ks)
// step 2, join all key=value by "&", ignore empty value
sb := bytes.Buffer{}
for _, k := range ks {
value := fields.FieldByName(ns[k]).Interface()
if value == "" || value == 0 {
continue
}
sb.WriteString(fmt.Sprintf("%s=%v&", k, value))
}
// step 3, append key=API_KEY
sb.WriteString(fmt.Sprintf("key=%s", paymentAPIKey))
// println("signature string before:", sb.String())
// step 4, encoding by MD5 and to upper-case
md5Ctx := md5.New()
md5Ctx.Write(sb.Bytes())
cipherStr := md5Ctx.Sum(nil)
sign = strings.ToUpper(hex.EncodeToString(cipherStr))
return
}
// UnifyOrder send unified order request to the Wechat payment system then return prepayID
func UnifyOrder(order *UnifyOrderRequest) (r UnifyOrderResponse, err error) {
// calculate signature
sign, err := CalculateSignature(order, order.SignatureType, "xml", "sign")
if err != nil {
return
}
order.Signature = sign
// post to the Wechat payment system
if err = xmlRequest(unifyOrderURL, order, &r); err != nil {
err = fmt.Errorf("Failed to send unified payment order to the Wechat payment system: %s", err.Error())
return
}
return r, nil
}
// getCouponValues find coupon values then return as "v0"
func getCouponValues(s, key string, l int) string {
re := regexp.MustCompile(fmt.Sprintf(`<%s_(\d+)>(.*)</%s_\d+>`, key, key))
as := re.FindAllStringSubmatch(s, -1)
if len(as) != l {
panic("Length not match")
}
val := make([]string, l, l)
for _, a := range as {
i, _ := strconv.Atoi(a[1])
// use string replace because maybe one day
// the wechat payment system change to number type without tag "<![DATA[]]>"
val[i] = strings.Replace(strings.Replace(a[2], "<![CDATA[", "", -1), "]]>", "", -1)
}
var res string
for i, v := range val {
if i == 0 {
res += v
} else {
res += fmt.Sprintf("&%s_%d=%s", key, i, v)
}
}
return res
}
// PaymentNoticeCallback return xml content and notice request data.
// panic when xml marshal error occus.
func PaymentNoticeCallback(s []byte) (outputXML []byte, req PaymentNoticeCallbackRequest, err error) {
res := struct {
XMLName xml.Name `xml:"xml"`
ReturnCode CDATA `xml:"return_code"`
ReturnMsg CDATA `xml:"return_msg"`
}{}
defer func() {
if err != nil {
res.ReturnCode = "FAIL"
res.ReturnMsg = CDATA(err.Error())
}
// xml content which will reply to the Wechat payment system
if sb, e := xml.Marshal(res); e != nil {
panic(fmt.Sprintf("Failed to marshal notice reply xml content: %s", e.Error()))
} else {
outputXML = sb
}
}()
// println(string(s))
// parse xml content which from the Wechat payment system
if e := xml.Unmarshal(s, &req); e != nil {
err = fmt.Errorf("Failed to convert payment notice callback xml request to Object: %s", e.Error())
return
}
// get coupon message
if req.CouponCount > 0 {
ss := string(s)
req.CouponIDs = getCouponValues(ss, "coupon_id", req.CouponCount)
req.CouponTypes = getCouponValues(ss, "coupon_type", req.CouponCount)
req.CouponFees = getCouponValues(ss, "coupon_fee", req.CouponCount)
}
// verify signature
if sign, e := CalculateSignature(&req, req.SignatureType, "sign"); e != nil {
err = fmt.Errorf("Failed to calculate payment notice callback signature: %s", e.Error())
return
} else if sign != req.Signature {
err = errors.New("Failed to verify payment notice callback signature, not equals to calculate signature")
return
} else {
// verify success
res.ReturnCode = "SUCCESS"
res.ReturnMsg = "OK"
}
return
}