Bladeren bron

feat: add virtual recharge payment api

Codex 1 maand geleden
bovenliggende
commit
f83701e8cd

+ 2 - 2
go/gopath/src/fohow.com/apps/controllers/pay_controller/init.go

@@ -28,8 +28,8 @@ import (
 
 var (
 	//不需要校验用户登录的Action
-	exceptCheckUserLoginAction   = []string{"PayAsync"}
-	exceptCheckWxUserLoginAction = []string{"PayAsync"}
+	exceptCheckUserLoginAction   = []string{"PayAsync", "VirtualRechargeNotify"}
+	exceptCheckWxUserLoginAction = []string{"PayAsync", "VirtualRechargeNotify"}
 )
 
 type PayController struct {

+ 369 - 0
go/gopath/src/fohow.com/apps/controllers/pay_controller/virtual_pay_controller.go

@@ -0,0 +1,369 @@
+package pay_controller
+
+import (
+	"bytes"
+	"crypto/hmac"
+	"crypto/sha256"
+	"encoding/hex"
+	"encoding/json"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"regexp"
+	"strings"
+	"time"
+
+	"fohow.com/apps"
+	"fohow.com/apps/models/balance_model"
+	"fohow.com/apps/models/user_model"
+	"fohow.com/libs/lib_redis"
+	"fohow.com/libs/wx_mp"
+
+	"github.com/astaxie/beego"
+)
+
+const (
+	virtualPayMode         = "short_series_coin"
+	virtualPayCurrencyType = "CNY"
+)
+
+type virtualPaySignData struct {
+	OfferID      string `json:"offerId"`
+	BuyQuantity  int64  `json:"buyQuantity"`
+	Env          int    `json:"env"`
+	CurrencyType string `json:"currencyType"`
+	Platform     string `json:"platform,omitempty"`
+	OutTradeNo   string `json:"outTradeNo"`
+	Attach       string `json:"attach"`
+}
+
+type virtualPayAttach struct {
+	Type      string `json:"type"`
+	OrderID   string `json:"order_id"`
+	WxUserID  int64  `json:"wx_user_id"`
+	UserID    int64  `json:"user_id"`
+	AmountFen int64  `json:"amount_fen"`
+}
+
+type virtualPayQueryOrderRequest struct {
+	OpenID  string `json:"openid"`
+	Env     int    `json:"env"`
+	OrderID string `json:"order_id,omitempty"`
+}
+
+type virtualPayQueryOrderResponse struct {
+	ErrCode int    `json:"errcode"`
+	ErrMsg  string `json:"errmsg"`
+	Order   *struct {
+		OrderID      string `json:"order_id"`
+		Status       int    `json:"status"`
+		OrderFee     int64  `json:"order_fee"`
+		PaidFee      int64  `json:"paid_fee"`
+		WxOrderID    string `json:"wx_order_id"`
+		WxPayOrderID string `json:"wxpay_order_id"`
+	} `json:"order"`
+}
+
+type virtualPayNotifyProvideGoodsRequest struct {
+	OrderID   string `json:"order_id,omitempty"`
+	WxOrderID string `json:"wx_order_id,omitempty"`
+	Env       int    `json:"env"`
+}
+
+type virtualPayCommonResponse struct {
+	ErrCode int    `json:"errcode"`
+	ErrMsg  string `json:"errmsg"`
+}
+
+// 小程序余额虚拟支付充值下单
+func (self *PayController) CreateVirtualRecharge() {
+	count, _ := self.GetInt64("count")
+	if count <= 0 || count%100 != 0 {
+		self.ReturnError(403, apps.RechargeCountWrong, "", nil)
+	}
+
+	user := self.GetCurrentUser(true)
+	if user == nil {
+		self.ReturnError(403, apps.NoExist, "", nil)
+	}
+	wxUser := self.GetCurrentWxUser(true)
+	if wxUser == nil {
+		self.ReturnError(403, apps.NoExist, "", nil)
+	}
+
+	_, sessionKey := lib_redis.GetSimpleValue(lib_redis.GetKeySessionKey(wxUser.Id))
+	if sessionKey == "" {
+		self.ReturnError(403, apps.XcxGetSessionKeyError, "", nil)
+	}
+
+	offerID, appKey, env := self.getVirtualPayConfig()
+	if offerID == "" || appKey == "" {
+		beego.BeeLogger.Error("CreateVirtualRecharge virtual pay config missing, env=%d", env)
+		self.ReturnError(403, apps.ParamsError, "", nil)
+	}
+
+	order := new(balance_model.RechargeCashOrder).Create(wxUser.Id, user.Id, count, balance_model.PAY_WAY_TYPE_RECHARGE_VIRTUAL)
+	if order == nil {
+		self.ReturnError(403, apps.ParamsError, "", nil)
+	}
+
+	attachBytes, _ := json.Marshal(&virtualPayAttach{
+		Type:      "cash_recharge",
+		OrderID:   order.OrderId,
+		WxUserID:  wxUser.Id,
+		UserID:    user.Id,
+		AmountFen: count,
+	})
+	signDataBytes, _ := json.Marshal(&virtualPaySignData{
+		OfferID:      offerID,
+		BuyQuantity:  count / 100,
+		Env:          env,
+		CurrencyType: virtualPayCurrencyType,
+		Platform:     "android",
+		OutTradeNo:   order.OrderId,
+		Attach:       string(attachBytes),
+	})
+	signData := string(signDataBytes)
+
+	type Ret struct {
+		OrderID   string `json:"order_id"`
+		OfferID   string `json:"offerId"`
+		Env       int    `json:"env"`
+		Mode      string `json:"mode"`
+		SignData  string `json:"signData"`
+		PaySig    string `json:"paySig"`
+		Signature string `json:"signature"`
+	}
+
+	self.Data["json"] = &Ret{
+		OrderID:   order.OrderId,
+		OfferID:   offerID,
+		Env:       env,
+		Mode:      virtualPayMode,
+		SignData:  signData,
+		PaySig:    virtualPayHMAC(appKey, "requestVirtualPayment&"+signData),
+		Signature: virtualPayHMAC(sessionKey, signData),
+	}
+	self.ServeJSON()
+}
+
+// 小程序轮询余额虚拟支付充值订单状态
+func (self *PayController) GetVirtualRechargeStatus() {
+	orderID := strings.TrimSpace(self.GetString("order_id"))
+	if orderID == "" {
+		self.ReturnError(403, apps.ParamsError, "", nil)
+	}
+	wxUser := self.GetCurrentWxUser(true)
+	if wxUser == nil {
+		self.ReturnError(403, apps.NoExist, "", nil)
+	}
+	order := balance_model.GetRechargeCashOrderByOId(orderID, false)
+	if order == nil || order.WxUserId != wxUser.Id {
+		self.ReturnError(403, apps.NoExist, "", nil)
+	}
+	if order.State != 1 {
+		self.syncVirtualRechargePaid(order, wxUser.Openid)
+	}
+	type Ret struct {
+		OrderID string `json:"order_id"`
+		State   int64  `json:"state"`
+		PaidAt  int64  `json:"paied_at"`
+	}
+	self.Data["json"] = &Ret{OrderID: order.OrderId, State: order.State, PaidAt: order.PaiedAt}
+	self.ServeJSON()
+}
+
+// 微信小程序虚拟支付推送回调
+func (self *PayController) VirtualRechargeNotify() {
+	if echo := self.GetString("echostr"); echo != "" {
+		self.Ctx.WriteString(echo)
+		return
+	}
+
+	body := string(self.Ctx.Input.CopyBody(102400))
+	orderID := extractVirtualPayOrderID(body)
+	if orderID == "" {
+		orderID = strings.TrimSpace(self.GetString("order_id"))
+	}
+	if orderID == "" {
+		beego.BeeLogger.Error("VirtualRechargeNotify order id missing, body=%s", body)
+		self.Ctx.WriteString("success")
+		return
+	}
+	if isVirtualPayNegativeNotify(body) {
+		beego.BeeLogger.Warn("VirtualRechargeNotify ignore negative notify, order_id=%s, body=%s", orderID, body)
+		self.Ctx.WriteString("success")
+		return
+	}
+
+	order := balance_model.GetRechargeCashOrderByOId(orderID, false)
+	if order == nil {
+		beego.BeeLogger.Error("VirtualRechargeNotify order not found, order_id=%s, body=%s", orderID, body)
+		self.Ctx.WriteString("success")
+		return
+	}
+	wxUser := user_model.GetWxUserById(order.WxUserId, true)
+	if wxUser == nil {
+		beego.BeeLogger.Error("VirtualRechargeNotify wxUser not found, order_id=%s, wx_user_id=%d", orderID, order.WxUserId)
+		self.Ctx.WriteString("success")
+		return
+	}
+	self.syncVirtualRechargePaid(order, wxUser.Openid)
+	self.Ctx.WriteString("success")
+}
+
+func (self *PayController) getVirtualPayConfig() (offerID string, appKey string, env int) {
+	offerID = beego.AppConfig.String("WxVirtualPayOfferId")
+	host := strings.ToLower(self.Ctx.Request.Host)
+	apiHost := strings.ToLower(beego.AppConfig.String("ApiHost"))
+	runEnv := strings.ToLower(beego.AppConfig.String("Env"))
+
+	if strings.Contains(host, "tfohowapi.hiwavo.com") || strings.Contains(apiHost, "tfohowapi.hiwavo.com") || runEnv != "production" {
+		return offerID, beego.AppConfig.String("WxVirtualPaySandboxAppKey"), 1
+	}
+	return offerID, beego.AppConfig.String("WxVirtualPayProdAppKey"), 0
+}
+
+func (self *PayController) syncVirtualRechargePaid(order *balance_model.RechargeCashOrder, openid string) bool {
+	if openid == "" {
+		return false
+	}
+	_, appKey, env := self.getVirtualPayConfig()
+	if appKey == "" {
+		return false
+	}
+	reqBody, _ := json.Marshal(&virtualPayQueryOrderRequest{
+		OpenID:  openid,
+		Env:     env,
+		OrderID: order.OrderId,
+	})
+	respBody, err := self.virtualPayPost("/xpay/query_order", string(reqBody), appKey)
+	if err != nil {
+		beego.BeeLogger.Warn("syncVirtualRechargePaid query_order failed, order_id=%s, err=%s", order.OrderId, err)
+		return false
+	}
+	resp := &virtualPayQueryOrderResponse{}
+	if err := json.Unmarshal([]byte(respBody), resp); err != nil {
+		beego.BeeLogger.Warn("syncVirtualRechargePaid parse response failed, order_id=%s, body=%s, err=%s", order.OrderId, respBody, err)
+		return false
+	}
+	if resp.ErrCode != 0 || resp.Order == nil {
+		beego.BeeLogger.Warn("syncVirtualRechargePaid query_order not paid, order_id=%s, errcode=%d, errmsg=%s", order.OrderId, resp.ErrCode, resp.ErrMsg)
+		return false
+	}
+	if resp.Order.Status != 2 && resp.Order.Status != 3 && resp.Order.Status != 4 {
+		return false
+	}
+	if resp.Order.PaidFee > 0 && resp.Order.PaidFee != order.TotalPrice {
+		beego.BeeLogger.Error("syncVirtualRechargePaid paid fee mismatch, order_id=%s, paid_fee=%d, total_price=%d", order.OrderId, resp.Order.PaidFee, order.TotalPrice)
+		return false
+	}
+	tradeNo := resp.Order.WxPayOrderID
+	if tradeNo == "" {
+		tradeNo = resp.Order.WxOrderID
+	}
+	if !markVirtualRechargePaid(order, tradeNo) {
+		return false
+	}
+	if resp.Order.Status == 2 || resp.Order.Status == 3 {
+		self.notifyVirtualPayProvided(order.OrderId, resp.Order.WxOrderID, appKey, env)
+	}
+	return true
+}
+
+func (self *PayController) notifyVirtualPayProvided(orderID, wxOrderID, appKey string, env int) {
+	reqBody, _ := json.Marshal(&virtualPayNotifyProvideGoodsRequest{
+		OrderID:   orderID,
+		WxOrderID: wxOrderID,
+		Env:       env,
+	})
+	respBody, err := self.virtualPayPost("/xpay/notify_provide_goods", string(reqBody), appKey)
+	if err != nil {
+		beego.BeeLogger.Warn("notifyVirtualPayProvided failed, order_id=%s, err=%s", orderID, err)
+		return
+	}
+	resp := &virtualPayCommonResponse{}
+	if err := json.Unmarshal([]byte(respBody), resp); err != nil || resp.ErrCode != 0 {
+		beego.BeeLogger.Warn("notifyVirtualPayProvided response failed, order_id=%s, body=%s, err=%v", orderID, respBody, err)
+	}
+}
+
+func (self *PayController) virtualPayPost(uri, body, appKey string) (string, error) {
+	accessToken := wx_mp.GetAccessToken(beego.AppConfig.String("WxFohowXcxAppId"), beego.AppConfig.String("WxFohowXcxAppSecret"))
+	if accessToken == "" {
+		return "", fmt.Errorf("access token empty")
+	}
+	paySig := virtualPayHMAC(appKey, uri+"&"+body)
+	apiURL := fmt.Sprintf("https://api.weixin.qq.com%s?access_token=%s&pay_sig=%s", uri, url.QueryEscape(accessToken), url.QueryEscape(paySig))
+	req, err := http.NewRequest("POST", apiURL, bytes.NewBufferString(body))
+	if err != nil {
+		return "", err
+	}
+	req.Header.Set("Content-Type", "application/json")
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return "", err
+	}
+	defer resp.Body.Close()
+	respBody, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return "", err
+	}
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		return string(respBody), fmt.Errorf("http status %d", resp.StatusCode)
+	}
+	return string(respBody), nil
+}
+
+func virtualPayHMAC(key, data string) string {
+	mac := hmac.New(sha256.New, []byte(key))
+	mac.Write([]byte(data))
+	return hex.EncodeToString(mac.Sum(nil))
+}
+
+func markVirtualRechargePaid(order *balance_model.RechargeCashOrder, tradeNo string) bool {
+	if order.State == 1 {
+		rechargeCash(order.WxUserId, order.UserId, order.TotalPrice, order.OrderId)
+		return true
+	}
+	order.State = 1
+	order.TradeNo = tradeNo
+	order.PayWay = balance_model.PAY_WAY_TYPE_RECHARGE_VIRTUAL
+	order.PaiedAt = time.Now().Unix()
+	if err := order.Save(); err != nil {
+		beego.BeeLogger.Error("markVirtualRechargePaid save order failed, order_id=%s, err=%s", order.OrderId, err)
+		return false
+	}
+	rechargeCash(order.WxUserId, order.UserId, order.TotalPrice, order.OrderId)
+	return true
+}
+
+func extractVirtualPayOrderID(payload string) string {
+	re := regexp.MustCompile(`CZCB[0-9A-Za-z_\-\|\*@]{4,28}`)
+	return re.FindString(payload)
+}
+
+func extractVirtualPayTradeNo(payload string) string {
+	for _, pattern := range []string{
+		`"wx_order_id"\s*:\s*"([^"]+)"`,
+		`"wxOrderId"\s*:\s*"([^"]+)"`,
+		`"transaction_id"\s*:\s*"([^"]+)"`,
+		`"transactionId"\s*:\s*"([^"]+)"`,
+		`<WxOrderId><!\[CDATA\[(.*?)\]\]></WxOrderId>`,
+		`<TransactionId><!\[CDATA\[(.*?)\]\]></TransactionId>`,
+	} {
+		re := regexp.MustCompile(pattern)
+		if matches := re.FindStringSubmatch(payload); len(matches) > 1 {
+			return matches[1]
+		}
+	}
+	return ""
+}
+
+func isVirtualPayNegativeNotify(payload string) bool {
+	payload = strings.ToLower(payload)
+	return strings.Contains(payload, "xpay_refund_notify") ||
+		strings.Contains(payload, "xpay_complaint_notify") ||
+		strings.Contains(payload, "refund")
+}

+ 2 - 1
go/gopath/src/fohow.com/apps/models/balance_model/cash_balance.go

@@ -86,7 +86,8 @@ const (
 	CASH_SOURCE_GOOD_PAYMENT      = "good_payment"
 	CASH_SOURCE_GOOD_PAYMENT_NAME = "货款"
 
-	PAY_WAY_TYPE_RECHARGE_WXPAY = "recharge_wxpay" // 充值支付方式
+	PAY_WAY_TYPE_RECHARGE_WXPAY   = "recharge_wxpay"   // 充值支付方式
+	PAY_WAY_TYPE_RECHARGE_VIRTUAL = "recharge_virtual" // 小程序虚拟支付充值
 
 	ORDER_ID_PREFIX_CZCB = "CZCB" //充值余额
 

+ 6 - 0
go/gopath/src/fohow.com/routers/routes.go

@@ -135,6 +135,12 @@ func init() {
 	beego.Router("/v1/balance_order/generate", &pay_controller.PayController{}, "post:CreateBalanceOrder")
 	//生成充值余额订单
 	beego.Router("/v1/recas_order/generate", &pay_controller.PayController{}, "post:CreateRechargeCashOrder")
+	//生成虚拟支付充值余额订单
+	beego.Router("/v1/virtual_pay/recharge", &pay_controller.PayController{}, "post:CreateVirtualRecharge")
+	//查询虚拟支付充值余额订单状态
+	beego.Router("/v1/virtual_pay/recharge/status", &pay_controller.PayController{}, "get:GetVirtualRechargeStatus")
+	//虚拟支付充值余额回调
+	beego.Router("/v1/virtual_pay/recharge/notify", &pay_controller.PayController{}, "post,get:VirtualRechargeNotify")
 	//----------- 文章相关 -----------
 	//新闻列表
 	beego.Router("/v1/artcat/:cat_id/articles", &article_controller.ArticleController{}, "get:GetList")