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-是 其他-否 } // MgOrderLog 订单管理表对应的结构体 type MgOrderLog 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 }