|
@@ -0,0 +1,288 @@
|
|
|
|
|
+package cron_controller
|
|
|
|
|
+
|
|
|
|
|
+import (
|
|
|
|
|
+ "fmt"
|
|
|
|
|
+ "strings"
|
|
|
|
|
+ "sync"
|
|
|
|
|
+ "time"
|
|
|
|
|
+ "unicode/utf8"
|
|
|
|
|
+
|
|
|
|
|
+ "github.com/astaxie/beego"
|
|
|
|
|
+
|
|
|
|
|
+ "fohow.com/apps/models/balance_model"
|
|
|
|
|
+ "fohow.com/apps/models/order_model"
|
|
|
|
|
+ "fohow.com/apps/models/user_model"
|
|
|
|
|
+ "fohow.com/libs/wx_mp"
|
|
|
|
|
+)
|
|
|
|
|
+
|
|
|
|
|
+var wechatOrderShippingLock sync.Mutex
|
|
|
|
|
+
|
|
|
|
|
+type WechatOrderShippingResult struct {
|
|
|
|
|
+ StartedAt string `json:"started_at"`
|
|
|
|
|
+ FinishedAt string `json:"finished_at"`
|
|
|
|
|
+ GoodsScanned int `json:"goods_scanned"`
|
|
|
|
|
+ GoodsUploaded int `json:"goods_uploaded"`
|
|
|
|
|
+ GoodsSkipped int `json:"goods_skipped"`
|
|
|
|
|
+ RechargeScanned int `json:"recharge_scanned"`
|
|
|
|
|
+ RechargeUploaded int `json:"recharge_uploaded"`
|
|
|
|
|
+ RechargeSkipped int `json:"recharge_skipped"`
|
|
|
|
|
+ WechatAlreadySynced int `json:"wechat_already_synced"`
|
|
|
|
|
+ Failed int `json:"failed"`
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func StartWechatOrderShippingScheduler() {
|
|
|
|
|
+ if !beego.AppConfig.DefaultBool("WechatOrderShippingCronEnabled", true) {
|
|
|
|
|
+ beego.BeeLogger.Warn("wechat order shipping scheduler disabled")
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ go func() {
|
|
|
|
|
+ for {
|
|
|
|
|
+ next := nextWechatOrderShippingRun(time.Now())
|
|
|
|
|
+ sleep := next.Sub(time.Now())
|
|
|
|
|
+ if sleep < 0 {
|
|
|
|
|
+ sleep = time.Minute
|
|
|
|
|
+ }
|
|
|
|
|
+ beego.BeeLogger.Warn("wechat order shipping scheduler next run at %s", next.Format("2006-01-02 15:04:05"))
|
|
|
|
|
+ time.Sleep(sleep)
|
|
|
|
|
+ SyncWechatOrderShipping()
|
|
|
|
|
+ time.Sleep(time.Minute)
|
|
|
|
|
+ }
|
|
|
|
|
+ }()
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func nextWechatOrderShippingRun(now time.Time) time.Time {
|
|
|
|
|
+ next := time.Date(now.Year(), now.Month(), now.Day(), 1, 3, 0, 0, now.Location())
|
|
|
|
|
+ if !next.After(now) {
|
|
|
|
|
+ next = next.AddDate(0, 0, 1)
|
|
|
|
|
+ }
|
|
|
|
|
+ return next
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func SyncWechatOrderShipping() *WechatOrderShippingResult {
|
|
|
|
|
+ if !wechatOrderShippingLock.TryLock() {
|
|
|
|
|
+ beego.BeeLogger.Warn("wechat order shipping task already running")
|
|
|
|
|
+ return &WechatOrderShippingResult{StartedAt: time.Now().Format("2006-01-02 15:04:05")}
|
|
|
|
|
+ }
|
|
|
|
|
+ defer wechatOrderShippingLock.Unlock()
|
|
|
|
|
+
|
|
|
|
|
+ now := time.Now()
|
|
|
|
|
+ ret := &WechatOrderShippingResult{StartedAt: now.Format("2006-01-02 15:04:05")}
|
|
|
|
|
+ startAt := now.AddDate(0, 0, -7)
|
|
|
|
|
+ accessToken := wx_mp.GetAccessToken(beego.AppConfig.String("WxFohowXcxAppId"), beego.AppConfig.String("WxFohowXcxAppSecret"))
|
|
|
|
|
+ if accessToken == "" {
|
|
|
|
|
+ beego.BeeLogger.Error("wechat order shipping access token is empty")
|
|
|
|
|
+ ret.Failed++
|
|
|
|
|
+ ret.FinishedAt = time.Now().Format("2006-01-02 15:04:05")
|
|
|
|
|
+ return ret
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ syncDispatchedGoodsOrders(accessToken, startAt, now, ret)
|
|
|
|
|
+ syncRechargeCashOrders(accessToken, startAt, now, ret)
|
|
|
|
|
+
|
|
|
|
|
+ ret.FinishedAt = time.Now().Format("2006-01-02 15:04:05")
|
|
|
|
|
+ beego.BeeLogger.Warn("wechat order shipping task finished: %+v", ret)
|
|
|
|
|
+ return ret
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func syncDispatchedGoodsOrders(accessToken string, startAt, endAt time.Time, ret *WechatOrderShippingResult) {
|
|
|
|
|
+ orders := order_model.GetRecentXcxWechatDispatchedOrders(startAt, endAt)
|
|
|
|
|
+ ret.GoodsScanned = len(orders)
|
|
|
|
|
+ detailsMap := order_model.GetAllDetailsByOrderIds(orderIdsOf(orders), true)
|
|
|
|
|
+ for _, order := range orders {
|
|
|
|
|
+ wxUser := user_model.GetWxUserById(order.WxUserId, true)
|
|
|
|
|
+ if wxUser == nil || wxUser.Openid == "" {
|
|
|
|
|
+ ret.GoodsSkipped++
|
|
|
|
|
+ beego.BeeLogger.Warn("wechat order shipping goods order=%s skipped: empty openid", order.OrderId)
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ itemDesc := buildGoodsItemDesc(detailsMap[order.OrderId])
|
|
|
|
|
+ if itemDesc == "" {
|
|
|
|
|
+ itemDesc = "商品订单"
|
|
|
|
|
+ }
|
|
|
|
|
+ logisticsType, shippingItem, ok := buildGoodsShippingItem(order, itemDesc)
|
|
|
|
|
+ if !ok {
|
|
|
|
|
+ ret.GoodsSkipped++
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ if !uploadWechatOrderShippingIfNeeded(accessToken, order.OrderId, order.TradeNo, wxUser.Openid, logisticsType, shippingItem, ret) {
|
|
|
|
|
+ ret.GoodsSkipped++
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ ret.GoodsUploaded++
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func syncRechargeCashOrders(accessToken string, startAt, endAt time.Time, ret *WechatOrderShippingResult) {
|
|
|
|
|
+ orders := balance_model.GetRecentPaidRechargeCashOrders(startAt, endAt)
|
|
|
|
|
+ ret.RechargeScanned = len(orders)
|
|
|
|
|
+ for _, order := range orders {
|
|
|
|
|
+ wxUser := user_model.GetWxUserById(order.WxUserId, true)
|
|
|
|
|
+ if wxUser == nil || wxUser.Openid == "" {
|
|
|
|
|
+ ret.RechargeSkipped++
|
|
|
|
|
+ beego.BeeLogger.Warn("wechat order shipping recharge order=%s skipped: empty openid", order.OrderId)
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ shippingItem := wx_mp.OrderShippingItem{ItemDesc: "余额充值"}
|
|
|
|
|
+ if !uploadWechatOrderShippingIfNeeded(accessToken, order.OrderId, order.TradeNo, wxUser.Openid, wx_mp.OrderShippingLogisticsVirtual, shippingItem, ret) {
|
|
|
|
|
+ ret.RechargeSkipped++
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ ret.RechargeUploaded++
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func uploadWechatOrderShippingIfNeeded(accessToken, orderId, transactionId, openid string, logisticsType int64, shippingItem wx_mp.OrderShippingItem, ret *WechatOrderShippingResult) bool {
|
|
|
|
|
+ if transactionId == "" {
|
|
|
|
|
+ beego.BeeLogger.Warn("wechat order shipping order=%s skipped: empty transaction_id", orderId)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ queryRet, err := wx_mp.GetOrderShippingInfo(accessToken, transactionId)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ ret.Failed++
|
|
|
|
|
+ beego.BeeLogger.Error("wechat order shipping query order=%s transaction=%s err=%s", orderId, transactionId, err)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ if queryRet.ErrCode != 0 {
|
|
|
|
|
+ ret.Failed++
|
|
|
|
|
+ beego.BeeLogger.Error("wechat order shipping query order=%s transaction=%s errcode=%d errmsg=%s", orderId, transactionId, queryRet.ErrCode, queryRet.ErrMsg)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ if queryRet.Order.OrderState != wx_mp.OrderShippingStateWaitShipping {
|
|
|
|
|
+ ret.WechatAlreadySynced++
|
|
|
|
|
+ beego.BeeLogger.Warn("wechat order shipping order=%s skipped: wechat order_state=%d", orderId, queryRet.Order.OrderState)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ req := wx_mp.NewUploadOrderShippingInfoRequest(transactionId, openid, logisticsType, shippingItem)
|
|
|
|
|
+ uploadRet, err := wx_mp.UploadOrderShippingInfo(accessToken, req)
|
|
|
|
|
+ if err != nil {
|
|
|
|
|
+ ret.Failed++
|
|
|
|
|
+ beego.BeeLogger.Error("wechat order shipping upload order=%s transaction=%s err=%s", orderId, transactionId, err)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ if uploadRet.ErrCode != 0 {
|
|
|
|
|
+ ret.Failed++
|
|
|
|
|
+ beego.BeeLogger.Error("wechat order shipping upload order=%s transaction=%s errcode=%d errmsg=%s", orderId, transactionId, uploadRet.ErrCode, uploadRet.ErrMsg)
|
|
|
|
|
+ return false
|
|
|
|
|
+ }
|
|
|
|
|
+ beego.BeeLogger.Warn("wechat order shipping upload order=%s transaction=%s success", orderId, transactionId)
|
|
|
|
|
+ return true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func buildGoodsShippingItem(order *order_model.Order, itemDesc string) (int64, wx_mp.OrderShippingItem, bool) {
|
|
|
|
|
+ if order.PickWay == order_model.PICK_SHOP {
|
|
|
|
|
+ return wx_mp.OrderShippingLogisticsPickup, wx_mp.OrderShippingItem{ItemDesc: itemDesc}, true
|
|
|
|
|
+ }
|
|
|
|
|
+ expressCompany := normalizeWechatExpressCompany(order.ExpressCode, order.ExpressCompany)
|
|
|
|
|
+ if strings.TrimSpace(order.ExpressOrderNo) == "" || expressCompany == "" {
|
|
|
|
|
+ beego.BeeLogger.Warn("wechat order shipping goods order=%s skipped: express_no=%s express_code=%s express_company=%s", order.OrderId, order.ExpressOrderNo, order.ExpressCode, order.ExpressCompany)
|
|
|
|
|
+ return 0, wx_mp.OrderShippingItem{}, false
|
|
|
|
|
+ }
|
|
|
|
|
+ item := wx_mp.OrderShippingItem{
|
|
|
|
|
+ TrackingNo: strings.TrimSpace(order.ExpressOrderNo),
|
|
|
|
|
+ ExpressCompany: expressCompany,
|
|
|
|
|
+ ItemDesc: itemDesc,
|
|
|
|
|
+ }
|
|
|
|
|
+ if contact := maskPhone(order.Tel); contact != "" {
|
|
|
|
|
+ item.Contact = &wx_mp.OrderShippingContact{ReceiverContact: contact}
|
|
|
|
|
+ }
|
|
|
|
|
+ return wx_mp.OrderShippingLogisticsExpress, item, true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func buildGoodsItemDesc(details []*order_model.OrderDetail) string {
|
|
|
|
|
+ parts := make([]string, 0, len(details))
|
|
|
|
|
+ for _, detail := range details {
|
|
|
|
|
+ name := strings.TrimSpace(detail.ProductName)
|
|
|
|
|
+ if name == "" {
|
|
|
|
|
+ continue
|
|
|
|
|
+ }
|
|
|
|
|
+ if detail.Count > 0 {
|
|
|
|
|
+ name = fmt.Sprintf("%s*%d", name, detail.Count)
|
|
|
|
|
+ }
|
|
|
|
|
+ parts = append(parts, name)
|
|
|
|
|
+ }
|
|
|
|
|
+ return truncateRunes(strings.Join(parts, ";"), 120)
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func orderIdsOf(orders []*order_model.Order) []string {
|
|
|
|
|
+ ids := make([]string, 0, len(orders))
|
|
|
|
|
+ for _, order := range orders {
|
|
|
|
|
+ if order.OrderId != "" {
|
|
|
|
|
+ ids = append(ids, order.OrderId)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return ids
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func normalizeWechatExpressCompany(expressCode, expressCompany string) string {
|
|
|
|
|
+ code := strings.ToUpper(strings.TrimSpace(expressCode))
|
|
|
|
|
+ code = strings.Replace(code, "-", "_", -1)
|
|
|
|
|
+ codeMap := map[string]string{
|
|
|
|
|
+ "SF": "SF",
|
|
|
|
|
+ "STO": "STO",
|
|
|
|
|
+ "ZTO": "ZTO",
|
|
|
|
|
+ "YTO": "YTO",
|
|
|
|
|
+ "YT": "YTO",
|
|
|
|
|
+ "YD": "YD",
|
|
|
|
|
+ "YUNDA": "YD",
|
|
|
|
|
+ "EMS": "EMS",
|
|
|
|
|
+ "EMSG": "EMS",
|
|
|
|
|
+ "JD": "JD",
|
|
|
|
|
+ "DB": "DBL",
|
|
|
|
|
+ "DBL": "DBL",
|
|
|
|
|
+ "ZJS": "ZJS",
|
|
|
|
|
+ "YZGN": "YZPY",
|
|
|
|
|
+ "YZPY": "YZPY",
|
|
|
|
|
+ "HT": "HTKY",
|
|
|
|
|
+ "HTKY": "HTKY",
|
|
|
|
|
+ "FAST": "FAST",
|
|
|
|
|
+ "QF": "QFKD",
|
|
|
|
|
+ "QFKD": "QFKD",
|
|
|
|
|
+ "FEDEX": "FEDEX",
|
|
|
|
|
+ "UPS": "UPS",
|
|
|
|
|
+ "ZTKY": "ZTKY",
|
|
|
|
|
+ "ZTOKY": "ZTKY",
|
|
|
|
|
+ "DEPPON": "DBL",
|
|
|
|
|
+ }
|
|
|
|
|
+ if v, ok := codeMap[code]; ok {
|
|
|
|
|
+ return v
|
|
|
|
|
+ }
|
|
|
|
|
+ company := strings.ToLower(strings.TrimSpace(expressCompany))
|
|
|
|
|
+ nameMap := map[string]string{
|
|
|
|
|
+ "顺丰": "SF",
|
|
|
|
|
+ "申通": "STO",
|
|
|
|
|
+ "中通": "ZTO",
|
|
|
|
|
+ "圆通": "YTO",
|
|
|
|
|
+ "韵达": "YD",
|
|
|
|
|
+ "邮政": "EMS",
|
|
|
|
|
+ "ems": "EMS",
|
|
|
|
|
+ "京东": "JD",
|
|
|
|
|
+ "德邦": "DBL",
|
|
|
|
|
+ }
|
|
|
|
|
+ for name, id := range nameMap {
|
|
|
|
|
+ if strings.Contains(company, strings.ToLower(name)) {
|
|
|
|
|
+ return id
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return code
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func maskPhone(phone string) string {
|
|
|
|
|
+ phone = strings.TrimSpace(phone)
|
|
|
|
|
+ if len(phone) < 7 {
|
|
|
|
|
+ return ""
|
|
|
|
|
+ }
|
|
|
|
|
+ runes := []rune(phone)
|
|
|
|
|
+ if len(runes) < 7 {
|
|
|
|
|
+ return ""
|
|
|
|
|
+ }
|
|
|
|
|
+ return fmt.Sprintf("%s****%s", string(runes[:3]), string(runes[len(runes)-4:]))
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+func truncateRunes(s string, max int) string {
|
|
|
|
|
+ if max <= 0 || utf8.RuneCountInString(s) <= max {
|
|
|
|
|
+ return s
|
|
|
|
|
+ }
|
|
|
|
|
+ runes := []rune(s)
|
|
|
|
|
+ return string(runes[:max])
|
|
|
|
|
+}
|