1054 lines
33 KiB
Go
1054 lines
33 KiB
Go
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-仅限原价购买时使用
|
||
SmsContent string `json:"sms_content" validate:"required"` // 短信提示内容
|
||
}
|
||
|
||
// 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-仅限原价购买时使用
|
||
SmsContent string `json:"sms_content" validate:"required"` // 短信提示内容
|
||
}
|
||
|
||
// ErpMarketingCouponDetailReq 优惠券详情入参
|
||
type ErpMarketingCouponDetailReq struct {
|
||
ErpCouponId uint32 `json:"erp_coupon_id" validate:"required"` // 优惠券id
|
||
}
|
||
|
||
// 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
|
||
}
|
||
|
||
for i, v := range couponList {
|
||
couponList[i].Amount = v.Amount / 100
|
||
}
|
||
|
||
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,
|
||
SmsContent: req.SmsContent,
|
||
}
|
||
|
||
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,
|
||
ActiveStart: Now(),
|
||
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
|
||
erpCoupon.SmsContent = req.SmsContent
|
||
|
||
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
|
||
coupon.ActiveStart = Now()
|
||
coupon.ActiveEnd = Now().AddDate(0, 0, int(req.ActiveDate))
|
||
|
||
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
|
||
// 获取尊享会员信息
|
||
var privilegeUserList []PrivilegeMember
|
||
|
||
switch userType {
|
||
case 2: // 2-未付费用户:MemberLevel 不能是 (2, 3, 4, 5),不包含已过期的租卡用户
|
||
err = orm.Eloquent.Model(&UserInfo{}).Where("id > ? and member_level not in ? and member_expire = ?",
|
||
lastUserID, []uint32{MemberLevelGold, MemberLevelPeriod, MemberLevelPlatinum, MemberLevelBlackGold},
|
||
"0000-00-00 00:00:00").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 4: // 尊享会员
|
||
err = orm.Eloquent.Model(&PrivilegeMember{}).Where("id > ? and member_level = ?",
|
||
lastUserID, MemberLevelPrivilege).Order("id asc").Limit(100).Find(&privilegeUserList).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
for _, privilegeUser := range privilegeUserList {
|
||
user, _ := GetUserInfoByUid(privilegeUser.Uid)
|
||
if user.ID == 0 { // 没查到数据
|
||
user.Uid = privilegeUser.Uid
|
||
user.Tel = privilegeUser.Tel
|
||
}
|
||
|
||
user.ID = privilegeUser.ID
|
||
user.MemberLevel = MemberLevelPrivilege
|
||
users = append(users, user)
|
||
}
|
||
|
||
case 5: // 测试用户
|
||
if lastUserID == 0 {
|
||
users, err = GetTestUserConfig()
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
}
|
||
|
||
case 1: // 1-所有人:不限制会员等级,所有人均可领取
|
||
fallthrough
|
||
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 {
|
||
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
|
||
}
|
||
|
||
if IsValidPrivilegeMember(user.Uid) { // 如果是尊享会员
|
||
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
|
||
}
|
||
|
||
if IsValidPrivilegeMember(user.Uid) { // 如果是尊享会员
|
||
return true
|
||
}
|
||
|
||
return false
|
||
case 4: // 尊享会员
|
||
if user.MemberLevel == MemberLevelPrivilege {
|
||
return true
|
||
}
|
||
|
||
if IsValidPrivilegeMember(user.Uid) { // 如果是尊享会员
|
||
return true
|
||
}
|
||
|
||
return false
|
||
case 5: // 测试用户
|
||
if user.MemberLevel == MemberLevelTest {
|
||
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 {
|
||
duration := coupon.ActiveEnd.Sub(coupon.ActiveStart).Hours() / 24
|
||
couponCode, _ := utils.GenerateRandomNumber19()
|
||
userCoupon := &UserCoupon{
|
||
Uid: user.Uid,
|
||
CouponId: coupon.ID,
|
||
CouponType: coupon.CouponType,
|
||
Value: coupon.Value,
|
||
State: 1,
|
||
ActiveStart: time.Now(),
|
||
ActiveEnd: time.Now().Add(time.Duration(duration) * 24 * time.Hour),
|
||
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
|
||
}
|