diff --git a/app/admin/apis/market/marketing.go b/app/admin/apis/market/marketing.go index 90b3db0..a647d2b 100644 --- a/app/admin/apis/market/marketing.go +++ b/app/admin/apis/market/marketing.go @@ -226,7 +226,134 @@ func ErpMarketingCouponStart(c *gin.Context) { // @Success 200 {object} models.ErpMarketingCouponDataResp // @Router /api/v1/marketing/coupon/data [post] func ErpMarketingCouponData(c *gin.Context) { + var req model.ErpMarketingCouponDataReq + if err := c.ShouldBindJSON(&req); err != nil { + app.Error(c, http.StatusBadRequest, err, "参数错误:"+err.Error()) + return + } - app.OK(c, nil, "OK") - return + userCouponIds, err := model.GetUserCouponIdsByErpCouponId(req.ErpCouponId) + if err != nil { + app.Error(c, http.StatusInternalServerError, err, err.Error()) + return + } + + // 查询使用该优惠券的订单商品 + var orderCommodities []model.ErpOrderCommodity + err = orm.Eloquent.Where("coupon_id in ?", userCouponIds). + Joins("JOIN erp_order ON erp_order.id = erp_order_commodity.erp_order_id"). + Where("erp_order.state = 'audited'").Find(&orderCommodities).Error + if err != nil { + app.Error(c, http.StatusInternalServerError, err, err.Error()) + return + } + + // 初始化统计数据 + var totalAmount, totalDiscount float64 + var orderCount, customerCount, productCount int + productsMap := make(map[uint32]model.ProductInfo) + customers := make(map[int]bool) // 用来去重客户 + + // 统计订单数据 + for _, orderCommodity := range orderCommodities { + // 获取订单详情 + var order model.ErpOrder + err = orm.Eloquent.First(&order, orderCommodity.ErpOrderId).Error + if err != nil { + app.Error(c, http.StatusInternalServerError, err, err.Error()) + return + } + + // 计算用券总成交额和总优惠金额 + totalAmount += orderCommodity.ReceivedAmount + totalDiscount += orderCommodity.CouponDiscount + + // 计算订单数 + orderCount++ + + // 计算客户数 + if _, exists := customers[order.Uid]; !exists { + customers[order.Uid] = true + customerCount++ + } + + // 计算购买商品总件数 + productCount += int(orderCommodity.Count) + if _, exists := productsMap[orderCommodity.ErpCommodityId]; !exists { + productsMap[orderCommodity.ErpCommodityId] = model.ProductInfo{ + ProductID: int(orderCommodity.ErpCommodityId), + ProductName: orderCommodity.ErpCommodityName, + PayCount: int(orderCommodity.Count), + PayCustomer: 1, + } + } else { + productInfo := productsMap[orderCommodity.ErpCommodityId] + productInfo.PayCount += int(orderCommodity.Count) + productInfo.PayCustomer++ + productsMap[orderCommodity.ErpCommodityId] = productInfo + } + } + + // 计算费效比 + var costEffectiveness float64 + if totalAmount > 0 { + costEffectiveness = tools.RoundFloat(totalDiscount / totalAmount) + } + + // 计算用券笔单价 + var unitPrice float64 + if orderCount > 0 { + unitPrice = tools.RoundFloat(totalAmount / float64(orderCount)) + } + + // 构造返回的数据结构 + resp := model.ErpMarketingCouponDataResp{ + TotalAmount: totalAmount, + TotalDiscount: totalDiscount, + CostEffectiveness: costEffectiveness, + OrderCount: orderCount, + UnitPrice: unitPrice, + CustomerCount: customerCount, + ProductCount: productCount, + } + + // 转换商品数据 + for _, productInfo := range productsMap { + resp.Products = append(resp.Products, productInfo) + } + + // 返回结果 + app.OK(c, resp, "OK") +} + +// GenerateScheme 获取小程序跳转链接 +// @Summary 获取小程序跳转链接 +// @Tags 营销管理,V1.4.4 +// @Produce json +// @Accept json +// @Param request body models.GenerateSchemeReq true "获取小程序跳转链接模型" +// @Success 200 {object} models.GenerateSchemeResp +// @Router /api/v1/marketing/generateScheme [post] +func GenerateScheme(c *gin.Context) { + var req model.GenerateSchemeReq + if err := c.ShouldBindJSON(&req); err != nil { + app.Error(c, http.StatusBadRequest, err, "参数错误:"+err.Error()) + return + } + + err := tools.Validate(req) //必填参数校验 + if err != nil { + app.Error(c, http.StatusBadRequest, err, err.Error()) + return + } + + resp, err := model.WXGenerateScheme(&req) + if err != nil { + logger.Error("WXGenerateScheme err:", logger.Field("err", err)) + app.Error(c, http.StatusInternalServerError, err, err.Error()) + return + } + + // 返回结果 + app.OK(c, resp, "OK") } diff --git a/app/admin/apis/usermanage/user.go b/app/admin/apis/usermanage/user.go index ca942d4..b33bfff 100644 --- a/app/admin/apis/usermanage/user.go +++ b/app/admin/apis/usermanage/user.go @@ -538,6 +538,214 @@ func ArticleDel(c *gin.Context) { app.OK(c, nil, "绑定店员成功") } +// AutoDepositRefund 每天早上9点30分退前一天的保证金 +func AutoDepositRefund() { + // 查询待审核的订单 + var depositRefunds []models.DepositRefundRecord + err := orm.Eloquent.Table("deposit_refund_record"). + Where("status = ?", models.DepositRefundStatusUnconfirmed).Order("id ASC").Limit(10).Find(&depositRefunds).Error + if err != nil { + logger.Error("AutoDepositRefund query deposit_refund_record err:", logger.Field("err", err)) + return + } + + for _, depositInfo := range depositRefunds { + err = NotarizeUserDepositRefundByID(depositInfo.ID) + if err.Error() == "押金退款失败,请检查余额" { // 余额不足,停止退款并发短信 + models.GtSendMessage([]string{"15019230751", "13510508605"}, "【go2ns】用户押金退款失败,请检查微信商户余额。") + return + } + } +} + +func NotarizeUserDepositRefundByID(depositRefundRecordId uint32) error { + if depositRefundRecordId == 0 { + return errors.New("invalid deposit refund record ID") + } + + fmt.Println("DepositRefundRecordId:", depositRefundRecordId) + var depositRefund models.DepositRefundRecord + err := orm.Eloquent.Table("deposit_refund_record").Where("id = ?", depositRefundRecordId).Find(&depositRefund).Error + if err != nil { + logger.Error("err:", logger.Field("err", err)) + return err + } + if depositRefund.Status != models.DepositRefundStatusUnconfirmed { + logger.Error("status not DepositRefundStatusUnconfirmed") + return err + } + + unreturnedOrders, err := models.IsUserHaveUnreturnedOrders(depositRefund.Uid) + if err != nil { + logger.Error("IsUserHaveUnreturnedOrders err:", logger.Field("err", err)) + return err + } + if unreturnedOrders { + logger.Error("unreturnedOrders") + return errors.New("用户有未完成订单") + } + + userInfo, err := models.GetUserInfoByUid(depositRefund.Uid) + if err != nil { + logger.Error("err:", logger.Field("err", err)) + return err + } + var transfer *pay.WxTransferResp + //if userInfo.Deposit == 80 { + nTempDeposit := userInfo.Deposit // 用户押金 + fmt.Println("******userInfo.Deposit is:******", nTempDeposit) + if userInfo.Deposit == 60000 { + for i := 0; i < 2; i++ { + transfer, err = pay.Transfer(userInfo.Deposit/2, userInfo.WxOpenID, fmt.Sprintf("押金退款(共2次到账)%d/2", i+1)) + if err != nil { + logger.Errorf("pay.Transfer 600 err, i is:", i) + logger.Error("pay.Transfer 600 err:", logger.Field("err", err)) + + if i != 0 { // 非首次退款报错 + updateUser := map[string]interface{}{ + "deposit": nTempDeposit, + "member_level": 1, + } + if userInfo.MemberExpire.After(time.Now()) { + updateUser["member_expire"] = time.Now() + } + err = orm.Eloquent.Table("user").Where("uid = ?", userInfo.Uid).Updates(updateUser).Error + if err != nil { + logger.Error("pay.Transfer 600 update deposit err:", logger.Field("err", err)) + } + } + + return errors.New("押金退款失败,请检查余额") + } + nTempDeposit -= 300 + } + } else if userInfo.Deposit == 150000 { + for i := 0; i < 5; i++ { + transfer, err = pay.Transfer(userInfo.Deposit/5, userInfo.WxOpenID, fmt.Sprintf("押金退款(共5次到账)%d/5", i+1)) + if err != nil { + logger.Errorf("pay.Transfer 1500 err, i is:", i) + logger.Error("pay.Transfer 1500 err:", logger.Field("err", err)) + + if i != 0 { // 非首次退款报错 + updateUser := map[string]interface{}{ + "deposit": nTempDeposit, + "member_level": 1, + } + if userInfo.MemberExpire.After(time.Now()) { + updateUser["member_expire"] = time.Now() + } + err = orm.Eloquent.Table("user").Where("uid = ?", userInfo.Uid).Updates(updateUser).Error + if err != nil { + logger.Error("pay.Transfer 1500 update deposit err:", logger.Field("err", err)) + } + } + + return errors.New("押金退款失败,请检查余额") + } + nTempDeposit -= 300 + } + } else if userInfo.Deposit == 120000 { + for i := 0; i < 4; i++ { + transfer, err = pay.Transfer(userInfo.Deposit/4, userInfo.WxOpenID, fmt.Sprintf("押金退款(共4次到账)%d/4", i+1)) + if err != nil { + logger.Errorf("pay.Transfer 1200 err, i is:", i) + logger.Error("pay.Transfer 1200 err:", logger.Field("err", err)) + + if i != 0 { // 非首次退款报错 + updateUser := map[string]interface{}{ + "deposit": nTempDeposit, + "member_level": 1, + } + if userInfo.MemberExpire.After(time.Now()) { + updateUser["member_expire"] = time.Now() + } + err = orm.Eloquent.Table("user").Where("uid = ?", userInfo.Uid).Updates(updateUser).Error + if err != nil { + logger.Error("pay.Transfer 1200 update deposit err:", logger.Field("err", err)) + } + } + + return errors.New("押金退款失败,请检查余额") + } + nTempDeposit -= 300 + } + } else if userInfo.Deposit == 90000 { + for i := 0; i < 3; i++ { + transfer, err = pay.Transfer(userInfo.Deposit/3, userInfo.WxOpenID, fmt.Sprintf("押金退款(共3次到账)%d/3", i+1)) + if err != nil { + logger.Errorf("pay.Transfer 900 err, i is:", i) + logger.Error("pay.Transfer 900 err:", logger.Field("err", err)) + + if i != 0 { // 非首次退款报错 + updateUser := map[string]interface{}{ + "deposit": nTempDeposit, + "member_level": 1, + } + if userInfo.MemberExpire.After(time.Now()) { + updateUser["member_expire"] = time.Now() + } + err = orm.Eloquent.Table("user").Where("uid = ?", userInfo.Uid).Updates(updateUser).Error + if err != nil { + logger.Error("pay.Transfer 900 update deposit err:", logger.Field("err", err)) + } + } + + return errors.New("押金退款失败,请检查余额") + } + nTempDeposit -= 300 + } + } else if userInfo.Deposit == 30000 { + transfer, err = pay.Transfer(userInfo.Deposit, userInfo.WxOpenID, "押金退款") + if err != nil { + logger.Error("pay.Transfer 300 err:", logger.Field("err", err)) + return errors.New("押金退款失败,请检查余额") + } + } else { + logger.Errorf("押金退款失败, 押金额有误,userInfo.Deposit is:", userInfo.Deposit) + return errors.New("押金退款失败,金额有误") + } + + fmt.Println("transfer:", transfer) + updateUser := map[string]interface{}{ + "deposit": 0, + "member_level": 1, + } + if userInfo.MemberExpire.After(time.Now()) { + updateUser["member_expire"] = time.Now() + } + err = orm.Eloquent.Table("user").Where("uid = ?", userInfo.Uid).Updates(updateUser).Error + if err != nil { + logger.Error("update deposit err:", logger.Field("err", err)) + return err + } + + err = orm.Eloquent.Table("deposit_refund_record").Where("id = ?", depositRefund.ID).Updates(map[string]interface{}{ + "status": models.DepositRefundStatusRefunded, + "confirm_time": time.Now(), + }).Error + if err != nil { + logger.Error("update deposit refund record err:", logger.Field("err", err)) + return err + } + fundRecord := &models.FundRecord{ + Uid: depositRefund.Uid, + FundType: models.FundTypeDepositRefund, + Amount: int64(depositRefund.Amount) * (-1), + OutTradeNo: transfer.PartnerTradeNo, + PaymentNo: transfer.PaymentNo, + Status: 2, + Remark: "退还押金", + + //TransactionId: , + } + err = orm.Eloquent.Create(fundRecord).Error + if err != nil { + logger.Error("create fund record err:", logger.Field("err", err)) + } + + return nil +} + func UserDepositRefundRecordList(c *gin.Context) { req := &models.UserDepositRefundRecordListReq{} if c.ShouldBindJSON(req) != nil { diff --git a/app/admin/models/coupon.go b/app/admin/models/coupon.go index e5497cf..2d51051 100644 --- a/app/admin/models/coupon.go +++ b/app/admin/models/coupon.go @@ -27,6 +27,7 @@ type Coupon struct { CategoryNumber string `json:"category_number"` // 可以使用该优惠券的商品分类,如果为空则表示没限制 CommodityNumber string `json:"commodity_number"` // 可以使用该优惠券的商品编号,如果为空则表示没限制 ErpCouponId uint32 `json:"erp_coupon_id"` // 零售业务推送优惠券ID + Limit uint32 `json:"limit"` // 优惠券叠加限制 0-不限制;1-仅限原价购买时使用 IsDraw bool `json:"is_draw" gorm:"-"` // } @@ -51,6 +52,7 @@ type UserCoupon struct { CategoryNumber string `json:"category_number"` // 可以使用该优惠券的商品分类,如果为空则表示没限制 CommodityNumber string `json:"commodity_number"` // 可以使用该优惠券的商品编号,如果为空则表示没限制 Code string `json:"code"` // 优惠券券码 + Limit uint32 `json:"limit"` // 优惠券叠加限制 0-不限制;1-仅限原价购买时使用 Coupon *Coupon `json:"coupon" gorm:"-"` // UserInfo UserInfo `json:"user_info" gorm:"-"` // } diff --git a/app/admin/models/erp_order.go b/app/admin/models/erp_order.go index 4bbac66..80b2e18 100644 --- a/app/admin/models/erp_order.go +++ b/app/admin/models/erp_order.go @@ -1268,7 +1268,7 @@ func UpdateCoupon(gdb *gorm.DB, erpOrder ErpOrder, state int) error { for i, _ := range commodities { var userCoupon UserCoupon - err := orm.Eloquent.Table("user_coupon").Where("id = ?", commodities[i].CouponID). + err = orm.Eloquent.Table("user_coupon").Where("id = ?", commodities[i].CouponID). Find(&userCoupon).Error if err != nil || err == RecordNotFound { return errors.New("未查询到优惠券") @@ -1327,6 +1327,36 @@ func UpdateCoupon(gdb *gorm.DB, erpOrder ErpOrder, state int) error { logger.Error("UpdateCoupon err:", logger.Field("err", err)) return err } + + // 根据优惠券状态执行相应的处理逻辑 + switch couponState { + case 1, 3: // 未使用,已过期;则扣减支付金额和已使用数量 + // 这里可以根据需要添加扣减的 SQL 更新逻辑,例如: + sql := ` + UPDATE erp_coupon + SET used_count = used_count - 1, + total_pay_amount = total_pay_amount - ?, + per_customer_amount = (total_pay_amount - ?) / (used_count - 1) + WHERE ID = ?` + err = gdb.Exec(sql, commodities[i].ReceivedAmount, commodities[i].ReceivedAmount, coupon.ErpCouponId).Error + if err != nil { + logger.Error("扣减优惠券数据失败:", logger.Field("err", err)) + //return err + } + case 2: // 已使用;则增加支付金额和已使用数量 + // 增加支付金额和已使用数量 + sql := ` + UPDATE erp_coupon + SET used_count = used_count + 1, + total_pay_amount = total_pay_amount + ?, + per_customer_amount = (total_pay_amount + ?) / (used_count + 1) + WHERE ID = ?` + err = gdb.Exec(sql, commodities[i].ReceivedAmount, commodities[i].ReceivedAmount, coupon.ErpCouponId).Error + if err != nil { + logger.Error("更新优惠券数据失败:", logger.Field("err", err)) + //return err + } + } } return nil diff --git a/app/admin/models/greentown_sms.go b/app/admin/models/greentown_sms.go index 7cf3df5..203541e 100644 --- a/app/admin/models/greentown_sms.go +++ b/app/admin/models/greentown_sms.go @@ -5,6 +5,7 @@ import ( "crypto/md5" "encoding/hex" "encoding/json" + "errors" "fmt" "go-admin/logger" "io" @@ -50,9 +51,9 @@ type GtSendMessageResp struct { } func GtSendMessage(phoneList []string, content string) error { - //if len(phoneList) > GetSmsNumberRemaining() { // 待发送短信超出剩余可用数量 - // return errors.New("短信剩余数量不足") - //} + if len(phoneList) > GetSmsNumberRemaining() { // 待发送短信超出剩余可用数量 + return errors.New("短信剩余数量不足") + } params := make(map[string]interface{}, 0) nowTime := time.Now() @@ -69,12 +70,12 @@ func GtSendMessage(phoneList []string, content string) error { } fmt.Println("resp:", resp) - //// 更新已使用的短信数量 - //err = UpdateSmsUsedCount(len(phoneList)) - //if err != nil { - // logger.Error("UpdateSmsUsedCount err", logger.Field("sms count", len(phoneList)), logger.Field("err", err)) - // return err - //} + // 更新已使用的短信数量 + err = UpdateSmsUsedCount(len(phoneList)) + if err != nil { + logger.Error("UpdateSmsUsedCount err", logger.Field("sms count", len(phoneList)), logger.Field("err", err)) + return err + } return nil } diff --git a/app/admin/models/marketing.go b/app/admin/models/marketing.go index a11d302..9d23fa5 100644 --- a/app/admin/models/marketing.go +++ b/app/admin/models/marketing.go @@ -8,10 +8,12 @@ import ( utils "go-admin/app/admin/models/tools" orm "go-admin/common/global" "go-admin/logger" + "go-admin/tools/config" "io/ioutil" "log" "net/http" "net/url" + "strings" "time" ) @@ -33,6 +35,10 @@ const ( WxSubscribeMessage = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send?access_token=" TemplateId = "aP3c503T3wn-tJtpDccvxtNi7b1qjrwavFFfo-k2SAQ" WxJumpUrl = "/pages/voucher/voucher" // 跳转微信小程序的页面路径 + + WXGenerateSchema = "https://api.weixin.qq.com/wxa/generatescheme?access_token=" + + SMSHead = "【go2ns】" ) // WxAccessToken 微信access_token @@ -140,6 +146,37 @@ type ErpMarketingCouponDataReq struct { // ErpMarketingCouponDataResp 优惠券数据出参 type ErpMarketingCouponDataResp struct { + TotalAmount float64 `json:"total_amount"` // 用券总成交额:使用该优惠券的订单付款总金额 + TotalDiscount float64 `json:"total_discount"` // 优惠总金额:使用该优惠券优惠的总金额 + CostEffectiveness float64 `json:"cost_effectiveness"` // 费效比:优惠总金额 / 用券总成交额 + OrderCount int `json:"order_count"` // 订单数:使用该优惠券的付款订单数 + UnitPrice float64 `json:"unit_price"` // 用券笔单价:用券总成交额 / 使用该优惠券的付款订单数 + CustomerCount int `json:"customer_count"` // 用券客户数:使用该优惠券的成交客户数 + ProductCount int `json:"product_count"` // 购买商品件数:使用该优惠券购买的商品数量 + Products []ProductInfo `json:"products"` // 商品信息:包含每个商品的ID、名称、付款件数、付款人数等信息 +} + +// ProductInfo 商品信息结构体 +type ProductInfo struct { + ProductID int `json:"product_id"` // 商品ID + ProductName string `json:"product_name"` // 商品名称 + PayCount int `json:"pay_count"` // 付款件数:使用该优惠券购买该商品的付款件数 + PayCustomer int `json:"pay_customer"` // 付款人数:购买该商品并使用优惠券的客户人数 +} + +type GenerateSchemeReq struct { + Path string `json:"path" validate:"required"` // 跳转小程序页面 + EnvVersion string `json:"env_version"` // 默认值"release"。要打开的小程序版本。正式版为"release",体验版为"trial",开发版为"develop" +} + +type GenerateSchemeResp struct { + Openlink string `json:"openlink"` // 小程序跳转链接 +} + +type WXGenerateSchemeResponse struct { + Errcode int `json:"errcode"` + Errmsg string `json:"errmsg"` + Openlink string `json:"openlink"` } // List 查询优惠券列表 @@ -228,8 +265,10 @@ func CreateErpMarketingCoupon(req *ErpMarketingCouponCreateReq) error { CouponType: "deduction", Value: req.Amount * 100, CategoryNumber: req.CategoryNumber, + ActivityType: 666, ErpCouponId: erpCoupon.ID, ActiveEnd: Now().AddDate(0, 0, int(req.ActiveDate)), + Limit: req.Limit, } err = orm.Eloquent.Create(coupon).Error @@ -263,7 +302,7 @@ func EditErpMarketingCoupon(req *ErpMarketingCouponEditReq) error { erpCoupon.Describe = req.Describe erpCoupon.CategoryNumber = req.CategoryNumber erpCoupon.ActiveDate = req.ActiveDate - erpCoupon.Amount = req.Amount + erpCoupon.Amount = req.Amount * 100 erpCoupon.UserType = req.UserType erpCoupon.Limit = req.Limit @@ -288,7 +327,7 @@ func EditErpMarketingCoupon(req *ErpMarketingCouponEditReq) error { coupon.Name = req.Name coupon.Describe = req.Describe coupon.Rule = req.Rule - coupon.Value = req.Amount + coupon.Value = req.Amount * 100 err = begin.Model(&Coupon{}).Where("erp_coupon_id = ?", req.ErpCouponId). Omit("created_at").Save(coupon).Error @@ -546,6 +585,19 @@ func markTaskAsCompleted(erpCouponId []uint32, nTotalCount int) error { // 校验用户是否符合领取优惠券的条件 func isEligibleForCoupon(user UserInfo, erpCoupon ErpCoupon) bool { + // 获取尊享会员信息 + var privilegeUserInfo PrivilegeMember + if err := orm.Eloquent.Table("privilege_member").Where("uid = ?", user.Uid).Find(&privilegeUserInfo).Error; err != nil { + logger.Errorf("获取尊享会员信息出错:", logger.Field("err", err)) + return false + } + + // 如果是尊享会员,将其视为付费会员 + if privilegeUserInfo.MemberLevel == MemberLevelPrivilege { + // 尊享会员归类为已付费用户 + user.MemberLevel = MemberLevelGold // 可以选择将尊享会员设为任意一个付费会员等级,通常设为黄金会员 + } + switch erpCoupon.UserType { case 1: // 1-所有人:不限制会员等级,所有人均可领取 @@ -587,7 +639,7 @@ func issueCouponsToUsers(users []UserInfo, erpCouponId []uint32) error { if isEligibleForCoupon(user, erpCoupon[0]) { // 发放优惠券 var coupons []Coupon - err = orm.Eloquent.Table("coupon").Where("erp_coupon_id in ?", erpCouponId).First(&coupons).Error + err = orm.Eloquent.Table("coupon").Where("erp_coupon_id in ?", erpCouponId).Find(&coupons).Error if err != nil { logger.Error("query coupon err:", logger.Field("err", err)) return err @@ -606,6 +658,8 @@ func issueCouponsToUsers(users []UserInfo, erpCouponId []uint32) error { RedeemCode: "", CategoryNumber: coupon.CategoryNumber, Code: couponCode, + ActivityType: coupon.ActivityType, + Limit: coupon.Limit, } err = orm.Eloquent.Create(userCoupon).Error @@ -644,10 +698,12 @@ func issueCouponsToUsers(users []UserInfo, erpCouponId []uint32) error { "thing4": map[string]interface{}{"value": coupons[0].Rule}, } //developer为开发版;trial为体验版;formal为正式版;默认为正式版 - _, err = SendMessage(wxToken.AccessToken, toUserOpenid, templateId, data, WxJumpUrl, "trial") + miniProgramState := config.MessageConfig.MiniProgramState + _, err = SendMessage(wxToken.AccessToken, toUserOpenid, templateId, data, WxJumpUrl, miniProgramState) if err != nil { + logger.Errorf("SendMessage err:", err) // 如果订阅通知发送失败,则发送短信 - err = GtSendMessage([]string{user.Tel}, erpCoupon[0].SmsContent) + err = GtSendMessage([]string{user.Tel}, ComposeSMSContent(erpCoupon[0].SmsContent)) if err != nil { logger.Error(err.Error()) } @@ -657,6 +713,20 @@ func issueCouponsToUsers(users []UserInfo, erpCouponId []uint32) error { return nil } +func ComposeSMSContent(content string) string { + // 1. 判断短信内容开头是否有【go2ns】,没有的话添加上 + if !strings.HasPrefix(content, SMSHead) { + content = SMSHead + content + } + + // 2. 判断短信内容中是否有 http 开头的链接,没有的话在最后加上 + if !strings.Contains(content, "http") { + content += " 详情请点击:" + config.MessageConfig.SmsUrl + } + + return content +} + // Response 请求微信返回基础数据 type Response struct { Errcode int `json:"errcode"` @@ -694,7 +764,7 @@ func CheckAccessToken() { // 更新access_token err = orm.Eloquent.Table("wx_access_token").Where("appid = ?", WXAppID). Updates(map[string]interface{}{ - "access_token": accessToken, + "access_token": accessToken.AccessToken, "expires_time": twoHoursLater, }).Error if err != nil { @@ -765,21 +835,41 @@ func code2url(appID, secret string) (string, error) { return url.String(), nil } +// SendMessageReq 消息订阅请求参数 +type SendMessageReq struct { + TemplateId string `json:"template_id"` + Page string `json:"page"` + Touser string `json:"touser"` + Data interface{} `json:"data"` + MiniprogramState string `json:"miniprogram_state"` + Lang string `json:"lang"` +} + // SendMessage 发送订阅消息 -func SendMessage(accessToken, toUserOpenid, templateId string, data interface{}, page, miniprogramState string) ( - []byte, error) { +func SendMessage(accessToken, toUserOpenid, templateId string, data interface{}, page, miniProgramState string) ( + sendMessageRsp *Response, err error) { url := fmt.Sprintf("%s%s", WxSubscribeMessage, accessToken) - body := map[string]interface{}{ - "touser": toUserOpenid, - "template_id": templateId, - "data": data, - "page": page, - "miniprogramState": miniprogramState, - "lang": "zh_CN", + + body := SendMessageReq{ + TemplateId: templateId, + Page: page, + Touser: toUserOpenid, + Data: data, + MiniprogramState: miniProgramState, + Lang: "zh_CN", } - rsp, err := HttpRequest(http.MethodPost, url, body) - return rsp, err + respRaw, err := HttpRequest(http.MethodPost, url, body) + + rsp := Response{} + + if err = json.Unmarshal(respRaw, &rsp); err != nil { + return nil, err + } + if rsp.Errcode != 0 { + return nil, errors.New(rsp.Errmsg) + } + return &rsp, err } func HttpRequest(method, url string, reqBody interface{}) ([]byte, error) { @@ -808,3 +898,93 @@ func HttpRequest(method, url string, reqBody interface{}) ([]byte, error) { } return rspBody, nil } + +// GetUserCouponIdsByErpCouponId 通过erp的优惠券id查找用户的优惠券id列表 +func GetUserCouponIdsByErpCouponId(erpCouponId uint32) ([]uint32, error) { + // 先查询 Coupon 表,根据 ErpCouponId 获取 coupon_id + var coupon Coupon + err := orm.Eloquent.Where("erp_coupon_id = ?", erpCouponId).First(&coupon).Error + if err != nil { + // 如果未找到对应优惠券ID,返回错误 + return nil, fmt.Errorf("优惠券ID不存在或查询失败") + } + + // 使用 coupon_id 查找 UserCoupon 表中的记录,获取所有符合条件的记录 + var userCoupons []UserCoupon + err = orm.Eloquent.Where("coupon_id = ?", coupon.ID).Find(&userCoupons).Error + if err != nil { + // 如果查询失败,返回错误 + return nil, fmt.Errorf("查询 UserCoupon 记录失败") + } + + // 提取所有找到的 userCoupon 的 ID + var userCouponIds []uint32 + for _, userCoupon := range userCoupons { + userCouponIds = append(userCouponIds, userCoupon.ID) + } + + // 返回所有 UserCoupon ID + return userCouponIds, nil +} + +// WXGenerateScheme 获取小程序跳转链接 +func WXGenerateScheme(req *GenerateSchemeReq) (*GenerateSchemeResp, error) { + var rsp WXGenerateSchemeResponse + + var wxToken WxAccessToken + err := orm.Eloquent.Table("wx_access_token").Where("appid = ?", WXAppID).First(&wxToken).Error + if err != nil { + logger.Error("query wx_access_token err:", logger.Field("err", err)) + } + + if wxToken.ID == 0 || wxToken.ExpiresTime.Before(time.Now()) { + CheckAccessToken() + + err = orm.Eloquent.Table("wx_access_token").Where("appid = ?", WXAppID).First(&wxToken).Error + if err != nil { + logger.Error("query wx_access_token err:", logger.Field("err", err)) + } + } + + url := WXGenerateSchema + wxToken.AccessToken + /* + { + "jump_wxa": { + "path": "/pages/startPage/index", + "query": "", + "env_version": "release" + + }, + "expire_type": 1, + "expire_interval": 30 + } + */ + reqScheme := map[string]interface{}{ + "jump_wxa": map[string]string{ + "path": req.Path, + "env_version": req.EnvVersion, + }, + "expire_type": 1, //小程序 URL Link 失效类型,时间戳:0,间隔天数:1 + "expire_interval": 30, //最长间隔天数为30天。expire_type 为 1 必填 + } + + data, err := HttpRequest(http.MethodPost, url, reqScheme) + /* + { + "errcode": 0, //成功返回 + "errmsg": "ok", + "url_link": "https://wxaurl.cn/pjKMfcQsULj" + } + */ + if err = json.Unmarshal(data, &rsp); err != nil { + return nil, err + } + if rsp.Errcode != 0 { + return nil, errors.New(fmt.Sprintf("生成小程序跳转链接失败:errcode %d,errmsg %s", rsp.Errcode, rsp.Errmsg)) + } + + resp := GenerateSchemeResp{ + Openlink: rsp.Openlink, + } + return &resp, nil +} diff --git a/app/admin/router/marketing.go b/app/admin/router/marketing.go index b277486..3e6a640 100644 --- a/app/admin/router/marketing.go +++ b/app/admin/router/marketing.go @@ -17,3 +17,9 @@ func registerMarketingManageRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJ r.POST("coupon/start", market.ErpMarketingCouponStart) // 启动优惠券发放 r.POST("coupon/data", market.ErpMarketingCouponData) // 优惠券数据 } + +func registerMarketingManageUnAuthRouter(v1 *gin.RouterGroup) { + r := v1.Group("/marketing") + + r.POST("generateScheme", market.GenerateScheme) // 获取小程序跳转链接 +} diff --git a/app/admin/router/router.go b/app/admin/router/router.go index 21eca23..567d8f8 100644 --- a/app/admin/router/router.go +++ b/app/admin/router/router.go @@ -58,6 +58,9 @@ func examplesNoCheckRoleRouter(r *gin.Engine) { registerCooperativeManageUnAuthRouter(v1) // 回收卡 registerRecycleCardManageUnAuthRouter(v1) + + // 营销管理 + registerMarketingManageUnAuthRouter(v1) } // 需要认证的路由示例 diff --git a/cmd/api/server.go b/cmd/api/server.go index 162651c..5c6007d 100644 --- a/cmd/api/server.go +++ b/cmd/api/server.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "github.com/jasonlvhit/gocron" + "go-admin/app/admin/apis/usermanage" "go-admin/app/admin/models" "go-admin/app/admin/router" "go-admin/logger" @@ -188,6 +189,12 @@ func run() error { fmt.Println("err:", err) } + // 每天早上9点30分退前一天的保证金 + err = s.Every(1).Day().At("09:30").Do(usermanage.AutoDepositRefund) + if err != nil { + fmt.Println("err:", err) + } + <-s.Start() }() diff --git a/cmd/config/server.go b/cmd/config/server.go index 8cae4ca..1c51c51 100644 --- a/cmd/config/server.go +++ b/cmd/config/server.go @@ -59,4 +59,9 @@ func run() { } fmt.Println("logger:", string(loggerConfig)) + messageConfig, errs := json.MarshalIndent(config.MessageConfig, "", " ") //转换成JSON返回的是byte[] + if errs != nil { + fmt.Println(errs.Error()) + } + fmt.Println("messageConfig:", string(messageConfig)) } diff --git a/config/settings.yml b/config/settings.yml index 84a739d..e48ee71 100644 --- a/config/settings.yml +++ b/config/settings.yml @@ -43,15 +43,16 @@ settings: # source: mh_pro:c5JBW3X6EEVQluYM@tcp(39.108.188.218:3306)/mh_pro?charset=utf8&parseTime=True&loc=Local&timeout=1000ms # source: mh_test:GPLzZ8rMmbJbKtMh@tcp(112.33.14.191:3306)/mh_test?charset=utf8&parseTime=True&loc=Local&timeout=1000ms # source: mh_new_pro:YnzexdTfBHMSGZki@tcp(39.108.188.218:3306)/mh_new_pro?charset=utf8&parseTime=True&loc=Local&timeout=1000ms - gen: # 代码生成读取的数据库名称 dbname: dbname # 代码生成是使用前端代码存放位置,需要指定到src文件夹,相对路径 frontpath: ../go-admin-ui/src - export: path: /Users/max/Documents/ url: /Users/max/Documents/ # path: /www/server/images/export/ # url: https://dev.admin.deovo.com/load/export/ + message: + mini_program_state: trial # 跳转小程序类型:developer为开发版;trial为体验版;formal为正式版;默认为正式版 + sms_url: https://dev.admin.deovo.com/visit.html # 短信调整微信小程序的链接地址,需要区分环境 diff --git a/docs/docs.go b/docs/docs.go index bf7e186..b1d251b 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -4480,6 +4480,39 @@ const docTemplate = `{ } } }, + "/api/v1/marketing/generateScheme": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "营销管理,V1.4.4" + ], + "summary": "获取小程序跳转链接", + "parameters": [ + { + "description": "获取小程序跳转链接模型", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.GenerateSchemeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.GenerateSchemeResp" + } + } + } + } + }, "/api/v1/menu": { "get": { "security": [ @@ -9652,7 +9685,44 @@ const docTemplate = `{ } }, "models.ErpMarketingCouponDataResp": { - "type": "object" + "type": "object", + "properties": { + "cost_effectiveness": { + "description": "费效比:优惠总金额 / 用券总成交额", + "type": "number" + }, + "customer_count": { + "description": "用券客户数:使用该优惠券的成交客户数", + "type": "integer" + }, + "order_count": { + "description": "订单数:使用该优惠券的付款订单数", + "type": "integer" + }, + "product_count": { + "description": "购买商品件数:使用该优惠券购买的商品数量", + "type": "integer" + }, + "products": { + "description": "商品信息:包含每个商品的ID、名称、付款件数、付款人数等信息", + "type": "array", + "items": { + "$ref": "#/definitions/models.ProductInfo" + } + }, + "total_amount": { + "description": "用券总成交额:使用该优惠券的订单付款总金额", + "type": "number" + }, + "total_discount": { + "description": "优惠总金额:使用该优惠券优惠的总金额", + "type": "number" + }, + "unit_price": { + "description": "用券笔单价:用券总成交额 / 使用该优惠券的付款订单数", + "type": "number" + } + } }, "models.ErpMarketingCouponDeleteReq": { "type": "object", @@ -13048,6 +13118,31 @@ const docTemplate = `{ } } }, + "models.GenerateSchemeReq": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "env_version": { + "description": "默认值\"release\"。要打开的小程序版本。正式版为\"release\",体验版为\"trial\",开发版为\"develop\"", + "type": "string" + }, + "path": { + "description": "跳转小程序页面", + "type": "string" + } + } + }, + "models.GenerateSchemeResp": { + "type": "object", + "properties": { + "openlink": { + "description": "小程序跳转链接", + "type": "string" + } + } + }, "models.GetErpPurchaseDemandReq": { "type": "object", "properties": { @@ -15676,6 +15771,27 @@ const docTemplate = `{ } } }, + "models.ProductInfo": { + "type": "object", + "properties": { + "pay_count": { + "description": "付款件数:使用该优惠券购买该商品的付款件数", + "type": "integer" + }, + "pay_customer": { + "description": "付款人数:购买该商品并使用优惠券的客户人数", + "type": "integer" + }, + "product_id": { + "description": "商品ID", + "type": "integer" + }, + "product_name": { + "description": "商品名称", + "type": "string" + } + } + }, "models.ProductInventoryAddReq": { "type": "object", "required": [ diff --git a/docs/swagger.json b/docs/swagger.json index b12a5fa..35249b0 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -4469,6 +4469,39 @@ } } }, + "/api/v1/marketing/generateScheme": { + "post": { + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "营销管理,V1.4.4" + ], + "summary": "获取小程序跳转链接", + "parameters": [ + { + "description": "获取小程序跳转链接模型", + "name": "request", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/models.GenerateSchemeReq" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.GenerateSchemeResp" + } + } + } + } + }, "/api/v1/menu": { "get": { "security": [ @@ -9641,7 +9674,44 @@ } }, "models.ErpMarketingCouponDataResp": { - "type": "object" + "type": "object", + "properties": { + "cost_effectiveness": { + "description": "费效比:优惠总金额 / 用券总成交额", + "type": "number" + }, + "customer_count": { + "description": "用券客户数:使用该优惠券的成交客户数", + "type": "integer" + }, + "order_count": { + "description": "订单数:使用该优惠券的付款订单数", + "type": "integer" + }, + "product_count": { + "description": "购买商品件数:使用该优惠券购买的商品数量", + "type": "integer" + }, + "products": { + "description": "商品信息:包含每个商品的ID、名称、付款件数、付款人数等信息", + "type": "array", + "items": { + "$ref": "#/definitions/models.ProductInfo" + } + }, + "total_amount": { + "description": "用券总成交额:使用该优惠券的订单付款总金额", + "type": "number" + }, + "total_discount": { + "description": "优惠总金额:使用该优惠券优惠的总金额", + "type": "number" + }, + "unit_price": { + "description": "用券笔单价:用券总成交额 / 使用该优惠券的付款订单数", + "type": "number" + } + } }, "models.ErpMarketingCouponDeleteReq": { "type": "object", @@ -13037,6 +13107,31 @@ } } }, + "models.GenerateSchemeReq": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "env_version": { + "description": "默认值\"release\"。要打开的小程序版本。正式版为\"release\",体验版为\"trial\",开发版为\"develop\"", + "type": "string" + }, + "path": { + "description": "跳转小程序页面", + "type": "string" + } + } + }, + "models.GenerateSchemeResp": { + "type": "object", + "properties": { + "openlink": { + "description": "小程序跳转链接", + "type": "string" + } + } + }, "models.GetErpPurchaseDemandReq": { "type": "object", "properties": { @@ -15665,6 +15760,27 @@ } } }, + "models.ProductInfo": { + "type": "object", + "properties": { + "pay_count": { + "description": "付款件数:使用该优惠券购买该商品的付款件数", + "type": "integer" + }, + "pay_customer": { + "description": "付款人数:购买该商品并使用优惠券的客户人数", + "type": "integer" + }, + "product_id": { + "description": "商品ID", + "type": "integer" + }, + "product_name": { + "description": "商品名称", + "type": "string" + } + } + }, "models.ProductInventoryAddReq": { "type": "object", "required": [ diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e0e9e9b..d83611c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -2219,6 +2219,33 @@ definitions: - erp_coupon_id type: object models.ErpMarketingCouponDataResp: + properties: + cost_effectiveness: + description: 费效比:优惠总金额 / 用券总成交额 + type: number + customer_count: + description: 用券客户数:使用该优惠券的成交客户数 + type: integer + order_count: + description: 订单数:使用该优惠券的付款订单数 + type: integer + product_count: + description: 购买商品件数:使用该优惠券购买的商品数量 + type: integer + products: + description: 商品信息:包含每个商品的ID、名称、付款件数、付款人数等信息 + items: + $ref: '#/definitions/models.ProductInfo' + type: array + total_amount: + description: 用券总成交额:使用该优惠券的订单付款总金额 + type: number + total_discount: + description: 优惠总金额:使用该优惠券优惠的总金额 + type: number + unit_price: + description: 用券笔单价:用券总成交额 / 使用该优惠券的付款订单数 + type: number type: object models.ErpMarketingCouponDeleteReq: properties: @@ -4687,6 +4714,23 @@ definitions: total_page: type: integer type: object + models.GenerateSchemeReq: + properties: + env_version: + description: 默认值"release"。要打开的小程序版本。正式版为"release",体验版为"trial",开发版为"develop" + type: string + path: + description: 跳转小程序页面 + type: string + required: + - path + type: object + models.GenerateSchemeResp: + properties: + openlink: + description: 小程序跳转链接 + type: string + type: object models.GetErpPurchaseDemandReq: properties: call_type: @@ -6574,6 +6618,21 @@ definitions: description: 总条数 type: integer type: object + models.ProductInfo: + properties: + pay_count: + description: 付款件数:使用该优惠券购买该商品的付款件数 + type: integer + pay_customer: + description: 付款人数:购买该商品并使用优惠券的客户人数 + type: integer + product_id: + description: 商品ID + type: integer + product_name: + description: 商品名称 + type: string + type: object models.ProductInventoryAddReq: properties: commodities: @@ -11713,6 +11772,27 @@ paths: summary: 启动优惠券发放 tags: - 营销管理,V1.4.4 + /api/v1/marketing/generateScheme: + post: + consumes: + - application/json + parameters: + - description: 获取小程序跳转链接模型 + in: body + name: request + required: true + schema: + $ref: '#/definitions/models.GenerateSchemeReq' + produces: + - application/json + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.GenerateSchemeResp' + summary: 获取小程序跳转链接 + tags: + - 营销管理,V1.4.4 /api/v1/menu: get: description: 获取JSON diff --git a/test/greentown_sms_test.go b/test/greentown_sms_test.go index 6a82c27..a20190b 100644 --- a/test/greentown_sms_test.go +++ b/test/greentown_sms_test.go @@ -14,5 +14,5 @@ func TestSend(t *testing.T) { } func TestGtSendMessage(t *testing.T) { - models.GtSendMessage([]string{"15019230751"}, "【go2ns】绿城短信推送测试,测试") + models.GtSendMessage([]string{"18872141360", "15019230751"}, "【go2ns】圣诞活动已开启,优惠券已发放到您的账户。详情点击:https://dev.admin.deovo.com/visit.html") } diff --git a/tools/config/config.go b/tools/config/config.go index 7be2bde..478337b 100644 --- a/tools/config/config.go +++ b/tools/config/config.go @@ -31,6 +31,8 @@ var cfgGen *viper.Viper // 导出文件配置项 非必须 var cfgExport *viper.Viper +var cfgMessage *viper.Viper + // Setup 载入配置文件 func Setup(path string) { viper.SetConfigFile(path) @@ -90,4 +92,10 @@ func Setup(path string) { panic("No found settings.export") } ExportConfig = InitExport(cfgExport) + + cfgMessage = viper.Sub("settings.message") + if cfgMessage == nil { + panic("No found settings.message") + } + MessageConfig = InitMessage(cfgMessage) } diff --git a/tools/config/message.go b/tools/config/message.go new file mode 100644 index 0000000..4921882 --- /dev/null +++ b/tools/config/message.go @@ -0,0 +1,17 @@ +package config + +import "github.com/spf13/viper" + +type Message struct { + MiniProgramState string + SmsUrl string +} + +func InitMessage(cfg *viper.Viper) *Message { + return &Message{ + MiniProgramState: cfg.GetString("mini_program_state"), + SmsUrl: cfg.GetString("sms_url"), + } +} + +var MessageConfig = new(Message)