Explorar el Código

feat: notify users for merchant transfer withdraw

Codex hace 1 mes
padre
commit
b7637f280c

+ 137 - 18
go/gopath/src/fohow.com/apps/controllers/balance_controller/balance_controller.go

@@ -5,6 +5,7 @@ import (
 	"fohow.com/apps/models/sys_config"
 	"fohow.com/libs/lib_redis"
 	"fohow.com/libs/tool"
+	"fohow.com/libs/wx_mp"
 	"github.com/astaxie/beego"
 	"math"
 	"strings"
@@ -27,9 +28,9 @@ import (
 )
 
 const (
-	TAKE_CASH_AMOUNT_LIMIT_MIN    = 1000    //10元
-	TAKE_CASH_AMOUNT_LIMIT_MAX    = 2000000 //2w
-	WX_TAKE_CASH_AMOUNT_LIMIT_MAX = 20000   //5000元
+	TAKE_CASH_AMOUNT_LIMIT_MIN    = 1000  //10元
+	TAKE_CASH_AMOUNT_LIMIT_MAX    = 20000 //200元
+	WX_TAKE_CASH_AMOUNT_LIMIT_MAX = 20000 //200元
 )
 
 // 提货券和提货券余额
@@ -120,6 +121,16 @@ func (self *BalanceController) GetCashBalanceDetail() {
 	item.CTime = item.CreatedAt.Unix()
 	item.CreatedAtText = item.CreatedAt.Format("2006-01-02 15:04:05")
 	item.SourceName = item.GetSourceName()
+	if item.Source == balance_model.CASH_SOURCE_TAKE_CASH && item.RelateId != "" {
+		takeCashOrder := balance_model.GetTakeCashOrderByOId(item.RelateId, false)
+		if takeCashOrder != nil {
+			wxUser := user_model.GetWxUserById(item.WxUId, false)
+			if wxUser != nil {
+				syncTakeCashMerchantTransfer(wxUser, takeCashOrder)
+				fillCashBalanceTransferInfo(item, wxUser, takeCashOrder)
+			}
+		}
+	}
 	self.Data["json"] = item
 	self.ServeJSON()
 }
@@ -195,7 +206,7 @@ func (self *BalanceController) TakeCash() {
 	}
 
 	if amount < TAKE_CASH_AMOUNT_LIMIT_MIN || amount > TAKE_CASH_AMOUNT_LIMIT_MAX {
-		self.ReturnError(403, []string{"amountLimit", "提现单日限额2W,最低10元"}, "", nil)
+		self.ReturnError(403, []string{"amountLimit", "单次提现金额最低10元,最高200元"}, "", nil)
 	}
 
 	/*	if user.IsCertification != 1 {
@@ -226,7 +237,7 @@ func (self *BalanceController) TakeCash() {
 		self.ReturnError(403, apps.BalanceNotEnough, "", nil)
 	}
 
-	//超过微信余额限制5K,按5K计算
+	//超过商家转账单笔限制200元,按200元计算
 	if amount > WX_TAKE_CASH_AMOUNT_LIMIT_MAX {
 		amount = WX_TAKE_CASH_AMOUNT_LIMIT_MAX
 	}
@@ -286,7 +297,7 @@ func (self *BalanceController) GetTakeCashLimit() {
 		FeeBl        int64 `json:"fee_bl"`         //手续费比例
 	}
 	limitInfo := new(LimitInfo)
-	limitInfo.MaxLimitCash = TAKE_CASH_AMOUNT_LIMIT_MAX
+	limitInfo.MaxLimitCash = WX_TAKE_CASH_AMOUNT_LIMIT_MAX
 	limitInfo.MinLimitCash = TAKE_CASH_AMOUNT_LIMIT_MIN
 	limitInfo.FeeBl = sys_config.GetTakeCashTax()
 
@@ -310,23 +321,28 @@ func (self *BalanceController) GetTakeCashOrders() {
 
 	//user := self.GetCurrentUser(cache)
 	wxUId := self.GetCurrentWxUserIdByToken()
+	wxUser := user_model.GetWxUserById(wxUId, false)
 
 	list := balance_model.GetTakeCashOrderListByWxUId(wxUId, page, perPage, cache)
 	listCount := balance_model.GetTakeCashOrderCountByWxUId(wxUId, cache)
 
 	type Order struct {
-		Id    int64 ` json:"id"`     // int(11)
-		WxUId int64 ` json:"wx_uid"` // int(11)
-		//OrderId      string ` json:"order_id"`    // varchar(64)
-		//TradeNo      string ` json:"trade_no"`    // varchar(64)
-		Count      int64  ` json:"count"`       // bigint(20)
-		State      int64  ` json:"pay_state"`   // tinyint(1)
-		AuditState int64  ` json:"audit_state"` // tinyint(1)
-		StateCN    string ` json:"state_cn"`
-		PaiedAt    int64  ` json:"paied_at"` // int(11)
-		Remark     string ` json:"remark"`   // varchar(255)
-		//ExpcPayAt  time.Time ` json:"expc_pay_at"` // datetime
-		CreatedAt int64 ` json:"created_at"` // datetime
+		Id            int64  ` json:"id"`     // int(11)
+		WxUId         int64  ` json:"wx_uid"` // int(11)
+		OrderId       string ` json:"order_id"`
+		TradeNo       string ` json:"trade_no"`
+		Count         int64  ` json:"count"`       // bigint(20)
+		State         int64  ` json:"pay_state"`   // tinyint(1)
+		AuditState    int64  ` json:"audit_state"` // tinyint(1)
+		StateCN       string ` json:"state_cn"`
+		PaiedAt       int64  ` json:"paied_at"` // int(11)
+		Remark        string ` json:"remark"`   // varchar(255)
+		MchId         string `json:"mch_id"`
+		AppId         string `json:"app_id"`
+		PackageInfo   string `json:"package_info"`
+		TransferState string `json:"transfer_state"`
+		CanConfirm    bool   `json:"can_confirm_transfer"`
+		CreatedAt     int64  ` json:"created_at"` // datetime
 	}
 
 	retList := make([]*Order, 0, 0)
@@ -336,12 +352,18 @@ func (self *BalanceController) GetTakeCashOrders() {
 
 		order.Id = item.Id
 		order.WxUId = item.WxUId
+		order.OrderId = item.OrderId
+		order.TradeNo = item.TradeNo
 		order.Count = item.Count
 		order.StateCN = item.GetStateCn()
 		order.PaiedAt = item.PaiedAt
 		order.CreatedAt = item.CreatedAt.Unix()
 		order.State = item.State
 		order.AuditState = item.AuditState
+		order.Remark = item.Remark
+		if wxUser != nil {
+			order.MchId, order.AppId, order.PackageInfo, order.TransferState, order.CanConfirm = getMerchantTransferConfirmInfo(wxUser, item)
+		}
 
 		retList = append(retList, order)
 	}
@@ -355,6 +377,103 @@ func (self *BalanceController) GetTakeCashOrders() {
 	self.ServeJSON()
 }
 
+func fillCashBalanceTransferInfo(item *balance_model.CashBalance, wxUser *user_model.WxUser, order *balance_model.TakeCashOrder) {
+	item.TakeCashId = order.Id
+	item.TakeCashState = order.State
+	item.TakeCashAudit = order.AuditState
+	item.MchId, item.AppId, item.PackageInfo, item.TransferState, item.CanConfirm = getMerchantTransferConfirmInfo(wxUser, order)
+}
+
+func getMerchantTransferConfirmInfo(wxUser *user_model.WxUser, order *balance_model.TakeCashOrder) (mchId, appId, packageInfo, transferState string, canConfirm bool) {
+	if wxUser == nil || order == nil {
+		return
+	}
+	payCode := sys_config.GetPayConfigByDepart(wxUser.Depart, true)
+	mechantInfo := wx_mp.GetMechantInfo(payCode)
+	mchId = mechantInfo.MchId
+	appId = beego.AppConfig.String("WxFohowXcxAppId")
+	packageInfo = getMerchantTransferPackageInfo(order.Remark)
+	transferState = getMerchantTransferState(order.Remark)
+	canConfirm = order.State == 0 && order.AuditState == 1 && packageInfo != "" && transferState == wx_mp.MerchantTransferWaitUserConfirm
+	return
+}
+
+func syncTakeCashMerchantTransfer(wxUser *user_model.WxUser, order *balance_model.TakeCashOrder) {
+	if wxUser == nil || order == nil || order.TradeNo == "" || order.State == 1 {
+		return
+	}
+	payCode := sys_config.GetPayConfigByDepart(wxUser.Depart, true)
+	ret, err := wx_mp.QueryMerchantTransferByOutBillNo(order.OrderId, payCode)
+	if err != nil || ret == nil {
+		return
+	}
+	if ret.TransferBillNo != "" {
+		order.TradeNo = ret.TransferBillNo
+	}
+	switch ret.State {
+	case wx_mp.MerchantTransferSuccess:
+		order.State = 1
+		order.Remark = "商家转账已成功"
+		order.PaiedAt = time.Now().Unix()
+	case wx_mp.MerchantTransferFail, wx_mp.MerchantTransferCancelled:
+		order.State = 2
+		order.Remark = truncateTakeCashRemark(firstNotEmpty(ret.FailReason, ret.Message, ret.State))
+	default:
+		if wx_mp.MerchantTransferStatePending(ret.State) {
+			order.State = 0
+			order.Remark = truncateTakeCashRemark(merchantTransferPendingRemark(ret, order.Remark))
+		}
+	}
+	order.Save()
+}
+
+func getMerchantTransferState(remark string) string {
+	const prefix = "商家转账状态:"
+	if !strings.Contains(remark, prefix) {
+		return ""
+	}
+	state := strings.TrimPrefix(remark[strings.Index(remark, prefix):], prefix)
+	if idx := strings.Index(state, ";"); idx >= 0 {
+		state = state[:idx]
+	}
+	return state
+}
+
+func getMerchantTransferPackageInfo(remark string) string {
+	const prefix = ";package_info="
+	idx := strings.Index(remark, prefix)
+	if idx < 0 {
+		return ""
+	}
+	return remark[idx+len(prefix):]
+}
+
+func merchantTransferPendingRemark(ret *wx_mp.MerchantTransferResponse, oldRemark string) string {
+	remark := "商家转账状态:" + ret.State
+	if ret.PackageInfo != "" {
+		remark += ";package_info=" + ret.PackageInfo
+	} else if idx := strings.Index(oldRemark, ";package_info="); idx >= 0 {
+		remark += oldRemark[idx:]
+	}
+	return remark
+}
+
+func firstNotEmpty(values ...string) string {
+	for _, value := range values {
+		if value != "" {
+			return value
+		}
+	}
+	return ""
+}
+
+func truncateTakeCashRemark(remark string) string {
+	if len([]rune(remark)) <= 240 {
+		return remark
+	}
+	return string([]rune(remark)[:240])
+}
+
 // 佣金转提货券
 func (self *BalanceController) TransferToBalance() {
 	//单位:分

+ 81 - 51
go/gopath/src/fohow.com/apps/controllers/cron_controller/sync_balance.go

@@ -8,6 +8,7 @@ import (
 	"github.com/go-redis/redis"
 	"io"
 	"strconv"
+	"strings"
 	"time"
 
 	"fohow.com/apps/models/balance_model"
@@ -30,7 +31,7 @@ func (self *SyncData) Sign() string {
 	return fmt.Sprintf("%x", h.Sum(nil))
 }
 
-//提现打款的定时任务
+// 提现打款的定时任务
 func takeCash() {
 	//选择出已审批通过的提现中订单
 	beego.BeeLogger.Warn("********** take cash cron task start at: %s********", time.Now())
@@ -44,41 +45,12 @@ func takeCash() {
 			beego.BeeLogger.Error("Take cash wxUser[%d] is not exist", item.WxUId)
 			continue
 		}
-		user := user_model.GetUserById(wxUser.UserId, false)
-		if user == nil {
-			beego.BeeLogger.Error("Take cash User[%d] is not exist", wxUser.UserId)
-			continue
-		}
-		var check_state string
-		check_state = wx_mp.PAY_NO_CHECK
-
-		/*if item.RealState == balance_model.REAL_STATE {
-			if user.RealName == "" {
-				beego.BeeLogger.Error("Take cash User[%d] has no real name", wxUser.UserId)
-				continue
-			}
-			check_state = wx_mp.PAY_FORCE_CHECK
-		} else {
-			check_state = wx_mp.PAY_NO_CHECK
-		}*/
 		payCode := sys_config.GetPayConfigByDepart(wxUser.Depart, true)
-		ret := wx_mp.Transfers(wxUser.Openid, item.Count, item.OrderId, check_state, user.RealName, payCode, "提现")
-		if ret["result_code"] == wx_mp.PAY_SUCCESS {
-			item.State = 1
-			item.TradeNo = ret["payment_no"]
-			// paiedAt, _ := time.Parse("2006-01-02 15:04:05", ret["payment_time"])
-			item.Remark = "提现已打款"
-			item.PaiedAt = time.Now().Unix()
-			item.Save()
-		} else {
-			item.State = 2
-			item.Remark = ret["err_code_des"]
-			item.Save()
-		}
+		_, _ = updateTakeCashByMerchantTransfer(item, wxUser.Openid, payCode)
 	}
 }
 
-//自动提现打款的定时任务
+// 自动提现打款的定时任务
 func autoTakeCash() {
 	db, err := beego.AppConfig.Int("AliRedisDb")
 	if err != nil {
@@ -116,31 +88,89 @@ func autoTakeCash() {
 			beego.BeeLogger.Error("Take cash wxUser[%d] is not exist", cashRecord.WxUId)
 			continue
 		}
-		user := user_model.GetUserById(wxUser.UserId, false)
-		if user == nil {
-			beego.BeeLogger.Error("Take cash User[%d] is not exist", wxUser.UserId)
-			continue
-		}
-
 		//提现成功,剔除队列
 		lib_redis.ThrowOutRedisList(cashKey, takeId)
 
-		var check_state string
-		check_state = wx_mp.PAY_NO_CHECK
 		payCode := sys_config.GetPayConfigByDepart(wxUser.Depart, true)
+		_, _ = updateTakeCashByMerchantTransfer(cashRecord, wxUser.Openid, payCode)
+	}
+}
+
+func updateTakeCashByMerchantTransfer(order *balance_model.TakeCashOrder, openid, payCode string) (*wx_mp.MerchantTransferResponse, error) {
+	var (
+		ret *wx_mp.MerchantTransferResponse
+		err error
+	)
+	createdTransfer := order.TradeNo == ""
+	if order.TradeNo != "" {
+		ret, err = wx_mp.QueryMerchantTransferByOutBillNo(order.OrderId, payCode)
+	} else {
+		ret, err = wx_mp.MerchantTransfer(openid, order.Count, order.OrderId, payCode, "佣金提现")
+	}
+	if err != nil {
+		beego.BeeLogger.Error("MerchantTransfer order[%s] err[%s]", order.OrderId, err)
+		if ret != nil && ret.Message != "" {
+			order.State = 2
+			order.Remark = truncateTakeCashRemark(ret.Message)
+			order.Save()
+		}
+		return ret, err
+	}
+	applyMerchantTransferResult(order, ret)
+	if createdTransfer {
+		if notifyErr := wx_mp.SendTakeCashArrivedSubscribe(openid, order.Id, order.Count+order.Fee, order.Fee, order.Count, ret.State, ret.PackageInfo); notifyErr != nil {
+			beego.BeeLogger.Error("Take cash subscribe order[%s] err[%s]", order.OrderId, notifyErr)
+		}
+	}
+	return ret, nil
+}
 
-		ret := wx_mp.Transfers(wxUser.Openid, cashRecord.Count, cashRecord.OrderId, check_state, user.RealName, payCode, "提现")
-		if ret["result_code"] == wx_mp.PAY_SUCCESS {
-			cashRecord.State = 1
-			cashRecord.TradeNo = ret["payment_no"]
-			// paiedAt, _ := time.Parse("2006-01-02 15:04:05", ret["payment_time"])
-			cashRecord.Remark = "提现已打款"
-			cashRecord.PaiedAt = time.Now().Unix()
-			cashRecord.Save()
+func applyMerchantTransferResult(order *balance_model.TakeCashOrder, ret *wx_mp.MerchantTransferResponse) {
+	if ret.TransferBillNo != "" {
+		order.TradeNo = ret.TransferBillNo
+	}
+	switch ret.State {
+	case wx_mp.MerchantTransferSuccess:
+		order.State = 1
+		order.Remark = "商家转账已成功"
+		order.PaiedAt = time.Now().Unix()
+	case wx_mp.MerchantTransferFail, wx_mp.MerchantTransferCancelled:
+		order.State = 2
+		order.Remark = truncateTakeCashRemark(firstNotEmpty(ret.FailReason, ret.Message, ret.State))
+	default:
+		if wx_mp.MerchantTransferStatePending(ret.State) {
+			order.State = 0
+			order.Remark = truncateTakeCashRemark(merchantTransferPendingRemark(ret, order.Remark))
 		} else {
-			cashRecord.State = 2
-			cashRecord.Remark = ret["err_code_des"]
-			cashRecord.Save()
+			order.State = 0
+			order.Remark = truncateTakeCashRemark(firstNotEmpty(ret.Message, ret.State, "商家转账已受理"))
 		}
 	}
+	order.Save()
+}
+
+func firstNotEmpty(values ...string) string {
+	for _, value := range values {
+		if value != "" {
+			return value
+		}
+	}
+	return ""
+}
+
+func merchantTransferPendingRemark(ret *wx_mp.MerchantTransferResponse, oldRemark string) string {
+	remark := "商家转账状态:" + ret.State
+	if ret.PackageInfo != "" {
+		remark += ";package_info=" + ret.PackageInfo
+	} else if idx := strings.Index(oldRemark, ";package_info="); idx >= 0 {
+		remark += oldRemark[idx:]
+	}
+	return remark
+}
+
+func truncateTakeCashRemark(remark string) string {
+	if len([]rune(remark)) <= 240 {
+		return remark
+	}
+	return string([]rune(remark)[:240])
 }

+ 160 - 0
go/gopath/src/fohow.com/apps/controllers/railsadmin_controller/take_cash_controller.go

@@ -0,0 +1,160 @@
+package railsadmin_controller
+
+import (
+	"fohow.com/apps"
+	"fohow.com/apps/models/balance_model"
+	"fohow.com/apps/models/sys_config"
+	"fohow.com/apps/models/user_model"
+	"fohow.com/libs/wx_mp"
+	"github.com/astaxie/beego"
+	"strconv"
+	"strings"
+	"time"
+)
+
+// 佣金提现商家转账
+func (self *RailsadminController) TakeCash() {
+	_id := self.Ctx.Input.Param(":id")
+	id, _ := strconv.ParseInt(_id, 10, 64)
+	beego.BeeLogger.Warn("Railsadmin TakeCash id:(%d)", id)
+
+	order := balance_model.GetTakeCashOrderById(id, false)
+	if order == nil {
+		self.ReturnError(404, apps.NoExist, "", nil)
+	}
+	if order.AuditState != 1 || order.State != 0 || order.PaiedAt > 0 {
+		self.ReturnError(403, apps.ParamsError, "", nil)
+	}
+	wxUser := user_model.GetWxUserById(order.WxUId, false)
+	if wxUser == nil {
+		self.ReturnError(404, apps.WxUserNotExist, "", nil)
+	}
+	payCode := sys_config.GetPayConfigByDepart(wxUser.Depart, true)
+	createdTransfer := order.TradeNo == ""
+	ret, err := railsadminUpdateTakeCashByMerchantTransfer(order, wxUser.Openid, payCode)
+	notifyErr := sendTakeCashSubscribeIfNeeded(createdTransfer, wxUser.Openid, order, ret)
+	self.Data["json"] = map[string]interface{}{
+		"order_id":         order.OrderId,
+		"trade_no":         order.TradeNo,
+		"pay_state":        order.State,
+		"remark":           order.Remark,
+		"wechat_state":     getMerchantTransferState(ret),
+		"package_info":     getMerchantTransferPackageInfo(ret),
+		"transfer_bill_no": getMerchantTransferBillNo(ret),
+		"error":            errString(err),
+		"notify_error":     errString(notifyErr),
+	}
+	self.ServeJSON()
+}
+
+func railsadminUpdateTakeCashByMerchantTransfer(order *balance_model.TakeCashOrder, openid, payCode string) (*wx_mp.MerchantTransferResponse, error) {
+	var (
+		ret *wx_mp.MerchantTransferResponse
+		err error
+	)
+	if order.TradeNo != "" {
+		ret, err = wx_mp.QueryMerchantTransferByOutBillNo(order.OrderId, payCode)
+	} else {
+		ret, err = wx_mp.MerchantTransfer(openid, order.Count, order.OrderId, payCode, "佣金提现")
+	}
+	if err != nil {
+		beego.BeeLogger.Error("Railsadmin MerchantTransfer order[%s] err[%s]", order.OrderId, err)
+		if ret != nil && ret.Message != "" {
+			order.State = 2
+			order.Remark = railsadminTruncateTakeCashRemark(ret.Message)
+			order.Save()
+		}
+		return ret, err
+	}
+	railsadminApplyMerchantTransferResult(order, ret)
+	return ret, nil
+}
+
+func railsadminApplyMerchantTransferResult(order *balance_model.TakeCashOrder, ret *wx_mp.MerchantTransferResponse) {
+	if ret.TransferBillNo != "" {
+		order.TradeNo = ret.TransferBillNo
+	}
+	switch ret.State {
+	case wx_mp.MerchantTransferSuccess:
+		order.State = 1
+		order.Remark = "商家转账已成功"
+		order.PaiedAt = time.Now().Unix()
+	case wx_mp.MerchantTransferFail, wx_mp.MerchantTransferCancelled:
+		order.State = 2
+		order.Remark = railsadminTruncateTakeCashRemark(railsadminFirstNotEmpty(ret.FailReason, ret.Message, ret.State))
+	default:
+		if wx_mp.MerchantTransferStatePending(ret.State) {
+			order.State = 0
+			order.Remark = railsadminTruncateTakeCashRemark(railsadminMerchantTransferPendingRemark(ret, order.Remark))
+		} else {
+			order.State = 0
+			order.Remark = railsadminTruncateTakeCashRemark(railsadminFirstNotEmpty(ret.Message, ret.State, "商家转账已受理"))
+		}
+	}
+	order.Save()
+}
+
+func getMerchantTransferState(ret *wx_mp.MerchantTransferResponse) string {
+	if ret == nil {
+		return ""
+	}
+	return ret.State
+}
+
+func getMerchantTransferPackageInfo(ret *wx_mp.MerchantTransferResponse) string {
+	if ret == nil {
+		return ""
+	}
+	return ret.PackageInfo
+}
+
+func getMerchantTransferBillNo(ret *wx_mp.MerchantTransferResponse) string {
+	if ret == nil {
+		return ""
+	}
+	return ret.TransferBillNo
+}
+
+func errString(err error) string {
+	if err == nil {
+		return ""
+	}
+	return err.Error()
+}
+
+func sendTakeCashSubscribeIfNeeded(createdTransfer bool, openid string, order *balance_model.TakeCashOrder, ret *wx_mp.MerchantTransferResponse) error {
+	if !createdTransfer || order == nil || ret == nil {
+		return nil
+	}
+	err := wx_mp.SendTakeCashArrivedSubscribe(openid, order.Id, order.Count+order.Fee, order.Fee, order.Count, ret.State, ret.PackageInfo)
+	if err != nil {
+		beego.BeeLogger.Error("Railsadmin take cash subscribe order[%s] err[%s]", order.OrderId, err)
+	}
+	return err
+}
+
+func railsadminFirstNotEmpty(values ...string) string {
+	for _, value := range values {
+		if value != "" {
+			return value
+		}
+	}
+	return ""
+}
+
+func railsadminMerchantTransferPendingRemark(ret *wx_mp.MerchantTransferResponse, oldRemark string) string {
+	remark := "商家转账状态:" + ret.State
+	if ret.PackageInfo != "" {
+		remark += ";package_info=" + ret.PackageInfo
+	} else if idx := strings.Index(oldRemark, ";package_info="); idx >= 0 {
+		remark += oldRemark[idx:]
+	}
+	return remark
+}
+
+func railsadminTruncateTakeCashRemark(remark string) string {
+	if len([]rune(remark)) <= 240 {
+		return remark
+	}
+	return string([]rune(remark)[:240])
+}

+ 11 - 3
go/gopath/src/fohow.com/apps/models/balance_model/cash_balance.go

@@ -102,9 +102,17 @@ type CashBalance struct {
 	RelateId      string    `orm:"column(relate_id);null"                              json:"relate_id"` // varchar(255)
 	Remark        string    `orm:"column(remark);null"                                 json:"remark"`    // varchar(255)
 	CTime         int64     `orm:"-"                                       json:"ctime"`                 // int(11)
-	CreatedAt     time.Time `orm:"column(created_at);null;auto_now_add;type(datetime)" json:"-"`         // datetime
-	CreatedAtText string    `orm:"-"                                       json:"created_at"`            // datetime
-	UpdatedAt     time.Time `orm:"column(updated_at);null;auto_now;type(datetime)"     json:"-"`         // datetime
+	TakeCashId    int64     `orm:"-"                                       json:"take_cash_id"`
+	TakeCashState int64     `orm:"-"                                       json:"take_cash_state"`
+	TakeCashAudit int64     `orm:"-"                                       json:"take_cash_audit_state"`
+	MchId         string    `orm:"-"                                       json:"mch_id"`
+	AppId         string    `orm:"-"                                       json:"app_id"`
+	PackageInfo   string    `orm:"-"                                       json:"package_info"`
+	TransferState string    `orm:"-"                                       json:"transfer_state"`
+	CanConfirm    bool      `orm:"-"                                       json:"can_confirm_transfer"`
+	CreatedAt     time.Time `orm:"column(created_at);null;auto_now_add;type(datetime)" json:"-"` // datetime
+	CreatedAtText string    `orm:"-"                                       json:"created_at"`    // datetime
+	UpdatedAt     time.Time `orm:"column(updated_at);null;auto_now;type(datetime)"     json:"-"` // datetime
 }
 
 // 充值订单表

+ 234 - 0
go/gopath/src/fohow.com/libs/wx_mp/mch_transfer.go

@@ -0,0 +1,234 @@
+package wx_mp
+
+import (
+	"bytes"
+	"crypto"
+	"crypto/rand"
+	"crypto/rsa"
+	"crypto/sha256"
+	"crypto/x509"
+	"encoding/base64"
+	"encoding/json"
+	"encoding/pem"
+	"fmt"
+	"io/ioutil"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	"github.com/astaxie/beego"
+	"github.com/uuid"
+)
+
+const (
+	MerchantTransferAccepted        = "ACCEPTED"
+	MerchantTransferProcessing      = "PROCESSING"
+	MerchantTransferWaitUserConfirm = "WAIT_USER_CONFIRM"
+	MerchantTransferTransfering     = "TRANSFERING"
+	MerchantTransferSuccess         = "SUCCESS"
+	MerchantTransferFail            = "FAIL"
+	MerchantTransferCancelled       = "CANCELLED"
+	MerchantTransferCanceling       = "CANCELING"
+)
+
+type MerchantTransferReportInfo struct {
+	InfoType    string `json:"info_type"`
+	InfoContent string `json:"info_content"`
+}
+
+type MerchantTransferRequest struct {
+	AppId                    string                       `json:"appid"`
+	OutBillNo                string                       `json:"out_bill_no"`
+	TransferSceneId          string                       `json:"transfer_scene_id"`
+	OpenId                   string                       `json:"openid"`
+	TransferAmount           int64                        `json:"transfer_amount"`
+	TransferRemark           string                       `json:"transfer_remark"`
+	NotifyUrl                string                       `json:"notify_url,omitempty"`
+	UserRecvPerception       string                       `json:"user_recv_perception,omitempty"`
+	TransferSceneReportInfos []MerchantTransferReportInfo `json:"transfer_scene_report_infos,omitempty"`
+}
+
+type MerchantTransferResponse struct {
+	OutBillNo      string `json:"out_bill_no"`
+	TransferBillNo string `json:"transfer_bill_no"`
+	CreateTime     string `json:"create_time"`
+	State          string `json:"state"`
+	FailReason     string `json:"fail_reason"`
+	PackageInfo    string `json:"package_info"`
+	Code           string `json:"code"`
+	Message        string `json:"message"`
+	RawBody        string `json:"-"`
+	HttpStatus     int    `json:"-"`
+}
+
+func MerchantTransfer(openid string, amount int64, outBillNo, payCode, remark string) (*MerchantTransferResponse, error) {
+	appId := beego.AppConfig.String("WxFohowXcxAppId")
+	mechantInfo := GetMechantInfo(payCode)
+	if remark == "" {
+		remark = merchantTransferConfig("MchTransferRemark", "佣金提现")
+	}
+	req := MerchantTransferRequest{
+		AppId:                    appId,
+		OutBillNo:                outBillNo,
+		TransferSceneId:          merchantTransferConfig("MchTransferSceneId", "1005"),
+		OpenId:                   openid,
+		TransferAmount:           amount,
+		TransferRemark:           remark,
+		NotifyUrl:                merchantTransferConfig("MchTransferNotifyUrl", ""),
+		UserRecvPerception:       merchantTransferConfig("MchTransferUserRecvPerception", "劳务报酬"),
+		TransferSceneReportInfos: merchantTransferReportInfos(),
+	}
+	body, err := json.Marshal(req)
+	if err != nil {
+		return nil, err
+	}
+	return doMerchantTransferRequest("POST", "/v3/fund-app/mch-transfer/transfer-bills", body, mechantInfo)
+}
+
+func QueryMerchantTransferByOutBillNo(outBillNo, payCode string) (*MerchantTransferResponse, error) {
+	mechantInfo := GetMechantInfo(payCode)
+	path := "/v3/fund-app/mch-transfer/transfer-bills/out-bill-no/" + url.PathEscape(outBillNo)
+	return doMerchantTransferRequest("GET", path, nil, mechantInfo)
+}
+
+func MerchantTransferStatePending(state string) bool {
+	switch state {
+	case MerchantTransferAccepted, MerchantTransferProcessing, MerchantTransferWaitUserConfirm, MerchantTransferTransfering, MerchantTransferCanceling:
+		return true
+	default:
+		return false
+	}
+}
+
+func merchantTransferConfig(name, def string) string {
+	value := strings.TrimSpace(beego.AppConfig.String(name))
+	if value == "" {
+		return def
+	}
+	return value
+}
+
+func merchantTransferReportInfos() []MerchantTransferReportInfo {
+	raw := merchantTransferConfig("MchTransferReportInfos", "岗位类型:推广员,报酬说明:佣金提现")
+	if raw == "" {
+		return nil
+	}
+	var list []MerchantTransferReportInfo
+	for _, item := range strings.Split(raw, ",") {
+		item = strings.TrimSpace(item)
+		if item == "" {
+			continue
+		}
+		parts := strings.SplitN(item, ":", 2)
+		if len(parts) != 2 {
+			continue
+		}
+		infoType := strings.TrimSpace(parts[0])
+		infoContent := strings.TrimSpace(parts[1])
+		if infoType != "" && infoContent != "" {
+			list = append(list, MerchantTransferReportInfo{InfoType: infoType, InfoContent: infoContent})
+		}
+	}
+	return list
+}
+
+func doMerchantTransferRequest(method, path string, body []byte, mechantInfo MechantPayInfo) (*MerchantTransferResponse, error) {
+	timestamp := fmt.Sprintf("%d", time.Now().Unix())
+	nonce := strings.Replace(uuid.NewV4().String(), "-", "", -1)
+	bodyText := ""
+	if len(body) > 0 {
+		bodyText = string(body)
+	}
+	signature, serialNo, err := signWechatPayV3Request(method, path, timestamp, nonce, bodyText, mechantInfo)
+	if err != nil {
+		return nil, err
+	}
+	req, err := http.NewRequest(method, "https://api.mch.weixin.qq.com"+path, bytes.NewReader(body))
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Accept", "application/json")
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Authorization", fmt.Sprintf(`WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",signature="%s",timestamp="%s",serial_no="%s"`, mechantInfo.MchId, nonce, signature, timestamp, serialNo))
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return nil, err
+	}
+	defer resp.Body.Close()
+	respBody, _ := ioutil.ReadAll(resp.Body)
+
+	ret := &MerchantTransferResponse{RawBody: string(respBody), HttpStatus: resp.StatusCode}
+	if len(respBody) > 0 {
+		_ = json.Unmarshal(respBody, ret)
+	}
+	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+		if ret.Message == "" {
+			ret.Message = string(respBody)
+		}
+		return ret, fmt.Errorf("wechat merchant transfer status=%d code=%s message=%s", resp.StatusCode, ret.Code, ret.Message)
+	}
+	return ret, nil
+}
+
+func signWechatPayV3Request(method, path, timestamp, nonce, body string, mechantInfo MechantPayInfo) (string, string, error) {
+	privateKey, err := loadMerchantPrivateKey(mechantInfo.MchKeyFile)
+	if err != nil {
+		return "", "", err
+	}
+	serialNo, err := loadMerchantCertSerialNo(mechantInfo.MchCertFile)
+	if err != nil {
+		return "", "", err
+	}
+	message := strings.ToUpper(method) + "\n" + path + "\n" + timestamp + "\n" + nonce + "\n" + body + "\n"
+	h := sha256.Sum256([]byte(message))
+	signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, h[:])
+	if err != nil {
+		return "", "", err
+	}
+	return base64.StdEncoding.EncodeToString(signature), serialNo, nil
+}
+
+func loadMerchantPrivateKey(filename string) (*rsa.PrivateKey, error) {
+	keyData, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return nil, err
+	}
+	block, _ := pem.Decode(keyData)
+	if block == nil {
+		return nil, fmt.Errorf("invalid merchant private key pem")
+	}
+	switch block.Type {
+	case "PRIVATE KEY":
+		key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
+		if err != nil {
+			return nil, err
+		}
+		privateKey, ok := key.(*rsa.PrivateKey)
+		if !ok {
+			return nil, fmt.Errorf("merchant private key is not rsa")
+		}
+		return privateKey, nil
+	case "RSA PRIVATE KEY":
+		return x509.ParsePKCS1PrivateKey(block.Bytes)
+	default:
+		return nil, fmt.Errorf("unsupported private key type: %s", block.Type)
+	}
+}
+
+func loadMerchantCertSerialNo(filename string) (string, error) {
+	certData, err := ioutil.ReadFile(filename)
+	if err != nil {
+		return "", err
+	}
+	block, _ := pem.Decode(certData)
+	if block == nil {
+		return "", fmt.Errorf("invalid merchant cert pem")
+	}
+	cert, err := x509.ParseCertificate(block.Bytes)
+	if err != nil {
+		return "", err
+	}
+	return strings.ToUpper(cert.SerialNumber.Text(16)), nil
+}

+ 92 - 4
go/gopath/src/fohow.com/libs/wx_mp/xcx.go

@@ -23,10 +23,9 @@ type SessionKeyError struct {
 	Errmsg  string `json:"errmsg"`  //会话密钥
 }
 
-//
-//appId :小程序唯一标识
-//appSecret: 小程序的 app secret
-//code: 登录时获取的 code
+// appId :小程序唯一标识
+// appSecret: 小程序的 app secret
+// code: 登录时获取的 code
 func GetXcxSessionKey(appId, appSecret, code string) *SessionKey {
 	url := fmt.Sprintf("https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", appId, appSecret, code)
 	resp := tool.HttpCall(url, "GET", nil, nil)
@@ -122,6 +121,45 @@ type Template3MessageData struct {
 	Kerword3 DataKeywordValue `json:"keyword3"`
 }
 
+const TakeCashSubscribeTemplateId = "pmQeRrQUpBvt2jgDF4XwSEfDd7n2Gnyk8k_au1G6wP8"
+
+type SubscribeDataValue struct {
+	Value string `json:"value"`
+}
+
+type SubscribeMessage struct {
+	ToUser      string          `json:"touser"`
+	TemplateId  string          `json:"template_id"`
+	Page        string          `json:"page,omitempty"`
+	Data        json.RawMessage `json:"data"`
+	Lang        string          `json:"lang,omitempty"`
+	MiniProgram string          `json:"miniprogram_state,omitempty"`
+}
+
+type SubscribeMessageResponse struct {
+	ErrCode int64  `json:"errcode"`
+	ErrMsg  string `json:"errmsg"`
+}
+
+type TakeCashArrivedSubscribeData struct {
+	Amount3 SubscribeDataValue `json:"amount3"`
+	Amount4 SubscribeDataValue `json:"amount4"`
+	Amount5 SubscribeDataValue `json:"amount5"`
+}
+
+func SendTakeCashArrivedSubscribe(openid string, orderId, withdrawAmount, fee, arriveAmount int64, state, packageInfo string) error {
+	if openid == "" || packageInfo == "" || !MerchantTransferStatePending(state) {
+		return nil
+	}
+	page := fmt.Sprintf("packageUser/pages/user/cashDetail/cashDetail?id=%d", orderId)
+	data := TakeCashArrivedSubscribeData{
+		Amount3: SubscribeDataValue{Value: formatFen(withdrawAmount)},
+		Amount4: SubscribeDataValue{Value: formatFen(fee)},
+		Amount5: SubscribeDataValue{Value: formatFen(arriveAmount)},
+	}
+	return SendXcxSubscribeMessage(openid, TakeCashSubscribeTemplateId, page, data)
+}
+
 func SendTemplateMessage(toUserOpenId string, templateId string, page string, formId string, data json.RawMessage, emphasisKeyword string) {
 	token := GetAccessToken(beego.AppConfig.String("WxFohowXcxAppId"), beego.AppConfig.String("WxFohowXcxAppSecret"))
 	beego.BeeLogger.Info("TestSendingTemplatemsgController got token: %s", token)
@@ -156,3 +194,53 @@ func SendTemplateMessage(toUserOpenId string, templateId string, page string, fo
 	}
 	beego.BeeLogger.Warn("wx_mp.SendTemplateMessage, finished")
 }
+
+func SendXcxSubscribeMessage(toUserOpenId string, templateId string, page string, data interface{}) error {
+	token := GetAccessToken(beego.AppConfig.String("WxFohowXcxAppId"), beego.AppConfig.String("WxFohowXcxAppSecret"))
+	if token == "" {
+		return fmt.Errorf("xcx subscribe message access token is empty")
+	}
+	jsonTemplateData, err := json.Marshal(data)
+	if err != nil {
+		return err
+	}
+	msg := SubscribeMessage{
+		ToUser:     toUserOpenId,
+		TemplateId: templateId,
+		Page:       page,
+		Data:       jsonTemplateData,
+		Lang:       "zh_CN",
+	}
+	postData, err := json.Marshal(msg)
+	if err != nil {
+		return err
+	}
+	apiUrl := fmt.Sprintf("https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=%s", token)
+	req, err := http.NewRequest("POST", apiUrl, bytes.NewBuffer(postData))
+	if err != nil {
+		return err
+	}
+	req.Header.Set("Content-Type", "application/json")
+	resp, err := (&http.Client{}).Do(req)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+	body, err := ioutil.ReadAll(resp.Body)
+	if err != nil {
+		return err
+	}
+	beego.BeeLogger.Warn("wx_mp.SendXcxSubscribeMessage response: %s", string(body))
+	ret := new(SubscribeMessageResponse)
+	if err := json.Unmarshal(body, ret); err != nil {
+		return err
+	}
+	if ret.ErrCode != 0 {
+		return fmt.Errorf("wechat subscribe message errcode=%d errmsg=%s", ret.ErrCode, ret.ErrMsg)
+	}
+	return nil
+}
+
+func formatFen(amount int64) string {
+	return fmt.Sprintf("%.2f元", float64(amount)/100)
+}

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

@@ -254,6 +254,7 @@ func init() {
 	beego.Router("/railsadmin/order/dispatch/:id([0-9]+)", &railsadmin_controller.RailsadminController{}, "get:OrderDispatch")
 	beego.Router("/railsadmin/order/static/:id([0-9]+)", &railsadmin_controller.RailsadminController{}, "get:OrderStatic")
 	beego.Router("/railsadmin/order/refund/:id([0-9]+)", &railsadmin_controller.RailsadminController{}, "get:OrderRefund")
+	beego.Router("/railsadmin/take_cash/:id([0-9]+)", &railsadmin_controller.RailsadminController{}, "get:TakeCash")
 	beego.Router("/railsadmin/user/perfomance/:id([0-9]+)", &railsadmin_controller.RailsadminController{}, "get:UserPerfomance")
 	beego.Router("/railsadmin/live/award/:id([0-9]+)", &railsadmin_controller.RailsadminController{}, "get:LiveAward")
 	beego.Router("/railsadmin/recharge/award/:id([0-9]+)", &railsadmin_controller.RailsadminController{}, "get:PatchCashAward")