Wechat Mini-program payment code writen by golang

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
}

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值