|
|
@@ -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")
|
|
|
+}
|