From e8ce327fc037d6c93b0cc1ace6be74e7dba26e1c Mon Sep 17 00:00:00 2001 From: gazebo Date: Tue, 25 Dec 2018 21:50:18 +0800 Subject: [PATCH] - normal store weixin msg. --- business/jxstore/cms/message.go | 154 +++++++++++++++++++++++- business/jxstore/cms/message_test.go | 14 +++ business/jxstore/cms/store.go | 2 +- business/jxutils/weixinmsg/weixinmsg.go | 24 ++++ business/model/api.go | 7 +- business/model/dao/dao.go | 11 ++ business/model/message.go | 28 +++-- controllers/cms_msg.go | 131 ++++++++++++++++++++ globals/beegodb/beegodb.go | 3 + routers/commentsRouter_controllers.go | 32 +++++ routers/router.go | 5 + 11 files changed, 396 insertions(+), 15 deletions(-) create mode 100644 business/jxstore/cms/message_test.go create mode 100644 controllers/cms_msg.go diff --git a/business/jxstore/cms/message.go b/business/jxstore/cms/message.go index 87e778911..f5376bb5b 100644 --- a/business/jxstore/cms/message.go +++ b/business/jxstore/cms/message.go @@ -1,12 +1,19 @@ package cms import ( + "time" + + "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/jxutils/weixinmsg" "git.rosy.net.cn/jx-callback/business/model" "git.rosy.net.cn/jx-callback/business/model/dao" + "git.rosy.net.cn/jx-callback/globals" ) -func SendStoreMessage(ctx *jxcontext.Context, title, content string, storeIDs []int, isAsync, isContinueWhenError bool) (err error) { +func SendStoreMessage(ctx *jxcontext.Context, title, content string, storeIDs []int, isAsync, isContinueWhenError bool) (hint string, err error) { db := dao.GetDB() dao.Begin(db) defer dao.Rollback(db) @@ -15,19 +22,156 @@ func SendStoreMessage(ctx *jxcontext.Context, title, content string, storeIDs [] Content: content, Type: model.MessageTypeStore, } + dao.WrapAddIDCULDEntity(msg, ctx.GetUserName()) if err = dao.CreateEntity(db, msg); err != nil { - return err + return "", err } - for _, storeID := range storeIDs { + msgStatusList := make([]*model.MessageStatus, len(storeIDs)) + for k, storeID := range storeIDs { msgStatus := &model.MessageStatus{ MessageID: msg.ID, StoreID: storeID, Status: model.MessageStatusNew, } + dao.WrapAddIDCULDEntity(msgStatus, ctx.GetUserName()) if err = dao.CreateEntity(db, msgStatus); err != nil { - return err + return "", err } + msgStatusList[k] = msgStatus } dao.Commit(db) - return err + + rootTask := tasksch.NewParallelTask("SendStoreMessage", nil, ctx.GetUserName(), func(task *tasksch.ParallelTask, batchItemList []interface{}, params ...interface{}) (retVal interface{}, err error) { + db := dao.GetDB() + msgStatus := batchItemList[0].(*model.MessageStatus) + if err = weixinmsg.NotifyStoreMessage(msgStatus.StoreID, msgStatus.MessageID, msgStatus.ID, msg.Title, msg.Content); err == nil { + msgStatus.Status = model.MessageStatusSendAllSuccess + } else { + msgStatus.Status = model.MessageStatusSendAllFailed + } + dao.WrapUpdateULEntity(msgStatus, ctx.GetUserName()) + globals.SugarLogger.Debug(utils.Format4Output(msgStatus, false)) + _, err = dao.UpdateEntity(db, msgStatus) + return nil, err + }, msgStatusList) + tasksch.ManageTask(rootTask).Run() + + if !isAsync { + _, err = rootTask.GetResult(0) + } else { + hint = rootTask.ID + } + return "", err +} + +func ReadStoreMessage(ctx *jxcontext.Context, msgID, msgStatusID int) (redirectURL string, err error) { + msgStatus := &model.MessageStatus{} + msgStatus.ID = msgStatusID + db := dao.GetDB() + if err = dao.GetEntity(db, msgStatus); err != nil { + return redirectURL, err + } + msgStatus.ReadCount++ + dao.WrapUpdateULEntity(msgStatus, ctx.GetUserName()) + _, err = dao.UpdateEntity(db, msgStatus) + return redirectURL, err +} + +func GetStoreMessages(ctx *jxcontext.Context, msgIDs, storeIDs, types []int, fromTime, toTime time.Time, keyword string, offset, pageSize int) (pagedInfo *model.PagedInfo, err error) { + sql := ` + SELECT SQL_CALC_FOUND_ROWS t1.* + FROM message t1 + WHERE 1 = 1 + ` + sqlParams := []interface{}{} + if len(storeIDs) > 0 { + sql += " AND (SELECT COUNT(*) FROM message_status t2 WHERE t2.message_id = t1.id AND t2.store_id IN (" + dao.GenQuestionMarks(len(storeIDs)) + ") ) > 0" + sqlParams = append(sqlParams, storeIDs) + } + if len(msgIDs) > 0 { + sql += " AND t1.id IN (" + dao.GenQuestionMarks(len(msgIDs)) + ")" + sqlParams = append(sqlParams, msgIDs) + } + if len(types) > 0 { + sql += " AND t1.type IN (" + dao.GenQuestionMarks(len(types)) + ")" + sqlParams = append(sqlParams, types) + } + if fromTime != utils.DefaultTimeValue { + sql += " AND t1.created_at >= ?" + sqlParams = append(sqlParams, fromTime) + } + if toTime != utils.DefaultTimeValue { + sql += " AND t1.created_at <= ?" + sqlParams = append(sqlParams, toTime) + } + if keyword != "" { + keywordLike := "%" + keyword + "%" + sql += " AND (t1.title LIKE ? OR t1.content LIKE ?)" + sqlParams = append(sqlParams, keywordLike, keywordLike) + } + sql += " LIMIT ? OFFSET ?" + pageSize = jxutils.FormalizePageSize(pageSize) + sqlParams = append(sqlParams, pageSize, offset) + db := dao.GetDB() + dao.Begin(db) + defer dao.Commit(db) + var msgList []*model.Message + if err = dao.GetRows(db, &msgList, sql, sqlParams...); err == nil { + pagedInfo = &model.PagedInfo{ + TotalCount: dao.GetLastTotalRowCount(db), + Data: msgList, + } + } + return pagedInfo, err +} + +func GetStoreMessageStatuses(ctx *jxcontext.Context, msgIDs, storeIDs []int, fromReadCount, toReadCount int, fromTime, toTime time.Time, keyword string, offset, pageSize int) (pagedInfo *model.PagedInfo, err error) { + sql := ` + SELECT SQL_CALC_FOUND_ROWS t1.* + FROM message_status t1 + WHERE 1 = 1 + ` + sqlParams := []interface{}{} + if len(storeIDs) > 0 { + sql += " AND t1.store_id IN (" + dao.GenQuestionMarks(len(storeIDs)) + ")" + sqlParams = append(sqlParams, storeIDs) + } + if len(msgIDs) > 0 { + sql += " AND t1.message_id IN (" + dao.GenQuestionMarks(len(msgIDs)) + ")" + sqlParams = append(sqlParams, msgIDs) + } + if fromTime != utils.DefaultTimeValue { + sql += " AND t1.created_at >= ?" + sqlParams = append(sqlParams, fromTime) + } + if toTime != utils.DefaultTimeValue { + sql += " AND t1.created_at <= ?" + sqlParams = append(sqlParams, toTime) + } + if fromReadCount >= 0 { + sql += " AND t1.read_count >= ?" + sqlParams = append(sqlParams, fromReadCount) + } + if toReadCount >= 0 { + sql += " AND t1.read_count <= ?" + sqlParams = append(sqlParams, toReadCount) + } + if keyword != "" { + keywordLike := "%" + keyword + "%" + sql += " AND (t1.last_operator LIKE ?)" + sqlParams = append(sqlParams, keywordLike) + } + sql += " LIMIT ? OFFSET ?" + sqlParams = append(sqlParams, jxutils.FormalizePageSize(pageSize), offset) + db := dao.GetDB() + dao.Begin(db) + defer dao.Commit(db) + var msgStatusList []*model.MessageStatus + if err = dao.GetRows(db, &msgStatusList, sql, sqlParams...); err == nil { + pagedInfo = &model.PagedInfo{ + TotalCount: dao.GetLastTotalRowCount(db), + Data: msgStatusList, + } + } + return pagedInfo, err } diff --git a/business/jxstore/cms/message_test.go b/business/jxstore/cms/message_test.go new file mode 100644 index 000000000..0476cf915 --- /dev/null +++ b/business/jxstore/cms/message_test.go @@ -0,0 +1,14 @@ +package cms + +import ( + "testing" + + "git.rosy.net.cn/jx-callback/business/jxutils/jxcontext" +) + +func TestSendStoreMessage(t *testing.T) { + err := SendStoreMessage(jxcontext.AdminCtx, "title", "content", []int{1}, false, true) + if err != nil { + t.Fatal(err) + } +} diff --git a/business/jxstore/cms/store.go b/business/jxstore/cms/store.go index 207ac4f11..9e3fb8326 100644 --- a/business/jxstore/cms/store.go +++ b/business/jxstore/cms/store.go @@ -324,7 +324,7 @@ func CreateStore(ctx *jxcontext.Context, storeExt *StoreExt, userName string) (i globals.SugarLogger.Debugf("CreateStore storeExt:%s", utils.Format4Output(storeExt, false)) store := &storeExt.Store if store.ID != 0 && !jxutils.IsLegalStoreID(store.ID) { - return 0, fmt.Errorf("ID:%d不是合法的京西门店编号") + return 0, fmt.Errorf("ID:%d不是合法的京西门店编号", store.ID) } existingID := store.ID diff --git a/business/jxutils/weixinmsg/weixinmsg.go b/business/jxutils/weixinmsg/weixinmsg.go index ab6cbc6c4..abd7e5802 100644 --- a/business/jxutils/weixinmsg/weixinmsg.go +++ b/business/jxutils/weixinmsg/weixinmsg.go @@ -337,6 +337,30 @@ func NotifyStoreOpRequestStatus(isAccepted bool, storeID, nameID int, spuName st return SendMsgToStore(storeID, templateID, fileURL, data) } +func NotifyStoreMessage(storeID, msgID, msgStatusID int, title, content string) (err error) { + globals.SugarLogger.Debugf("NotifyStoreMessage storeID:%d, msgID:%d, title:%s, content:%s", storeID, msgID, title, content) + templateID := "gIG2olBZtQbjXmp6doNB_dESu60By5xuXYOGxksLv3Y" + fileURL := fmt.Sprintf("%s%d", WX_TO_STORE_SKU_PAGE_URL, storeID) + data := map[string]interface{}{ + "first": map[string]interface{}{ + "value": title, + "color": "#333333", + }, + "keyword1": map[string]interface{}{ + "value": content, + "color": "#2E408E", + }, + "keyword2": map[string]interface{}{ + "value": utils.Time2Str(time.Now()), + "color": "#2E408E", + }, + "remark": map[string]interface{}{ + "value": "点击查看详情", + }, + } + return SendMsgToStore(storeID, templateID, fileURL, data) +} + func FormatDeliveryTime(order *model.GoodsOrder) string { var tmpTime time.Time if order.ExpectedDeliveredTime == utils.DefaultTimeValue { diff --git a/business/model/api.go b/business/model/api.go index 03ab0f120..a68a0ac1d 100644 --- a/business/model/api.go +++ b/business/model/api.go @@ -1,10 +1,13 @@ package model -import "time" +import ( + "math" + "time" +) const ( DefPageSize = 50 - UnlimitedPageSize = 999999999 + UnlimitedPageSize = math.MaxInt32 ) type GoodsOrderExt struct { diff --git a/business/model/dao/dao.go b/business/model/dao/dao.go index 7f55f3f10..45b709866 100644 --- a/business/model/dao/dao.go +++ b/business/model/dao/dao.go @@ -137,6 +137,17 @@ func CreateEntity(db *DaoDB, item interface{}) (err error) { return err } +// InsertMulti执行成功后ID不会改写成正确的(象Insert一样) +func CreateMultiEntities(db *DaoDB, item interface{}) (err error) { + if db == nil { + db = GetDB() + } + if _, err = db.db.InsertMulti(20, item); err != nil && !IsDuplicateError(err) { + globals.SugarLogger.Errorf("CreateEntity %s failed with error:%v", reflect.TypeOf(item).Name(), err) + } + return err +} + func CreateOrUpdate(db *DaoDB, item interface{}, colConflitAndArgs ...string) (err error) { if db == nil { db = GetDB() diff --git a/business/model/message.go b/business/model/message.go index 858371da1..1e1ee347e 100644 --- a/business/model/message.go +++ b/business/model/message.go @@ -9,19 +9,33 @@ const ( MessageStatusSendAllSuccess = 1 MessageStatusSendSuccess = 2 MessageStatusSendAllFailed = 3 - MessageStatusRead = 4 ) type Message struct { ModelIDCULD - Type int8 - Title string - Content string + Type int8 `json:"type"` + Title string `json:"title"` + Content string `json:"content"` +} + +func (*Message) TableIndex() [][]string { + return [][]string{ + []string{"CreatedAt"}, + []string{"DeletedAt"}, + } } type MessageStatus struct { ModelIDCULD - MessageID int - StoreID int - Status int8 + MessageID int `orm:"column(message_id)" json:"messageID"` + StoreID int `orm:"column(store_id)" json:"storeID"` + Status int8 `json:"status"` + ReadCount int `json:"readCount"` +} + +func (*MessageStatus) TableIndex() [][]string { + return [][]string{ + []string{"MessageID", "Status"}, + []string{"StoreID", "MessageID", "Status"}, + } } diff --git a/controllers/cms_msg.go b/controllers/cms_msg.go new file mode 100644 index 000000000..7c2d6a64f --- /dev/null +++ b/controllers/cms_msg.go @@ -0,0 +1,131 @@ +package controllers + +import ( + "net/http" + "time" + + "git.rosy.net.cn/jx-callback/business/jxstore/cms" + "git.rosy.net.cn/jx-callback/business/jxutils" + "git.rosy.net.cn/jx-callback/business/model" + "github.com/astaxie/beego" +) + +type MsgController struct { + beego.Controller +} + +// @Title 发送微信消息 +// @Description 发送微信消息 +// @Param token header string true "认证token" +// @Param storeIDs formData string true "门店 ID列表" +// @Param title formData string true "消息标题" +// @Param content formData string true "消息内容" +// @Param isAsync formData bool false "是否异步操作,缺省否" +// @Param isContinueWhenError formData bool false "单个失败是否继续,缺省false" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /SendStoreMessage [post] +func (c *MsgController) SendStoreMessage() { + c.callSendStoreMessage(func(params *tMsgSendStoreMessageParams) (retVal interface{}, errCode string, err error) { + var storeIDs []int + if err = jxutils.Strings2Objs(params.StoreIDs, &storeIDs); err != nil { + return retVal, "", err + } + retVal, err = cms.SendStoreMessage(params.Ctx, params.Title, params.Content, storeIDs, params.IsAsync, params.IsContinueWhenError) + return retVal, "", err + }) +} + +// @Title 门店读微信消息 +// @Description 门店读微信消息(此API是自动调用的) +// @Param token header string true "认证token" +// @Param msgID formData int true "消息ID" +// @Param msgStatusID formData int true "消息状态ID" +// @Param redirectURL formData string false "重定向URL" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /ReadStoreMessage [put] +func (c *MsgController) ReadStoreMessage() { + redirectURL := "" + c.callReadStoreMessage(func(params *tMsgReadStoreMessageParams) (retVal interface{}, errCode string, err error) { + retVal, err = cms.ReadStoreMessage(params.Ctx, params.MsgID, params.MsgStatusID) + retRedirectURL := retVal.(string) + if retRedirectURL != "" { + redirectURL = retRedirectURL + } else { + redirectURL = params.RedirectURL + } + if redirectURL != "" { + errCode = model.ErrorCodeIgnore + } + return retVal, errCode, err + }) + if redirectURL != "" { + c.Redirect(redirectURL, http.StatusTemporaryRedirect) + } +} + +// @Title 得到发送的微信消息列表 +// @Description 得到发送的微信消息列表 +// @Param token header string true "认证token" +// @Param keyword query string false "查询关键字(可以为空,为空表示不限制)" +// @Param msgIDs query string false "msg IDs列表" +// @Param storeIDs query string false "门店 ID列表" +// @Param types query string false "类型列表,当前只有一个类型,写死[1]" +// @Param fromTime query string false "创建起始时间" +// @Param toTime query string false "创建结束时间" +// @Param offset query int false "门店列表起始序号(以0开始,缺省为0)" +// @Param pageSize query int false "门店列表页大小(缺省为50,-1表示全部)" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /GetStoreMessages [get] +func (c *MsgController) GetStoreMessages() { + c.callGetStoreMessages(func(params *tMsgGetStoreMessagesParams) (retVal interface{}, errCode string, err error) { + var msgIDs, storeIDs, types []int + var timeList []time.Time + if err = jxutils.Strings2Objs(params.MsgIDs, &msgIDs, params.StoreIDs, &storeIDs, params.Types, &types); err != nil { + return retVal, "", err + } + if timeList, err = jxutils.BatchStr2Time(params.FromTime, params.ToTime); err != nil { + return retVal, "", err + } + retVal, err = cms.GetStoreMessages(params.Ctx, msgIDs, storeIDs, types, timeList[0], timeList[1], params.Keyword, params.Offset, params.PageSize) + return retVal, "", err + }) +} + +// @Title 得到发送的微信消息状态列表 +// @Description 得到发送的微信消息状态列表 +// @Param token header string true "认证token" +// @Param keyword query string false "查询关键字(可以为空,为空表示不限制)" +// @Param msgIDs query string false "msg IDs列表" +// @Param storeIDs query string false "门店 ID列表" +// @Param fromTime query string false "创建起始时间" +// @Param toTime query string false "创建结束时间" +// @Param fromReadCount query int false "创建起始时间" +// @Param toReadCount query int false "创建结束时间" +// @Param offset query int false "门店列表起始序号(以0开始,缺省为0)" +// @Param pageSize query int false "门店列表页大小(缺省为50,-1表示全部)" +// @Success 200 {object} controllers.CallResult +// @Failure 200 {object} controllers.CallResult +// @router /GetStoreMessageStatuses [get] +func (c *MsgController) GetStoreMessageStatuses() { + c.callGetStoreMessageStatuses(func(params *tMsgGetStoreMessageStatusesParams) (retVal interface{}, errCode string, err error) { + var msgIDs, storeIDs []int + var timeList []time.Time + if err = jxutils.Strings2Objs(params.MsgIDs, &msgIDs, params.StoreIDs, &storeIDs); err != nil { + return retVal, "", err + } + if timeList, err = jxutils.BatchStr2Time(params.FromTime, params.ToTime); err != nil { + return retVal, "", err + } + if _, ok := params.MapData["fromReadCount"]; !ok { + params.FromReadCount = -1 + } + if _, ok := params.MapData["toReadCount"]; !ok { + params.ToReadCount = -1 + } + retVal, err = cms.GetStoreMessageStatuses(params.Ctx, msgIDs, storeIDs, params.FromReadCount, params.ToReadCount, timeList[0], timeList[1], params.Keyword, params.Offset, params.PageSize) + return retVal, "", err + }) +} diff --git a/globals/beegodb/beegodb.go b/globals/beegodb/beegodb.go index 2601db738..8887989bf 100644 --- a/globals/beegodb/beegodb.go +++ b/globals/beegodb/beegodb.go @@ -28,6 +28,9 @@ func Init() { orm.RegisterModel(new(model.Waybill)) orm.RegisterModel(new(model.OrderStatus)) + orm.RegisterModel(new(model.Message)) + orm.RegisterModel(new(model.MessageStatus)) + if globals.EnableStore { orm.RegisterModel(&model.Place{}) orm.RegisterModel(&model.Store{}, &model.StoreSub{}, &model.StoreMap{}, &model.StoreCourierMap{}) diff --git a/routers/commentsRouter_controllers.go b/routers/commentsRouter_controllers.go index d9cded737..787101c10 100644 --- a/routers/commentsRouter_controllers.go +++ b/routers/commentsRouter_controllers.go @@ -175,6 +175,38 @@ func init() { MethodParams: param.Make(), Params: nil}) + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:MsgController"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:MsgController"], + beego.ControllerComments{ + Method: "GetStoreMessageStatuses", + Router: `/GetStoreMessageStatuses`, + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Params: nil}) + + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:MsgController"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:MsgController"], + beego.ControllerComments{ + Method: "GetStoreMessages", + Router: `/GetStoreMessages`, + AllowHTTPMethods: []string{"get"}, + MethodParams: param.Make(), + Params: nil}) + + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:MsgController"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:MsgController"], + beego.ControllerComments{ + Method: "ReadStoreMessage", + Router: `/ReadStoreMessage`, + AllowHTTPMethods: []string{"put"}, + MethodParams: param.Make(), + Params: nil}) + + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:MsgController"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:MsgController"], + beego.ControllerComments{ + Method: "SendStoreMessage", + Router: `/SendStoreMessage`, + AllowHTTPMethods: []string{"post"}, + MethodParams: param.Make(), + Params: nil}) + beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:OrderController"] = append(beego.GlobalControllerRouter["git.rosy.net.cn/jx-callback/controllers:OrderController"], beego.ControllerComments{ Method: "CreateWaybillOnProviders", diff --git a/routers/router.go b/routers/router.go index 1b48f396a..03294ee7b 100644 --- a/routers/router.go +++ b/routers/router.go @@ -76,6 +76,11 @@ func init() { &controllers.InitDataController{}, ), ), + beego.NSNamespace("/msg", + beego.NSInclude( + &controllers.MsgController{}, + ), + ), ) beego.AddNamespace(ns)