package promotion import ( "encoding/gob" "errors" "fmt" "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/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 = 80 ) const ( colSkuIDIndex = 0 colSkuPriceIndex = 3 colNameIndex = 5 colStoreIDIndex = 6 colBeginAtIndex = 9 colEndAtIndex = 10 ) const ( DefaultLimitSkuCount = 100 MaxPromotionSkuCount = jdapi.MaxPromotionSkuCount ) const ( defSearchDays = 7 ) type SkuPrice struct { SkuID int `json:"skuID"` PriceType int `json:"priceType"` Price int `json:"price"` // 分,这个不是单价,是这个sku的活动价 LimitSkuCount int `json:"limitSkuCount"` IsLock int8 `json:"isLock"` } 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 JdID int64 `orm:"column(jd_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没有被门店认领,请检查") ) 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 []map[string]interface{}) (skusResult []map[string]interface{}, 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.CreatePromotionRules(infoId, outInfoId, limitDevice, limitPin, limitCount, limitDaily) } func (p *JdDirectDownHandler) CreatePromotionSku(infoId int64, outInfoId string, skus []map[string]interface{}) (skusResult []map[string]interface{}, 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.CreatePromotionRules(infoId, outInfoId, limitDevice, limitPin, limitCount, limitDaily) } func (p *JdLimitedTimeHandler) CreatePromotionSku(infoId int64, outInfoId string, skus []map[string]interface{}) (skusResult []map[string]interface{}, 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 []map[string]interface{}) (skusResult []map[string]interface{}, 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 CreateJdPromotion(ctx *jxcontext.Context, isIDJd bool, isAsync bool, params *PromotionParams, userName string) (hint string, err error) { if len(params.SkuPrices) == 0 { return "", ErrEmptySkus } db := dao.GetDB() 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 } sql := "" var sqlParam []interface{} if isIDJd { sql = ` SELECT t1.*, t2.jd_id FROM store_sku_bind t1 JOIN sku t2 ON t1.sku_id = t2.id AND t2.deleted_at = ? JOIN store_map t3 ON t1.store_id = t3.store_id AND t3.vendor_id = ? AND t3.deleted_at = ? WHERE t1.jd_sync_status = 0 AND t1.deleted_at = ? AND t2.jd_id IN ( ` + dao.GenQuestionMarks(len(skuIDs)) + ") AND t3.vendor_store_id = ?" sqlParam = append(sqlParam, utils.DefaultTimeValue, model.VendorIDJD, utils.DefaultTimeValue) } else { sql = ` SELECT t1.*, 0 jd_id FROM store_sku_bind t1 WHERE t1.jd_sync_status = 0 AND t1.deleted_at = ? AND t1.sku_id IN ( ` + dao.GenQuestionMarks(len(skuIDs)) + ") AND t1.store_id = ?" } sqlParam = append(sqlParam, utils.DefaultTimeValue, skuIDs) errMsg := "" modifyPricesList := make(map[int][]*jdapi.SkuPriceInfo) promotionPrices := make([]map[string]interface{}, len(params.StoreIDs)*len(params.SkuPrices)) index := 0 var jxStoreIDs []int 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.JdID } promotionSkuPrice := skuPriceMap[mapSkuID] if promotionSkuPrice.PriceType == PriceTypePercentage { promotionSkuPrice.Price = skuBind.Price * promotionSkuPrice.Price / 100 } if 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 + 1, }) } } promotionPrices[index] = map[string]interface{}{ jdapi.KeyOutStationNo: utils.Int2Str(skuBind.StoreID), jdapi.KeyOutSkuId: utils.Int2Str(skuBind.SkuID), jdapi.KeyPromotionPrice: promotionSkuPrice.Price, jdapi.KeyLimitSkuCount: promotionSkuPrice.LimitSkuCount, } index++ } } } if errMsg != "" { return "", errors.New(errMsg) } promotionPrices = promotionPrices[:index] if len(promotionPrices) == 0 { return "", ErrEmptySkus } promotion := &model.Promotion{ Name: params.Name, Advertising: params.Advertising, VendorID: model.VendorIDJD, Type: params.Type, Status: model.PromotionStatusLocalCreated, BeginAt: params.BeginAt, EndAt: params.EndAt, } dao.WrapAddIDCULDEntity(promotion, userName) // if promotion.Params, err = jxutils.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, } dao.WrapAddIDCULDEntity(promotionSku, ctx.GetUserName()) if err = dao.CreateEntity(db, promotionSku); err != nil { return "", err } } 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", userName, func(task *tasksch.SeqTask, step int, params ...interface{}) (result interface{}, err error) { if step == 0 { task1 := tasksch.NewParallelTask("CreateJdPromotion update sku price", nil, userName, func(t *tasksch.ParallelTask, batchItemList []interface{}, params ...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 _, err = api.JdAPI.UpdateVendorStationPrice(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, "", 1, 1, 1, 1) } else if step == 2 { task2 := tasksch.NewParallelTask("CreateJdPromotion CreatePromotionSku", tasksch.NewParallelConfig().SetBatchSize(MaxPromotionSkuCount), userName, func(task *tasksch.ParallelTask, batchItemList []interface{}, params ...interface{}) (retVal interface{}, err error) { skus := make([]map[string]interface{}, len(batchItemList)) for k, v := range batchItemList { skus[k] = v.(map[string]interface{}) } _, err = promotionHandler.CreatePromotionSku(infoId, "", skus) return nil, err }, promotionPrices) task.AddChild(task2).Run() _, err = task2.GetResult(0) } else if step == 3 { err = promotionHandler.ConfirmPromotion(infoId, "") if err == nil { promotion.Status = model.PromotionStatusRemoteCreated } else { promotion.Status = model.PromotionStatusRemoteFailed } db := dao.GetDB() _, err = dao.UpdateEntityLogically(db, promotion, map[string]interface{}{ "Status": promotion.Status, }, ctx.GetUserName(), nil) } return nil, err }, 4) ctx.SetTaskOrAddChild(rootTask, nil) tasksch.ManageTask(rootTask).Run() if !isAsync { _, err = rootTask.GetResult(0) } return rootTask.ID, err } // func CreatePromotionByExcel(ctx *jxcontext.Context, isAsync bool, promotionType int, fileHeader *multipart.FileHeader, userName string) (hint string, err error) { // file, err := fileHeader.Open() // if err != nil { // return "", err // } // contents := excel.Excel2Slice(file) // file.Close() // var promotionParams *PromotionParams // for _, v := range contents { // promotionParams = &PromotionParams{ // Name: v[1][colNameIndex], // Type: promotionType, // } // if promotionParams.BeginAt, err = excelStr2Time(v[1][colBeginAtIndex]); err != nil { // return "", err // } // if promotionParams.EndAt, err = excelStr2Time(v[1][colEndAtIndex]); err != nil { // return "", err // } // for rowIndex, row := range v { // if rowIndex > 0 { // isBreak := true // if row[colSkuIDIndex] != "" { // isBreak = false // jdSkuID := int(utils.Str2Int64(row[colSkuIDIndex])) // promotionParams.SkuPrices = append(promotionParams.SkuPrices, &SkuPrice{ // SkuID: jdSkuID, // PriceType: PriceTypePrice, // Price: int(jxutils.StandardPrice2Int(utils.Str2Float64(row[colSkuPriceIndex]))), // }) // } // if row[colStoreIDIndex] != "" { // isBreak = false // jdStoreID := int(utils.Str2Int64(row[colStoreIDIndex])) // promotionParams.StoreIDs = append(promotionParams.StoreIDs, jdStoreID) // } // if isBreak { // break // } // } // } // break // } // // globals.SugarLogger.Debug(utils.Format4Output(promotionParams, false)) // // globals.SugarLogger.Debug(isAsync) // return CreateJdPromotion(ctx, true, isAsync, promotionParams, userName) // } 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":"', t22.name, '"}')), "]") 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, '}')), "]") 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"].(int)) } 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 { sql += " AND (SELECT COUNT(*) FROM promotion_store tt1 WHERE tt1.promotion_id = t1.id AND tt1.store_id = ?) > 0" sqlParams = append(sqlParams, params["storeID"].(int)) } 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 { 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 } } } pagedInfo = &model.PagedInfo{} countInfo := &struct{ Ct int }{} if err = dao.GetRow(db, countInfo, "SELECT FOUND_ROWS() ct"); err == nil { pagedInfo.TotalCount = countInfo.Ct } pagedInfo.Data = promotionList } dao.Commit(db) return pagedInfo, err } func CancelJdPromotion(ctx *jxcontext.Context, promotionID int) (err error) { db := dao.GetDB() promotion := &model.Promotion{} promotion.ID = promotionID if err = dao.GetEntity(db, promotion); err != nil { return err } promotionHandler := getPromotionHander(promotion.Type) if promotionHandler == nil { return errors.New("非法的促销类型") } if err = promotionHandler.CancelPromotion(utils.Str2Int64(promotion.VendorPromotionID), ""); err == nil { _, err = dao.UpdateEntityLogically(db, promotion, map[string]interface{}{ "Status": model.PromotionStatusCanceled, }, 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.EnableStoreWrite { promotionHandler = &JdNullHandler{} } return promotionHandler }