package promotion import ( "encoding/gob" "errors" "fmt" "strings" "time" "git.rosy.net.cn/baseapi/platformapi/jdapi" "git.rosy.net.cn/baseapi/utils" "git.rosy.net.cn/jx-callback/business/jxutils" "git.rosy.net.cn/jx-callback/business/jxutils/jxcontext" "git.rosy.net.cn/jx-callback/business/jxutils/storeskulock" "git.rosy.net.cn/jx-callback/business/jxutils/tasksch" "git.rosy.net.cn/jx-callback/business/model" "git.rosy.net.cn/jx-callback/business/model/dao" "git.rosy.net.cn/jx-callback/globals" "git.rosy.net.cn/jx-callback/globals/api" ) const ( PromotionTypeNormal = 1 PromotionTypeDirectDown = 3 PromotionTypeLimitedTime = 4 ) const ( PriceTypePrice = 1 // 绝对价格 PriceTypePercentage = 2 // 百分比 ) const ( PromotionLimitedTimeMinPercentage = 79 ) const ( colSkuIDIndex = 0 colSkuPriceIndex = 3 colNameIndex = 5 colStoreIDIndex = 6 colBeginAtIndex = 9 colEndAtIndex = 10 ) const ( DefaultLimitSkuCount = 100 MaxPromotionSkuCount = jdapi.MaxPromotionSkuCount ) const ( defSearchDays = 7 stockRefreshGap = 5 * time.Minute userName = "jdpromotion" ) const ( keyPromotionSource = "promotionSource" keyPromotionStatus = "promotionStatus" keyLimitDevice = "limitDevice" keyLimitPin = "limitPin" keyLimitCount = "limitCount" keyLimitDaily = "limitDaily" ) const ( PromotionSourceOpenPlatform = "开放平台" ) type SkuPrice struct { SkuID int `json:"skuID"` PriceType int `json:"priceType"` Price int `json:"price"` // 分,这个不是单价,是这个sku的活动价 LimitSkuCount int `json:"limitSkuCount"` IsLock int8 `json:"isLock"` EarningPrice int `json:"earningPrice"` // 活动商品设置,结算给门店老板的钱 } type tPromotionItemInfo struct { SkuID int `orm:"column(sku_id)"` IsLock int8 // 是否锁定门店商品信息 EndAt time.Time JdStoreID string `orm:"column(vendor_store_id)"` JdSkuID int64 `orm:"column(jd_id)"` } type tSimpleStore struct { StoreID int `orm:"column(store_id)" json:"storeID"` Name string `orm:"column(name)" json:"name"` } type PromotionParams struct { Name string Advertising string Type int BeginAt time.Time EndAt time.Time StoreIDs []int SkuPrices []*SkuPrice } type tStoreSkuBindExt struct { model.StoreSkuBind JdSkuID int64 `orm:"column(jd_id)"` VendorStoreID string `orm:"column(vendor_store_id)"` } type tPromotionInfo struct { model.Promotion StoreStr string `orm:"column(store_str)" json:"-"` SkuPriceStr string `orm:"column(sku_price_str)" json:"-"` Stores []*tSimpleStore `orm:"-" json:"stores"` SkuPrices []*SkuPrice `orm:"-" json:"skuPrices"` } var ( ErrEmptySkus = errors.New("空sku或指定的SKU没有被门店认领,请检查") ) var ( jd2jxPromotionStatusMap = map[int]int{ jdapi.PromotionStateNotConfirm: model.PromotionStatusRemoteCreated, jdapi.PromotionStateConfirmed: model.PromotionStatusRemoteCreated, jdapi.PromotionStateCanceled: model.PromotionStatusCanceled, jdapi.PromotionStateEnded: model.PromotionStatusEnded, } ) var ( ErrLimitDeviceIsInvalid = errors.New("必须指定一个limitCount,当limitPin或limitDevice不都为0时") ) type JdPromotionHandler interface { CreatePromotionInfos(name string, beginDate, endDate time.Time, outInfoId, advertising string) (infoId int64, err error) CreatePromotionRules(infoId int64, outInfoId string, limitDevice, limitPin, limitCount, limitDaily int) (err error) CreatePromotionSku(infoId int64, outInfoId string, skus []*jdapi.PromotionSku) (skusResult []*jdapi.PromotionSku, err error) ConfirmPromotion(infoId int64, outInfoId string) (err error) CancelPromotion(infoId int64, outInfoId string) (err error) } type JdDirectDownHandler struct { } func (p *JdDirectDownHandler) CreatePromotionInfos(name string, beginDate, endDate time.Time, outInfoId, advertising string) (infoId int64, err error) { return api.JdAPI.CreatePromotionInfosSingle(name, beginDate, endDate, outInfoId, advertising, "") } func (p *JdDirectDownHandler) CreatePromotionRules(infoId int64, outInfoId string, limitDevice, limitPin, limitCount, limitDaily int) (err error) { return api.JdAPI.CreatePromotionRulesSingle(infoId, outInfoId, limitDevice, limitPin, limitCount, limitDaily, "") } func (p *JdDirectDownHandler) CreatePromotionSku(infoId int64, outInfoId string, skus []*jdapi.PromotionSku) (skusResult []*jdapi.PromotionSku, err error) { return api.JdAPI.CreatePromotionSkuSingle(infoId, outInfoId, skus, "") } func (p *JdDirectDownHandler) ConfirmPromotion(infoId int64, outInfoId string) (err error) { return api.JdAPI.ConfirmPromotionSingle(infoId, outInfoId, "") } func (p *JdDirectDownHandler) CancelPromotion(infoId int64, outInfoId string) (err error) { return api.JdAPI.CancelPromotionSingle(infoId, outInfoId, "") } type JdLimitedTimeHandler struct { } func (p *JdLimitedTimeHandler) CreatePromotionInfos(name string, beginDate, endDate time.Time, outInfoId, advertising string) (infoId int64, err error) { return api.JdAPI.CreatePromotionInfosLimitTime(name, beginDate, endDate, outInfoId, advertising, "") } func (p *JdLimitedTimeHandler) CreatePromotionRules(infoId int64, outInfoId string, limitDevice, limitPin, limitCount, limitDaily int) (err error) { return api.JdAPI.CreatePromotionRulesLimitTime(infoId, outInfoId, limitDevice, limitPin, limitCount, limitDaily, "") } func (p *JdLimitedTimeHandler) CreatePromotionSku(infoId int64, outInfoId string, skus []*jdapi.PromotionSku) (skusResult []*jdapi.PromotionSku, err error) { return api.JdAPI.CreatePromotionSkuLimitTime(infoId, outInfoId, skus, "") } func (p *JdLimitedTimeHandler) ConfirmPromotion(infoId int64, outInfoId string) (err error) { return api.JdAPI.ConfirmPromotionLimitTime(infoId, outInfoId, "") } func (p *JdLimitedTimeHandler) CancelPromotion(infoId int64, outInfoId string) (err error) { return api.JdAPI.CancelPromotionLimitTime(infoId, outInfoId, "") } type JdNullHandler struct { } func (p *JdNullHandler) CreatePromotionInfos(name string, beginDate, endDate time.Time, outInfoId, advertising string) (infoId int64, err error) { return jxutils.GenFakeID(), nil } func (p *JdNullHandler) CreatePromotionRules(infoId int64, outInfoId string, limitDevice, limitPin, limitCount, limitDaily int) (err error) { return nil } func (p *JdNullHandler) CreatePromotionSku(infoId int64, outInfoId string, skus []*jdapi.PromotionSku) (skusResult []*jdapi.PromotionSku, err error) { return nil, nil } func (p *JdNullHandler) ConfirmPromotion(infoId int64, outInfoId string) (err error) { return nil } func (p *JdNullHandler) CancelPromotion(infoId int64, outInfoId string) (err error) { return nil } func init() { gob.Register(&PromotionParams{}) gob.Register([]*SkuPrice{}) } func scheduleDailyRoutine(isFirst bool) { executeTime := utils.GetCurDate().Add(24*time.Hour + 5*time.Minute) // 凌晨00:05执行 duration := executeTime.Sub(time.Now()) if isFirst && duration > 1*time.Hour { UpdateJdPromotionStatus() } // globals.SugarLogger.Debug(duration) utils.AfterFuncWithRecover(duration, func() { UpdateJdPromotionStatus() scheduleDailyRoutine(false) }) } func scheduleRoutine(isFirst bool) { if isFirst { utils.CallFuncAsync(func() { RefreshJdLockStoreSku() RefreshJdStoreSkuStock(0, nil) }) } utils.AfterFuncWithRecover(stockRefreshGap, func() { RefreshJdLockStoreSku() RefreshJdStoreSkuStock(0, nil) scheduleRoutine(false) }) } func Init() { scheduleDailyRoutine(true) // scheduleRoutine(true) } func CreateJdPromotion(ctx *jxcontext.Context, vendorID int, isIDJd bool, isAsync, isContinueWhenError bool, vendorPromotionID string, params *PromotionParams, mapData map[string]interface{}) (hint string, err error) { if vendorID != model.VendorIDJD && vendorID != model.VendorIDJX { return "", fmt.Errorf("当前只支持京西与京东活动") } if vendorPromotionID != "" && len(vendorPromotionID) != len("14863853") { return "", fmt.Errorf("%s看起来不像是一个有效的京东活动ID,请仔细检查一下", vendorPromotionID) } if len(params.SkuPrices) == 0 { return "", ErrEmptySkus } limitDaily := 1 if limitDaily2, ok := mapData[keyLimitDaily]; ok { limitDaily = jxutils.Int2OneZero(limitDaily2.(int)) } limitPin := 1 if limitPin2, ok := mapData[keyLimitPin]; ok { limitPin = jxutils.Int2OneZero(limitPin2.(int)) } limitDevice := 1 if limitDevice2, ok := mapData[keyLimitDevice]; ok { limitDevice = jxutils.Int2OneZero(limitDevice2.(int)) } limitCount := 1 if limitCount2, ok := mapData[keyLimitCount]; ok { limitCount = limitCount2.(int) } if (limitDevice == 1 || limitPin == 1) && limitCount == 0 { return "", ErrLimitDeviceIsInvalid } userName := ctx.GetUserName() db := dao.GetDB() modifyPricesList := make(map[int][]*jdapi.SkuPriceInfo) promotionPrices := make([]*jdapi.PromotionSku, len(params.StoreIDs)*len(params.SkuPrices)) var jxStoreIDs []int promotion := &model.Promotion{ Name: params.Name, Advertising: params.Advertising, VendorID: vendorID, Type: params.Type, Status: model.PromotionStatusLocalCreated, LimitDevice: int8(limitDevice), LimitPin: int8(limitPin), LimitCount: limitCount, LimitDaily: int8(limitDaily), BeginAt: params.BeginAt, EndAt: params.EndAt, CreateType: model.PromotionCreateTypeByJX, Source: PromotionSourceOpenPlatform, } skuIDs := make([]int, len(params.SkuPrices)) skuPriceMap := make(map[int64]*SkuPrice) for k, v := range params.SkuPrices { skuIDs[k] = v.SkuID skuPriceMap[int64(v.SkuID)] = v } if len(skuIDs) == 0 { return "", fmt.Errorf("商品列表为空") } if vendorID == model.VendorIDJX { conflictPromotion, err2 := dao.GetPromotionSkuPriceMap(db, model.VendorIDJX, params.StoreIDs, skuIDs, promotion.BeginAt, promotion.EndAt) if err = err2; err != nil { return "", err } if len(conflictPromotion) > 0 { return "", fmt.Errorf("有冲突配置:%s", utils.Format4Output(conflictPromotion, false)) } } if vendorPromotionID == "" { if vendorID == model.VendorIDJD { sql := ` SELECT t1.*, t2.jd_id, t3.vendor_store_id FROM store_sku_bind t1 JOIN sku t2 ON t1.sku_id = t2.id JOIN store_map t3 ON t1.store_id = t3.store_id AND t3.vendor_id = ? AND t3.deleted_at = ? WHERE t1.deleted_at = ? ` sqlParam := []interface{}{ model.VendorIDJD, utils.DefaultTimeValue, utils.DefaultTimeValue, skuIDs, } if isIDJd { sql += " AND t2.jd_id IN (" + dao.GenQuestionMarks(len(skuIDs)) + ") AND t3.vendor_store_id = ?" } else { sql += " AND t1.sku_id IN (" + dao.GenQuestionMarks(len(skuIDs)) + ") AND t1.store_id = ?" } errMsg := "" index := 0 for _, storeID := range params.StoreIDs { var skuBinds []*tStoreSkuBindExt if err = dao.GetRows(db, &skuBinds, sql, append(sqlParam, storeID)...); err != nil { return "", err } for k, skuBind := range skuBinds { if k == 0 { jxStoreIDs = append(jxStoreIDs, skuBind.StoreID) } mapSkuID := int64(skuBind.SkuID) if isIDJd { mapSkuID = skuBind.JdSkuID } promotionSkuPrice := skuPriceMap[mapSkuID] if promotionSkuPrice.PriceType == PriceTypePercentage { promotionSkuPrice.Price = skuBind.Price * promotionSkuPrice.Price / 100 } if vendorID != model.VendorIDJX && promotionSkuPrice.Price >= skuBind.Price { errMsg += fmt.Sprintf("活动价大于等于原价,storeID:%d, skuID:%d\n", skuBind.StoreID, skuBind.SkuID) } if promotionSkuPrice.LimitSkuCount <= 0 { promotionSkuPrice.LimitSkuCount = DefaultLimitSkuCount } if errMsg == "" { if params.Type == PromotionTypeLimitedTime { if skuBind.Price*PromotionLimitedTimeMinPercentage/100 < promotionSkuPrice.Price { modifyPricesList[skuBind.StoreID] = append(modifyPricesList[skuBind.StoreID], &jdapi.SkuPriceInfo{ OutSkuId: utils.Int2Str(skuBind.SkuID), Price: promotionSkuPrice.Price*100/PromotionLimitedTimeMinPercentage + 5, }) } } promotionPrices[index] = &jdapi.PromotionSku{ StationNo: utils.Str2Int64(skuBind.VendorStoreID), SkuID: skuBind.JdSkuID, PromotionPrice: int64(promotionSkuPrice.Price), LimitSkuCount: promotionSkuPrice.LimitSkuCount, } index++ } } } if errMsg != "" { return "", errors.New(errMsg) } promotionPrices = promotionPrices[:index] if len(promotionPrices) == 0 { return "", ErrEmptySkus } } else { promotion.Status = model.PromotionStatusRemoteCreated } } else { promotion.VendorPromotionID = vendorPromotionID promotion.CreateType = model.PromotionCreateTypeByVendor if status, ok := mapData[keyPromotionStatus]; ok { promotion.Status = status.(int) } else { promotion.Status = model.PromotionStatusRemoteCreated } if source, ok := mapData[keyPromotionSource]; ok { promotion.Source = source.(string) } } dao.WrapAddIDCULDEntity(promotion, userName) // if promotion.Params, err = refutil.SerializeData(params); err != nil { // return "", err // } // promotion.Params = string(utils.MustMarshal(params)) dao.Begin(db) defer func() { dao.Rollback(db) }() if err = dao.CreateEntity(db, promotion); err != nil { return "", err } for _, storeID := range params.StoreIDs { promotionStore := &model.PromotionStore{ PromotionID: promotion.ID, StoreID: storeID, } dao.WrapAddIDCULDEntity(promotionStore, ctx.GetUserName()) if err = dao.CreateEntity(db, promotionStore); err != nil { return "", err } } for _, skuPrice := range params.SkuPrices { promotionSku := &model.PromotionSku{ PromotionID: promotion.ID, SkuID: skuPrice.SkuID, PriceType: skuPrice.PriceType, Price: skuPrice.Price, LimitSkuCount: skuPrice.LimitSkuCount, IsLock: skuPrice.IsLock, EarningPrice: skuPrice.EarningPrice, } dao.WrapAddIDCULDEntity(promotionSku, ctx.GetUserName()) if err = dao.CreateEntity(db, promotionSku); err != nil { return "", err } } if vendorID != model.VendorIDJX && vendorPromotionID == "" { promotionHandler := getPromotionHander(params.Type) if promotionHandler == nil { return "", errors.New("非法的活动类型") } infoId, err2 := promotionHandler.CreatePromotionInfos(params.Name, params.BeginAt, params.EndAt, utils.Int2Str(promotion.ID), params.Advertising) if err = err2; err != nil { return "", err } promotion.VendorPromotionID = utils.Int64ToStr(infoId) if _, err = dao.UpdateEntity(db, promotion); err != nil { return "", err } dao.Commit(db) rootTask := tasksch.NewSeqTask("CreateJdPromotion", ctx, func(task *tasksch.SeqTask, step int, params2 ...interface{}) (result interface{}, err error) { if step == 0 { task1 := tasksch.NewParallelTask("CreateJdPromotion update sku price", nil, ctx, func(t *tasksch.ParallelTask, batchItemList []interface{}, params2 ...interface{}) (retVal interface{}, err error) { storeID := batchItemList[0].(int) modifyPricesList := jxutils.SplitSlice(modifyPricesList[storeID], jdapi.MaxStoreSkuBatchSize) for _, modifyPrices := range modifyPricesList { modifyPrices2 := make([]*jdapi.SkuPriceInfo, len(modifyPrices)) for k, v := range modifyPrices { modifyPrices2[k] = v.(*jdapi.SkuPriceInfo) } if globals.EnableJdStoreWrite { if _, err = api.JdAPI.UpdateVendorStationPrice(ctx.GetTrackInfo(), utils.Int2Str(storeID), "", modifyPrices2); err != nil { return nil, err } } } return nil, nil }, jxStoreIDs) task.AddChild(task1).Run() _, err = task1.GetResult(0) } else if step == 1 { err = promotionHandler.CreatePromotionRules(infoId, "", limitDevice, limitPin, limitCount, limitDaily) } else if step == 2 { task2 := tasksch.NewParallelTask("CreateJdPromotion CreatePromotionSku", tasksch.NewParallelConfig().SetBatchSize(MaxPromotionSkuCount).SetIsContinueWhenError(isContinueWhenError), ctx, func(task *tasksch.ParallelTask, batchItemList []interface{}, params2 ...interface{}) (retVal interface{}, err error) { skus := make([]*jdapi.PromotionSku, len(batchItemList)) for k, v := range batchItemList { skus[k] = v.(*jdapi.PromotionSku) } _, err = promotionHandler.CreatePromotionSku(infoId, "", skus) return nil, err }, promotionPrices) task.AddChild(task2).Run() _, err = task2.GetResult(0) if isContinueWhenError && err != nil { // todo isContinueWhenError为true时,强制忽略此步的错误 err = nil } } else if step == 3 { err = promotionHandler.ConfirmPromotion(infoId, "") if err == nil { db := dao.GetDB() if _, err = dao.UpdateEntityLogically(db, promotion, map[string]interface{}{ model.FieldStatus: model.PromotionStatusRemoteCreated, }, ctx.GetUserName(), nil); err == nil { RefreshJdPromotionLockStatus(ctx, promotion.ID) } } } if err != nil { db := dao.GetDB() dao.UpdateEntityLogically(db, promotion, map[string]interface{}{ model.FieldStatus: model.PromotionStatusRemoteFailed, model.FieldRemark: err.Error(), }, ctx.GetUserName(), nil) } return nil, err }, 4) tasksch.HandleTask(rootTask, nil, true).Run() if !isAsync { _, err = rootTask.GetResult(0) } hint = rootTask.ID } else { dao.Commit(db) } return hint, err } func GetJdPromotions(ctx *jxcontext.Context, keyword string, params map[string]interface{}, offset, pageSize int) (pagedInfo *model.PagedInfo, err error) { sql := ` SELECT SQL_CALC_FOUND_ROWS t1.id, t1.created_at, t1.updated_at, t1.last_operator, t1.vendor_id, t1.name, t1.type, t1.status, t1.vendor_promotion_id, t1.begin_at, t1.end_at, t1.advertising, CONCAT("[", GROUP_CONCAT(DISTINCT CONCAT('{"storeID":', t2.store_id, ', "name":"', REPLACE(REPLACE(t22.name, '\t', ''), '"', ''), '"}')), "]") store_str, CONCAT("[", GROUP_CONCAT(DISTINCT CONCAT('{"skuID":', t3.sku_id, ', "priceType":', t3.price_type, ', "price":', t3.price, ', "limitSkuCount":', t3.limit_sku_count, ', "isLock":', t3.is_lock, ', "earningPrice":', t3.earning_price, '}')), "]") sku_price_str FROM promotion t1 JOIN promotion_store t2 ON t1.id = t2.promotion_id JOIN store t22 ON t2.store_id = t22.id JOIN promotion_sku t3 ON t1.id = t3.promotion_id WHERE t1.deleted_at = ? ` sqlParams := []interface{}{ utils.DefaultTimeValue, } if keyword != "" { keywordLike := "%" + keyword + "%" sql += " AND ( t1.name LIKE ?" sqlParams = append(sqlParams, keywordLike) keywordInt := utils.Str2Int64WithDefault(keyword, 0) if keywordInt > 0 { sql += ` OR t1.id = ? OR t1.vendor_promotion_id = ? OR (SELECT COUNT(*) FROM promotion_store tt1 WHERE tt1.promotion_id = t1.id AND tt1.store_id = ?) > 0 OR (SELECT COUNT(*) FROM promotion_sku tt1 WHERE tt1.promotion_id = t1.id AND tt1.sku_id = ?) > 0 ` sqlParams = append(sqlParams, keywordInt, keywordInt, keywordInt, keywordInt) } sql += ")" } if params["vendorID"] != nil { sql += " AND t1.vendor_id = ?" sqlParams = append(sqlParams, params["vendorID"].(int)) } if params["promotionID"] != nil { sql += " AND t1.id = ?" sqlParams = append(sqlParams, params["promotionID"].(int)) } if params["vendorPromotionID"] != nil { sql += " AND t1.vendor_promotion_id = ?" sqlParams = append(sqlParams, params["vendorPromotionID"].(string)) } if params["name"] != nil { sql += " AND t1.name LIKE ?" sqlParams = append(sqlParams, "%"+params["name"].(string)+"%") } if params["beginAt"] != nil { sql += " AND t1.begin_at <= ?" beginAt, err2 := utils.TryStr2Time(params["beginAt"].(string)) if err = err2; err != nil { return nil, err } sqlParams = append(sqlParams, beginAt) } if params["endAt"] != nil { sql += " AND t1.end_at >= ?" endAt, err2 := utils.TryStr2Time(params["endAt"].(string)) if err = err2; err != nil { return nil, err } sqlParams = append(sqlParams, endAt) } days := defSearchDays if params["days"] != nil { days = params["days"].(int) } sql += " AND t1.created_at >= ?" sqlParams = append(sqlParams, time.Now().Add(-time.Duration(days)*24*time.Hour)) if params["type"] != nil { sql += " AND t1.type = ?" sqlParams = append(sqlParams, params["type"].(int)) } if params["storeID"] != nil || params["cityCode"] != nil { sql += " AND (SELECT COUNT(*) FROM promotion_store tt1 " if params["cityCode"] != nil { sql += " JOIN store st ON st.id = tt1.store_id AND st.city_code = ?" sqlParams = append(sqlParams, params["cityCode"].(int)) } sql += " WHERE tt1.promotion_id = t1.id" if params["storeID"] != nil { sql += " AND tt1.store_id = ?" sqlParams = append(sqlParams, params["storeID"].(int)) } sql += ") > 0" } if params["skuID"] != nil { sql += " AND (SELECT COUNT(*) FROM promotion_sku tt1 WHERE tt1.promotion_id = t1.id AND tt1.sku_id = ?) > 0" sqlParams = append(sqlParams, params["skuID"].(int)) } sql += ` GROUP BY 1,2,3,4,5,6,7,8,9,10,11,12 ORDER BY t1.id DESC LIMIT ? OFFSET ? ` pageSize = jxutils.FormalizePageSize(pageSize) if offset < 0 { offset = 0 } sqlParams = append(sqlParams, pageSize, offset) db := dao.GetDB() dao.Begin(db) defer func() { dao.Rollback(db) if r := recover(); r != nil { panic(r) } }() // globals.SugarLogger.Debug(utils.Format4Output(sqlParams, false)) // globals.SugarLogger.Debug(sql) var promotionList []*tPromotionInfo if err = dao.GetRows(db, &promotionList, sql, sqlParams...); err == nil { pagedInfo = &model.PagedInfo{ TotalCount: dao.GetLastTotalRowCount(db), Data: promotionList, } dao.Commit(db) for _, v := range promotionList { if v.StoreStr != "" { if err = utils.UnmarshalUseNumber([]byte(v.StoreStr), &v.Stores); err != nil { return nil, err } } if v.SkuPriceStr != "" { if err = utils.UnmarshalUseNumber([]byte(v.SkuPriceStr), &v.SkuPrices); err != nil { return nil, err } } } } else { dao.Rollback(db) } return pagedInfo, err } func CancelJdPromotion(ctx *jxcontext.Context, promotionID int) (err error) { globals.SugarLogger.Debug("CancelJdPromotion promotionID:%d", promotionID) db := dao.GetDB() promotion := &model.Promotion{} promotion.ID = promotionID if err = dao.GetEntity(db, promotion); err != nil { return err } if promotion.Status == model.PromotionStatusCanceled { return errors.New("当前状态已经是取消") } if promotion.Status == model.PromotionStatusRemoteCreated { if promotion.VendorPromotionID != "" { promotionHandler := getPromotionHander(promotion.Type) if promotionHandler == nil { return errors.New("非法的活动类型") } if err = promotionHandler.CancelPromotion(utils.Str2Int64(promotion.VendorPromotionID), ""); err != nil { return err } } } if _, err = dao.UpdateEntityLogically(db, promotion, map[string]interface{}{ "Status": model.PromotionStatusCanceled, }, ctx.GetUserName(), nil); err == nil { // RefreshJdPromotionLockStatus(ctx, promotionID) } return err } // 每一段时间运行一次 func RefreshJdLockStoreSku() (err error) { globals.SugarLogger.Debug("RefreshJdLockStoreSku") sql := ` SELECT t22.vendor_store_id, t3.sku_id, t32.jd_id, MAX(t3.is_lock) is_lock, MAX(t1.end_at) end_at FROM promotion t1 JOIN promotion_store t2 ON t1.id = t2.promotion_id JOIN store_map t22 ON t2.store_id = t22.store_id AND t22.vendor_id = ? AND t22.deleted_at = ? JOIN promotion_sku t3 ON t1.id = t3.promotion_id AND t3.is_lock = 1 JOIN sku t32 ON t3.sku_id = t32.id WHERE t1.deleted_at = ? AND t1.vendor_id = ? AND t1.status = ? AND (t1.begin_at <= ? AND t1.end_at >= ?) GROUP BY 1,2,3 ` nowDate := time.Now() sqlParams := []interface{}{ model.VendorIDJD, utils.DefaultTimeValue, utils.DefaultTimeValue, model.VendorIDJD, model.PromotionStatusRemoteCreated, nowDate, nowDate, } var promotionItemList []*tPromotionItemInfo db := dao.GetDB() if err = dao.GetRows(db, &promotionItemList, sql, sqlParams...); err != nil { if !dao.IsNoRowsError(err) { globals.SugarLogger.Warnf("RefreshJdLockStoreSku GetRows failed with error:%v", err) } return err } storeskulock.ClearJdStoreSkuLock() return RefreshJdPromotionItemListLockStatus(promotionItemList) } func RefreshJdStoreSkuStock(promotionID int, skuIDs []int) (err error) { globals.SugarLogger.Debugf("RefreshJdStoreSkuStock promotionID:%d", promotionID) sql := ` SELECT t22.vendor_store_id, t3.sku_id, t32.jd_id, MAX(t3.is_lock) is_lock, MAX(t1.end_at) end_at FROM promotion t1 JOIN promotion_store t2 ON t1.id = t2.promotion_id JOIN store_map t22 ON t2.store_id = t22.store_id AND t22.vendor_id = ? AND t22.deleted_at = ? JOIN promotion_sku t3 ON t1.id = t3.promotion_id AND t3.is_lock = 1 JOIN sku t32 ON t3.sku_id = t32.id JOIN store_sku_bind t4 ON t2.store_id = t4.store_id AND t3.sku_id = t4.sku_id AND t4.deleted_at = ? WHERE t1.deleted_at = ? AND t1.vendor_id = ? AND t1.status = ? AND (t1.begin_at <= ? AND t1.end_at >= ?) ` nowDate := time.Now() sqlParams := []interface{}{ model.VendorIDJD, utils.DefaultTimeValue, utils.DefaultTimeValue, utils.DefaultTimeValue, model.VendorIDJD, model.PromotionStatusRemoteCreated, nowDate, nowDate, } if promotionID != 0 { sql += " AND t1.id = ?" sqlParams = append(sqlParams, promotionID) } if len(skuIDs) > 0 { sql += " AND t3.sku_id IN (" + dao.GenQuestionMarks(len(skuIDs)) + ")" sqlParams = append(sqlParams, skuIDs) } sql += ` GROUP BY 1,2,3 ORDER BY 1,2,3 ` var promotionItemList []*tPromotionItemInfo db := dao.GetDB() if err = dao.GetRows(db, &promotionItemList, sql, sqlParams...); err != nil { if !dao.IsNoRowsError(err) { globals.SugarLogger.Warnf("RefreshJdStoreSkuStock GetRows failed with error:%v", err) } return err } globals.SugarLogger.Debugf("RefreshJdStoreSkuStock promotionID:%d, len(promotionItemList)=%d", promotionID, len(promotionItemList)) if len(promotionItemList) > 0 { task := tasksch.NewParallelTask("RefreshJdStoreSkuStock", tasksch.NewParallelConfig().SetBatchSize(jdapi.MaxStoreSkuBatchSize).SetIsContinueWhenError(true), jxcontext.AdminCtx, func(task *tasksch.ParallelTask, batchItemList []interface{}, params ...interface{}) (retVal interface{}, err error) { stockList := make([]*jdapi.SkuStock, 0) stationNo := batchItemList[0].(*tPromotionItemInfo).JdStoreID for _, v := range batchItemList { promotionItem := v.(*tPromotionItemInfo) if promotionItem.JdStoreID != stationNo { // globals.SugarLogger.Debugf("RefreshJdStoreSkuStock BatchUpdateCurrentQtys stationNo:%s, stockList:%s", stationNo, utils.Format4Output(stockList, false)) _, err = api.JdAPI.BatchUpdateCurrentQtys("", "", stationNo, stockList, userName) if err != nil { globals.SugarLogger.Warnf("RefreshJdStoreSkuStock BatchUpdateCurrentQtys failed with error:%v", err) } stockList = make([]*jdapi.SkuStock, 0) stationNo = promotionItem.JdStoreID } stockList = append(stockList, &jdapi.SkuStock{ OutSkuId: utils.Int2Str(promotionItem.SkuID), StockQty: model.MaxStoreSkuStockQty, }) } // globals.SugarLogger.Debugf("RefreshJdStoreSkuStock BatchUpdateCurrentQtys stationNo:%s, stockList:%s", stationNo, utils.Format4Output(stockList, false)) _, err = api.JdAPI.BatchUpdateCurrentQtys("", "", stationNo, stockList, userName) if err != nil { globals.SugarLogger.Warnf("RefreshJdStoreSkuStock BatchUpdateCurrentQtys failed with error:%v", err) } return nil, err }, promotionItemList) task.Run() _, err = task.GetResult(0) } return err } // 每晚凌晨运行一次 func UpdateJdPromotionStatus() (num int64, err error) { sql := ` UPDATE promotion t1 SET t1.status = ? WHERE t1.deleted_at = ? AND t1.vendor_id IN (?, ?) AND t1.status = ? AND t1.end_at < ? ` nowDate := utils.GetCurDate() sqlParams := []interface{}{ model.PromotionStatusEnded, utils.DefaultTimeValue, []int{model.VendorIDJX, model.VendorIDJD}, model.PromotionStatusRemoteCreated, nowDate, } db := dao.GetDB() return dao.ExecuteSQL(db, sql, sqlParams...) } func RefreshJdPromotionItemListLockStatus(promotionItemList []*tPromotionItemInfo) (err error) { globals.SugarLogger.Debugf("RefreshJdPromotionItemListLockStatus len(promotionItemList):%d", len(promotionItemList)) if len(promotionItemList) > 0 { expire := promotionItemList[0].EndAt.Add(24 * time.Hour) for _, item := range promotionItemList { if item.IsLock != 0 { storeskulock.LockJdStoreSku(item.JdStoreID, item.JdSkuID, expire) } else { storeskulock.UnlockJdStoreSku(item.JdStoreID, item.JdSkuID) } } } return err } func RefreshJdPromotionLockStatus(ctx *jxcontext.Context, promotionID int) (err error) { globals.SugarLogger.Debugf("RefreshJdPromotionLockStatus promotionID:%d", promotionID) sql := ` SELECT t22.vendor_store_id, t3.sku_id, t32.jd_id, IF(t1.begin_at <= ? AND t1.end_at >= ? AND t1.status = ?, t3.is_lock, 0) is_lock, t1.end_at FROM promotion t1 JOIN promotion_store t2 ON t1.id = t2.promotion_id JOIN store_map t22 ON t2.store_id = t22.store_id AND t22.vendor_id = ? AND t22.deleted_at = ? JOIN promotion_sku t3 ON t1.id = t3.promotion_id JOIN sku t32 ON t3.sku_id = t32.id WHERE t1.id = ? ` nowDate := time.Now() sqlParams := []interface{}{ nowDate, nowDate, model.PromotionStatusRemoteCreated, model.VendorIDJD, utils.DefaultTimeValue, promotionID, } var promotionItemList []*tPromotionItemInfo db := dao.GetDB() // globals.SugarLogger.Debug(sql) // globals.SugarLogger.Debug(utils.Format4Output(sqlParams, false)) if err = dao.GetRows(db, &promotionItemList, sql, sqlParams...); err != nil { return err } return RefreshJdPromotionItemListLockStatus(promotionItemList) } func LockPromotionSkus(ctx *jxcontext.Context, promotionID int, isLock int, skuIDs []int) (num int64, err error) { globals.SugarLogger.Debugf("begin LockPromotionSkus promotionID:%d, isLock:%d, skuIDs:%v", promotionID, isLock, skuIDs) if isLock != 0 { isLock = 1 } sql := ` UPDATE promotion_sku t1 SET t1.is_lock = ? WHERE t1.promotion_id = ? ` sqlParams := []interface{}{ isLock, promotionID, } if len(skuIDs) > 0 { sql += " AND t1.sku_id IN (" + dao.GenQuestionMarks(len(skuIDs)) + ")" sqlParams = append(sqlParams, skuIDs) } db := dao.GetDB() num, err = dao.ExecuteSQL(db, sql, sqlParams...) if err == nil { if isLock != 0 { if err = UpdatePromotionStatusFromRemote(ctx, promotionID); err != nil { return 0, err } } RefreshJdPromotionLockStatus(ctx, promotionID) if isLock != 0 { // RefreshJdStoreSkuStock(promotionID, skuIDs) 这里比较耗时,实时更新意义也不大,留到定时任务自动处理 } } globals.SugarLogger.Debugf("end LockPromotionSkus promotionID:%d, isLock:%d, skuIDs:%v", promotionID, isLock, skuIDs) return num, err } func UpdatePromotionSkusEarningPrice(ctx *jxcontext.Context, promotionID int, skuPriceList []*SkuPrice) (num int64, err error) { db := dao.GetDB() dao.Begin(db) defer func() { if r := recover(); r != nil || err != nil { dao.Rollback(db) if r != nil { panic(r) } } }() for _, v := range skuPriceList { var tmpNum int64 if tmpNum, err = dao.UpdateEntityLogically(db, &model.PromotionSku{}, map[string]interface{}{ "EarningPrice": v.EarningPrice, }, ctx.GetUserName(), map[string]interface{}{ "PromotionID": promotionID, model.FieldSkuID: v.SkuID, model.FieldDeletedAt: utils.DefaultTimeValue, }); err != nil { return 0, err } num += tmpNum } dao.Commit(db) return num, err } func OnStoreStockMsg(msg *jdapi.CallbackStoreStockMsg) (retVal *jdapi.CallbackResponse) { var err error // globals.SugarLogger.Debugf("OnStoreStockMsg IsJdStoreSkuLocked:%t", storeskulock.IsJdStoreSkuLocked(msg.StationNo, msg.SkuId)) if (msg.Vendibility == 1 || !msg.Have) && storeskulock.IsJdStoreSkuLocked(msg.StationNo, msg.SkuId) { globals.SugarLogger.Debugf("OnStoreStockMsg msg:%s", utils.Format4Output(msg, false)) db := dao.GetDB() sku := &model.Sku{} sku.JdID = msg.SkuId if err = dao.GetEntity(db, sku, model.FieldJdID); err == nil { utils.CallFuncAsync(func() { if msg.Vendibility == 1 { vendibility := &jdapi.StockVendibility{ OutSkuId: utils.Int2Str(sku.ID), DoSale: true, } _, err = api.JdAPI.BatchUpdateVendibility("", "", msg.StationNo, []*jdapi.StockVendibility{ vendibility, }, userName) } if !msg.Have { stock := &jdapi.SkuStock{ OutSkuId: utils.Int2Str(sku.ID), StockQty: model.MaxStoreSkuStockQty, } _, err = api.JdAPI.BatchUpdateCurrentQtys("", "", msg.StationNo, []*jdapi.SkuStock{ stock, }, userName) } }) } } return jdapi.Err2CallbackResponse(err, "") } func OnNewPromotionMsg(msg *jdapi.CallbackOrderMsg) (retVal *jdapi.CallbackResponse) { return createLocalPromotionFromRemote(utils.Str2Int64(msg.BillID)) } func createLocalPromotionFromRemote(promotionInfoId int64) (retVal *jdapi.CallbackResponse) { utils.CallFuncAsync(func() { result, err := api.JdAPI.QueryPromotionInfo(promotionInfoId) if err == nil { db := dao.GetDB() promotion := &model.Promotion{ VendorPromotionID: utils.Int64ToStr(promotionInfoId), } if err = dao.GetEntity(db, promotion, "VendorPromotionID"); dao.IsNoRowsError(err) { storeIDMap := make(map[int64]int) skuIDMap := make(map[int64]int) skuMap := make(map[int64]*jdapi.PromotionLspQuerySkuResult) // 注意,这样处理可能是有问题,我们假定的是门店信息与SKU信息的叉乘 for _, v := range result.SkuResultList { storeIDMap[v.StationNo] = 1 skuIDMap[v.SkuID] = 1 skuMap[v.SkuID] = v } jdStoreIDs := make([]string, len(storeIDMap)) index := 0 for k := range storeIDMap { jdStoreIDs[index] = utils.Int64ToStr(k) index++ } jdSkuIDs := jxutils.Int64Map2List(skuIDMap) var skuList []*model.Sku var storeMapList []*model.StoreMap if err = dao.GetRows(db, &storeMapList, ` SELECT * FROM store_map WHERE vendor_id = ? AND deleted_at = ? AND vendor_store_id IN (`+ dao.GenQuestionMarks(len(jdStoreIDs))+")", model.VendorIDJD, utils.DefaultTimeValue, jdStoreIDs); err != nil { globals.SugarLogger.Warnf("createLocalPromotionFromRemote get storeMapList failed with error:%v", err) return } if err = dao.GetRows(db, &skuList, ` SELECT * FROM sku WHERE jd_id IN (`+ dao.GenQuestionMarks(len(jdSkuIDs))+")", jdSkuIDs); err != nil { globals.SugarLogger.Warnf("createLocalPromotionFromRemote get skuList failed with error:%v", err) return } jxStoreIDs := make([]int, len(storeMapList)) for k, v := range storeMapList { jxStoreIDs[k] = v.StoreID } priceList := make([]*SkuPrice, len(skuList)) var skuResult *jdapi.PromotionLspQuerySkuResult for k, v := range skuList { skuResult = skuMap[v.JdID] priceList[k] = &SkuPrice{ SkuID: v.ID, PriceType: PriceTypePrice, Price: skuResult.PromotionPrice, LimitSkuCount: 0, IsLock: 0, } } // globals.SugarLogger.Debugf("jxStoreIDs:%s", utils.Format4Output(jxStoreIDs, false)) // globals.SugarLogger.Debugf("priceList:%s", utils.Format4Output(priceList, false)) source := strings.Trim(result.Source, "来源") promotionParams := &PromotionParams{ Name: source + "-" + utils.Int64ToStr(result.PromotionInfoID), Advertising: "", Type: result.PromotionType, BeginAt: result.BeginTime.GoTime(), EndAt: result.EndTime.GoTime(), StoreIDs: jxStoreIDs, SkuPrices: priceList, } mapData := map[string]interface{}{ keyPromotionStatus: jd2jxPromotionStatusMap[result.PromotionState], keyPromotionSource: source, } if skuResult != nil { mapData[keyLimitDaily] = skuResult.LimitDaily mapData[keyLimitDevice] = skuResult.LimitDevice mapData[keyLimitPin] = skuResult.LimitPin } _, err = CreateJdPromotion(jxcontext.AdminCtx, model.VendorIDJD, false, true, false, utils.Int64ToStr(promotionInfoId), promotionParams, mapData) if dao.IsDuplicateError(err) || err == ErrLimitDeviceIsInvalid { err = nil } } } }) return jdapi.Err2CallbackResponse(nil, "") } func UpdatePromotionStatusFromRemote(ctx *jxcontext.Context, promotionID int) (err error) { globals.SugarLogger.Debugf("UpdatePromotionStatusFromRemote promotionID:%d", promotionID) db := dao.GetDB() promotion := &model.Promotion{} promotion.ID = promotionID if err = dao.GetEntity(db, promotion); err != nil { return err } result, err := api.JdAPI.QueryPromotionInfo(utils.Str2Int64(promotion.VendorPromotionID)) if err != nil { return err } newStatus := jd2jxPromotionStatusMap[result.PromotionState] if newStatus != promotion.Status { _, err = dao.UpdateEntityLogically(db, promotion, map[string]interface{}{ model.FieldStatus: newStatus, }, ctx.GetUserName(), nil) } return err } func excelStr2Time(timeStr string) (tm time.Time, err error) { return time.ParseInLocation("2006年1月2日15点4分5秒", timeStr, time.Local) } func getPromotionHander(promotionType int) JdPromotionHandler { var promotionHandler JdPromotionHandler if promotionType == PromotionTypeDirectDown { promotionHandler = &JdDirectDownHandler{} } else if promotionType == PromotionTypeLimitedTime { promotionHandler = &JdLimitedTimeHandler{} } else { // panic(fmt.Sprintf("unknown promotion type:%d", promotionType)) return nil } if !globals.EnableJdStoreWrite { promotionHandler = &JdNullHandler{} } return promotionHandler }