mh_goadmin_server/app/admin/models/marketing.go

994 lines
32 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 models
import (
"bytes"
"encoding/json"
"errors"
"fmt"
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"
)
const (
ErpCouponUnStarted = 1 // 未开始
ErpCouponSending = 2 // 发放中
ErpCouponInProgress = 3 // 进行中
ErpCouponFinished = 4 // 已结束
ErpCouponSendFailed = 5 // 发放失败
TaskInProgress = "in_progress" // 执行中
TaskCompleted = "completed" // 已完成
TaskFailed = "failed" // 发送失败
AccessTokenUrl = "https://api.weixin.qq.com/cgi-bin/token"
WXAppID = "wx806c079463b5b56c"
WXSecret = "cb125688bf4e482f66e8c46062d568fc"
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
type WxAccessToken struct {
Model
Appid string `json:"appid"` // 小程序appid
AccessToken string `json:"access_token"` // 获取到的凭证
ExpiresTime time.Time `json:"expires_time"` // 凭证有效时间
}
// CouponIssuanceTask 优惠券发放任务表
type CouponIssuanceTask struct {
Model
ErpCouponId uint32 `json:"erp_coupon_id"` // 优惠券id
ErpCouponName string `json:"erp_coupon_name"` // 优惠券名称
Status string `json:"status"` // 任务状态in_progress(执行中、completed已完成、failed发送失败
LastUserID uint32 `json:"last_user_id"` // 上次处理的用户ID
ErrorMessage string `json:"error_message" gorm:"type:text"` // 错误信息,若任务失败时记录
}
// ErpCoupon 营销管理-优惠券
type ErpCoupon struct {
Model
Name string `json:"name" gorm:"index"` // 优惠券名称
Remark string `json:"remark"` // 备注
Describe string `json:"describe" gorm:"type:text"` // 优惠券简介
Rule string `json:"rule" gorm:"type:text"` // 优惠券使用规则
CategoryNumber string `json:"category_number"` // 可以使用该优惠券的商品分类,如果为空则表示没限制
ActiveDate uint32 `json:"active_date"` // 有效期(天)
Amount uint32 `json:"amount"` // 金额(元)
UserType uint32 `json:"user_type"` // 领取人限制1-所有人 2-未付费用户 3-已付费用户 4-尊享会员
Limit uint32 `json:"limit"` // 优惠券叠加限制 0-不限制1-仅限原价购买时使用
StartTime *time.Time `json:"start_time"` // 启动时间
State uint32 `json:"state" gorm:"index"` // 当前状态 1-未开始2-发放中3-进行中4-已结束5-发放失败
SendCount uint32 `json:"send_count"` // 已发放
UsedCount uint32 `json:"used_count"` // 已使用
TotalPayAmount float64 `json:"total_pay_amount"` // 支付金额(元)
PerCustomerAmount float64 `json:"per_customer_amount"` // 客单价(元)
SmsContent string `json:"sms_content" gorm:"type:text"` // 短信提示内容
}
// ErpMarketingCouponListReq 优惠券列表入参
type ErpMarketingCouponListReq struct {
Name string `json:"name" gorm:"index"` // 优惠券名称
State uint32 `json:"state" gorm:"index"` // 当前状态 1-未开始2-发放中3-进行中4-已结束
CreatedTimeStart string `json:"created_time_start"` // 创建开始时间
CreatedTimeEnd string `json:"created_time_end"` // 创建结束时间
PageIndex int `json:"pageIndex"` // 页码
PageSize int `json:"pageSize"` // 页面条数
IsExport uint32 `json:"is_export"` // 1-导出
}
// ErpMarketingCouponListResp 优惠券列表出参
type ErpMarketingCouponListResp struct {
List []ErpCoupon `json:"list"`
Total int `json:"total"` // 总条数
PageIndex int `json:"pageIndex"` // 页码
PageSize int `json:"pageSize"` // 页面条数
ExportUrl string `json:"export_url"` // 导出excel路径
}
// ErpMarketingCouponCreateReq 新增优惠券入参
type ErpMarketingCouponCreateReq struct {
Name string `json:"name" validate:"required"` // 优惠券名称
Remark string `json:"remark"` // 备注
Describe string `json:"describe" gorm:"type:text"` // 优惠券简介
Rule string `json:"rule" gorm:"type:text"` // 优惠券使用规则
CategoryNumber string `json:"category_number" validate:"required"` // 可以使用该优惠券的商品分类编号,如果为空则表示没限制
ActiveDate uint32 `json:"active_date" validate:"required"` // 有效期(天)
Amount uint32 `json:"amount" validate:"required"` // 金额(元)
UserType uint32 `json:"user_type" validate:"required"` // 领取人限制1-所有人 2-未付费用户 3-已付费用户 4-尊享会员
Limit uint32 `json:"limit"` // 优惠券叠加限制 0-不限制1-仅限原价购买时使用
}
// ErpMarketingCouponEditReq 编辑优惠券入参
type ErpMarketingCouponEditReq struct {
ErpCouponId uint32 `json:"erp_coupon_id" validate:"required"` // 优惠券id
Name string `json:"name" validate:"required"` // 优惠券名称
Remark string `json:"remark"` // 名称备注
Describe string `json:"describe" gorm:"type:text"` // 优惠券简介
Rule string `json:"rule" gorm:"type:text"` // 优惠券使用规则
CategoryNumber string `json:"category_number" validate:"required"` // 可以使用该优惠券的商品分类,如果为空则表示没限制
ActiveDate uint32 `json:"active_date" validate:"required"` // 有效期(天)
Amount uint32 `json:"amount" validate:"required"` // 金额(元)
UserType uint32 `json:"user_type" validate:"required"` // 领取人限制1-所有人 2-未付费用户 3-已付费用户 4-尊享会员
Limit uint32 `json:"limit"` // 优惠券叠加限制 0-不限制1-仅限原价购买时使用
}
// ErpMarketingCouponDeleteReq 删除优惠券入参
type ErpMarketingCouponDeleteReq struct {
ErpCouponId uint32 `json:"erp_coupon_id" validate:"required"` // 优惠券id
}
// ErpMarketingCouponStartReq 启动优惠券发放
type ErpMarketingCouponStartReq struct {
ErpCouponId []uint32 `json:"erp_coupon_id" validate:"required"` // 优惠券id
Remark string `json:"remark" validate:"required"` // 活动名称备注
SmsContent string `json:"sms_content" validate:"required"` // 短信提示内容
}
// ErpMarketingCouponDataReq 优惠券数据入参
type ErpMarketingCouponDataReq struct {
ErpCouponId uint32 `json:"erp_coupon_id" validate:"required"` // 优惠券id
}
// 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 查询优惠券列表
func (m *ErpMarketingCouponListReq) List() (*ErpMarketingCouponListResp, error) {
resp := &ErpMarketingCouponListResp{
PageIndex: m.PageIndex,
PageSize: m.PageSize,
}
page := m.PageIndex - 1
if page < 0 {
page = 0
}
if m.PageSize == 0 {
m.PageSize = 10
}
qs := orm.Eloquent.Table("erp_coupon")
if m.Name != "" {
qs = qs.Where("name = ?", m.Name)
}
if m.State != 0 {
qs = qs.Where("state = ?", m.State)
}
if m.CreatedTimeStart != "" {
parse, err := time.Parse(QueryTimeFormat, m.CreatedTimeStart)
if err != nil {
logger.Errorf("erpPurchaseOrderList err:", err)
return nil, err
}
qs = qs.Where("created_at > ?", parse)
}
if m.CreatedTimeEnd != "" {
parse, err := time.Parse(QueryTimeFormat, m.CreatedTimeEnd)
if err != nil {
logger.Errorf("erpPurchaseOrderList err:", err)
return nil, err
}
qs = qs.Where("created_at < ?", parse)
}
var count int64
err := qs.Count(&count).Error
if err != nil {
logger.Error("count err:", logger.Field("err", err))
return resp, err
}
resp.Total = int(count)
var couponList []ErpCoupon
err = qs.Order("id DESC").Offset(page * m.PageSize).Limit(m.PageSize).Find(&couponList).Error
if err != nil && err != RecordNotFound {
logger.Error("erp_coupon list err:", logger.Field("err", err))
return resp, err
}
resp.List = couponList
return resp, nil
}
// CreateErpMarketingCoupon 新增优惠券
func CreateErpMarketingCoupon(req *ErpMarketingCouponCreateReq) error {
erpCoupon := &ErpCoupon{
Name: req.Name,
Remark: req.Remark,
Describe: req.Describe,
Rule: req.Rule,
CategoryNumber: req.CategoryNumber,
ActiveDate: req.ActiveDate,
Amount: req.Amount * 100,
UserType: req.UserType,
Limit: req.Limit,
State: ErpCouponUnStarted,
}
err := orm.Eloquent.Create(erpCoupon).Error
if err != nil {
logger.Error("create purchase order err:", logger.Field("err", err))
return err
}
coupon := &Coupon{
Name: req.Name,
Describe: req.Describe,
Rule: req.Rule,
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
if err != nil {
logger.Error("create coupon order err:", logger.Field("err", err))
return err
}
return nil
}
// EditErpMarketingCoupon 编辑优惠券
func EditErpMarketingCoupon(req *ErpMarketingCouponEditReq) error {
// 查询订单信息
var erpCoupon ErpCoupon
err := orm.Eloquent.Table("erp_coupon").Where("id=?", req.ErpCouponId).Find(&erpCoupon).Error
if err != nil {
logger.Error("query erp_coupon err:", logger.Field("err", err))
return err
}
if erpCoupon.ID == 0 {
logger.Error("delete err, erpCoupon ID is:", logger.Field("erpCoupon.ID", req.ErpCouponId))
return errors.New(fmt.Sprintf("编辑失败:未查询到优惠券id[%d]", req.ErpCouponId))
}
// 1-更新优惠券信息
erpCoupon.Name = req.Name
erpCoupon.Remark = req.Remark
erpCoupon.Rule = req.Rule
erpCoupon.Describe = req.Describe
erpCoupon.CategoryNumber = req.CategoryNumber
erpCoupon.ActiveDate = req.ActiveDate
erpCoupon.Amount = req.Amount * 100
erpCoupon.UserType = req.UserType
erpCoupon.Limit = req.Limit
begin := orm.Eloquent.Begin()
err = begin.Model(&ErpCoupon{}).Where("id = ?", req.ErpCouponId).
Omit("created_at").Save(erpCoupon).Error
if err != nil {
begin.Rollback()
logger.Error("update erp_coupon err:", logger.Field("err", err))
return err
}
// 查询订单信息
var coupon Coupon
err = orm.Eloquent.Table("coupon").Where("erp_coupon_id=?", req.ErpCouponId).Find(&coupon).Error
if err != nil {
begin.Rollback()
logger.Error("query coupon err:", logger.Field("err", err))
return err
}
coupon.Name = req.Name
coupon.Describe = req.Describe
coupon.Rule = req.Rule
coupon.Value = req.Amount * 100
err = begin.Model(&Coupon{}).Where("erp_coupon_id = ?", req.ErpCouponId).
Omit("created_at").Save(coupon).Error
if err != nil {
begin.Rollback()
logger.Error("update coupon err:", logger.Field("err", err))
return err
}
err = begin.Commit().Error
if err != nil {
begin.Rollback()
logger.Error("commit update erp_coupon err:", logger.Field("err", err))
return err
}
return nil
}
// DeleteErpMarketingCoupon 删除优惠券
func DeleteErpMarketingCoupon(req *ErpMarketingCouponDeleteReq) error {
// 查询订单信息
var erpCoupon ErpCoupon
err := orm.Eloquent.Table("erp_coupon").Where("id=?", req.ErpCouponId).Find(&erpCoupon).Error
if err != nil {
logger.Error("purchase order err:", logger.Field("err", err))
return err
}
if erpCoupon.ID == 0 {
logger.Error("delete err, erpCoupon ID is:", logger.Field("erpCoupon.ID", req.ErpCouponId))
return errors.New(fmt.Sprintf("删除失败:未查询到优惠券id[%d]", req.ErpCouponId))
}
// 仅未开始和已结束的订单可删除
if erpCoupon.State != ErpCouponUnStarted && erpCoupon.State != ErpCouponFinished {
logger.Error("delete err, erpCoupon.State is:", logger.Field("erpCoupon.State", erpCoupon.State))
return errors.New("删除失败:仅未开始和已结束的优惠券可删除")
}
// 删除优惠订单
begin := orm.Eloquent.Begin()
err = begin.Delete(erpCoupon).Error
if err != nil {
begin.Rollback()
logger.Error("erp_coupon delete err:", logger.Field("err", err))
return err
}
err = begin.Table("coupon").Where("erp_coupon_id", req.ErpCouponId).Delete(&Coupon{}).Error
if err != nil {
begin.Rollback()
logger.Error("coupon delete err:", logger.Field("err", err))
return err
}
err = begin.Commit().Error
if err != nil {
begin.Rollback()
logger.Error("commit erp_coupon delete err:", logger.Field("err", err))
return err
}
return nil
}
// StartCouponIssuanceTask 启动发放优惠券的任务
func StartCouponIssuanceTask(req *ErpMarketingCouponStartReq, taskList []CouponIssuanceTask) error {
var erpCouponList []ErpCoupon
err := orm.Eloquent.Table("erp_coupon").Where("id in ?", req.ErpCouponId).Find(&erpCouponList).Error
if err != nil {
logger.Error("purchase order err:", logger.Field("err", err))
return err
}
if len(taskList) < len(erpCouponList) {
for _, erpCoupon := range erpCouponList {
exitFlag := false
for _, task := range taskList {
if erpCoupon.ID == task.ErpCouponId {
exitFlag = true
break
} else {
continue
}
}
if !exitFlag {
// 新建任务
taskInfo := CouponIssuanceTask{
ErpCouponId: erpCoupon.ID,
ErpCouponName: erpCoupon.Name,
Status: TaskInProgress,
LastUserID: 0,
ErrorMessage: "",
}
err := orm.Eloquent.Create(&taskInfo).Error
if err != nil {
logger.Error("create CouponIssuanceTask err:", logger.Field("err", err))
return err
}
taskList = append(taskList, taskInfo)
}
}
}
// 遍历taskList获取最小的lastUserID
var lastUserID uint32
for _, task := range taskList {
if lastUserID == 0 {
lastUserID = task.LastUserID
}
if lastUserID > task.LastUserID {
lastUserID = task.LastUserID
}
}
var nTotalCount int
// 处理任务从上次中断的用户ID开始
for {
// 查询用户ID大于 lastUserID 的用户
users, err := getUsersAfterID(lastUserID, erpCouponList[0].UserType)
if err != nil || len(users) == 0 {
log.Println("没有更多用户,任务结束")
break
}
// 发放优惠券
count, err := issueCouponsToUsers(users, req.ErpCouponId)
if err != nil {
log.Printf("发放优惠券失败: %v", err)
return err
}
// 更新任务进度记录处理到的最后一个用户ID
lastUserID = users[len(users)-1].ID
nTotalCount += count
//err = updateTaskProgress(req.ErpCouponId, lastUserID, err.Error())
//if err != nil {
// log.Printf("更新任务进度失败: %v", err)
// break
//}
}
// 任务完成,更新状态
err = markTaskAsCompleted(req.ErpCouponId, nTotalCount)
if err != nil {
return nil
}
return nil
}
// CheckUserType 校验批量选择的优惠券使用人群是否相同
func CheckUserType(erpCouponId []uint32) bool {
var erpCouponList []ErpCoupon
err := orm.Eloquent.Table("erp_coupon").Where("id in ?", erpCouponId).Find(&erpCouponList).Error
if err != nil || err == RecordNotFound {
logger.Error("query erp_coupon err:", logger.Field("err", err))
return false
}
var userType uint32
for _, erpCoupon := range erpCouponList {
if userType == 0 {
userType = erpCoupon.UserType
}
if userType != erpCoupon.UserType {
return false
}
}
return true
}
// GetTaskProgress 查询优惠券任务执行情况
func GetTaskProgress(erpCouponId []uint32) ([]CouponIssuanceTask, error) {
var task []CouponIssuanceTask
err := orm.Eloquent.Table("coupon_issuance_task").Where("erp_coupon_id in ?", erpCouponId).
Order("updated_at desc").First(&task).Error
if err != nil && err.Error() != "record not found" {
return nil, err
}
return task, nil
}
func getUsersAfterID(lastUserID, userType uint32) ([]UserInfo, error) {
var err error
var users []UserInfo
switch userType {
case 2: // 2-未付费用户MemberLevel 不能是 (2, 3, 4, 5)
err = orm.Eloquent.Model(&UserInfo{}).Where("id > ? and member_level not in ?", lastUserID, []uint32{
MemberLevelGold, MemberLevelPeriod, MemberLevelPlatinum, MemberLevelBlackGold}).
Order("id asc").Limit(100).Find(&users).Error
case 3:
// 3-已付费用户MemberLevel 必须是 (2, 3, 4, 5)
err = orm.Eloquent.Model(&UserInfo{}).Where("id > ? and member_level in ?", lastUserID, []uint32{
MemberLevelGold, MemberLevelPeriod, MemberLevelPlatinum, MemberLevelBlackGold}).
Order("id asc").Limit(100).Find(&users).Error
case 1: // 1-所有人:不限制会员等级,所有人均可领取
default:
err = orm.Eloquent.Model(&UserInfo{}).Where("id > ?", lastUserID).
Order("id asc").Limit(100).Find(&users).Error
}
if err != nil {
return nil, err
}
return users, nil
}
func updateTaskProgress(erpCouponId []uint32, lastUserID uint32, errMsg string) error {
err := orm.Eloquent.Table("coupon_issuance_task").Where("erp_coupon_id in ?", erpCouponId).
Updates(map[string]interface{}{
"last_user_id": lastUserID,
"error_message": errMsg,
"updated_at": time.Now(),
}).Error
if err != nil {
return err
}
return nil
}
func markTaskAsCompleted(erpCouponId []uint32, nTotalCount int) error {
err := orm.Eloquent.Table("coupon_issuance_task").Where("erp_coupon_id in ?", erpCouponId).
Updates(map[string]interface{}{
"Status": TaskCompleted,
"updated_at": time.Now(),
}).Error
if err != nil {
log.Printf("更新任务完成状态失败: %v", err)
return err
}
err = orm.Eloquent.Table("erp_coupon").Where("id in ?", erpCouponId).
Updates(map[string]interface{}{
"state": ErpCouponInProgress,
"send_count": nTotalCount,
"updated_at": time.Now(),
}).Error
if err != nil {
log.Printf("更新任务完成状态失败: %v", err)
return err
}
log.Println("任务完成")
return nil
}
// 校验用户是否符合领取优惠券的条件
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-所有人:不限制会员等级,所有人均可领取
return true
case 2:
// 2-未付费用户MemberLevel 不能是 (2, 3, 4, 5)
if user.MemberLevel == MemberLevelGold ||
user.MemberLevel == MemberLevelPeriod ||
user.MemberLevel == MemberLevelPlatinum ||
user.MemberLevel == MemberLevelBlackGold {
return false
}
return true
case 3:
// 3-已付费用户MemberLevel 必须是 (2, 3, 4, 5)
if user.MemberLevel == MemberLevelGold ||
user.MemberLevel == MemberLevelPeriod ||
user.MemberLevel == MemberLevelPlatinum ||
user.MemberLevel == MemberLevelBlackGold {
return true
}
return false
default:
// 未知类型,不发放
return false
}
}
func issueCouponsToUsers(users []UserInfo, erpCouponId []uint32) (int, error) {
var erpCoupon []ErpCoupon
err := orm.Eloquent.Table("erp_coupon").Where("id in ?", erpCouponId).Find(&erpCoupon).Error
if err != nil || err == RecordNotFound {
logger.Error("query erp_coupon err:", logger.Field("err", err))
return 0, err
}
var nSendCount int
for _, user := range users {
// 根据条件发放优惠券
if isEligibleForCoupon(user, erpCoupon[0]) {
// 发放优惠券
var coupons []Coupon
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 0, err
}
for _, coupon := range coupons {
couponCode, _ := utils.GenerateRandomNumber19()
userCoupon := &UserCoupon{
Uid: user.Uid,
CouponId: coupon.ID,
CouponType: coupon.CouponType,
Value: coupon.Value,
State: 1,
ActiveStart: time.Now(),
ActiveEnd: coupon.ActiveEnd,
RedeemCode: "",
CategoryNumber: coupon.CategoryNumber,
Code: couponCode,
ActivityType: coupon.ActivityType,
Limit: coupon.Limit,
}
err = orm.Eloquent.Create(userCoupon).Error
if err != nil {
logger.Error("create user coupon err:", logger.Field("err", err))
err = updateTaskProgress(erpCouponId, user.ID, err.Error())
if err != nil {
log.Printf("更新任务进度失败: %v", err)
}
return 0, err
}
}
// 发送订阅通知
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))
}
}
toUserOpenid := user.WxOpenID
templateId := TemplateId
data := map[string]interface{}{
"thing1": map[string]interface{}{"value": coupons[0].Name},
"thing5": map[string]interface{}{"value": coupons[0].Describe},
"date3": map[string]interface{}{"value": coupons[0].ActiveEnd.Format("2006年01月02日")},
"thing4": map[string]interface{}{"value": coupons[0].Rule},
}
//developer为开发版trial为体验版formal为正式版默认为正式版
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}, ComposeSMSContent(erpCoupon[0].SmsContent))
if err != nil {
logger.Error(err.Error())
}
}
nSendCount++
}
}
return nSendCount, 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"`
Errmsg string `json:"errmsg"`
}
// AccessTokenResponse 返回给用户的数据
type AccessTokenResponse struct {
AccessToken string `json:"access_token"` // 获取到的凭证
ExpiresIn int64 `json:"expires_in"` // 凭证有效时间单位秒。目前是7200秒之内的值。
}
type accessTokenResponse struct {
Response
AccessTokenResponse
}
// CheckAccessToken 定期更新access_token
func CheckAccessToken() {
accessToken, err := GetWxAccessToken()
if err != nil {
logger.Error("query wx_access_token err:", logger.Field("err", err))
} else {
now := time.Now()
twoHoursLater := now.Add(2 * time.Hour)
// 查询access_token
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 { // 更新
// 更新access_token
err = orm.Eloquent.Table("wx_access_token").Where("appid = ?", WXAppID).
Updates(map[string]interface{}{
"access_token": accessToken.AccessToken,
"expires_time": twoHoursLater,
}).Error
if err != nil {
logger.Errorf("更新wx_access_token失败: %v", err)
}
} else { // 插入
tempToken := &WxAccessToken{
Appid: WXAppID,
AccessToken: accessToken.AccessToken,
ExpiresTime: twoHoursLater,
}
err = orm.Eloquent.Create(tempToken).Error
if err != nil {
logger.Error("create wx_access_token err:", logger.Field("err", err))
}
}
}
}
// GetWxAccessToken 获取小程序的access_token
func GetWxAccessToken() (lures AccessTokenResponse, err error) {
api, err := code2url(WXAppID, WXSecret)
if err != nil {
return
}
res, err := http.Get(api)
if err != nil {
return
}
defer res.Body.Close()
if res.StatusCode != 200 {
err = errors.New("微信服务器发生错误")
return
}
var data accessTokenResponse
err = json.NewDecoder(res.Body).Decode(&data)
if err != nil {
return
}
if data.Errcode != 0 {
err = errors.New(data.Errmsg)
return
}
lures = data.AccessTokenResponse
return
}
// 拼接 获取 session_key 的 URL
func code2url(appID, secret string) (string, error) {
url, err := url.Parse(AccessTokenUrl)
if err != nil {
return "", err
}
query := url.Query()
query.Set("appid", appID)
query.Set("secret", secret)
query.Set("grant_type", "client_credential")
url.RawQuery = query.Encode()
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) (
sendMessageRsp *Response, err error) {
url := fmt.Sprintf("%s%s", WxSubscribeMessage, accessToken)
body := SendMessageReq{
TemplateId: templateId,
Page: page,
Touser: toUserOpenid,
Data: data,
MiniprogramState: miniProgramState,
Lang: "zh_CN",
}
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) {
client := http.Client{}
reqBodyBytes, err := json.Marshal(reqBody)
if err != nil {
return nil, err
}
reqBodyReader := bytes.NewReader(reqBodyBytes)
req, err := http.NewRequest(method, url, reqBodyReader)
if err != nil {
return nil, err
}
rsp, err := client.Do(req)
if err != nil {
return nil, err
}
defer rsp.Body.Close()
if rsp.StatusCode != 200 {
return nil, errors.New(fmt.Sprintf("response Status err:%d", rsp.StatusCode))
}
rspBody, err := ioutil.ReadAll(rsp.Body)
if err != nil {
return nil, err
}
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
}