2268 lines
85 KiB
Go
2268 lines
85 KiB
Go
package models
|
||
|
||
import (
|
||
"bytes"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"github.com/go-admin-team/go-admin-core/logger"
|
||
"github.com/xuri/excelize/v2"
|
||
"go-admin/common/database"
|
||
"go-admin/tools"
|
||
"gorm.io/gorm"
|
||
"io"
|
||
"log"
|
||
"math/rand"
|
||
"net/http"
|
||
"net/url"
|
||
"strconv"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
const (
|
||
MiGUSendCaptchaUrl = "https://mg.zeqinkeji.cn/coupon-provider/captcha/request"
|
||
MiGUSubmitCaptchaUrl = "https://mg.zeqinkeji.cn/coupon-provider/captcha/submit"
|
||
MiGUCheckOrderUrl = "https://mg.zeqinkeji.cn/coupon-provider/api/orders/exchange-type/check"
|
||
MiGUQueryRightsInfoUrl = "https://betagame.migufun.com/member/shareRights/v1.1.0.7/queryRightsInfo"
|
||
|
||
ProductID = 1
|
||
ChannelCode = "40458652536"
|
||
|
||
SM4KEy = "ve3N1I75AJ0Oy6nA"
|
||
|
||
SubscribeOK = 1 // 订阅成功
|
||
UnsubscribeOK = 2 // 退订
|
||
|
||
SKUCODE = "miguyouxizuanshihuiyuan-yy"
|
||
|
||
ExportFile = "/www/server/images/export/"
|
||
MiGuExportUrl = "https://migu.admin.deovo.com/load/export/"
|
||
//MiGuExportUrl = "/Users/max/Documents/" // 本地环境
|
||
)
|
||
|
||
// 以下是数据库表结构
|
||
|
||
// MgProduct 产品管理表对应的结构体
|
||
type MgProduct struct {
|
||
Model
|
||
Name string `gorm:"size:255;not null" json:"name"` // 产品名称
|
||
UniqueCode string `gorm:"size:255;not null" json:"unique_code"` // 产品唯一标识编码
|
||
SkuName string `gorm:"size:255" json:"sku_name"` // SKU名称
|
||
BillingPointID int64 `json:"billing_point_id"` // 计费点ID
|
||
ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码
|
||
ProductApiID string `gorm:"size:255" json:"product_api_id"` // 产品ID(接口用)
|
||
ChannelApiID string `gorm:"size:255" json:"channel_api_id"` // 渠道ID(接口用)
|
||
OfficialPage string `gorm:"size:255" json:"official_page"` // 官方落地页
|
||
}
|
||
|
||
// MgOrder 订单管理表对应的结构体
|
||
type MgOrder struct {
|
||
Model
|
||
ProductID int64 `json:"product_id"` // 产品ID
|
||
ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码
|
||
OrderSerial string `gorm:"size:255;not null" json:"order_serial"` // 订单流水号
|
||
SubscribeTime *time.Time `json:"subscribe_time"` // 订阅时间
|
||
PhoneNumber string `gorm:"size:20;not null" json:"phone_number"` // 手机号
|
||
SM4PhoneNumber string `gorm:"size:255" json:"sm4_phone_number"` // SM4加密手机号
|
||
ExternalOrderID string `gorm:"size:255" json:"external_order_id"` // 外部平台订单号(如咪咕等)
|
||
ChannelTradeNo string `gorm:"size:255" json:"channel_trade_no"` // 渠道订单号
|
||
State int `gorm:"size:255" json:"state"` // 用户订阅状态 1-订阅成功 2-已取消订阅
|
||
UnsubscribeTime *time.Time `json:"unsubscribe_time"` // 取消订阅时间
|
||
IsOneHourCancel int `json:"is_one_hour_cancel"` // 是否1小时内退订 1-是 其他-否
|
||
}
|
||
|
||
type MgOrderCopy struct {
|
||
Model
|
||
ProductID int64 `json:"product_id"` // 产品ID
|
||
ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码
|
||
OrderSerial string `gorm:"size:255;not null" json:"order_serial"` // 订单流水号
|
||
SubscribeTime *time.Time `json:"subscribe_time"` // 订阅时间
|
||
PhoneNumber string `gorm:"size:20;not null" json:"phone_number"` // 手机号
|
||
SM4PhoneNumber string `gorm:"size:255" json:"sm4_phone_number"` // SM4加密手机号
|
||
ExternalOrderID string `gorm:"size:255" json:"external_order_id"` // 外部平台订单号(如咪咕等)
|
||
ChannelTradeNo string `gorm:"size:255" json:"channel_trade_no"` // 渠道订单号
|
||
State int `gorm:"size:255" json:"state"` // 用户订阅状态 1-订阅成功 2-已取消订阅
|
||
UnsubscribeTime *time.Time `json:"unsubscribe_time"` // 取消订阅时间
|
||
IsOneHourCancel int `json:"is_one_hour_cancel"` // 是否1小时内退订 1-是 其他-否
|
||
}
|
||
|
||
// MgChannel 渠道列表
|
||
type MgChannel struct {
|
||
Model
|
||
ProductID int `json:"product_id"` // 产品ID
|
||
MainChannelCode string `json:"main_channel_code"` // 主渠道编码
|
||
SubChannelCode string `json:"sub_channel_code"` // 子渠道编码
|
||
SubscribeURL string `json:"subscribe_url"` // 订购成功通知接口url
|
||
UnsubscribeURL string `json:"unsubscribe_url"` // 退订通知接口url
|
||
Status int `json:"status"` // 状态 (0: 停用, 1: 启用)
|
||
Remarks string `json:"remarks"` // 备注
|
||
}
|
||
|
||
// HourSummaryListResp 历史汇总(按小时)-出参
|
||
type HourSummaryListResp struct {
|
||
List []MgHourSummary `json:"list"` // 列表数据
|
||
Count int `json:"count"` // 数据总数
|
||
PageSize int `json:"page_size"` // 每页条数
|
||
PageNum int `json:"page_num"` // 当前页数
|
||
SummaryData *TotalHourSummary `json:"summary_data"` // 汇总数据,单条数据时返回
|
||
}
|
||
|
||
// TotalHourSummary 历史汇总查询表汇总数据对应的结构体(按小时)
|
||
type TotalHourSummary struct {
|
||
SubmissionCount int `json:"submission_count"` // 提交数
|
||
NewUserCount int `json:"new_user_count"` // 新用户数
|
||
SubmissionSuccessRate string `json:"submission_success_rate"` // 提交成功率
|
||
NewUserUnsubWithinHour int `json:"new_user_unsub_within_hour"` // 当日新用户退订数(1小时以内)
|
||
NewUserUnsubWithinHourRate string `json:"new_user_unsub_within_hour_rate"` // 当日新用户退订率(1小时以内)
|
||
NewUserUnsubOnDay int `json:"new_user_unsub_on_day"` // 当日新用户退订数
|
||
NewUserUnsubOnDayRate string `json:"new_user_unsub_on_day_rate"` // 当日新用户退订率
|
||
TotalNewUserUnsub int `json:"total_new_user_unsub"` // 累计新用户退订数
|
||
TotalNewUserUnsubRate string `json:"total_new_user_unsub_rate"` // 累计新用户退订率
|
||
}
|
||
|
||
// MgHourSummary 历史汇总查询表对应的结构体(按小时)
|
||
type MgHourSummary struct {
|
||
Hour string `json:"hour"` // 日期
|
||
ProductID int64 `json:"product_id"` // 产品ID
|
||
ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码
|
||
SubmissionCount int `json:"submission_count"` // 提交数
|
||
NewUserCount int `json:"new_user_count"` // 新用户数
|
||
SubmissionSuccessRate string `json:"submission_success_rate"` // 提交成功率
|
||
NewUserUnsubWithinHour int `json:"new_user_unsub_within_hour"` // 当日新用户退订数(1小时以内)
|
||
NewUserUnsubWithinHourRate string `json:"new_user_unsub_within_hour_rate"` // 当日新用户退订率(1小时以内)
|
||
NewUserUnsubOnDay int `json:"new_user_unsub_on_day"` // 当日新用户退订数
|
||
NewUserUnsubOnDayRate string `json:"new_user_unsub_on_day_rate"` // 当日新用户退订率
|
||
TotalNewUserUnsub int `json:"total_new_user_unsub"` // 累计新用户退订数
|
||
TotalNewUserUnsubRate string `json:"total_new_user_unsub_rate"` // 累计新用户退订率
|
||
}
|
||
|
||
// MgHistoricalSummary 历史汇总查询表对应的结构体
|
||
type MgHistoricalSummary struct {
|
||
Model
|
||
|
||
Date string `json:"date"` // 日期
|
||
ProductID int64 `json:"product_id"` // 产品ID
|
||
ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码
|
||
SubmissionCount int `json:"submission_count"` // 提交数
|
||
NewUserCount int `json:"new_user_count"` // 新用户数
|
||
SubmissionSuccessRate string `json:"submission_success_rate"` // 提交成功率
|
||
NewUserUnsubWithinHour int `json:"new_user_unsub_within_hour"` // 当日新用户退订数(1小时以内)
|
||
NewUserUnsubWithinHourRate string `json:"new_user_unsub_within_hour_rate"` // 当日新用户退订率(1小时以内)
|
||
NewUserUnsubOnDay int `json:"new_user_unsub_on_day"` // 当日新用户退订数
|
||
NewUserUnsubOnDayRate string `json:"new_user_unsub_on_day_rate"` // 当日新用户退订率
|
||
NewUserUnsubWithin24H int `gorm:"column:new_user_unsub_within_24h" json:"new_user_unsub_within_24h"` // 当日新用户24小时退订数
|
||
NewUserUnsubWithin24HRate string `gorm:"column:new_user_unsub_within_24h_rate" json:"new_user_unsub_within_24h_rate"` // 当日新用户24小时退订率
|
||
TotalNewUserUnsub int `json:"total_new_user_unsub"` // 累计新用户退订数
|
||
TotalNewUserUnsubRate string `json:"total_new_user_unsub_rate"` // 累计新用户退订率
|
||
//Province string `gorm:"size:255" json:"province"` // 省份
|
||
}
|
||
|
||
// MgRealtimeSummary 当日实时汇总表对应的结构体
|
||
type MgRealtimeSummary struct {
|
||
ProductID int64 `json:"product_id"` // 产品ID
|
||
ChannelCode string `json:"channel_code"` // 渠道编码
|
||
SubmissionCount int `json:"submission_count"` // 提交数
|
||
NewUserCount int `json:"new_user_count"` // 新用户数
|
||
SubmissionSuccessRate string `json:"submission_success_rate"` // 提交成功率
|
||
NewUserUnsubWithinHour int `json:"new_user_unsub_within_hour"` // 当日新用户退订数(1小时以内)
|
||
NewUserUnsubWithinHourRate string `json:"new_user_unsub_within_hour_rate"` // 当日新用户退订率(1小时以内)
|
||
NewUserUnsubOnDay int `json:"new_user_unsub_on_day"` // 当日新用户退订数
|
||
NewUserUnsubOnDayRate string `json:"new_user_unsub_on_day_rate"` // 当日新用户退订率
|
||
//Province string `json:"province"` // 省份
|
||
}
|
||
|
||
// MgTransactionLog 交易流水记录表对应的结构体
|
||
type MgTransactionLog struct {
|
||
Model
|
||
ProductID int64 `json:"product_id"` // 产品ID
|
||
ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码
|
||
Province string `gorm:"size:255" json:"province"` // 省份
|
||
PhoneNumber string `gorm:"size:20" json:"phone_number"` // 手机号
|
||
OutTradeNo string `gorm:"size:255" json:"out_trade_no"` // 平台订单号
|
||
LinkId string `gorm:"size:255" json:"link_id"` // linkId(咪咕订单号)
|
||
ChannelTradeNo string `gorm:"size:255" json:"channel_trade_no"` // 渠道订单号
|
||
Result string `gorm:"size:255" json:"result"` // 交易结果
|
||
Reason string `gorm:"size:255" json:"reason"` // 交易失败原因
|
||
VerificationCode string `gorm:"size:255" json:"verification_code"` // 验证码
|
||
OrderTime *time.Time `gorm:"type:datetime" json:"order_time"` // 订单时间
|
||
}
|
||
|
||
// MgUserRetention 用户留存记录表对应的结构体
|
||
type MgUserRetention struct {
|
||
RetentionMonth string `gorm:"size:7" json:"retention_month"` // 留存月份(格式:YYYY-MM)
|
||
ChannelCode string `gorm:"size:255" json:"channel_code"` // 渠道编码
|
||
ProductID int64 `json:"product_id"` // 产品ID
|
||
NewUserCount int `json:"new_user_count"` // 新增用户数
|
||
RetainedUserCount int `json:"retained_user_count"` // 留存用户数(实时)
|
||
RetentionRate string `json:"retention_rate"` // 留存率(实时,以百分比形式存储)
|
||
LastTwoMonthDate string `json:"last_two_month_date"` // 最近2个月留存日期(格式:YYYY-MM-DD)
|
||
LastTwoMonthRetentionCount int `json:"last_two_month_retention_count"` // 最近2个月留存用户数(如12/1, 11/1)
|
||
LastTwoMonthRetentionRate string `json:"last_two_month_retention_rate"` // 最近2个月留存率(如12/1, 11/1)
|
||
LastMonthDate string `json:"last_month_date"` // 最近1个月留存日期(格式:YYYY-MM-DD)
|
||
LastMonthRetentionCount int `json:"last_month_retention_count"` // 最近1个月留存用户数(如12/1)
|
||
LastMonthRetentionRate string `json:"last_month_retention_rate"` // 最近1个月留存率(如12/1)
|
||
}
|
||
|
||
// MgUserDayRetention 用户留存记录表(按天)对应的结构体
|
||
type MgUserDayRetention struct {
|
||
Date string `json:"date"` // 留存日期(格式:YYYY-MM-DD)
|
||
RetainedUserCount int `json:"retained_user_count"` // 留存用户数
|
||
RetentionRate string `json:"retention_rate"` // 留存率
|
||
UserUnsubOnDay int `json:"user_unsub_on_day"` // 当日退订数
|
||
}
|
||
|
||
// 以下是接口出入参结构体
|
||
|
||
type Action struct {
|
||
OrderChannel string `json:"orderChannel"` // 订购渠道
|
||
OrderApp string `json:"orderApp"` // 订购包名
|
||
}
|
||
|
||
type SendCaptchaReq struct {
|
||
Phone string `json:"phone" binding:"required"` // 手机号码
|
||
Channel string `json:"channel" binding:"required"` // 渠道号
|
||
SkuCode string `json:"skuCode" binding:"required"` // 产品编号
|
||
UserAction Action `json:"userAction"` // 用户操作记录
|
||
OutTradeNo string `json:"outTradeNo"` // 自定义订单号
|
||
}
|
||
|
||
type SendCaptchaReqEx struct {
|
||
Phone string `json:"phone" binding:"required"` // 手机号码
|
||
Channel string `json:"channel" binding:"required"` // 渠道号
|
||
SkuCode string `json:"skuCode" binding:"required"` // 产品编号
|
||
OutTradeNo string `json:"outTradeNo"` // 自定义订单号
|
||
}
|
||
|
||
type MiGuSendCaptchaReq struct {
|
||
Phone string `json:"phone" binding:"required"` // 手机号码
|
||
Channel string `json:"channel" binding:"required"` // 渠道号
|
||
SkuCode string `json:"skuCode" binding:"required"` // 产品编号
|
||
UserAction Action `json:"userAction"` // 用户操作记录
|
||
OutTradeNo string `json:"outTradeNo"` // 自定义订单号
|
||
}
|
||
|
||
type SendCaptchaResp struct {
|
||
LinkId string `json:"linkId"`
|
||
}
|
||
|
||
type SubmitOrderReq struct {
|
||
LinkId string `json:"linkId" binding:"required"` // 验证码接口返回的linkId
|
||
SmsCode string `json:"smsCode" binding:"required"` // 验证码
|
||
Phone string `json:"phone" binding:"required"` // 手机号码
|
||
Channel string `json:"channel" binding:"required"` // 渠道号
|
||
SkuCode string `json:"skuCode" binding:"required"` // 产品编号
|
||
UserAction Action `json:"userAction"` // 用户操作记录
|
||
OutTradeNo string `json:"outTradeNo"` // 自定义订单号
|
||
}
|
||
|
||
type SubmitOrderReqEx struct {
|
||
LinkId string `json:"linkId" binding:"required"` // 验证码接口返回的linkId
|
||
SmsCode string `json:"smsCode" binding:"required"` // 验证码
|
||
Phone string `json:"phone" binding:"required"` // 手机号码
|
||
Channel string `json:"channel" binding:"required"` // 渠道号
|
||
SkuCode string `json:"skuCode" binding:"required"` // 产品编号
|
||
OutTradeNo string `json:"outTradeNo"` // 自定义订单号
|
||
}
|
||
|
||
type MiGuSubmitOrderReq struct {
|
||
LinkId string `json:"linkId" binding:"required"` // 验证码接口返回的linkId
|
||
SmsCode string `json:"smsCode" binding:"required"` // 验证码
|
||
Phone string `json:"phone" binding:"required"` // 手机号码
|
||
Channel string `json:"channel" binding:"required"` // 渠道号
|
||
SkuCode string `json:"skuCode" binding:"required"` // 产品编号
|
||
UserAction Action `json:"userAction"` // 用户操作记录
|
||
OutTradeNo string `json:"outTradeNo"` // 自定义订单号
|
||
}
|
||
|
||
type SubmitOrderResp struct {
|
||
LinkId string `json:"linkId"`
|
||
}
|
||
|
||
type MiGuRsp struct {
|
||
RequestId string `json:"requestId"`
|
||
Code string `json:"code"`
|
||
Msg string `json:"message"`
|
||
Data SubmitOrderResp `json:"data"`
|
||
}
|
||
|
||
type MiGuCheckRsp struct {
|
||
RequestId string `json:"requestId"`
|
||
Code int `json:"code"`
|
||
Msg string `json:"message"`
|
||
Data SubmitOrderResp `json:"data"`
|
||
}
|
||
|
||
type CheckOrderReq struct {
|
||
OutTradeNo string `json:"outTradeNo" binding:"required"` // 自定义订单号
|
||
}
|
||
|
||
type CheckOrderReqEx struct {
|
||
LinkId string `json:"linkId"`
|
||
}
|
||
|
||
type CheckOrderResp struct {
|
||
RequestId string `json:"requestId"`
|
||
Code string `json:"code"` //-1:已退订,0:未退订,404:订单记录不存在
|
||
Msg string `json:"message"`
|
||
}
|
||
|
||
// QueryRightsInfoReq 查询用户会员权益订购信息接口-入参
|
||
type QueryRightsInfoReq struct {
|
||
AppChannelList []string `json:"appChannelList" binding:"required"` // 渠道号
|
||
Mobile string `json:"mobile" binding:"required"` // 手机号(sm4加密)
|
||
PackageId string `json:"packageId"` // 计费点id
|
||
}
|
||
|
||
type QueryRightsInfoReqEx struct {
|
||
Channel string `json:"channel" binding:"required"` // 渠道号
|
||
SkuCode string `json:"skuCode" binding:"required"` // 产品编号
|
||
Phone string `json:"phone" binding:"required"` // 手机号码
|
||
}
|
||
|
||
// QueryRightsInfoResp 查询用户会员权益订购信息接口-出参
|
||
type QueryRightsInfoResp struct {
|
||
ReturnCode string `json:"returnCode"`
|
||
Message string `json:"message"`
|
||
ResultData []Package `json:"resultData"`
|
||
ServerTime int64 `json:"serverTime"`
|
||
}
|
||
|
||
type Package struct {
|
||
AppChannel string `json:"appChannel"`
|
||
PackageID string `json:"packageId"`
|
||
PackageName string `json:"packageName"`
|
||
SubTime string `json:"subTime"`
|
||
ExpireTime string `json:"expireTime"`
|
||
MonthlyContinuous int `json:"monthlyContinuous"`
|
||
IsUnsub int `json:"isUnsub"`
|
||
UnsubTime string `json:"unsubTime"`
|
||
}
|
||
|
||
// TransactionListReq 查询交易流水记录-入参
|
||
type TransactionListReq struct {
|
||
Phone string `json:"phone"` // 手机号码
|
||
Channel string `json:"channel"` // 渠道号
|
||
SkuCode int `json:"skuCode"` // 产品编号
|
||
Result string `json:"result"` // 交易结果
|
||
StartTime string `json:"start_time"` // 开始时间
|
||
EndTime string `json:"end_time"` // 结束时间
|
||
PageNum int `json:"page_num"` // 页码
|
||
PageSize int `json:"page_size"` // 每页条数
|
||
IsExport uint32 `json:"is_export"` // 1-导出
|
||
}
|
||
|
||
// TransactionListResp 查询交易流水记录-出参
|
||
type TransactionListResp struct {
|
||
List []MgTransactionLog `json:"list"`
|
||
Count int `json:"count"`
|
||
PageSize int `json:"page_size"`
|
||
PageNum int `json:"page_num"`
|
||
}
|
||
|
||
// ProductListReq 查询权益产品列表-入参
|
||
type ProductListReq struct {
|
||
PageNum int `json:"page_num"` // 页码
|
||
PageSize int `json:"page_size"` // 每页条数
|
||
}
|
||
|
||
// ProductListResp 查询权益产品列表-出参
|
||
type ProductListResp struct {
|
||
List []MgProduct `json:"list"`
|
||
Count int `json:"count"`
|
||
PageSize int `json:"page_size"`
|
||
PageNum int `json:"page_num"`
|
||
}
|
||
|
||
// ChannelListReq 查询渠道列表-入参
|
||
type ChannelListReq struct {
|
||
PageNum int `json:"page_num"` // 页码
|
||
PageSize int `json:"page_size"` // 每页条数
|
||
IsExport uint32 `json:"is_export"` // 1-导出
|
||
}
|
||
|
||
// ChannelListResp 查询渠道列表-出参
|
||
type ChannelListResp struct {
|
||
List []MgChannel `json:"list"`
|
||
Count int `json:"count"`
|
||
PageSize int `json:"page_size"`
|
||
PageNum int `json:"page_num"`
|
||
}
|
||
|
||
// OrderListReq 查询订单列表-入参
|
||
type OrderListReq struct {
|
||
StartTime string `json:"start_time"` // 开始时间
|
||
EndTime string `json:"end_time"` // 结束时间
|
||
CancelStartTime string `json:"cancel_start_time"` // 退订开始时间
|
||
CancelEndTime string `json:"cancel_end_time"` // 退订结束时间
|
||
SkuCode int `json:"skuCode"` // 产品编号
|
||
Channel string `json:"channel"` // 渠道号
|
||
OrderSerial string `json:"order_serial"` // 订单流水号
|
||
OutTradeNo string `json:"outTradeNo"` // 外部订单号
|
||
ChannelTradeNo string `json:"channelTradeNo"` // 渠道订单号
|
||
Phone string `json:"phone"` // 手机号码
|
||
SM4PhoneNumber string `json:"sm4_phone_number"` // SM4加密手机号
|
||
State int `json:"state"` // 退订状态 0-查所有 1-已退订 2-未退订 3-1小时内退订 4-24小时退订
|
||
PageNum int `json:"page_num"` // 页码
|
||
PageSize int `json:"page_size"` // 每页条数
|
||
IsExport uint32 `json:"is_export"` // 1-导出
|
||
}
|
||
|
||
// OrderListResp 查询订单列表-出参
|
||
type OrderListResp struct {
|
||
List []MgOrder `json:"list"`
|
||
Count int `json:"count"`
|
||
PageSize int `json:"page_size"`
|
||
PageNum int `json:"page_num"`
|
||
}
|
||
|
||
// HistoricalSummaryListReq 历史汇总查询-入参
|
||
type HistoricalSummaryListReq struct {
|
||
StartTime string `json:"start_time"` // 开始时间 xxxx-xx-xx
|
||
EndTime string `json:"end_time"` // 结束时间 xxxx-xx-xx
|
||
SkuCode int `json:"skuCode"` // 产品编号
|
||
Channel string `json:"channel"` // 渠道号
|
||
PageNum int `json:"page_num"` // 页码
|
||
PageSize int `json:"page_size"` // 每页条数
|
||
IsExport uint32 `json:"is_export"` // 1-导出
|
||
//Province string `json:"province"` // 省份
|
||
}
|
||
|
||
// HistoricalSummaryListResp 历史汇总查询-出参
|
||
type HistoricalSummaryListResp struct {
|
||
List []MgHistoricalSummary `json:"list"`
|
||
Count int `json:"count"`
|
||
PageSize int `json:"page_size"`
|
||
PageNum int `json:"page_num"`
|
||
}
|
||
|
||
// RealtimeSummaryListReq 当日实时汇总查询-入参
|
||
type RealtimeSummaryListReq struct {
|
||
StartTime string `json:"start_time"` // 开始时间 00:00:00
|
||
EndTime string `json:"end_time"` // 结束时间 23:59:59
|
||
SkuCode int `json:"skuCode"` // 产品编号
|
||
Channel string `json:"channel"` // 渠道号
|
||
PageNum int `json:"page_num"` // 页码
|
||
PageSize int `json:"page_size"` // 每页条数
|
||
IsExport uint32 `json:"is_export"` // 1-导出
|
||
//Province string `json:"province"` // 省份
|
||
}
|
||
|
||
// RealtimeSummaryListResp 当日实时汇总查询-出参
|
||
type RealtimeSummaryListResp struct {
|
||
List []MgRealtimeSummary `json:"list"`
|
||
Count int `json:"count"`
|
||
PageSize int `json:"page_size"`
|
||
PageNum int `json:"page_num"`
|
||
}
|
||
|
||
// UserRetentionListReq 用户留存记录查询-入参
|
||
type UserRetentionListReq struct {
|
||
//Date string `json:"date"` // 月用户(格式:YYYY-MM)
|
||
RetentionMonth string `json:"retention_month"` // 留存月份(格式:YYYY-MM)
|
||
SkuCode int `json:"skuCode"` // 产品编号
|
||
Channel string `json:"channel"` // 渠道号
|
||
Province string `json:"province"` // 省份
|
||
PageNum int `json:"page_num"` // 页码
|
||
PageSize int `json:"page_size"` // 每页条数
|
||
IsExport uint32 `json:"is_export"` // 1-导出
|
||
}
|
||
|
||
// UserRetentionListResp 用户留存记录查询-出参
|
||
type UserRetentionListResp struct {
|
||
List []MgUserRetention `json:"list"`
|
||
Count int `json:"count"`
|
||
PageSize int `json:"page_size"`
|
||
PageNum int `json:"page_num"`
|
||
}
|
||
|
||
// UserDayRetentionListReq 用户留存记录(按天)查询-入参
|
||
type UserDayRetentionListReq struct {
|
||
RetentionMonth string `json:"retention_month" binding:"required"` // 留存月份(格式:YYYY-MM)
|
||
SkuCode int `json:"skuCode" binding:"required"` // 产品编号
|
||
Channel string `json:"channel" binding:"required"` // 渠道号
|
||
PageNum int `json:"page_num"` // 页码
|
||
PageSize int `json:"page_size"` // 每页条数
|
||
OnlyFirstDay bool `json:"only_first_day"` // 是否只查询每个月1号的数据
|
||
IsExport uint32 `json:"is_export"` // 1-导出
|
||
}
|
||
|
||
// UserDayRetentionListResp 用户留存记录(按天)查询-出参
|
||
type UserDayRetentionListResp struct {
|
||
List []MgUserDayRetention `json:"list"`
|
||
Count int `json:"count"`
|
||
PageSize int `json:"page_size"`
|
||
PageNum int `json:"page_num"`
|
||
}
|
||
|
||
// SysChannelListReq 查询渠道列表入参
|
||
type SysChannelListReq struct {
|
||
PageNum int `json:"page_num"` // 页码
|
||
PageSize int `json:"page_size"` // 每页条数
|
||
}
|
||
|
||
// SysChannelListResp 查询渠道列表出参
|
||
type SysChannelListResp struct {
|
||
List []ChannelData `json:"list"`
|
||
Count int `json:"count"`
|
||
PageSize int `json:"page_size"`
|
||
PageNum int `json:"page_num"`
|
||
}
|
||
|
||
type ChannelData struct {
|
||
ChannelCode string `json:"channel_code"` // 渠道编码
|
||
}
|
||
|
||
// AddProductReq 添加新产品请求结构体
|
||
type AddProductReq struct {
|
||
Name string `json:"name" binding:"required"` // 产品名称
|
||
UniqueCode string `json:"unique_code" binding:"required"` // 产品唯一标识编码
|
||
SkuName string `json:"sku_name" binding:"required"` // SKU名称
|
||
BillingPointID int64 `json:"billing_point_id" binding:"required"` // 计费点ID
|
||
ChannelCode string `json:"channel_code" binding:"required"` // 渠道编码
|
||
ProductApiID string `json:"product_api_id"` // 产品ID(接口用)
|
||
ChannelApiID string `json:"channel_api_id" binding:"required"` // 渠道ID(接口用)
|
||
OfficialPage string `json:"official_page"` // 官方落地页
|
||
}
|
||
|
||
// AddProductResp 添加新产品响应结构体
|
||
type AddProductResp struct {
|
||
ID uint32 `json:"id"` // 新增产品的ID
|
||
}
|
||
|
||
// UpdateProductReq 修改产品请求结构体
|
||
type UpdateProductReq struct {
|
||
ID int64 `json:"id" binding:"required"` // 产品ID
|
||
Name string `json:"name"` // 产品名称
|
||
UniqueCode string `json:"unique_code"` // 产品唯一标识编码
|
||
SkuName string `json:"sku_name"` // SKU名称
|
||
BillingPointID int64 `json:"billing_point_id"` // 计费点ID
|
||
ChannelCode string `json:"channel_code"` // 渠道编码
|
||
ProductApiID string `json:"product_api_id"` // 产品ID(接口用)
|
||
ChannelApiID string `json:"channel_api_id"` // 渠道ID(接口用)
|
||
OfficialPage string `json:"official_page"` // 官方落地页
|
||
}
|
||
|
||
// UpdateProductResp 修改产品响应结构体
|
||
type UpdateProductResp struct {
|
||
ID uint32 `json:"id"` // 修改的产品ID
|
||
}
|
||
|
||
// DeleteProductReq 删除产品请求结构体
|
||
type DeleteProductReq struct {
|
||
ID int64 `json:"id" binding:"required"` // 产品ID
|
||
}
|
||
|
||
// AddChannelReq 创建渠道请求结构体
|
||
type AddChannelReq struct {
|
||
ProductID int `json:"product_id" binding:"required"` // 产品ID (必填)
|
||
MainChannelCode string `json:"main_channel_code" binding:"required"` // 主渠道编码 (必填)
|
||
SubChannelCode string `json:"sub_channel_code" binding:"required"` // 子渠道编码 (必填)
|
||
SubscribeURL string `json:"subscribe_url"` // 订购成功通知接口url (非必填)
|
||
UnsubscribeURL string `json:"unsubscribe_url"` // 退订通知接口url (非必填)
|
||
Status int `json:"status" binding:"required"` // 状态 (2: 停用, 1: 启用) (必填)
|
||
Remarks string `json:"remarks"` // 备注 (非必填)
|
||
}
|
||
|
||
// AddChannelResp 创建渠道响应结构体
|
||
type AddChannelResp struct {
|
||
ChannelID uint32 `json:"channel_id"` // 创建的渠道ID
|
||
}
|
||
|
||
// UpdateChannelReq 更新渠道请求结构体
|
||
type UpdateChannelReq struct {
|
||
ID int `json:"id" binding:"required"` // 渠道ID (必填)
|
||
MainChannelCode string `json:"main_channel_code"` // 主渠道编码 (非必填)
|
||
SubChannelCode string `json:"sub_channel_code"` // 子渠道编码 (非必填)
|
||
SubscribeURL string `json:"subscribe_url"` // 订购成功通知接口url (非必填)
|
||
UnsubscribeURL string `json:"unsubscribe_url"` // 退订通知接口url (非必填)
|
||
Status int `json:"status"` // 状态 (0: 停用, 1: 启用) (非必填)
|
||
Remarks string `json:"remarks"` // 备注 (非必填)
|
||
}
|
||
|
||
// UpdateChannelResp 更新渠道响应结构体
|
||
type UpdateChannelResp struct {
|
||
// Any relevant response fields can go here
|
||
}
|
||
|
||
// DeleteChannelReq 删除渠道请求结构体
|
||
type DeleteChannelReq struct {
|
||
ID int `json:"id" binding:"required"` // 渠道ID (必填)
|
||
}
|
||
|
||
// DeleteChannelResp 删除渠道响应结构体
|
||
type DeleteChannelResp struct {
|
||
// Any relevant response fields can go here
|
||
}
|
||
|
||
type HomepageDataSummaryReq struct {
|
||
StartTime string `json:"start_time"` // 查询开始时间
|
||
EndTime string `json:"end_time"` // 查询结束时间
|
||
ProductID int `json:"product_id"` // 产品ID
|
||
Channel string `json:"channel"` // 渠道名称
|
||
}
|
||
|
||
type DailyData struct {
|
||
Date string `json:"date"` // 日期
|
||
NewUserCount int64 `json:"new_user_count"` // 新增用户数
|
||
UnsubscribedUserCount int64 `json:"unsubscribed_user_count"` // 退订用户数
|
||
UnsubscribedWithinOneHour int64 `json:"unsubscribed_within_one_hour"` // 1小时内退订用户数
|
||
UnsubscribeRate string `json:"unsubscribe_rate"` // 退订率
|
||
UnsubscribeWithinOneHourRate string `json:"unsubscribe_within_one_hour_rate"` // 1小时内退订率
|
||
TotalCancelCount int64 `json:"total_cancel_count"` // 每日退订用户数合计
|
||
}
|
||
|
||
type HomepageDataSummaryResp struct {
|
||
Summary SummaryData `json:"summary"` // 汇总数据
|
||
DailyDataList []DailyData `json:"daily_data"` // 每天的数据列表
|
||
}
|
||
|
||
// SummaryData represents the overall summary data
|
||
type SummaryData struct {
|
||
TotalUserCount int `json:"total_user_count"` // 总用户数
|
||
UnsubscribedUserCount int `json:"unsubscribed_user_count"` // 退订用户数
|
||
OneHourUnsubscribedUserCount int `json:"one_hour_unsubscribed_user_count"` // 1小时内退订用户数
|
||
RetainedUserCount int `json:"retained_user_count"` // 留存用户数
|
||
UnsubscribeRate string `json:"unsubscribe_rate"` // 退订率 (字符串,保留两位小数,如 "26.35%")
|
||
OneHourUnsubscribeRate string `json:"one_hour_unsubscribe_rate"` // 1小时退订率 (字符串,保留两位小数,如 "26.35%")
|
||
RetentionRate string `json:"retention_rate"` // 留存率 (字符串,保留两位小数,如 "73.65%")
|
||
}
|
||
|
||
// RetentionMonthsReq 查询用户留存月份的请求结构体
|
||
type RetentionMonthsReq struct {
|
||
StartTime string `json:"start_time"` // 查询开始时间
|
||
EndTime string `json:"end_time"` // 查询结束时间
|
||
ProductID int `json:"product_id"` // 产品ID
|
||
Channel string `json:"channel"` // 渠道名称
|
||
IsExport uint32 `json:"is_export"` // 1-导出
|
||
}
|
||
|
||
// MonthlyRetention 表示每月留存数据
|
||
type MonthlyRetention struct {
|
||
Month string `json:"month"` // 年月
|
||
NewUserCount int `json:"new_user_count"` // 新用户数
|
||
ValidUsersCount int `json:"valid_users_count"` // 当月新增有效用户数
|
||
RetainedUsersCount int `json:"retained_users_count"` // 历史推广用户本月留存数
|
||
TotalValidUsersCount int `json:"total_valid_users_count"` // 总有效用户数
|
||
}
|
||
|
||
// RetentionMonthsResp 表示响应参数结构体
|
||
type RetentionMonthsResp struct {
|
||
TotalNewUsers int `json:"total_new_users"` // 总新用户数
|
||
TotalValidUsers int `json:"total_valid_users"` // 总有效用户数
|
||
MonthlyRetentionData []MonthlyRetention `json:"monthly_retention_data"` // 每月留存数据
|
||
}
|
||
|
||
// MiGuCaptchaRequest 调用下单接口(森越转发)
|
||
func MiGuCaptchaRequest(r *MiGuSendCaptchaReq) (MiGuRsp, error) {
|
||
var miGuResp MiGuRsp
|
||
|
||
data, err := json.Marshal(r)
|
||
if err != nil {
|
||
logger.Error("MiGuCaptchaRequest err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
fmt.Println("data json:", string(data))
|
||
client := http.Client{}
|
||
req, err := http.NewRequest("POST", MiGUSendCaptchaUrl, bytes.NewBuffer(data))
|
||
if err != nil {
|
||
logger.Error("MiGuCaptchaRequest err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
logger.Error("MiGuCaptchaRequest err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
logger.Error("MiGuCaptchaRequest err:", err)
|
||
return miGuResp, err
|
||
}
|
||
fmt.Println("body:", string(body))
|
||
|
||
defer resp.Body.Close()
|
||
|
||
err = json.Unmarshal(body, &miGuResp)
|
||
if err != nil {
|
||
logger.Error("MiGuCaptchaRequest err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
return miGuResp, nil
|
||
}
|
||
|
||
// MiGuCaptchaSubmit 调用提交接口(森越转发)
|
||
func MiGuCaptchaSubmit(r *MiGuSubmitOrderReq) (MiGuRsp, error) {
|
||
var miGuResp MiGuRsp
|
||
|
||
data, err := json.Marshal(r)
|
||
if err != nil {
|
||
logger.Error("oppoSendData err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
fmt.Println("data json:", string(data))
|
||
client := http.Client{}
|
||
req, err := http.NewRequest("POST", MiGUSubmitCaptchaUrl, bytes.NewBuffer(data))
|
||
if err != nil {
|
||
logger.Error("oppoSendData err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
logger.Error("oppoSendData err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
logger.Error("oppoSendData err:", err)
|
||
return miGuResp, err
|
||
}
|
||
fmt.Println("body:", string(body))
|
||
|
||
defer resp.Body.Close()
|
||
|
||
err = json.Unmarshal(body, &miGuResp)
|
||
if err != nil {
|
||
logger.Error("clueResp err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
return miGuResp, nil
|
||
}
|
||
|
||
// MiGuCheckOrder 查询是否已经退订接口(森越转发)
|
||
func MiGuCheckOrder(r *CheckOrderReq) (MiGuCheckRsp, error) {
|
||
var miGuResp MiGuCheckRsp
|
||
|
||
data, err := json.Marshal(r)
|
||
if err != nil {
|
||
logger.Error("oppoSendData err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
fmt.Println("data json:", string(data))
|
||
client := http.Client{}
|
||
req, err := http.NewRequest("POST", MiGUCheckOrderUrl, bytes.NewBuffer(data))
|
||
if err != nil {
|
||
logger.Error("oppoSendData err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
logger.Error("oppoSendData err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
logger.Error("oppoSendData err:", err)
|
||
return miGuResp, err
|
||
}
|
||
fmt.Println("body:", string(body))
|
||
|
||
defer resp.Body.Close()
|
||
|
||
err = json.Unmarshal(body, &miGuResp)
|
||
if err != nil {
|
||
logger.Error("clueResp err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
return miGuResp, nil
|
||
}
|
||
|
||
// MiGuQueryRightsInfo 查询用户会员权益订购信息接口(咪咕接口)
|
||
func MiGuQueryRightsInfo(r *QueryRightsInfoReq) (QueryRightsInfoResp, error) {
|
||
var miGuResp QueryRightsInfoResp
|
||
sm4Phone, err := tools.SM4Encrypt(SM4KEy, r.Mobile)
|
||
if err != nil {
|
||
logger.Error("SM4Encrypt err:", err)
|
||
return miGuResp, err
|
||
}
|
||
r.Mobile = sm4Phone
|
||
|
||
data, err := json.Marshal(r)
|
||
if err != nil {
|
||
logger.Error("MiGuQueryRightsInfo json.Marshal err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
fmt.Println("data json:", string(data))
|
||
client := http.Client{}
|
||
req, err := http.NewRequest("POST", MiGUQueryRightsInfoUrl, bytes.NewBuffer(data))
|
||
if err != nil {
|
||
logger.Error("MiGuQueryRightsInfo err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
req.Header.Set("Content-Type", "application/json; charset=utf-8")
|
||
|
||
resp, err := client.Do(req)
|
||
if err != nil {
|
||
logger.Error("MiGuQueryRightsInfo err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
logger.Error("MiGuQueryRightsInfo err:", err)
|
||
return miGuResp, err
|
||
}
|
||
fmt.Println("body:", string(body))
|
||
|
||
defer resp.Body.Close()
|
||
|
||
err = json.Unmarshal(body, &miGuResp)
|
||
if err != nil {
|
||
logger.Error("miGuResp err:", err)
|
||
return miGuResp, err
|
||
}
|
||
|
||
return miGuResp, nil
|
||
}
|
||
|
||
// NoticeSubChannel 回调通知接口 (GET 请求,返回string类型,响应头为text/plain)
|
||
func NoticeSubChannel(baseUrl, linkId, extData, status string) (string, error) {
|
||
// 构建 GET 请求的 URL
|
||
requestUrl, err := url.Parse(baseUrl)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to parse baseUrl: %v", err)
|
||
}
|
||
|
||
// 添加查询参数
|
||
query := requestUrl.Query()
|
||
query.Set("orderId", linkId)
|
||
query.Set("extData", extData)
|
||
query.Set("status", status)
|
||
requestUrl.RawQuery = query.Encode()
|
||
|
||
fmt.Println("NoticeSubChannel url:", requestUrl.String())
|
||
|
||
// 发送 GET 请求
|
||
resp, err := http.Get(requestUrl.String())
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to send GET request: %v", err)
|
||
}
|
||
defer resp.Body.Close()
|
||
|
||
// 检查响应头是否为text/plain
|
||
//contentType := resp.Header.Get("Content-Type")
|
||
//if contentType != "text/plain; charset=utf-8" {
|
||
// return "", fmt.Errorf("unexpected content type: %s", contentType)
|
||
//}
|
||
|
||
// 读取响应体内容
|
||
body, err := io.ReadAll(resp.Body)
|
||
if err != nil {
|
||
return "", fmt.Errorf("failed to read response body: %v", err)
|
||
}
|
||
|
||
// 将响应体转换为string
|
||
responseString := string(body)
|
||
|
||
// 打印结果
|
||
fmt.Println("Notification response:", responseString)
|
||
|
||
return responseString, nil
|
||
}
|
||
|
||
var mu sync.Mutex
|
||
|
||
// GetOrderSerial generates a unique inventory serial number
|
||
func GetOrderSerial(db *gorm.DB) string {
|
||
const maxRetries = 5
|
||
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
|
||
for retryCount := 0; retryCount < maxRetries; retryCount++ {
|
||
nowTime := time.Now()
|
||
// 使用日期格式精确到天
|
||
datePart := nowTime.Format("060102") // 格式为 YYMMDD
|
||
// 生成13位随机数
|
||
rand.Seed(nowTime.UnixNano() + int64(retryCount)) // 为了确保每次生成不同的随机数
|
||
randomNum := rand.Int63n(1e13) // 10位随机数,范围从0到9999999999999
|
||
randomPart := fmt.Sprintf("%013d", randomNum) // 确保随机数是13位的,前面补零
|
||
|
||
// 拼接日期部分和随机数部分
|
||
sn := fmt.Sprintf("%s%s", datePart, randomPart)
|
||
|
||
exist, err := QueryRecordExist(fmt.Sprintf("SELECT * FROM mg_transaction_log WHERE out_trade_no='%s'", sn), db)
|
||
if err != nil {
|
||
logger.Error("sn err:", err)
|
||
continue
|
||
}
|
||
if !exist {
|
||
return sn
|
||
}
|
||
}
|
||
|
||
return "" // 返回空字符串,如果在最大重试次数后仍未找到唯一编号
|
||
}
|
||
|
||
// GetExcelOrderSerial generates a unique inventory serial number based on subscribeTime
|
||
func GetExcelOrderSerial(db *gorm.DB, subscribeTime time.Time) string {
|
||
const maxRetries = 5
|
||
mu.Lock()
|
||
defer mu.Unlock()
|
||
|
||
for retryCount := 0; retryCount < maxRetries; retryCount++ {
|
||
// 使用 subscribeTime 的日期部分生成订单号
|
||
datePart := subscribeTime.Format("060102") // 格式为 YYMMDD
|
||
// 生成13位随机数
|
||
nowTime := time.Now()
|
||
rand.Seed(nowTime.UnixNano() + int64(retryCount)) // 确保每次生成不同的随机数
|
||
randomNum := rand.Int63n(1e13) // 生成范围为0到9999999999999的随机数
|
||
randomPart := fmt.Sprintf("%013d", randomNum) // 确保随机数是13位,前面补零
|
||
|
||
// 拼接日期部分和随机数部分
|
||
sn := fmt.Sprintf("%s%s", datePart, randomPart)
|
||
|
||
// 查询订单号是否已存在
|
||
exist, err := QueryRecordExist(fmt.Sprintf("SELECT * FROM mg_transaction_log WHERE out_trade_no='%s'", sn), db)
|
||
if err != nil {
|
||
logger.Error("sn err:", err)
|
||
continue
|
||
}
|
||
if !exist {
|
||
return sn
|
||
}
|
||
}
|
||
|
||
return "" // 在最大重试次数后仍未找到唯一编号则返回空字符串
|
||
}
|
||
|
||
// IsValidChannel 判断渠道号是否为 12, 13, 14
|
||
func IsValidChannel(channel string) bool {
|
||
return channel == "0012" || channel == "0013" || channel == "0014"
|
||
}
|
||
|
||
// IsValidChannelEx 判断渠道号是否为子渠道
|
||
func IsValidChannelEx(channel string, db *gorm.DB) bool {
|
||
exist, err := QueryRecordExist(fmt.Sprintf("SELECT * FROM mg_channel WHERE sub_channel_code='%s'", channel), db)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
return exist
|
||
}
|
||
|
||
// IsValidSkuCode 判断skuCode是否有效
|
||
func IsValidSkuCode(skuCode string) bool {
|
||
return skuCode == SKUCODE
|
||
}
|
||
|
||
// IsValidSkuCodeEx 判断skuCode是否有效
|
||
func IsValidSkuCodeEx(skuCode string, db *gorm.DB) bool {
|
||
exist, err := QueryRecordExist(fmt.Sprintf("SELECT * FROM mg_product WHERE unique_code='%s'", skuCode), db)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
return exist
|
||
}
|
||
|
||
// IsWithinOneHour 判断订阅时间是否在1小时之内
|
||
func IsWithinOneHour(order MgOrder) bool {
|
||
if order.SubscribeTime == nil {
|
||
return false
|
||
}
|
||
|
||
// 获取当前时间
|
||
now := time.Now()
|
||
|
||
// 计算当前时间与订阅时间的差值
|
||
diff := now.Sub(*order.SubscribeTime)
|
||
|
||
// 判断是否在1小时以内
|
||
return diff <= time.Hour
|
||
}
|
||
|
||
// IsWithinOneHourCancel 判断订阅时间是否在取消订阅时间的1小时之内
|
||
func IsWithinOneHourCancel(subscribeTime time.Time, unsubscribeTime string) bool {
|
||
// 将 unsubscribeTime(string)转换为 time.Time 类型
|
||
unsubTime, err := time.ParseInLocation("2006-01-02 15:04:05", unsubscribeTime, subscribeTime.Location())
|
||
if err != nil {
|
||
fmt.Println("Error parsing UnsubscribeTime:", err)
|
||
return false
|
||
}
|
||
|
||
// 计算取消订阅时间与订阅时间的差值
|
||
diff := unsubTime.Sub(subscribeTime)
|
||
|
||
// 判断是否在1小时以内,且取消时间在订阅时间之后
|
||
return diff <= time.Hour && diff >= 0
|
||
}
|
||
|
||
// ParseYearMonth 解析年月格式字符串,返回年和月
|
||
func ParseYearMonth(retentionMonth string) (int, time.Month, error) {
|
||
var year int
|
||
var month int
|
||
_, err := fmt.Sscanf(retentionMonth, "%d-%d", &year, &month)
|
||
if err != nil {
|
||
return 0, 0, err
|
||
}
|
||
return year, time.Month(month), nil
|
||
}
|
||
|
||
// GetMainChannelCodeAndSkuCode 通过子渠道编号和sku查询主渠道编号及其sku
|
||
func GetMainChannelCodeAndSkuCode(channel, skuCode string, db *gorm.DB) (mainChannel MgChannel, mainSkuCode MgProduct, err error) {
|
||
// 查询是否有记录
|
||
var channelInfo MgChannel
|
||
err = db.Table("mg_channel").Where("sub_channel_code = ?", channel).First(&channelInfo).Error
|
||
if err != nil {
|
||
logger.Errorf("SubmitOrder query mg_transaction_log err:", err.Error())
|
||
return MgChannel{}, MgProduct{}, err
|
||
}
|
||
|
||
var productInfo MgProduct
|
||
err = db.Table("mg_product").Where("unique_code = ?", skuCode).First(&productInfo).Error
|
||
if err != nil {
|
||
logger.Errorf("SubmitOrder query mg_transaction_log err:", err.Error())
|
||
return MgChannel{}, MgProduct{}, err
|
||
}
|
||
|
||
return channelInfo, productInfo, nil
|
||
}
|
||
|
||
// GetChannelInfoByChannelCode 通过渠道编号查询渠道信息
|
||
func GetChannelInfoByChannelCode(channel string, db *gorm.DB) (MgChannel, error) {
|
||
// 查询是否有记录
|
||
var channelInfo MgChannel
|
||
err := db.Table("mg_channel").Where("sub_channel_code = ?", channel).First(&channelInfo).Error
|
||
if err != nil {
|
||
logger.Errorf("SubmitOrder query mg_transaction_log err:", err.Error())
|
||
return MgChannel{}, err
|
||
}
|
||
|
||
return channelInfo, nil
|
||
}
|
||
|
||
// 模拟的数据库操作
|
||
func updateOrderState(order MgOrder, cancelFlag int, unsubTime string) error {
|
||
err := database.Db.Table("mg_order").Where("order_serial = ?", order.OrderSerial).Updates(map[string]interface{}{
|
||
"state": UnsubscribeOK,
|
||
"is_one_hour_cancel": cancelFlag,
|
||
"unsubscribe_time": unsubTime,
|
||
"updated_at": time.Now(),
|
||
}).Error
|
||
if err != nil {
|
||
fmt.Println("CheckOrderState update mg_order err:", err.Error())
|
||
logger.Errorf("CheckOrderState update mg_order err:", err.Error())
|
||
logger.Errorf("CheckOrderState order_serial:", order.OrderSerial)
|
||
return err
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// processBatch 处理批次
|
||
func processBatch(batch []MgOrder, wg *sync.WaitGroup) {
|
||
defer wg.Done()
|
||
|
||
for _, order := range batch {
|
||
for j := 0; j < 3; j++ {
|
||
var req QueryRightsInfoReq
|
||
req.AppChannelList = append(req.AppChannelList, ChannelCode)
|
||
req.Mobile = order.PhoneNumber
|
||
|
||
resp, err := MiGuQueryRightsInfo(&req)
|
||
if err != nil {
|
||
fmt.Println("CheckOrderState MiGuQueryRightsInfo err:", err.Error())
|
||
logger.Errorf("CheckOrderState MiGuQueryRightsInfo err:", err.Error())
|
||
continue
|
||
}
|
||
|
||
// 有退订数据
|
||
if len(resp.ResultData) != 0 {
|
||
if resp.ResultData[0].IsUnsub == 1 { // 已退订
|
||
var cancelFlag int
|
||
subscribeTime := order.CreatedAt
|
||
// 检查 subscribeTime 是否为 nil
|
||
if IsWithinOneHourCancel(subscribeTime, resp.ResultData[0].UnsubTime) {
|
||
cancelFlag = 1
|
||
}
|
||
|
||
err = updateOrderState(order, cancelFlag, resp.ResultData[0].UnsubTime)
|
||
if err != nil {
|
||
fmt.Println("CheckOrderState update mg_order err:", err.Error())
|
||
logger.Errorf("CheckOrderState update mg_order err:", err.Error())
|
||
logger.Errorf("CheckOrderState order_serial:", order.OrderSerial)
|
||
continue
|
||
}
|
||
break
|
||
} else if resp.ResultData[0].IsUnsub == 0 { // 没有退订
|
||
break
|
||
}
|
||
} else {
|
||
if j == 2 && resp.ReturnCode != "000010" && resp.Message != "服务调用失败" {
|
||
var cancelFlag int
|
||
subscribeTime := order.CreatedAt
|
||
unsubTime := time.Now().Format("2006-01-02 15:04:05")
|
||
// 检查 subscribeTime 是否为 nil
|
||
if IsWithinOneHourCancel(subscribeTime, unsubTime) {
|
||
cancelFlag = 1
|
||
}
|
||
|
||
err = updateOrderState(order, cancelFlag, unsubTime)
|
||
if err != nil {
|
||
fmt.Println("CheckOrderState update mg_order err:", err.Error())
|
||
logger.Errorf("CheckOrderState update mg_order err:", err.Error())
|
||
logger.Errorf("CheckOrderState order_serial:", order.OrderSerial)
|
||
continue
|
||
}
|
||
break
|
||
}
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// CheckAllOrderStateBatch 定时任务,批量检查历史订阅用户有无退订
|
||
func CheckAllOrderStateBatch() {
|
||
if database.Db == nil {
|
||
log.Println("Database connection is nil")
|
||
fmt.Println("Database connection is nil")
|
||
return
|
||
}
|
||
|
||
// 查询订单列表中未退订的用户,查询其是否退订;如果退订,则更新退订时间,判断是否为1小时内退订
|
||
var orderList []MgOrder
|
||
|
||
err := database.Db.Where("state = 1").Order("created_at desc").
|
||
Find(&orderList).Error
|
||
|
||
//err := database.Db.Where("state = 1 and created_at >= ? and created_at <= ?", "2024-12-01 00:00:00", "2024-12-30 23:59:59").Order("created_at desc").
|
||
// Find(&orderList).Error
|
||
|
||
if err != nil {
|
||
fmt.Println("query mg_order err:", err.Error())
|
||
return
|
||
}
|
||
|
||
// 控制每次处理的批次大小
|
||
batchSize := 100
|
||
totalOrders := len(orderList)
|
||
var wg sync.WaitGroup
|
||
|
||
// 按批次分割处理
|
||
for i := 0; i < totalOrders; i += batchSize {
|
||
end := i + batchSize
|
||
if end > totalOrders {
|
||
end = totalOrders
|
||
}
|
||
batch := orderList[i:end]
|
||
|
||
wg.Add(1)
|
||
go processBatch(batch, &wg)
|
||
// 控制批次之间的延时,避免同时过多请求
|
||
time.Sleep(500 * time.Millisecond) // 控制批次间隔
|
||
}
|
||
|
||
wg.Wait() // 等待所有 goroutines 完成
|
||
}
|
||
|
||
// CheckAllOrderState 定时任务,检查历史订阅用户有无退订
|
||
func CheckAllOrderState() {
|
||
if database.Db == nil {
|
||
log.Println("Database connection is nil")
|
||
fmt.Println("Database connection is nil")
|
||
return
|
||
}
|
||
|
||
// 查询订单列表中未退订的用户,查询其是否退订;如果退订,则更新退订时间,判断是否为1小时内退订
|
||
var orderList []MgOrder
|
||
|
||
err := database.Db.Where("state = 1").
|
||
Where("product_id = ?", ProductID).
|
||
Order("created_at desc").
|
||
Find(&orderList).Error
|
||
|
||
//err := database.Db.Where("state = 1 and created_at >= ? and created_at <= ?", "2024-10-01 00:00:00", "2024-10-31 23:59:59").Order("created_at desc").
|
||
// Find(&orderList).Error
|
||
|
||
if err != nil {
|
||
fmt.Println("query mg_order err:", err.Error())
|
||
return
|
||
}
|
||
|
||
for i, _ := range orderList {
|
||
for j := 0; j < 3; j++ {
|
||
var req QueryRightsInfoReq
|
||
req.AppChannelList = append(req.AppChannelList, ChannelCode)
|
||
req.Mobile = orderList[i].PhoneNumber
|
||
|
||
resp, err := MiGuQueryRightsInfo(&req)
|
||
if err != nil {
|
||
fmt.Println("CheckOrderState MiGuQueryRightsInfo err:", err.Error())
|
||
logger.Errorf("CheckOrderState MiGuQueryRightsInfo err:", err.Error())
|
||
continue
|
||
}
|
||
|
||
// 有退订数据
|
||
if len(resp.ResultData) != 0 {
|
||
if resp.ResultData[0].IsUnsub == 1 { // 已退订
|
||
var cancelFlag int
|
||
subscribeTime := orderList[i].CreatedAt
|
||
// 检查 subscribeTime 是否为 nil
|
||
if IsWithinOneHourCancel(subscribeTime, resp.ResultData[0].UnsubTime) {
|
||
cancelFlag = 1
|
||
}
|
||
|
||
err = database.Db.Table("mg_order").Where("order_serial = ?", orderList[i].OrderSerial).Updates(map[string]interface{}{
|
||
"state": UnsubscribeOK,
|
||
"is_one_hour_cancel": cancelFlag,
|
||
"unsubscribe_time": resp.ResultData[0].UnsubTime,
|
||
"updated_at": time.Now(),
|
||
}).Error
|
||
if err != nil {
|
||
fmt.Println("CheckOrderState update mg_order err:", err.Error())
|
||
logger.Errorf("CheckOrderState update mg_order err:", err.Error())
|
||
logger.Errorf("CheckOrderState order_serial:", orderList[i].OrderSerial)
|
||
continue
|
||
}
|
||
break
|
||
} else if resp.ResultData[0].IsUnsub == 0 { // 没有退订
|
||
break
|
||
}
|
||
} else {
|
||
if j == 2 && resp.ReturnCode != "000010" && resp.Message != "服务调用失败" {
|
||
var cancelFlag int
|
||
subscribeTime := orderList[i].CreatedAt
|
||
unsubTime := time.Now().Format("2006-01-02 15:04:05")
|
||
// 检查 subscribeTime 是否为 nil
|
||
if IsWithinOneHourCancel(subscribeTime, unsubTime) {
|
||
cancelFlag = 1
|
||
}
|
||
|
||
err = database.Db.Table("mg_order").Where("order_serial = ?", orderList[i].OrderSerial).Updates(map[string]interface{}{
|
||
"state": UnsubscribeOK,
|
||
"is_one_hour_cancel": cancelFlag,
|
||
"unsubscribe_time": unsubTime,
|
||
"updated_at": time.Now(),
|
||
}).Error
|
||
if err != nil {
|
||
fmt.Println("CheckOrderState update mg_order err:", err.Error())
|
||
logger.Errorf("CheckOrderState update mg_order err:", err.Error())
|
||
logger.Errorf("CheckOrderState order_serial:", orderList[i].OrderSerial)
|
||
continue
|
||
}
|
||
break
|
||
}
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// CheckCancelOrderState 定时任务,检查历史退订阅用户是否有误判
|
||
func CheckCancelOrderState() {
|
||
if database.Db == nil {
|
||
log.Println("Database connection is nil")
|
||
fmt.Println("Database connection is nil")
|
||
return
|
||
}
|
||
|
||
// 查询订单列表已退订的用户
|
||
var orderList []MgOrder
|
||
|
||
// 获取当前时间前72个小时
|
||
threeDaysAgo := time.Now().Add(-72 * time.Hour)
|
||
|
||
err := database.Db.Where("state = 2").
|
||
Where("unsubscribe_time >= ?", threeDaysAgo).
|
||
Where("product_id = ?", ProductID).
|
||
Order("created_at desc").
|
||
Find(&orderList).Error
|
||
|
||
if err != nil {
|
||
fmt.Println("query mg_order err:", err.Error())
|
||
return
|
||
}
|
||
|
||
for i, _ := range orderList {
|
||
for j := 0; j < 5; j++ {
|
||
var req QueryRightsInfoReq
|
||
req.AppChannelList = append(req.AppChannelList, ChannelCode)
|
||
req.Mobile = orderList[i].PhoneNumber
|
||
|
||
resp, err := MiGuQueryRightsInfo(&req)
|
||
if err != nil {
|
||
fmt.Println("CheckOrderState MiGuQueryRightsInfo err:", err.Error())
|
||
logger.Errorf("CheckOrderState MiGuQueryRightsInfo err:", err.Error())
|
||
continue
|
||
}
|
||
|
||
// 有退订数据
|
||
if len(resp.ResultData) != 0 {
|
||
if resp.ResultData[0].IsUnsub == 1 { // 已退订
|
||
break
|
||
} else if resp.ResultData[0].IsUnsub == 0 { // 没有退订
|
||
fmt.Println("**********CheckCancelOrderState get:", orderList[i].PhoneNumber)
|
||
var unsubTime *time.Time = nil
|
||
err = database.Db.Table("mg_order").Where("order_serial = ?", orderList[i].OrderSerial).Updates(map[string]interface{}{
|
||
"state": SubscribeOK,
|
||
"is_one_hour_cancel": 0,
|
||
"unsubscribe_time": unsubTime,
|
||
"updated_at": time.Now(),
|
||
}).Error
|
||
break
|
||
}
|
||
} else {
|
||
if j == 1 {
|
||
break
|
||
}
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// CheckOneHourCancelOrderState 定时任务,检查1小时内退订的用户是否有误判
|
||
func CheckOneHourCancelOrderState() {
|
||
if database.Db == nil {
|
||
log.Println("Database connection is nil")
|
||
fmt.Println("Database connection is nil")
|
||
return
|
||
}
|
||
|
||
// 查询订单列表1小时内退订的用户
|
||
var orderList []MgOrder
|
||
|
||
//err := database.Db.Where("is_one_hour_cancel = 1 and created_at >= ? and created_at <= ?", "2024-11-01 00:00:00", "2024-11-30 23:59:59").Order("created_at desc").
|
||
// Find(&orderList).Error
|
||
|
||
err := database.Db.Where("is_one_hour_cancel = 1").Order("created_at desc").
|
||
Find(&orderList).Error
|
||
|
||
if err != nil {
|
||
fmt.Println("query mg_order err:", err.Error())
|
||
return
|
||
}
|
||
|
||
for i, _ := range orderList {
|
||
for j := 0; j < 5; j++ {
|
||
var req QueryRightsInfoReq
|
||
req.AppChannelList = append(req.AppChannelList, ChannelCode)
|
||
req.Mobile = orderList[i].PhoneNumber
|
||
|
||
resp, err := MiGuQueryRightsInfo(&req)
|
||
if err != nil {
|
||
fmt.Println("CheckOrderState MiGuQueryRightsInfo err:", err.Error())
|
||
logger.Errorf("CheckOrderState MiGuQueryRightsInfo err:", err.Error())
|
||
continue
|
||
}
|
||
|
||
// 有退订数据
|
||
if len(resp.ResultData) != 0 {
|
||
if resp.ResultData[0].IsUnsub == 1 { // 已退订
|
||
break
|
||
} else if resp.ResultData[0].IsUnsub == 0 { // 没有退订
|
||
fmt.Println("**********CheckCancelOrderState get:", orderList[i].PhoneNumber)
|
||
var unsubTime *time.Time = nil
|
||
err = database.Db.Table("mg_order").Where("order_serial = ?", orderList[i].OrderSerial).Updates(map[string]interface{}{
|
||
"state": SubscribeOK,
|
||
"is_one_hour_cancel": 0,
|
||
"unsubscribe_time": unsubTime,
|
||
"updated_at": time.Now(),
|
||
}).Error
|
||
break
|
||
}
|
||
} else {
|
||
if j == 1 {
|
||
break
|
||
}
|
||
continue
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ExportHistoricalSummaryToExcel 历史汇总数据导出excel
|
||
func ExportHistoricalSummaryToExcel(data []MgHistoricalSummary, db *gorm.DB) (string, error) {
|
||
// 创建一个新的Excel文件
|
||
file := excelize.NewFile()
|
||
sheet := "Sheet1"
|
||
|
||
// 设置标题栏
|
||
titles := []string{"日期", "产品ID", "渠道编码", "提交数", "新用户数", "提交成功率", "1小时退订数", "1小时退订率",
|
||
"当日退订数", "当日退订率", "24小时退订数", "24小时退订率", "累计退订数", "累计退订率"}
|
||
for i, title := range titles {
|
||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||
file.SetCellValue(sheet, cell, title)
|
||
}
|
||
|
||
// 设置所有单元格的样式: 居中、加边框
|
||
style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},
|
||
"border":[{"type":"left","color":"000000","style":1},
|
||
{"type":"top","color":"000000","style":1},
|
||
{"type":"right","color":"000000","style":1},
|
||
{"type":"bottom","color":"000000","style":1}]}`)
|
||
|
||
// 设置单元格高度
|
||
file.SetRowHeight(sheet, 1, 20)
|
||
|
||
// 设置列宽
|
||
file.SetColWidth(sheet, "A", "A", 15)
|
||
file.SetColWidth(sheet, "B", "B", 18)
|
||
file.SetColWidth(sheet, "C", "C", 18)
|
||
file.SetColWidth(sheet, "F", "F", 15)
|
||
file.SetColWidth(sheet, "G", "G", 15)
|
||
file.SetColWidth(sheet, "H", "H", 15)
|
||
file.SetColWidth(sheet, "I", "I", 15)
|
||
file.SetColWidth(sheet, "J", "J", 15)
|
||
file.SetColWidth(sheet, "K", "K", 15)
|
||
file.SetColWidth(sheet, "L", "L", 15)
|
||
file.SetColWidth(sheet, "M", "M", 15)
|
||
file.SetColWidth(sheet, "N", "N", 15)
|
||
|
||
// 创建一个产品ID到名称的映射
|
||
productMap := make(map[int64]string)
|
||
for _, order := range data {
|
||
if _, exists := productMap[order.ProductID]; !exists {
|
||
var product MgProduct
|
||
// 查询产品信息
|
||
if err := db.First(&product, order.ProductID).Error; err == nil {
|
||
productMap[order.ProductID] = product.Name
|
||
} else {
|
||
productMap[order.ProductID] = "未知产品"
|
||
}
|
||
}
|
||
}
|
||
|
||
// 填充数据
|
||
for i, record := range data {
|
||
row := i + 2
|
||
productName := productMap[record.ProductID] // 获取产品名称
|
||
file.SetCellValue(sheet, "A"+strconv.Itoa(row), record.Date[:10])
|
||
file.SetCellValue(sheet, "B"+strconv.Itoa(row), productName)
|
||
file.SetCellValue(sheet, "C"+strconv.Itoa(row), record.ChannelCode)
|
||
file.SetCellValue(sheet, "D"+strconv.Itoa(row), record.SubmissionCount)
|
||
file.SetCellValue(sheet, "E"+strconv.Itoa(row), record.NewUserCount)
|
||
file.SetCellValue(sheet, "F"+strconv.Itoa(row), record.SubmissionSuccessRate)
|
||
file.SetCellValue(sheet, "G"+strconv.Itoa(row), record.NewUserUnsubWithinHour)
|
||
file.SetCellValue(sheet, "H"+strconv.Itoa(row), record.NewUserUnsubWithinHourRate)
|
||
file.SetCellValue(sheet, "I"+strconv.Itoa(row), record.NewUserUnsubOnDay)
|
||
file.SetCellValue(sheet, "J"+strconv.Itoa(row), record.NewUserUnsubOnDayRate)
|
||
file.SetCellValue(sheet, "K"+strconv.Itoa(row), record.NewUserUnsubWithin24H)
|
||
file.SetCellValue(sheet, "L"+strconv.Itoa(row), record.NewUserUnsubWithin24HRate)
|
||
file.SetCellValue(sheet, "M"+strconv.Itoa(row), record.TotalNewUserUnsub)
|
||
file.SetCellValue(sheet, "N"+strconv.Itoa(row), record.TotalNewUserUnsubRate)
|
||
}
|
||
|
||
endRow := fmt.Sprintf("N%d", len(data)+1)
|
||
// 应用样式到整个表格
|
||
_ = file.SetCellStyle(sheet, "A1", endRow, style)
|
||
|
||
// 从配置文件读取保存路径和URL前缀
|
||
fileName := time.Now().Format("20060102150405") + "_历史汇总数据.xlsx"
|
||
url := MiGuExportUrl + fileName
|
||
|
||
// 保存Excel文件
|
||
if err := file.SaveAs(ExportFile + fileName); err != nil {
|
||
logger.Errorf("Failed to save Excel file: %v", err)
|
||
return "", err
|
||
}
|
||
|
||
return url, nil
|
||
}
|
||
|
||
// ExportRealtimeSummaryToExcel 当日汇总数据导出excel
|
||
func ExportRealtimeSummaryToExcel(data []MgRealtimeSummary, db *gorm.DB) (string, error) {
|
||
// 创建一个新的Excel文件
|
||
file := excelize.NewFile()
|
||
sheet := "Sheet1"
|
||
|
||
// 设置标题栏
|
||
titles := []string{"产品ID", "渠道编码", "提交数", "新用户数", "提交成功率", "1小时内退订数", "1小时内退订率", "当日退订数", "当日退订率"}
|
||
for i, title := range titles {
|
||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||
file.SetCellValue(sheet, cell, title)
|
||
}
|
||
|
||
// 设置所有单元格的样式: 居中、加边框
|
||
style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},
|
||
"border":[{"type":"left","color":"000000","style":1},
|
||
{"type":"top","color":"000000","style":1},
|
||
{"type":"right","color":"000000","style":1},
|
||
{"type":"bottom","color":"000000","style":1}]}`)
|
||
|
||
// 设置单元格高度
|
||
file.SetRowHeight(sheet, 1, 20)
|
||
|
||
// 设置列宽
|
||
file.SetColWidth(sheet, "A", "A", 18)
|
||
file.SetColWidth(sheet, "B", "B", 18)
|
||
file.SetColWidth(sheet, "E", "E", 15)
|
||
file.SetColWidth(sheet, "F", "F", 15)
|
||
file.SetColWidth(sheet, "G", "G", 15)
|
||
file.SetColWidth(sheet, "H", "H", 15)
|
||
file.SetColWidth(sheet, "I", "I", 15)
|
||
|
||
// 创建一个产品ID到名称的映射
|
||
productMap := make(map[int64]string)
|
||
for _, order := range data {
|
||
if _, exists := productMap[order.ProductID]; !exists {
|
||
var product MgProduct
|
||
// 查询产品信息
|
||
if err := db.First(&product, order.ProductID).Error; err == nil {
|
||
productMap[order.ProductID] = product.Name
|
||
} else {
|
||
productMap[order.ProductID] = "未知产品"
|
||
}
|
||
}
|
||
}
|
||
|
||
// 填充数据
|
||
for i, record := range data {
|
||
row := i + 2
|
||
productName := productMap[record.ProductID] // 获取产品名称
|
||
file.SetCellValue(sheet, "A"+strconv.Itoa(row), productName)
|
||
file.SetCellValue(sheet, "B"+strconv.Itoa(row), record.ChannelCode)
|
||
file.SetCellValue(sheet, "C"+strconv.Itoa(row), record.SubmissionCount)
|
||
file.SetCellValue(sheet, "D"+strconv.Itoa(row), record.NewUserCount)
|
||
file.SetCellValue(sheet, "E"+strconv.Itoa(row), record.SubmissionSuccessRate)
|
||
file.SetCellValue(sheet, "F"+strconv.Itoa(row), record.NewUserUnsubWithinHour)
|
||
file.SetCellValue(sheet, "G"+strconv.Itoa(row), record.NewUserUnsubWithinHourRate)
|
||
file.SetCellValue(sheet, "H"+strconv.Itoa(row), record.NewUserUnsubOnDay)
|
||
file.SetCellValue(sheet, "I"+strconv.Itoa(row), record.NewUserUnsubOnDayRate)
|
||
}
|
||
|
||
endRow := fmt.Sprintf("I%d", len(data)+1)
|
||
// 应用样式到整个表格
|
||
_ = file.SetCellStyle(sheet, "A1", endRow, style)
|
||
|
||
// 从配置文件读取保存路径和URL前缀
|
||
fileName := time.Now().Format("20060102150405") + "_当日实时汇总数据.xlsx"
|
||
url := MiGuExportUrl + fileName
|
||
|
||
// 保存Excel文件
|
||
if err := file.SaveAs(ExportFile + fileName); err != nil {
|
||
logger.Errorf("Failed to save Excel file: %v", err)
|
||
return "", err
|
||
}
|
||
|
||
return url, nil
|
||
}
|
||
|
||
// ExportOrderListToExcel 订单表导出excel
|
||
func ExportOrderListToExcel(orderList []MgOrder, db *gorm.DB) (string, error) {
|
||
// 创建一个新的Excel文件
|
||
file := excelize.NewFile()
|
||
sheet := "Sheet1"
|
||
|
||
// 设置标题栏
|
||
titles := []string{"产品", "渠道", "订单流水号", "手机号", "SM4手机号", "外部订单号", "渠道订单号", "是否1小时内退订", "用户订购状态", "订购时间", "退订时间"}
|
||
for i, title := range titles {
|
||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||
file.SetCellValue(sheet, cell, title)
|
||
}
|
||
|
||
// 设置所有单元格的样式: 居中、加边框
|
||
style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},
|
||
"border":[{"type":"left","color":"000000","style":1},
|
||
{"type":"top","color":"000000","style":1},
|
||
{"type":"right","color":"000000","style":1},
|
||
{"type":"bottom","color":"000000","style":1}]}`)
|
||
|
||
// 设置单元格高度
|
||
file.SetRowHeight(sheet, 1, 20)
|
||
|
||
// 设置列宽
|
||
file.SetColWidth(sheet, "A", "A", 18) // 产品
|
||
file.SetColWidth(sheet, "B", "B", 15) // 渠道
|
||
file.SetColWidth(sheet, "C", "C", 20) // 订单流水号
|
||
file.SetColWidth(sheet, "D", "D", 15) // 手机号
|
||
file.SetColWidth(sheet, "E", "E", 30) // SM4手机号
|
||
file.SetColWidth(sheet, "F", "F", 20) // 外部订单号
|
||
file.SetColWidth(sheet, "G", "G", 23) // 渠道订单号
|
||
file.SetColWidth(sheet, "H", "H", 15) // 是否1小时内退订
|
||
file.SetColWidth(sheet, "I", "I", 15) // 用户订购状态
|
||
file.SetColWidth(sheet, "J", "J", 20) // 订购时间
|
||
file.SetColWidth(sheet, "K", "K", 20) // 退订时间
|
||
|
||
// 创建一个产品ID到名称的映射
|
||
productMap := make(map[int64]string)
|
||
for _, order := range orderList {
|
||
if _, exists := productMap[order.ProductID]; !exists {
|
||
var product MgProduct
|
||
// 查询产品信息
|
||
if err := db.First(&product, order.ProductID).Error; err == nil {
|
||
productMap[order.ProductID] = product.Name
|
||
} else {
|
||
productMap[order.ProductID] = "未知产品"
|
||
}
|
||
}
|
||
}
|
||
|
||
// 填充数据
|
||
for i, order := range orderList {
|
||
row := i + 2
|
||
productName := productMap[order.ProductID] // 获取产品名称
|
||
file.SetCellValue(sheet, "A"+strconv.Itoa(row), productName)
|
||
file.SetCellValue(sheet, "B"+strconv.Itoa(row), order.ChannelCode)
|
||
file.SetCellValue(sheet, "C"+strconv.Itoa(row), order.OrderSerial)
|
||
file.SetCellValue(sheet, "D"+strconv.Itoa(row), order.PhoneNumber)
|
||
file.SetCellValue(sheet, "E"+strconv.Itoa(row), order.SM4PhoneNumber)
|
||
file.SetCellValue(sheet, "F"+strconv.Itoa(row), order.ExternalOrderID)
|
||
file.SetCellValue(sheet, "G"+strconv.Itoa(row), order.ChannelTradeNo)
|
||
|
||
// 判断时间是否为空,若为空则设置为""
|
||
if order.SubscribeTime != nil {
|
||
file.SetCellValue(sheet, "J"+strconv.Itoa(row), order.SubscribeTime.Format("2006-01-02 15:04:05"))
|
||
} else {
|
||
file.SetCellValue(sheet, "J"+strconv.Itoa(row), "")
|
||
}
|
||
|
||
// 判断退订时间是否为空,若为空则设置为""
|
||
if order.UnsubscribeTime != nil {
|
||
file.SetCellValue(sheet, "K"+strconv.Itoa(row), order.UnsubscribeTime.Format("2006-01-02 15:04:05"))
|
||
} else {
|
||
file.SetCellValue(sheet, "K"+strconv.Itoa(row), "")
|
||
}
|
||
|
||
// 退订状态转换为中文
|
||
subscriptionStatus := "未知状态"
|
||
if order.State == 1 {
|
||
subscriptionStatus = "订购成功"
|
||
} else if order.State == 2 {
|
||
subscriptionStatus = "已退订"
|
||
}
|
||
file.SetCellValue(sheet, "I"+strconv.Itoa(row), subscriptionStatus)
|
||
|
||
// 是否1小时内退订转换为中文
|
||
isOneHourCancel := "否"
|
||
if order.IsOneHourCancel == 1 {
|
||
isOneHourCancel = "是"
|
||
}
|
||
file.SetCellValue(sheet, "H"+strconv.Itoa(row), isOneHourCancel)
|
||
|
||
// 设置当前行的单元格样式
|
||
for col := 'A'; col <= 'K'; col++ {
|
||
cell, _ := excelize.CoordinatesToCellName(int(col-'A'+1), row)
|
||
file.SetCellStyle(sheet, cell, cell, style)
|
||
}
|
||
}
|
||
|
||
endRow := fmt.Sprintf("K%d", len(orderList)+1)
|
||
// 应用样式到整个表格
|
||
_ = file.SetCellStyle(sheet, "A1", endRow, style)
|
||
|
||
// 从配置文件读取保存路径和URL前缀
|
||
fileName := time.Now().Format("20060102150405") + "_订单列表.xlsx"
|
||
url := MiGuExportUrl + fileName
|
||
|
||
// 保存Excel文件
|
||
if err := file.SaveAs(ExportFile + fileName); err != nil {
|
||
logger.Errorf("Failed to save Excel file: %v", err)
|
||
return "", err
|
||
}
|
||
|
||
return url, nil
|
||
}
|
||
|
||
// ConvertStringToTime 将日期字符串转换为时间格式
|
||
func ConvertStringToTime(dateStr string) (string, error) {
|
||
// 解析输入字符串为时间
|
||
t, err := time.Parse("20060102", dateStr)
|
||
if err != nil {
|
||
return "", err // 返回错误
|
||
}
|
||
|
||
// 格式化为所需的时间格式
|
||
formattedTime := t.Format("2006-01-02 15:04:05")
|
||
return formattedTime, nil
|
||
}
|
||
|
||
// ExportHourSummaryToExcel 历史汇总查询(按小时)导出excel
|
||
func ExportHourSummaryToExcel(data []MgHourSummary, sumData TotalHourSummary, db *gorm.DB) (string, error) {
|
||
// 创建一个新的Excel文件
|
||
file := excelize.NewFile()
|
||
sheet := "Sheet1"
|
||
|
||
nExcelStartRow := 0
|
||
// 设置标题栏
|
||
titles := []string{"小时", "产品", "渠道", "提交数", "新用户数", "提交成功率", "1小时退订数", "1小时退订率",
|
||
"当日退订数", "当日退订率", "累计退订数", "累计退订率"}
|
||
for i, title := range titles {
|
||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||
file.SetCellValue(sheet, cell, title)
|
||
}
|
||
nExcelStartRow += 1
|
||
|
||
// 设置所有单元格的样式: 居中、加边框
|
||
style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},
|
||
"border":[{"type":"left","color":"000000","style":1},
|
||
{"type":"top","color":"000000","style":1},
|
||
{"type":"right","color":"000000","style":1},
|
||
{"type":"bottom","color":"000000","style":1}]}`)
|
||
|
||
// 设置单元格高度
|
||
file.SetRowHeight(sheet, 1, 20)
|
||
|
||
// 设置列宽
|
||
file.SetColWidth(sheet, "A", "A", 15)
|
||
file.SetColWidth(sheet, "B", "B", 18)
|
||
file.SetColWidth(sheet, "C", "C", 18)
|
||
file.SetColWidth(sheet, "F", "F", 15)
|
||
file.SetColWidth(sheet, "G", "G", 15)
|
||
file.SetColWidth(sheet, "H", "H", 15)
|
||
file.SetColWidth(sheet, "I", "I", 15)
|
||
file.SetColWidth(sheet, "J", "J", 15)
|
||
file.SetColWidth(sheet, "K", "K", 15)
|
||
file.SetColWidth(sheet, "L", "L", 15)
|
||
|
||
// 创建一个产品ID到名称的映射
|
||
productMap := make(map[int64]string)
|
||
for _, order := range data {
|
||
if _, exists := productMap[order.ProductID]; !exists {
|
||
var product MgProduct
|
||
// 查询产品信息
|
||
if err := db.First(&product, order.ProductID).Error; err == nil {
|
||
productMap[order.ProductID] = product.Name
|
||
} else {
|
||
productMap[order.ProductID] = "未知产品"
|
||
}
|
||
}
|
||
}
|
||
|
||
// 填充数据
|
||
for i, record := range data {
|
||
row := i + 2
|
||
productName := productMap[record.ProductID] // 获取产品名称
|
||
file.SetCellValue(sheet, "A"+strconv.Itoa(row), record.Hour)
|
||
file.SetCellValue(sheet, "B"+strconv.Itoa(row), productName)
|
||
file.SetCellValue(sheet, "C"+strconv.Itoa(row), record.ChannelCode)
|
||
file.SetCellValue(sheet, "D"+strconv.Itoa(row), record.SubmissionCount)
|
||
file.SetCellValue(sheet, "E"+strconv.Itoa(row), record.NewUserCount)
|
||
file.SetCellValue(sheet, "F"+strconv.Itoa(row), record.SubmissionSuccessRate)
|
||
file.SetCellValue(sheet, "G"+strconv.Itoa(row), record.NewUserUnsubWithinHour)
|
||
file.SetCellValue(sheet, "H"+strconv.Itoa(row), record.NewUserUnsubWithinHourRate)
|
||
file.SetCellValue(sheet, "I"+strconv.Itoa(row), record.NewUserUnsubOnDay)
|
||
file.SetCellValue(sheet, "J"+strconv.Itoa(row), record.NewUserUnsubOnDayRate)
|
||
file.SetCellValue(sheet, "K"+strconv.Itoa(row), record.TotalNewUserUnsub)
|
||
file.SetCellValue(sheet, "L"+strconv.Itoa(row), record.TotalNewUserUnsubRate)
|
||
}
|
||
|
||
nExcelStartRow += len(data)
|
||
|
||
totalData := "订单数:" + strconv.FormatInt(int64(len(data)), 10)
|
||
end := []interface{}{totalData, "", "",
|
||
sumData.SubmissionCount,
|
||
sumData.NewUserCount,
|
||
sumData.SubmissionSuccessRate,
|
||
sumData.NewUserUnsubWithinHour,
|
||
sumData.NewUserUnsubWithinHourRate,
|
||
sumData.NewUserUnsubOnDay,
|
||
sumData.NewUserUnsubOnDayRate,
|
||
sumData.TotalNewUserUnsub,
|
||
sumData.TotalNewUserUnsubRate,
|
||
}
|
||
for i, _ := range end {
|
||
cell, _ := excelize.CoordinatesToCellName(1+i, nExcelStartRow+1)
|
||
err := file.SetCellValue(sheet, cell, end[i])
|
||
if err != nil {
|
||
logger.Errorf("file set value err:", err)
|
||
}
|
||
}
|
||
|
||
endRow := fmt.Sprintf("L%d", nExcelStartRow+1)
|
||
// 应用样式到整个表格
|
||
_ = file.SetCellStyle(sheet, "A1", endRow, style)
|
||
|
||
// 从配置文件读取保存路径和URL前缀
|
||
fileName := time.Now().Format("20060102150405") + "_历史汇总(按小时).xlsx"
|
||
url := MiGuExportUrl + fileName
|
||
|
||
// 保存Excel文件
|
||
if err := file.SaveAs(ExportFile + fileName); err != nil {
|
||
logger.Errorf("Failed to save Excel file: %v", err)
|
||
return "", err
|
||
}
|
||
|
||
return url, nil
|
||
}
|
||
|
||
// ExportRevenueAnalysisToExcel 营收分析数据导出excel
|
||
func ExportRevenueAnalysisToExcel(data RetentionMonthsResp, db *gorm.DB) (string, error) {
|
||
// 创建一个新的Excel文件
|
||
file := excelize.NewFile()
|
||
sheet := "Sheet1"
|
||
|
||
// 设置标题栏
|
||
titles := []string{"年月", "新用户数", "当月新增有效用户数", "历史推广用户本月留存数", "总有效用户数"}
|
||
for i, title := range titles {
|
||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||
file.SetCellValue(sheet, cell, title)
|
||
}
|
||
|
||
// 设置所有单元格的样式: 居中、加边框
|
||
style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},
|
||
"border":[{"type":"left","color":"000000","style":1},
|
||
{"type":"top","color":"000000","style":1},
|
||
{"type":"right","color":"000000","style":1},
|
||
{"type":"bottom","color":"000000","style":1}]}`)
|
||
|
||
// 设置单元格高度
|
||
file.SetRowHeight(sheet, 1, 20)
|
||
|
||
// 设置列宽
|
||
file.SetColWidth(sheet, "A", "A", 15)
|
||
file.SetColWidth(sheet, "B", "B", 15)
|
||
file.SetColWidth(sheet, "C", "C", 20)
|
||
file.SetColWidth(sheet, "D", "D", 20)
|
||
file.SetColWidth(sheet, "E", "E", 18)
|
||
|
||
// 填充数据
|
||
for i, record := range data.MonthlyRetentionData {
|
||
row := i + 2
|
||
file.SetCellValue(sheet, "A"+strconv.Itoa(row), record.Month)
|
||
file.SetCellValue(sheet, "B"+strconv.Itoa(row), record.NewUserCount)
|
||
file.SetCellValue(sheet, "C"+strconv.Itoa(row), record.ValidUsersCount)
|
||
file.SetCellValue(sheet, "D"+strconv.Itoa(row), record.RetainedUsersCount)
|
||
file.SetCellValue(sheet, "E"+strconv.Itoa(row), record.TotalValidUsersCount)
|
||
}
|
||
|
||
// 填充合计行数据
|
||
file.SetCellValue(sheet, "A"+strconv.Itoa(len(data.MonthlyRetentionData)+2), "合计:")
|
||
file.SetCellValue(sheet, "B"+strconv.Itoa(len(data.MonthlyRetentionData)+2), data.TotalNewUsers)
|
||
file.SetCellValue(sheet, "C"+strconv.Itoa(len(data.MonthlyRetentionData)+2), "--")
|
||
file.SetCellValue(sheet, "D"+strconv.Itoa(len(data.MonthlyRetentionData)+2), "--")
|
||
file.SetCellValue(sheet, "E"+strconv.Itoa(len(data.MonthlyRetentionData)+2), data.TotalValidUsers)
|
||
|
||
endRow := fmt.Sprintf("E%d", len(data.MonthlyRetentionData)+2)
|
||
// 应用样式到整个表格
|
||
_ = file.SetCellStyle(sheet, "A1", endRow, style)
|
||
|
||
// 从配置文件读取保存路径和URL前缀
|
||
fileName := time.Now().Format("20060102150405") + "_营收分析.xlsx"
|
||
url := MiGuExportUrl + fileName
|
||
|
||
// 保存Excel文件
|
||
if err := file.SaveAs(ExportFile + fileName); err != nil {
|
||
logger.Errorf("Failed to save Excel file: %v", err)
|
||
return "", err
|
||
}
|
||
|
||
return url, nil
|
||
}
|
||
|
||
// CheckDateRange 检查时间间隔是否超过 n 天
|
||
func CheckDateRange(start, end string, nDay int) error {
|
||
if start == "" || end == "" {
|
||
return errors.New("导出失败,时间不能为空")
|
||
}
|
||
|
||
// 解析时间,假设时间格式为 "2006-01-02"
|
||
startTime, err := time.Parse(MiGuTimeFormat, start)
|
||
if err != nil {
|
||
return errors.New("开始时间格式错误")
|
||
}
|
||
endTime, err := time.Parse(MiGuTimeFormat, end)
|
||
if err != nil {
|
||
return errors.New("结束时间格式错误")
|
||
}
|
||
|
||
// 计算时间间隔
|
||
if endTime.Sub(startTime).Hours() > float64(nDay*24) {
|
||
return errors.New(fmt.Sprintf("导出时间间隔不能超过 %d 天", nDay))
|
||
}
|
||
|
||
return nil
|
||
}
|
||
|
||
// ExportUserRetentionToExcel 用户留存记录导出excel
|
||
func ExportUserRetentionToExcel(data []MgUserRetention, db *gorm.DB) (string, error) {
|
||
// 创建一个新的Excel文件
|
||
file := excelize.NewFile()
|
||
sheet := "Sheet1"
|
||
|
||
lastMonthFirstDay := time.Date(time.Now().Year(), time.Now().Month(), 1, 0, 0, 0, 0, time.Local) // 当前月1号
|
||
lastTwoMonthFirstDay := lastMonthFirstDay.AddDate(0, -1, 0) // 上个月1号
|
||
|
||
lastMonthCountTitle := lastMonthFirstDay.Format("2006-01-02") + "留存数"
|
||
lastMonthRateTitle := lastMonthFirstDay.Format("2006-01-02") + "留存率"
|
||
|
||
lastTwoMonthCountTitle := lastTwoMonthFirstDay.Format("2006-01-02") + "留存数"
|
||
lastTwoMonthRateTitle := lastTwoMonthFirstDay.Format("2006-01-02") + "留存率"
|
||
|
||
// 设置标题栏
|
||
titles := []string{"留存月份", "渠道", "产品", "新增数", "留存数", "留存率", lastTwoMonthCountTitle, lastTwoMonthRateTitle,
|
||
lastMonthCountTitle, lastMonthRateTitle}
|
||
for i, title := range titles {
|
||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||
file.SetCellValue(sheet, cell, title)
|
||
}
|
||
|
||
// 设置所有单元格的样式: 居中、加边框
|
||
style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},
|
||
"border":[{"type":"left","color":"000000","style":1},
|
||
{"type":"top","color":"000000","style":1},
|
||
{"type":"right","color":"000000","style":1},
|
||
{"type":"bottom","color":"000000","style":1}]}`)
|
||
|
||
// 设置单元格高度
|
||
file.SetRowHeight(sheet, 1, 20)
|
||
|
||
// 设置列宽
|
||
file.SetColWidth(sheet, "A", "A", 15)
|
||
file.SetColWidth(sheet, "B", "B", 15)
|
||
file.SetColWidth(sheet, "C", "C", 18)
|
||
file.SetColWidth(sheet, "D", "D", 15)
|
||
file.SetColWidth(sheet, "E", "E", 15)
|
||
file.SetColWidth(sheet, "F", "F", 15)
|
||
file.SetColWidth(sheet, "G", "G", 18)
|
||
file.SetColWidth(sheet, "H", "H", 18)
|
||
file.SetColWidth(sheet, "I", "I", 18)
|
||
file.SetColWidth(sheet, "J", "J", 18)
|
||
|
||
// 创建一个产品ID到名称的映射
|
||
productMap := make(map[int64]string)
|
||
for _, order := range data {
|
||
if _, exists := productMap[order.ProductID]; !exists {
|
||
var product MgProduct
|
||
// 查询产品信息
|
||
if err := db.First(&product, order.ProductID).Error; err == nil {
|
||
productMap[order.ProductID] = product.Name
|
||
} else {
|
||
productMap[order.ProductID] = "未知产品"
|
||
}
|
||
}
|
||
}
|
||
|
||
// 填充数据
|
||
for i, record := range data {
|
||
row := i + 2
|
||
productName := productMap[record.ProductID] // 获取产品名称
|
||
file.SetCellValue(sheet, "A"+strconv.Itoa(row), record.RetentionMonth)
|
||
file.SetCellValue(sheet, "B"+strconv.Itoa(row), record.ChannelCode)
|
||
file.SetCellValue(sheet, "C"+strconv.Itoa(row), productName)
|
||
file.SetCellValue(sheet, "D"+strconv.Itoa(row), record.NewUserCount)
|
||
file.SetCellValue(sheet, "E"+strconv.Itoa(row), record.RetainedUserCount)
|
||
file.SetCellValue(sheet, "F"+strconv.Itoa(row), record.RetentionRate)
|
||
file.SetCellValue(sheet, "G"+strconv.Itoa(row), record.LastTwoMonthRetentionCount)
|
||
file.SetCellValue(sheet, "H"+strconv.Itoa(row), record.LastTwoMonthRetentionRate)
|
||
file.SetCellValue(sheet, "I"+strconv.Itoa(row), record.LastMonthRetentionCount)
|
||
file.SetCellValue(sheet, "J"+strconv.Itoa(row), record.LastMonthRetentionRate)
|
||
}
|
||
|
||
endRow := fmt.Sprintf("J%d", len(data)+1)
|
||
// 应用样式到整个表格
|
||
_ = file.SetCellStyle(sheet, "A1", endRow, style)
|
||
|
||
// 从配置文件读取保存路径和URL前缀
|
||
fileName := time.Now().Format("20060102150405") + "_用户留存记录.xlsx"
|
||
url := MiGuExportUrl + fileName
|
||
|
||
// 保存Excel文件
|
||
if err := file.SaveAs(ExportFile + fileName); err != nil {
|
||
logger.Errorf("Failed to save Excel file: %v", err)
|
||
return "", err
|
||
}
|
||
|
||
return url, nil
|
||
}
|
||
|
||
// ExportTransactionToExcel 交易流水记录导出excel
|
||
func ExportTransactionToExcel(data []MgTransactionLog, db *gorm.DB) (string, error) {
|
||
// 创建一个新的Excel文件
|
||
file := excelize.NewFile()
|
||
sheet := "Sheet1"
|
||
|
||
// 设置标题栏
|
||
titles := []string{"调用时间", "产品", "渠道", "手机号", "平台订单号", "外部平台订单号", "渠道订单号", "结果", "原因",
|
||
"验证码", "订单时间"}
|
||
for i, title := range titles {
|
||
cell, _ := excelize.CoordinatesToCellName(i+1, 1)
|
||
file.SetCellValue(sheet, cell, title)
|
||
}
|
||
|
||
// 设置所有单元格的样式: 居中、加边框
|
||
style, _ := file.NewStyle(`{"alignment":{"horizontal":"center","vertical":"center"},
|
||
"border":[{"type":"left","color":"000000","style":1},
|
||
{"type":"top","color":"000000","style":1},
|
||
{"type":"right","color":"000000","style":1},
|
||
{"type":"bottom","color":"000000","style":1}]}`)
|
||
|
||
// 设置单元格高度
|
||
file.SetRowHeight(sheet, 1, 20)
|
||
|
||
// 设置列宽
|
||
file.SetColWidth(sheet, "A", "A", 18)
|
||
file.SetColWidth(sheet, "B", "B", 18)
|
||
file.SetColWidth(sheet, "C", "C", 15)
|
||
file.SetColWidth(sheet, "D", "D", 15)
|
||
file.SetColWidth(sheet, "E", "E", 20)
|
||
file.SetColWidth(sheet, "F", "F", 20)
|
||
file.SetColWidth(sheet, "G", "G", 28)
|
||
file.SetColWidth(sheet, "I", "I", 28)
|
||
file.SetColWidth(sheet, "K", "K", 18)
|
||
|
||
// 创建一个产品ID到名称的映射
|
||
productMap := make(map[int64]string)
|
||
for _, order := range data {
|
||
if _, exists := productMap[order.ProductID]; !exists {
|
||
var product MgProduct
|
||
// 查询产品信息
|
||
if err := db.First(&product, order.ProductID).Error; err == nil {
|
||
productMap[order.ProductID] = product.Name
|
||
} else {
|
||
productMap[order.ProductID] = "未知产品"
|
||
}
|
||
}
|
||
}
|
||
|
||
// 填充数据
|
||
for i, record := range data {
|
||
row := i + 2
|
||
var orderTime string
|
||
if record.OrderTime == nil {
|
||
orderTime = ""
|
||
} else {
|
||
orderTime = record.OrderTime.Format(MiGuTimeFormat)
|
||
}
|
||
productName := productMap[record.ProductID] // 获取产品名称
|
||
file.SetCellValue(sheet, "A"+strconv.Itoa(row), record.CreatedAt.Format(MiGuTimeFormat))
|
||
file.SetCellValue(sheet, "B"+strconv.Itoa(row), productName)
|
||
file.SetCellValue(sheet, "C"+strconv.Itoa(row), record.ChannelCode)
|
||
file.SetCellValue(sheet, "D"+strconv.Itoa(row), record.PhoneNumber)
|
||
file.SetCellValue(sheet, "E"+strconv.Itoa(row), record.OutTradeNo)
|
||
file.SetCellValue(sheet, "F"+strconv.Itoa(row), record.LinkId)
|
||
file.SetCellValue(sheet, "G"+strconv.Itoa(row), record.ChannelTradeNo)
|
||
file.SetCellValue(sheet, "H"+strconv.Itoa(row), record.Result)
|
||
file.SetCellValue(sheet, "I"+strconv.Itoa(row), record.Reason)
|
||
file.SetCellValue(sheet, "J"+strconv.Itoa(row), record.VerificationCode)
|
||
file.SetCellValue(sheet, "K"+strconv.Itoa(row), orderTime)
|
||
}
|
||
|
||
endRow := fmt.Sprintf("K%d", len(data)+2)
|
||
// 应用样式到整个表格
|
||
_ = file.SetCellStyle(sheet, "A1", endRow, style)
|
||
|
||
// 从配置文件读取保存路径和URL前缀
|
||
fileName := time.Now().Format("20060102150405") + "_交易流水记录.xlsx"
|
||
url := MiGuExportUrl + fileName
|
||
|
||
// 保存Excel文件
|
||
if err := file.SaveAs(ExportFile + fileName); err != nil {
|
||
logger.Errorf("Failed to save Excel file: %v", err)
|
||
return "", err
|
||
}
|
||
|
||
return url, nil
|
||
}
|
||
|
||
type MonthlyEffectiveUserStats struct {
|
||
Date string `json:"date"` // 留存日期(格式:YYYY-MM-DD)
|
||
NewUserCount int `json:"new_user_count"`
|
||
ValidUserCount int `json:"valid_user_count"`
|
||
EffectiveRate string `json:"effective_rate"` // 百分比格式:如 "85.63%"
|
||
UnsubscribedToday int64 `json:"unsubscribed_today"`
|
||
}
|
||
|
||
// GetMonthlyEffectiveUserStats 获取某月份有效用户统计
|
||
func GetMonthlyEffectiveUserStats(db *gorm.DB, retentionMonth string, skuCode int, channelCode string) (*MonthlyEffectiveUserStats, error) {
|
||
var stats MonthlyEffectiveUserStats
|
||
|
||
// 解析月份时间范围
|
||
startTime, err := time.Parse("2006-01", retentionMonth)
|
||
if err != nil {
|
||
return nil, fmt.Errorf("invalid retentionMonth format: %v", err)
|
||
}
|
||
endTime := startTime.AddDate(0, 1, 0) // 下个月
|
||
lastDay := endTime.AddDate(0, 0, -1) // 当前月的最后一天
|
||
lastDayStr := lastDay.Format("2006-01-02") // 格式化为字符串
|
||
|
||
// 设置返回的留存日期字段
|
||
stats.Date = lastDayStr
|
||
|
||
// 查询每月新用户数和有效用户数
|
||
var monthlyData []struct {
|
||
Month string
|
||
NewUserCount int
|
||
ValidUsersCount int
|
||
}
|
||
err = db.Model(&MgOrder{}).
|
||
Select(`DATE_FORMAT(subscribe_time, '%Y-%m') AS month,
|
||
COUNT(DISTINCT phone_number) AS new_user_count,
|
||
COUNT(DISTINCT CASE
|
||
WHEN unsubscribe_time IS NULL OR TIMESTAMPDIFF(HOUR, subscribe_time, unsubscribe_time) > 24
|
||
THEN phone_number END) AS valid_users_count`).
|
||
Where("product_id = ?", skuCode).
|
||
Where("channel_code = ?", channelCode).
|
||
Group("month").
|
||
Order("month").
|
||
Find(&monthlyData).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
// 匹配当前月份的数据
|
||
for _, data := range monthlyData {
|
||
if data.Month == retentionMonth {
|
||
stats.NewUserCount = data.NewUserCount
|
||
stats.ValidUserCount = data.ValidUsersCount
|
||
break
|
||
}
|
||
}
|
||
|
||
// 计算有效用户率
|
||
if stats.NewUserCount > 0 {
|
||
rate := float64(stats.ValidUserCount) / float64(stats.NewUserCount) * 100
|
||
stats.EffectiveRate = fmt.Sprintf("%.2f%%", rate)
|
||
} else {
|
||
stats.EffectiveRate = "0.00%"
|
||
}
|
||
|
||
// 查询今天的退订用户数
|
||
err = db.Model(&MgOrder{}).
|
||
Where("unsubscribe_time >= ? AND unsubscribe_time <= ?", lastDayStr+" 00:00:00", lastDayStr+" 23:59:59").
|
||
Where("state = 2").
|
||
Where("product_id = ?", skuCode).
|
||
Where("channel_code = ?", channelCode).
|
||
Count(&stats.UnsubscribedToday).Error
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return &stats, nil
|
||
}
|
||
|
||
func UpdateHistoricalSummaryCache() {
|
||
logger.Info("****** UpdateHistoricalSummaryCache start ******")
|
||
fmt.Println("****** UpdateHistoricalSummaryCache start ******")
|
||
if database.Db == nil {
|
||
logger.Error("Database connection is nil")
|
||
fmt.Println("Database connection is nil")
|
||
return
|
||
}
|
||
|
||
startTime := "1970-01-01 00:00:00"
|
||
endTime := time.Now().AddDate(0, 0, -1).Format("2006-01-02") + " 23:59:59"
|
||
|
||
// 1. 查询某天的完整实时数据
|
||
summaries, err := CalculateDailySummaryFromRealtime(database.Db, startTime, endTime, "", 0)
|
||
if err != nil {
|
||
log.Printf("calculateDailySummaryFromRealtime failed: %v", err)
|
||
return
|
||
}
|
||
|
||
if len(summaries) == 0 {
|
||
return
|
||
}
|
||
|
||
tx := database.Db.Begin()
|
||
defer func() {
|
||
if r := recover(); r != nil {
|
||
tx.Rollback()
|
||
}
|
||
}()
|
||
|
||
// 2. 清空表中的所有数据
|
||
if err = tx.Exec("TRUNCATE TABLE mg_historical_summary").Error; err != nil {
|
||
logger.Error("TRUNCATE TABLE mg_historical_summary error:", err)
|
||
fmt.Println("TRUNCATE TABLE mg_historical_summary error")
|
||
tx.Rollback()
|
||
return
|
||
}
|
||
|
||
// 3. 插入新数据到缓存表
|
||
if err = tx.Create(&summaries).Error; err != nil {
|
||
logger.Error("UpdateHistoricalSummaryCache Create error:", err)
|
||
fmt.Println("UpdateHistoricalSummaryCache Create error")
|
||
tx.Rollback()
|
||
return
|
||
}
|
||
|
||
err = tx.Commit().Error
|
||
if err != nil {
|
||
logger.Error("UpdateHistoricalSummaryCache Commit error:", err)
|
||
fmt.Println("UpdateHistoricalSummaryCache Commit error")
|
||
return
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
func CalculateDailySummaryFromRealtime(db *gorm.DB, startTime, endTime, channelCode string, productID int) ([]MgHistoricalSummary, error) {
|
||
|
||
var results []MgHistoricalSummary
|
||
|
||
qs := db.Model(&MgOrder{}).
|
||
Select(`
|
||
DATE_FORMAT(mg_order.subscribe_time, '%Y-%m-%d') AS date,
|
||
mg_order.product_id,
|
||
mg_order.channel_code,
|
||
IFNULL(submission_count.submission_count, 0) AS submission_count,
|
||
COUNT(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 END) AS new_user_unsub_within_hour,
|
||
COUNT(CASE WHEN mg_order.state = 2 AND DATE(mg_order.unsubscribe_time) = DATE(mg_order.subscribe_time) THEN 1 END) AS new_user_unsub_on_day,
|
||
COUNT(CASE WHEN mg_order.state = 2 AND TIMESTAMPDIFF(HOUR, mg_order.subscribe_time, mg_order.unsubscribe_time) <= 24 THEN 1 END) AS new_user_unsub_within_24h,
|
||
COUNT(*) AS new_user_count,
|
||
SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) AS total_new_user_unsub,
|
||
CONCAT(ROUND(COUNT(CASE WHEN mg_order.is_one_hour_cancel = 1 THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_within_hour_rate,
|
||
CONCAT(ROUND(COUNT(CASE WHEN mg_order.state = 2 AND DATE(mg_order.unsubscribe_time) = DATE(mg_order.subscribe_time) THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_on_day_rate,
|
||
CONCAT(ROUND(COUNT(CASE WHEN mg_order.state = 2 AND TIMESTAMPDIFF(HOUR, mg_order.subscribe_time, mg_order.unsubscribe_time) <= 24 THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS new_user_unsub_within_24h_rate,
|
||
CONCAT(ROUND(SUM(CASE WHEN mg_order.state = 2 THEN 1 ELSE 0 END) * 100.0 / NULLIF(COUNT(*), 0), 2), '%') AS total_new_user_unsub_rate,
|
||
IFNULL(
|
||
CONCAT(
|
||
LEAST(
|
||
ROUND(COUNT(*) * 100.0 / NULLIF(submission_count.submission_count, 0), 2),
|
||
100.00
|
||
),
|
||
'%'
|
||
),
|
||
'0.00%'
|
||
) AS submission_success_rate
|
||
`).
|
||
// 使用 Joins 来替代 LeftJoin 进行左连接
|
||
Joins(`
|
||
LEFT JOIN (
|
||
SELECT
|
||
channel_code,
|
||
DATE(created_at) AS created_date,
|
||
COUNT(*) AS submission_count
|
||
FROM mg_transaction_log
|
||
WHERE verification_code != ''
|
||
GROUP BY channel_code, created_date
|
||
) AS submission_count
|
||
ON submission_count.channel_code = mg_order.channel_code
|
||
AND submission_count.created_date = DATE(mg_order.subscribe_time)
|
||
`).
|
||
Where("mg_order.subscribe_time BETWEEN ? AND ?", startTime, endTime).
|
||
Group("DATE(mg_order.subscribe_time), mg_order.product_id, mg_order.channel_code").
|
||
Order("mg_order.subscribe_time DESC")
|
||
|
||
// 🔍 条件筛选:渠道
|
||
if channelCode != "" {
|
||
qs = qs.Where("mg_order.channel_code = ?", channelCode)
|
||
}
|
||
|
||
// 🔍 条件筛选:产品
|
||
if productID != 0 {
|
||
qs = qs.Where("mg_order.product_id = ?", productID)
|
||
}
|
||
|
||
err := qs.Find(&results).Error
|
||
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return results, nil
|
||
}
|