1、配置文件增加消息订阅环境和短信url配置项;

2、coupon和user_coupon表增加limit字段;
3、零售开单时,如果使用优惠券,则同步更新优惠券对应的数据;
4、增加优惠券数据接口;
5、增加获取小程序跳转链接接口;
6、增加保定金自动审核的定时任务,每天早上9点30分退前一天的保证金,最多10条;
This commit is contained in:
chenlin 2024-12-05 18:47:16 +08:00
parent 40227538f9
commit cafc26e5bd
17 changed files with 941 additions and 34 deletions

View File

@ -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")
}

View File

@ -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 {

View File

@ -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:"-"` //
}

View File

@ -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

View File

@ -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
}

View File

@ -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
}

View File

@ -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) // 获取小程序跳转链接
}

View File

@ -58,6 +58,9 @@ func examplesNoCheckRoleRouter(r *gin.Engine) {
registerCooperativeManageUnAuthRouter(v1)
// 回收卡
registerRecycleCardManageUnAuthRouter(v1)
// 营销管理
registerMarketingManageUnAuthRouter(v1)
}
// 需要认证的路由示例

View File

@ -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()
}()

View File

@ -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))
}

View File

@ -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 # 短信调整微信小程序的链接地址,需要区分环境

View File

@ -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": [

View File

@ -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": [

View File

@ -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

View File

@ -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")
}

View File

@ -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)
}

17
tools/config/message.go Normal file
View File

@ -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)