Files
jx-callback/business/jxstore/promotion/jd_promotion.go

1112 lines
37 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}