Explorar o código

fix: timeout stale take cash orders

Codex hai 1 mes
pai
achega
e260671b59

+ 4 - 0
go/gopath/src/fohow.com/apps/controllers/cron_controller/init.go

@@ -67,6 +67,10 @@ func (self *CronController) run(name string) {
 		self.Data["json"] = SyncWechatOrderShipping()
 		return
 	}
+	if name == "take_cash_timeout" {
+		self.Data["json"] = ProcessTakeCashTimeoutOrders()
+		return
+	}
 	exec(name)
 }
 

+ 182 - 2
go/gopath/src/fohow.com/apps/controllers/cron_controller/sync_balance.go

@@ -9,6 +9,7 @@ import (
 	"io"
 	"strconv"
 	"strings"
+	"sync"
 	"time"
 
 	"fohow.com/apps/models/balance_model"
@@ -24,6 +25,19 @@ type SyncData struct {
 	CreatedAt time.Time `orm:"column(created_at);null;auto_now_add;type(datetime)"`
 }
 
+var takeCashTimeoutLock sync.Mutex
+
+type TakeCashTimeoutResult struct {
+	StartedAt  string `json:"started_at"`
+	FinishedAt string `json:"finished_at"`
+	Scanned    int    `json:"scanned"`
+	TimedOut   int    `json:"timed_out"`
+	Refunded   int    `json:"refunded"`
+	Notified   int    `json:"notified"`
+	Skipped    int    `json:"skipped"`
+	Failed     int    `json:"failed"`
+}
+
 func (self *SyncData) Sign() string {
 	h := md5.New()
 	s := fmt.Sprintf("%s,%s,%d,!iGUESS", self.UnionId, self.Amount, self.CreatedAt.Unix())
@@ -73,11 +87,16 @@ func autoTakeCash() {
 		}
 		//提现记录状态判断
 		cashRecord := balance_model.GetTakeCashOrderById(id, false)
-		//已支付或记录不存在,剔除队列
-		if cashRecord == nil || cashRecord.State == 1 {
+		//已处理、记录不存在,或超过24小时未处理,剔除队列
+		if cashRecord == nil || cashRecord.State != 0 {
 			lib_redis.ThrowOutRedisList(cashKey, takeId)
 			continue
 		}
+		if isTakeCashTimeout(cashRecord) {
+			lib_redis.ThrowOutRedisList(cashKey, takeId)
+			processTakeCashTimeoutOrder(cashRecord, nil)
+			continue
+		}
 		//未审批或者不达处理时间不提现,则下次处理
 		if cashRecord.AuditState < 1 || cashRecord.ExpcPayAt.Unix() > time.Now().Unix() {
 			continue
@@ -96,6 +115,167 @@ func autoTakeCash() {
 	}
 }
 
+func StartTakeCashTimeoutScheduler() {
+	if !beego.AppConfig.DefaultBool("TakeCashTimeoutCronEnabled", true) {
+		beego.BeeLogger.Warn("take cash timeout scheduler disabled")
+		return
+	}
+	go func() {
+		time.Sleep(30 * time.Second)
+		for {
+			ProcessTakeCashTimeoutOrders()
+			time.Sleep(5 * time.Minute)
+		}
+	}()
+}
+
+func ProcessTakeCashTimeoutOrders() *TakeCashTimeoutResult {
+	if !takeCashTimeoutLock.TryLock() {
+		beego.BeeLogger.Warn("take cash timeout task already running")
+		return &TakeCashTimeoutResult{StartedAt: time.Now().Format("2006-01-02 15:04:05")}
+	}
+	defer takeCashTimeoutLock.Unlock()
+
+	ret := &TakeCashTimeoutResult{StartedAt: time.Now().Format("2006-01-02 15:04:05")}
+	orders := balance_model.GetTimeoutTakingCashOrders(200)
+	ret.Scanned = len(orders)
+	for _, order := range orders {
+		processTakeCashTimeoutOrder(order, ret)
+	}
+	ret.FinishedAt = time.Now().Format("2006-01-02 15:04:05")
+	beego.BeeLogger.Warn("take cash timeout task finished: %+v", ret)
+	return ret
+}
+
+func processTakeCashTimeoutOrder(order *balance_model.TakeCashOrder, result *TakeCashTimeoutResult) {
+	if order == nil {
+		return
+	}
+	if order.State != 0 || order.AuditState != 1 || !isTakeCashTimeout(order) {
+		incTakeCashTimeoutSkipped(result)
+		return
+	}
+
+	wxUser := user_model.GetWxUserById(order.WxUId, false)
+	if wxUser == nil {
+		beego.BeeLogger.Error("take cash timeout wxUser[%d] is not exist, order=%s", order.WxUId, order.OrderId)
+		incTakeCashTimeoutFailed(result)
+		return
+	}
+
+	canTimeout := false
+	if order.TradeNo == "" {
+		canTimeout = true
+	} else {
+		payCode := sys_config.GetPayConfigByDepart(wxUser.Depart, true)
+		wechatState, ok := prepareTimeoutMerchantTransfer(order, payCode)
+		if !ok {
+			incTakeCashTimeoutSkipped(result)
+			return
+		}
+		switch wechatState {
+		case wx_mp.MerchantTransferSuccess:
+			incTakeCashTimeoutSkipped(result)
+			return
+		case wx_mp.MerchantTransferFail, wx_mp.MerchantTransferCancelled:
+			canTimeout = true
+		default:
+			incTakeCashTimeoutSkipped(result)
+			return
+		}
+	}
+
+	if !canTimeout {
+		incTakeCashTimeoutSkipped(result)
+		return
+	}
+	marked, err := balance_model.MarkTakeCashOrderTimeout(order.OrderId)
+	if err != nil {
+		incTakeCashTimeoutFailed(result)
+		return
+	}
+	if !marked {
+		incTakeCashTimeoutSkipped(result)
+		return
+	}
+	if result != nil {
+		result.TimedOut++
+	}
+	if refundTakeCashTimeout(order) && result != nil {
+		result.Refunded++
+	}
+	if notifyErr := wx_mp.SendTakeCashTimeoutSubscribe(wxUser.Openid, order.Count+order.Fee); notifyErr != nil {
+		beego.BeeLogger.Error("take cash timeout subscribe order[%s] err[%s]", order.OrderId, notifyErr)
+	} else if result != nil {
+		result.Notified++
+	}
+}
+
+func prepareTimeoutMerchantTransfer(order *balance_model.TakeCashOrder, payCode string) (string, bool) {
+	ret, err := wx_mp.QueryMerchantTransferByOutBillNo(order.OrderId, payCode)
+	if err != nil || ret == nil {
+		beego.BeeLogger.Error("take cash timeout query transfer order[%s] err[%v]", order.OrderId, err)
+		return "", false
+	}
+	switch ret.State {
+	case wx_mp.MerchantTransferSuccess:
+		applyMerchantTransferResult(order, ret)
+		return ret.State, true
+	case wx_mp.MerchantTransferFail, wx_mp.MerchantTransferCancelled:
+		return ret.State, true
+	case wx_mp.MerchantTransferCanceling:
+		applyMerchantTransferResult(order, ret)
+		return ret.State, true
+	}
+	if !wx_mp.MerchantTransferStatePending(ret.State) {
+		beego.BeeLogger.Warn("take cash timeout order[%s] skipped unexpected transfer state=%s", order.OrderId, ret.State)
+		return ret.State, false
+	}
+
+	cancelRet, err := wx_mp.CancelMerchantTransferByOutBillNo(order.OrderId, payCode)
+	if err != nil || cancelRet == nil {
+		beego.BeeLogger.Error("take cash timeout cancel transfer order[%s] err[%v]", order.OrderId, err)
+		return ret.State, false
+	}
+	if cancelRet.State == wx_mp.MerchantTransferSuccess {
+		applyMerchantTransferResult(order, cancelRet)
+		return cancelRet.State, true
+	}
+	if cancelRet.State == wx_mp.MerchantTransferCanceling {
+		applyMerchantTransferResult(order, cancelRet)
+		return cancelRet.State, true
+	}
+	return cancelRet.State, true
+}
+
+func refundTakeCashTimeout(order *balance_model.TakeCashOrder) bool {
+	amount := order.Count + order.Fee
+	if amount <= 0 {
+		return false
+	}
+	if balance_model.GetCashBalanceBySourceAndRId(balance_model.CASH_SOURCE_TAKE_CASH_TIMEOUT, order.OrderId) != nil {
+		return false
+	}
+	item := new(balance_model.CashBalance).Create(order.WxUId, amount, balance_model.CASH_SOURCE_TAKE_CASH_TIMEOUT, order.OrderId, balance_model.CASH_SOURCE_TAKE_CASH_TIMEOUT_NAME)
+	return item != nil
+}
+
+func isTakeCashTimeout(order *balance_model.TakeCashOrder) bool {
+	return order != nil && !order.CreatedAt.IsZero() && time.Now().After(order.CreatedAt.Add(24*time.Hour))
+}
+
+func incTakeCashTimeoutSkipped(result *TakeCashTimeoutResult) {
+	if result != nil {
+		result.Skipped++
+	}
+}
+
+func incTakeCashTimeoutFailed(result *TakeCashTimeoutResult) {
+	if result != nil {
+		result.Failed++
+	}
+}
+
 func updateTakeCashByMerchantTransfer(order *balance_model.TakeCashOrder, openid, payCode string) (*wx_mp.MerchantTransferResponse, error) {
 	var (
 		ret *wx_mp.MerchantTransferResponse

+ 12 - 8
go/gopath/src/fohow.com/apps/models/balance_model/cash_balance.go

@@ -37,14 +37,16 @@ const (
 	CANCEL_FOOD      = "cancel_food"
 	CANCEL_FOOD_NAME = "取消订餐"
 
-	CASH_SOURCE_EXTRACT_REJECT       = "extract_reject"  //拒绝提现
-	CASH_SOURCE_EXTRACT_REJECT_NAME  = "拒绝提现"            //拒绝提现
-	CASH_SOURCE_PRODUCT_BENEFIT      = "product_benefit" // 一级商品佣金
-	CASH_SOURCE_PRODUCT_BENEFIT_NAME = "商品佣金"
-	CASH_SOURCE_SHOP_BENEFIT         = "shop_benefit" // 一级商品佣金
-	CASH_SOURCE_SHOP_BENEFIT_NAME    = "店长申请佣金"
-	BALANCE_SOURCE_BENEFIT           = "balance_benefit" // 提货券充值佣金
-	BALANCE_SOURCE_BENEFIT_NAME      = "提货券充值佣金"
+	CASH_SOURCE_EXTRACT_REJECT         = "extract_reject" //拒绝提现
+	CASH_SOURCE_EXTRACT_REJECT_NAME    = "拒绝提现"           //拒绝提现
+	CASH_SOURCE_TAKE_CASH_TIMEOUT      = "take_cash_timeout"
+	CASH_SOURCE_TAKE_CASH_TIMEOUT_NAME = "提现超时退回"
+	CASH_SOURCE_PRODUCT_BENEFIT        = "product_benefit" // 一级商品佣金
+	CASH_SOURCE_PRODUCT_BENEFIT_NAME   = "商品佣金"
+	CASH_SOURCE_SHOP_BENEFIT           = "shop_benefit" // 一级商品佣金
+	CASH_SOURCE_SHOP_BENEFIT_NAME      = "店长申请佣金"
+	BALANCE_SOURCE_BENEFIT             = "balance_benefit" // 提货券充值佣金
+	BALANCE_SOURCE_BENEFIT_NAME        = "提货券充值佣金"
 
 	FX_CASH_SOURCE_PRODUCT_BENEFIT      = "fx_product_benefit" // 二级商品佣金
 	FX_CASH_SOURCE_PRODUCT_BENEFIT_NAME = "分享商品佣金"
@@ -182,6 +184,8 @@ func (self *CashBalance) GetSourceName() string {
 		sourceName = CASH_SOURCE_RESERVE_ACT_RETURN_NAME
 	case CASH_SOURCE_EXTRACT_REJECT:
 		sourceName = CASH_SOURCE_EXTRACT_REJECT_NAME
+	case CASH_SOURCE_TAKE_CASH_TIMEOUT:
+		sourceName = CASH_SOURCE_TAKE_CASH_TIMEOUT_NAME
 	case CASH_SOURCE_RECHARGE_CASH:
 		sourceName = CASH_SOURCE_RECHARGE_CASH_NAME
 	case CASH_SOURCE_RECEIVE_BUYBACK:

+ 31 - 0
go/gopath/src/fohow.com/apps/models/balance_model/takecash.go

@@ -24,6 +24,8 @@ const (
 	AUTOTAKE_STATE  = int64(1) //自动提现状态
 
 	REAL_STATE = int64(0) //强制检验
+
+	TAKE_CASH_TIMEOUT_REMARK = "超时未确认,请您重新提现!"
 )
 
 type TakeCashOrder struct {
@@ -237,6 +239,35 @@ func GetAllTakingCashOrders() (list []*TakeCashOrder) {
 	return list
 }
 
+// 取超过24小时仍未完成的提现订单,避免继续审核或继续等待已过期的微信确认收款。
+func GetTimeoutTakingCashOrders(limit int) (list []*TakeCashOrder) {
+	if limit <= 0 {
+		limit = 100
+	}
+	sql := fmt.Sprintf("SELECT * FROM %s WHERE pay_state=? AND audit_state=? AND created_at < DATE_SUB(NOW(), INTERVAL 24 HOUR) ORDER BY created_at ASC, id ASC LIMIT ?", take_cash_orders_tablename)
+	if _, err := orm.NewOrm().Raw(sql, 0, 1, limit).QueryRows(&list); err != nil {
+		beego.BeeLogger.Error("GetTimeoutTakingCashOrders err=[%s]", err)
+		return nil
+	}
+	return list
+}
+
+func MarkTakeCashOrderTimeout(orderId string) (bool, error) {
+	tbn := new(TakeCashOrder).TableName()
+	sql := fmt.Sprintf("UPDATE `%s` SET `pay_state`=2, `remark`=?, `updated_at`=NOW() WHERE `order_id`=? AND `pay_state`=0 AND `audit_state`=1 LIMIT 1", tbn)
+	res, err := orm.NewOrm().Raw(sql, TAKE_CASH_TIMEOUT_REMARK, orderId).Exec()
+	if err != nil {
+		beego.BeeLogger.Error("MarkTakeCashOrderTimeout order_id=[%s] err=[%s]", orderId, err)
+		return false, err
+	}
+	rows, err := res.RowsAffected()
+	if err != nil {
+		beego.BeeLogger.Error("MarkTakeCashOrderTimeout rows affected order_id=[%s] err=[%s]", orderId, err)
+		return false, err
+	}
+	return rows > 0, nil
+}
+
 // 充值账户余额
 func GetTakeFeeBalance(wxUId int64) int64 {
 	type Ret struct {

+ 14 - 3
go/gopath/src/fohow.com/libs/wx_mp/xcx.go

@@ -148,19 +148,30 @@ type TakeCashArrivedSubscribeData struct {
 	Amount5 SubscribeDataValue `json:"amount5"`
 }
 
-func SendTakeCashArrivedSubscribe(openid string, orderId, withdrawAmount, fee, arriveAmount int64, state, packageInfo string) error {
-	if openid == "" || packageInfo == "" || !MerchantTransferStatePending(state) {
+func SendTakeCashStatusSubscribe(openid string, withdrawAmount int64, remark string) error {
+	if openid == "" {
 		return nil
 	}
 	page := "packageUser/pages/user/withdraw/withdraw"
 	data := map[string]SubscribeDataValue{
 		"date1":   {Value: time.Now().Format("2006-01-02 15:04")},
 		"amount3": {Value: formatFen(withdrawAmount)},
-		"thing5":  {Value: "已审核 ,待您确认后到帐!"},
+		"thing5":  {Value: remark},
 	}
 	return SendXcxSubscribeMessage(openid, TakeCashSubscribeTemplateId, page, data)
 }
 
+func SendTakeCashArrivedSubscribe(openid string, orderId, withdrawAmount, fee, arriveAmount int64, state, packageInfo string) error {
+	if openid == "" || packageInfo == "" || !MerchantTransferStatePending(state) {
+		return nil
+	}
+	return SendTakeCashStatusSubscribe(openid, withdrawAmount, "已审核 ,待您确认后到帐!")
+}
+
+func SendTakeCashTimeoutSubscribe(openid string, withdrawAmount int64) error {
+	return SendTakeCashStatusSubscribe(openid, withdrawAmount, "超时未确认,请您重新提现!")
+}
+
 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)

+ 1 - 0
go/gopath/src/fohow.com/main.go

@@ -70,6 +70,7 @@ func main() {
 	}))
 
 	cron_controller.StartWechatOrderShippingScheduler()
+	cron_controller.StartTakeCashTimeoutScheduler()
 
 	beego.Run()
 }