Compare commits
9
Commits
01d9f7bc86
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
37757d6aa5 | ||
|
|
e1674ec96b | ||
|
|
3c8cefbb2a | ||
|
|
3fd3e82a01 | ||
|
|
3d9bdb6f6e | ||
|
|
22def0b5cb | ||
|
|
4cdca06289 | ||
|
|
836e42b364 | ||
|
|
0deb1bbedc |
@@ -14,6 +14,7 @@ type AdvitiseInfoListReq struct {
|
||||
Title string `json:"title" form:"title"`
|
||||
Remark string `json:"remark" form:"remark"`
|
||||
Status *bool `json:"status" form:"status"`
|
||||
OrderBy string `json:"orderBy" form:"orderBy"` // 排序方式:sortAsc(默认,从小到大) / sortDesc(从大到小) / newest(最新)
|
||||
PageNum int64 `json:"pageNum" form:"pageNum" binding:"required,gte=1"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize" binding:"required,gte=1,lte=500"`
|
||||
}
|
||||
@@ -37,6 +38,7 @@ type AddAdvitiseInfoReq struct {
|
||||
RandomNames []string `json:"randomNames"`
|
||||
Link string `json:"link"`
|
||||
Status bool `json:"status"`
|
||||
Sort int64 `json:"sort"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
Remark string `json:"remark"`
|
||||
@@ -58,6 +60,7 @@ type UpdateAdvitiseInfoReq struct {
|
||||
RandomNames *[]string `json:"randomNames"`
|
||||
Link *string `json:"link"`
|
||||
Status *bool `json:"status"`
|
||||
Sort *int64 `json:"sort"`
|
||||
StartTime *time.Time `json:"startTime"`
|
||||
EndTime *time.Time `json:"endTime"`
|
||||
Remark *string `json:"remark"`
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package proto
|
||||
|
||||
import (
|
||||
"stat/models/refundOrderMod"
|
||||
)
|
||||
|
||||
// ============ 退款订单管理 ============
|
||||
|
||||
// RefundOrderListReq 退款订单查询。所有过滤条件均为可选,不传表示不限制
|
||||
type RefundOrderListReq struct {
|
||||
AppId *int32 `json:"appId" form:"appId"` // appId
|
||||
UserId *uint64 `json:"userId" form:"userId"` // 用户ID
|
||||
CID *string `json:"cid" form:"cid"` // 订单id
|
||||
TransNo *string `json:"transNo" form:"transNo"` // 请求订单号
|
||||
IP *string `json:"ip" form:"ip"` // 下单用户IP
|
||||
ChannelName *string `json:"channelName" form:"channelName"` // 支付渠道名字
|
||||
DistrictCode *string `json:"districtCode" form:"districtCode"` // 渠道码
|
||||
PageNum int64 `json:"pageNum" form:"pageNum" binding:"required,gte=1"`
|
||||
PageSize int64 `json:"pageSize" form:"pageSize" binding:"required,gte=1,lte=500"`
|
||||
}
|
||||
|
||||
type RefundOrderListResp struct {
|
||||
Total int64 `json:"total"`
|
||||
TotalMoney int64 `json:"totalMoney"` // 符合当前筛选条件的共退款金额(单位:分),非仅当前页
|
||||
List refundOrderMod.RefundOrderList `json:"list"`
|
||||
}
|
||||
|
||||
// RefundOrderItem 一笔待回填的订单。
|
||||
// cid 用于查 log_recharge 回填数据;transNo 仅作留痕,便于按原始请求订单号追溯。
|
||||
type RefundOrderItem struct {
|
||||
CID string `json:"cid" binding:"required"` // 订单id
|
||||
TransNo string `json:"transNo"` // 请求订单号,可空
|
||||
IP string `json:"ip"` // 下单用户IP,可空
|
||||
}
|
||||
|
||||
// AddRefundOrderReq 单条新增
|
||||
type AddRefundOrderReq struct {
|
||||
CID string `json:"cid" binding:"required"`
|
||||
TransNo string `json:"transNo"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// BatchAddRefundOrderReq 批量新增
|
||||
type BatchAddRefundOrderReq struct {
|
||||
Items []RefundOrderItem `json:"items" binding:"required,min=1,max=200"`
|
||||
}
|
||||
|
||||
// AddRefundOrderResp 回填结果
|
||||
type AddRefundOrderResp struct {
|
||||
Matched int64 `json:"matched"` // log_recharge 命中的成功订单数
|
||||
Inserted int64 `json:"inserted"` // 实际新增条数
|
||||
Skipped int64 `json:"skipped"` // 因已存在而跳过的条数
|
||||
Details []RefundCidStat `json:"details"` // 按 cid 分项,单条新增时只有一项
|
||||
}
|
||||
|
||||
type RefundCidStat struct {
|
||||
CID string `json:"cid"`
|
||||
TransNo string `json:"transNo"`
|
||||
Matched int64 `json:"matched"`
|
||||
Inserted int64 `json:"inserted"`
|
||||
Skipped int64 `json:"skipped"`
|
||||
}
|
||||
|
||||
// DelRefundOrderReq 按 _id 删除
|
||||
type DelRefundOrderReq struct {
|
||||
Id string `json:"id" binding:"required"` // 记录 _id (24位十六进制)
|
||||
}
|
||||
+20
-17
@@ -87,14 +87,15 @@ type TfInfo struct {
|
||||
|
||||
// 返回下载链接
|
||||
type PageButtonClickResp struct {
|
||||
VersionNum string `from:"versionNum" json:"versionNum"` //版本号
|
||||
Terminal string `from:"terminal" json:"terminal"` //终端ios android light
|
||||
Link []string `from:"link" json:"link"` //下载链接
|
||||
Tf []TfInfo `from:"tf" json:"tf"` //TFinfo
|
||||
BackUrl string `bson:"backUrl" json:"BackUrl"` //企业签URL (不用)
|
||||
PackageType versionMod.PkgType `from:"packageType" json:"packageType" binding:"required"` //包类型 1:企业包 2:商店包 3:TF包
|
||||
Status bool `from:"status" json:"status"` //是否启用
|
||||
LightData []byte `from:"lightData" json:"lightData"` //如果是轻量版,返回数据
|
||||
VersionNum string `from:"versionNum" json:"versionNum"` //版本号
|
||||
Terminal string `from:"terminal" json:"terminal"` //终端ios android light
|
||||
Link []string `from:"link" json:"link"` //下载链接
|
||||
Tf []TfInfo `from:"tf" json:"tf"` //TFinfo
|
||||
BackUrl string `bson:"backUrl" json:"BackUrl"` //企业签URL (不用)
|
||||
PackageType versionMod.PkgType `from:"packageType" json:"packageType" binding:"required"` //包类型 1:企业包 2:商店包 3:TF包
|
||||
Status bool `from:"status" json:"status"` //是否启用
|
||||
LightData []byte `from:"lightData" json:"lightData"` //如果是轻量版,返回数据
|
||||
ChannelApkAndDownUrl string `json:"channelApkAndDownUrl"` //双端下载配置里该渠道的Android下载地址,有则客户端优先用它
|
||||
}
|
||||
|
||||
// 落地页获取显示的按钮信息(appid标示具体应用)
|
||||
@@ -107,15 +108,17 @@ type PageButtonListReq struct {
|
||||
|
||||
// 落地页获取显示的按钮信息
|
||||
type PageButtonListResp struct {
|
||||
IosDown bool `json:"iosDown"` // 显示完整版,是企业包
|
||||
IosStore bool `json:"iosStore"` // 显示商店包
|
||||
IosTF bool `json:"iosTF"` // TF包
|
||||
IosH5 bool `json:"iosH5"` // H5跳转链接
|
||||
IosLink string `json:"iosLink"` // 商店包下载链接
|
||||
AndroidButton int `json:"androidButton"` // android下载按钮
|
||||
HasChannelApk bool `json:"hasChannelApk"` // 有安卓渠道包
|
||||
PictureList []PageButtonPicture `json:"pictureList"` // 资源数据
|
||||
Contact ContactInfo `json:"contact"` // 联系方式
|
||||
IosDown bool `json:"iosDown"` // 显示完整版,是企业包
|
||||
IosStore bool `json:"iosStore"` // 显示商店包
|
||||
IosTF bool `json:"iosTF"` // TF包
|
||||
IosH5 bool `json:"iosH5"` // H5跳转链接
|
||||
IosLink string `json:"iosLink"` // 商店包下载链接
|
||||
AndroidButton int `json:"androidButton"` // android下载按钮
|
||||
HasChannelApk bool `json:"hasChannelApk"` // 有安卓渠道包
|
||||
// 双端下载配置里该渠道的 Android 下载地址,有则落地页优先用它
|
||||
ChannelApkAndDownUrl string `json:"channelApkAndDownUrl"`
|
||||
PictureList []PageButtonPicture `json:"pictureList"` // 资源数据
|
||||
Contact ContactInfo `json:"contact"` // 联系方式
|
||||
}
|
||||
|
||||
type PageButtonPicture struct {
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
package refundOrderApi
|
||||
|
||||
import (
|
||||
"stat/api/proto"
|
||||
"stat/common/stdLog"
|
||||
"stat/common/utils"
|
||||
"stat/service/opLogSer"
|
||||
"stat/service/refundOrderSer"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags 退款订单管理
|
||||
// @Summary 退款订单列表
|
||||
// @Description 支持 appId / userId / cid / channelName / districtCode 任意组合过滤
|
||||
// @Router /api/stat/refundOrder/list [POST]
|
||||
// @Param param body proto.RefundOrderListReq true "参数"
|
||||
// @Success 200 {object} proto.RefundOrderListResp "成功"
|
||||
// @Security ApiKeyAuth
|
||||
func List(ctx *gin.Context) {
|
||||
req := &proto.RefundOrderListReq{}
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := refundOrderSer.List(ctx, req)
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
stdLog.ServerJSON(ctx, stdLog.Success, resp)
|
||||
}
|
||||
|
||||
// @Tags 退款订单管理
|
||||
// @Summary 新增退款订单
|
||||
// @Description 传入 cid,查询 log_recharge 中该渠道付款成功的订单并逐条回填
|
||||
// @Router /api/stat/refundOrder/add [POST]
|
||||
// @Param param body proto.AddRefundOrderReq true "参数"
|
||||
// @Success 200 {object} proto.AddRefundOrderResp "成功"
|
||||
// @Security ApiKeyAuth
|
||||
func Add(ctx *gin.Context) {
|
||||
account, err := utils.GetAdmin(ctx)
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error())
|
||||
return
|
||||
}
|
||||
req := &proto.AddRefundOrderReq{}
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := refundOrderSer.Add(ctx, req)
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
opLogSer.AddOpLog(ctx, account, true, req)
|
||||
stdLog.ServerJSON(ctx, stdLog.Success, resp)
|
||||
}
|
||||
|
||||
// @Tags 退款订单管理
|
||||
// @Summary 批量新增退款订单
|
||||
// @Description 传入 cid 列表,逐个查询 log_recharge 回填
|
||||
// @Router /api/stat/refundOrder/batchAdd [POST]
|
||||
// @Param param body proto.BatchAddRefundOrderReq true "参数"
|
||||
// @Success 200 {object} proto.AddRefundOrderResp "成功"
|
||||
// @Security ApiKeyAuth
|
||||
func BatchAdd(ctx *gin.Context) {
|
||||
account, err := utils.GetAdmin(ctx)
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error())
|
||||
return
|
||||
}
|
||||
req := &proto.BatchAddRefundOrderReq{}
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error())
|
||||
return
|
||||
}
|
||||
resp, err := refundOrderSer.BatchAdd(ctx, req)
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
opLogSer.AddOpLog(ctx, account, true, req)
|
||||
stdLog.ServerJSON(ctx, stdLog.Success, resp)
|
||||
}
|
||||
|
||||
// @Tags 退款订单管理
|
||||
// @Summary 删除退款订单
|
||||
// @Description 按记录 _id 删除
|
||||
// @Router /api/stat/refundOrder/del [POST]
|
||||
// @Param param body proto.DelRefundOrderReq true "参数"
|
||||
// @Success 200 {string} string "成功"
|
||||
// @Security ApiKeyAuth
|
||||
func Del(ctx *gin.Context) {
|
||||
account, err := utils.GetAdmin(ctx)
|
||||
if err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.NoToken, err.Error())
|
||||
return
|
||||
}
|
||||
req := &proto.DelRefundOrderReq{}
|
||||
if err := ctx.ShouldBindJSON(req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.ParamError, err.Error())
|
||||
return
|
||||
}
|
||||
if err := refundOrderSer.Del(ctx, req); err != nil {
|
||||
stdLog.ServerJSON(ctx, stdLog.Failure, err.Error())
|
||||
return
|
||||
}
|
||||
opLogSer.AddOpLog(ctx, account, true, req)
|
||||
stdLog.ServerJSON(ctx, stdLog.Success, "")
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
@@ -25,11 +25,11 @@
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Web Clip</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.apple.webClip.managed.7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E</string>
|
||||
<string>com.apple.webClip.managed.a94a8343-410a-5dae-8bcf-c9952b0869c5</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.webClip.managed</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E</string>
|
||||
<string>a94a8343-410a-5dae-8bcf-c9952b0869c5</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>Precomposed</key>
|
||||
@@ -47,7 +47,7 @@
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>91fans</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>luli.C10D036B-C38C-4830-9767-D68130AEFA78</string>
|
||||
<string>luli.2648743f-e9a9-5142-97f1-6c4cb18e9def</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>91fans</string>
|
||||
<key>PayloadRemovalDisallowed</key>
|
||||
@@ -55,7 +55,7 @@
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>FBB5F556-C0EF-4DBC-AFB5-558AA3662493</string>
|
||||
<string>2648743f-e9a9-5142-97f1-6c4cb18e9def</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
|
||||
|
||||
|
||||
{
|
||||
"_id" : ObjectId("6044d1202672fbdb5b058c45"),
|
||||
"appid" : NumberInt(6),
|
||||
"job" : "rechargeAll",
|
||||
"platformId" : "5fe6c04e8bf6f35ff64cec58",
|
||||
"createdAt" : ISODate("2020-09-21T14:02:48.943+0000"),
|
||||
"successAt" : ISODate("2026-10-14T09:29:55.667+0000"),
|
||||
"updatedAt" : ISODate("2026-10-14T09:34:04.265+0000"),
|
||||
"userId" : NumberLong(1146953)
|
||||
}
|
||||
{
|
||||
"_id" : ObjectId("6044d11d2672fbdb5b058c66"),
|
||||
"appid" : NumberInt(6),
|
||||
"job" : "registerLight",
|
||||
"platformId" : "",
|
||||
"createdAt" : ISODate("2020-09-21T14:02:48.943+0000"),
|
||||
"successAt" : ISODate("2026-09-10T17:47:32.528+0000"),
|
||||
"updatedAt" : ISODate("2026-09-10T17:47:35.114+0000"),
|
||||
"userId" : NumberLong(14009799),
|
||||
"disabled" : false
|
||||
}
|
||||
@@ -25,11 +25,11 @@
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Web Clip</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.apple.webClip.managed.7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E</string>
|
||||
<string>com.apple.webClip.managed.731dee7e-0d49-5b5d-adb7-94c7a9f9c5be</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.webClip.managed</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E</string>
|
||||
<string>731dee7e-0d49-5b5d-adb7-94c7a9f9c5be</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>Precomposed</key>
|
||||
@@ -47,7 +47,7 @@
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>哔哩视频</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>luli.C10D036B-C38C-4830-9767-D68130AEFA78</string>
|
||||
<string>luli.da9e9196-fdd7-5868-b8aa-ea90bfdfb639</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>哔哩视频</string>
|
||||
<key>PayloadRemovalDisallowed</key>
|
||||
@@ -55,7 +55,7 @@
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>FBB5F556-C0EF-4DBC-AFB5-558AA3662493</string>
|
||||
<string>da9e9196-fdd7-5868-b8aa-ea90bfdfb639</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -25,11 +25,11 @@
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>Web Clip</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>com.apple.webClip.managed.7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E</string>
|
||||
<string>com.apple.webClip.managed.b60dfccf-b54d-5b20-81ee-e2b49e961748</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.webClip.managed</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E</string>
|
||||
<string>b60dfccf-b54d-5b20-81ee-e2b49e961748</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>Precomposed</key>
|
||||
@@ -47,7 +47,7 @@
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>黄果短剧</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>luli.C10D036B-C38C-4830-9767-D68130AEFA78</string>
|
||||
<string>luli.10270f90-fa18-5df7-8262-b036b7bd11cb</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>黄果短剧</string>
|
||||
<key>PayloadRemovalDisallowed</key>
|
||||
@@ -55,7 +55,7 @@
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>FBB5F556-C0EF-4DBC-AFB5-558AA3662493</string>
|
||||
<string>10270f90-fa18-5df7-8262-b036b7bd11cb</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -20,10 +20,31 @@ func Init(ctx context.Context) {
|
||||
{Key: FN.AdvitiseInfo.Status, Value: 1},
|
||||
}},
|
||||
{Keys: bson.D{{Key: FN.AdvitiseInfo.CreatedAt, Value: -1}}},
|
||||
{Keys: bson.D{
|
||||
{Key: FN.AdvitiseInfo.Sort, Value: 1},
|
||||
{Key: FN.AdvitiseInfo.CreatedAt, Value: -1},
|
||||
}},
|
||||
}
|
||||
if _, err := db.CreateIndex(ctx, indexes); err != nil {
|
||||
panic(fmt.Sprintf("(advitiseInfoMod:Init) CreateIndex failed: %+v", err))
|
||||
}
|
||||
backfillSort(ctx)
|
||||
}
|
||||
|
||||
// backfillSort 给历史数据补 sort 字段。缺字段的文档在升序排序里会排在 0 之前,
|
||||
// 且按 sort=0 精确查询查不到,所以启动时统一补 0。幂等,补完后不再命中。
|
||||
func backfillSort(ctx context.Context) {
|
||||
ret, err := db.UpdateMMany(ctx,
|
||||
bson.M{FN.AdvitiseInfo.Sort: bson.M{"$exists": false}},
|
||||
bson.M{"$set": bson.M{FN.AdvitiseInfo.Sort: int64(0)}},
|
||||
)
|
||||
if err != nil {
|
||||
log.Warn("advitiseInfoMod backfill sort failed", log.E(err))
|
||||
return
|
||||
}
|
||||
if ret != nil && ret.ModifiedCount > 0 {
|
||||
log.Info("advitiseInfoMod backfill sort done", log.Any("modified", ret.ModifiedCount))
|
||||
}
|
||||
}
|
||||
|
||||
func InsertOneAdvitiseInfo(ctx context.Context, doc *AdvitiseInfo) error {
|
||||
@@ -76,4 +97,3 @@ func FindManyAdvitiseInfos(ctx context.Context, filter bson.M, opts ...*options.
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ type TagAdvitiseInfo struct {
|
||||
RandomNames string
|
||||
Link string
|
||||
Status string
|
||||
Sort string
|
||||
StartTime string
|
||||
EndTime string
|
||||
Remark string
|
||||
@@ -46,6 +47,7 @@ func init() {
|
||||
FN.AdvitiseInfo.RandomNames = "randomNames"
|
||||
FN.AdvitiseInfo.Link = "link"
|
||||
FN.AdvitiseInfo.Status = "status"
|
||||
FN.AdvitiseInfo.Sort = "sort"
|
||||
FN.AdvitiseInfo.StartTime = "startTime"
|
||||
FN.AdvitiseInfo.EndTime = "endTime"
|
||||
FN.AdvitiseInfo.Remark = "remark"
|
||||
|
||||
@@ -24,6 +24,7 @@ type AdvitiseInfo struct {
|
||||
RandomNames []string `json:"randomNames" bson:"randomNames"` // 随机名称
|
||||
Link string `json:"link" bson:"link"` // 跳转链接
|
||||
Status bool `json:"status" bson:"status"` // 是否激活
|
||||
Sort int64 `json:"sort" bson:"sort"` // 排序值,从小到大
|
||||
StartTime time.Time `json:"startTime" bson:"startTime"` // 开始时间
|
||||
EndTime time.Time `json:"endTime" bson:"endTime"` // 结束时间
|
||||
Remark string `json:"remark" bson:"remark"` // 备注
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"stat/common/statutils"
|
||||
"time"
|
||||
)
|
||||
|
||||
func LineCount(iTemp string, iValue int64, permilValue int64) string {
|
||||
@@ -442,7 +443,7 @@ func (s *BotHourStat) GenerateTodayTable(title string, statName string, filepath
|
||||
}
|
||||
}
|
||||
|
||||
func (s *BotHourStat) DisplayDayHourTextWithTitle(appName string, compare *BotHourStat) (text string, title string, descript string) {
|
||||
func (s *BotHourStat) DisplayDayHourTextWithTitle(appName string, compare, lastWeek *BotHourStat) (text string, title string, descript string) {
|
||||
calc := func(curr, hist *statutils.Stat) (*statutils.Stat, *statutils.Stat) {
|
||||
diff := curr.Sub(hist)
|
||||
rate := diff.Permil(hist)
|
||||
@@ -587,6 +588,7 @@ func (s *BotHourStat) DisplayDayHourTextWithTitle(appName string, compare *BotHo
|
||||
text += fmt.Sprintf("┗游戏下分: %d\n", s.TodayGame.DecAmount)
|
||||
}
|
||||
}()
|
||||
text += s.DisplayCompareText("9️⃣", compare, lastWeek)
|
||||
text += fmt.Sprintf("🆚%s同比数据:当日的昨天%d点\n", appName, compare.SumDate.Hour())
|
||||
} else {
|
||||
func() {
|
||||
@@ -693,7 +695,103 @@ func (s *BotHourStat) DisplayDayHourTextWithTitle(appName string, compare *BotHo
|
||||
text += fmt.Sprintf(" ┗游戏下分: %d\n", s.TodayGame.DecAmount)
|
||||
}
|
||||
}()
|
||||
text += s.DisplayCompareText("6️⃣", compare, lastWeek)
|
||||
text += fmt.Sprintf("🆚【%s】同比数据未找到\n", appName)
|
||||
}
|
||||
return text, title, descript
|
||||
}
|
||||
|
||||
// compareMetric 核心对比指标。数据机器人汇报末尾的"核心对比"块用,只挑最常看的几项;
|
||||
// 全量指标在上面各段落里已经逐行带了同比涨跌,这里不重复。
|
||||
type compareMetric struct {
|
||||
Name string
|
||||
IsMoney bool // true 表示底层值的单位是"分",展示前要除以 100
|
||||
Pick func(*BotHourStat) int64
|
||||
}
|
||||
|
||||
var compareMetrics = []compareMetric{
|
||||
{Name: "当日充值", IsMoney: true, Pick: func(s *BotHourStat) int64 { return s.TodayStat.Recharge.Value() }},
|
||||
{Name: "当日新增", Pick: func(s *BotHourStat) int64 { return s.TodayStat.Register.Count() }},
|
||||
{Name: "当日日活", Pick: func(s *BotHourStat) int64 { return s.TodayStat.Visit.Count() }},
|
||||
{Name: "小时充值", IsMoney: true, Pick: func(s *BotHourStat) int64 { return s.HourStat.Recharge.Value() }},
|
||||
{Name: "小时新增", Pick: func(s *BotHourStat) int64 { return s.HourStat.Register.Count() }},
|
||||
}
|
||||
|
||||
// weekdayCN 星期的中文名,用来拼"周三"这种基准时间标签
|
||||
var weekdayCN = [...]string{"日", "一", "二", "三", "四", "五", "六"}
|
||||
|
||||
// formatRate 把万分比涨跌渲染成带箭头的百分数,箭头用法和 LineCount 保持一致。
|
||||
// 这里按万分比算而不是沿用 LineCount 的千分比: 千分比只能给出一位小数,
|
||||
// 23.45% 会被截成 23.40%,而这一块就是专门拿来看涨跌的。
|
||||
func formatRate(bp int64) string {
|
||||
v := float64(bp) / 100
|
||||
switch {
|
||||
case v < 0:
|
||||
return fmt.Sprintf("⬇%.2f%%", v)
|
||||
case v > 0:
|
||||
return fmt.Sprintf("⬆+%.2f%%", v)
|
||||
default:
|
||||
return fmt.Sprintf("%.2f%%", v)
|
||||
}
|
||||
}
|
||||
|
||||
// divisor 该指标展示时的除数
|
||||
func (m compareMetric) divisor() int64 {
|
||||
if m.IsMoney {
|
||||
return 100
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
// compareCell 渲染一个指标相对某一期基准的涨跌幅。
|
||||
// base 为 nil 表示那一期的记录没查到;基准为 0 时算不出涨跌幅,
|
||||
// 写 -- 而不是一个看起来正常的 0.00%。
|
||||
func compareCell(label string, m compareMetric, curr int64, base *BotHourStat) string {
|
||||
if base == nil {
|
||||
return fmt.Sprintf("%s:无数据", label)
|
||||
}
|
||||
baseValue := m.Pick(base)
|
||||
if baseValue == 0 {
|
||||
return fmt.Sprintf("%s:--", label)
|
||||
}
|
||||
bp := (curr - baseValue) * 10000 / baseValue
|
||||
return fmt.Sprintf("%s:%s", label, formatRate(bp))
|
||||
}
|
||||
|
||||
// hourDesc 把小时对齐的时点写成"11-14 周六 15点"
|
||||
func hourDesc(t time.Time) string {
|
||||
return fmt.Sprintf("%s 周%s %d点", t.Format("01-02"), weekdayCN[t.Weekday()], t.Hour())
|
||||
}
|
||||
|
||||
// compareDesc 基准时间的可读描述。不重复写小时(两期基准都跟当日同一个小时),
|
||||
// 但保留星期: 环比是整七天前,星期几必然和当日一致,写出来才能一眼看出没错位。
|
||||
// 记录没查到时直接在标签上标出来,免得看到一排"无数据"还要猜是哪天的。
|
||||
func compareDesc(want time.Time, got *BotHourStat) string {
|
||||
desc := fmt.Sprintf("%s 周%s", want.Format("01-02"), weekdayCN[want.Weekday()])
|
||||
if got == nil {
|
||||
return desc + "(无数据)"
|
||||
}
|
||||
return desc
|
||||
}
|
||||
|
||||
// DisplayCompareText 汇报末尾的"核心对比"块。
|
||||
// 业务口径: 同比 = 对比昨天同一小时, 环比 = 对比上周当日同一小时。
|
||||
// 这个叫法和财务上的习惯正好相反,是按需求方的要求定的,不要"纠正"过来。
|
||||
// 两期基准都拿不到时返回空串,不往汇报里塞一块全是"无数据"的内容。
|
||||
func (s *BotHourStat) DisplayCompareText(index string, yesterday, lastWeek *BotHourStat) string {
|
||||
if s == nil || (yesterday == nil && lastWeek == nil) {
|
||||
return ""
|
||||
}
|
||||
text := fmt.Sprintf("%s核心对比:\n", index)
|
||||
text += fmt.Sprintf("┣当日:%s\n", hourDesc(s.SumDate))
|
||||
text += fmt.Sprintf("┣基准:环比 %s / 同比 %s\n",
|
||||
compareDesc(s.SumDate.AddDate(0, 0, -7), lastWeek),
|
||||
compareDesc(s.SumDate.AddDate(0, 0, -1), yesterday))
|
||||
for _, m := range compareMetrics {
|
||||
curr := m.Pick(s)
|
||||
text += fmt.Sprintf("┣%s:%d %s %s\n", m.Name, curr/m.divisor(),
|
||||
compareCell("环比", m, curr, lastWeek),
|
||||
compareCell("同比", m, curr, yesterday))
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
@@ -603,6 +603,27 @@ func InsertOneChannelStatYestoday(ctx context.Context, media *ChannelStatYesterd
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpsertOneChannelStatYestoday 按 (channel, appId, sumDate) 覆盖写入一条同时段对比数据。
|
||||
// 用 ReplaceOne 而不是 InsertOne: 整点任务可能因服务重启或重试在同一小时内跑第二次,
|
||||
// 而该表有 (channel, appId, sumDate) 唯一索引,InsertOne 第二次必然冲突失败。
|
||||
// 整条替换也保证快照不会残留上一次的旧值。
|
||||
// 注意不可改用 UpsertMOne: 它内部用 Struct2Map 走 JSON 序列化,time.Time 会被写成字符串,
|
||||
// sumDate 将不再是 BSON Date,按时间范围的查询会全部失效。
|
||||
func UpsertOneChannelStatYestoday(ctx context.Context, doc *ChannelStatYesterday) error {
|
||||
filter := bson.M{
|
||||
FN.ChannelStatYestoday.Channel: doc.Channel,
|
||||
FN.ChannelStatYestoday.AppId: doc.AppId,
|
||||
FN.ChannelStatYestoday.SumDate: doc.SumDate,
|
||||
}
|
||||
opt := options.Replace().SetUpsert(true)
|
||||
if _, err := dbStatYestoday.GetCollection().ReplaceOne(ctx, filter, doc, opt); err != nil {
|
||||
log.Warn("ChannelStatYestoday:UpsertOneChannelStatYestoday ReplaceOne failed",
|
||||
log.E(err), log.Any("filter", filter))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// CountChannelStatYestoday 统计渠道扣量规则数量
|
||||
func CountChannelStatYestoday(ctx context.Context, filter bson.M) (int64, error) {
|
||||
count, err := dbStatYestoday.Count(ctx, filter)
|
||||
|
||||
@@ -46,6 +46,7 @@ import (
|
||||
"stat/models/rechargechannelmod"
|
||||
"stat/models/rechargegamemod"
|
||||
"stat/models/rechargemod"
|
||||
"stat/models/refundOrderMod"
|
||||
"stat/models/registermod"
|
||||
statrecordmod "stat/models/statrecord"
|
||||
"stat/models/syncgamerecharge"
|
||||
@@ -119,4 +120,5 @@ func InitIndex() {
|
||||
advitiseBindMod.Init(ctx)
|
||||
advitiseClickStatMod.Init(ctx)
|
||||
advitiseDayClickStatMod.Init(ctx)
|
||||
refundOrderMod.Init(ctx)
|
||||
}
|
||||
|
||||
@@ -212,3 +212,13 @@ func UpdateManyRecharge(ctx context.Context, filter bson.M, update bson.M) error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FindOneRecharge 按条件查询单条充值订单(退款订单管理按 cid 回填数据使用)。
|
||||
// 使用 FindOneEx,查不到时透传 mongo.ErrNoDocuments,由调用方区分"未命中"与"出错"。
|
||||
func FindOneRecharge(ctx context.Context, filter bson.M) (*RechargeMod, error) {
|
||||
var doc RechargeMod
|
||||
if err := db.FindOneEx(ctx, &doc, filter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package refundOrderMod
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"stat/common/log"
|
||||
"stat/common/mongos"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
func Init(ctx context.Context) {
|
||||
db = mongos.New(mongos.MgoManager, TN.RefundOrder)
|
||||
indexes := []mongo.IndexModel{
|
||||
// (appId, orderId) 唯一,从数据库层杜绝重复添加 —— 应用层的"先查后插"
|
||||
// 存在并发竞态,两个请求可能同时通过检查。与 log_recharge 的唯一键对齐。
|
||||
// orderId 为空的历史数据不参与唯一约束,否则多条空值会互相冲突。
|
||||
{
|
||||
Keys: bson.D{
|
||||
{Key: FN.RefundOrder.AppId, Value: 1},
|
||||
{Key: FN.RefundOrder.OrderId, Value: 1},
|
||||
},
|
||||
Options: options.Index().SetUnique(true).
|
||||
SetPartialFilterExpression(bson.M{FN.RefundOrder.OrderId: bson.M{"$gt": ""}}),
|
||||
},
|
||||
{Keys: bson.D{{Key: FN.RefundOrder.CID, Value: 1}}},
|
||||
{Keys: bson.D{{Key: FN.RefundOrder.TransNo, Value: 1}}},
|
||||
{Keys: bson.D{{Key: FN.RefundOrder.IP, Value: 1}}},
|
||||
{Keys: bson.D{{Key: FN.RefundOrder.UserId, Value: 1}}},
|
||||
{Keys: bson.D{{Key: FN.RefundOrder.ChannelName, Value: 1}}},
|
||||
{Keys: bson.D{{Key: FN.RefundOrder.DistrictCode, Value: 1}}},
|
||||
{Keys: bson.D{{Key: FN.RefundOrder.CreatedAt, Value: -1}}},
|
||||
}
|
||||
if _, err := db.CreateIndex(ctx, indexes); err != nil {
|
||||
panic(fmt.Sprintf("(refundOrderMod:Init) CreateIndex failed: %+v", err))
|
||||
}
|
||||
}
|
||||
|
||||
// InsertOneRefundOrder 插入一条。
|
||||
// 必须用 InsertOneTiny: db.InsertOne 会调 addCreatedAt 把 CreatedAt 无条件
|
||||
// 改写成 time.Now(),而本表的 createdAt 存的是原订单创建时间,不能被覆盖。
|
||||
// 命中唯一索引时返回的 error 可用 IsDuplicateKey 判定。
|
||||
func InsertOneRefundOrder(ctx context.Context, doc *RefundOrder) error {
|
||||
if err := db.InsertOneTiny(ctx, doc); err != nil {
|
||||
if !IsDuplicateKey(err) {
|
||||
log.Warn("refundOrderMod InsertOne failed", log.E(err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsDuplicateKey 判断是否唯一索引冲突。
|
||||
// 驱动为 v1.4.1,没有 mongo.IsDuplicateKeyError(1.11 才引入),按错误码 11000 判定。
|
||||
func IsDuplicateKey(err error) bool {
|
||||
var we mongo.WriteException
|
||||
if errors.As(err, &we) {
|
||||
for _, e := range we.WriteErrors {
|
||||
if e.Code == 11000 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func FindOneRefundOrder(ctx context.Context, filter bson.M) (*RefundOrder, error) {
|
||||
var doc RefundOrder
|
||||
if err := db.FindOne(ctx, &doc, filter); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &doc, nil
|
||||
}
|
||||
|
||||
func FindManyRefundOrders(ctx context.Context, filter bson.M, opts ...*options.FindOptions) (RefundOrderList, error) {
|
||||
list := make(RefundOrderList, 0)
|
||||
if err := db.Find(ctx, &list, filter, opts...); err != nil {
|
||||
log.Warn("refundOrderMod Find failed", log.E(err), log.Any("filter", filter))
|
||||
return nil, err
|
||||
}
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func CountRefundOrders(ctx context.Context, filter bson.M) (int64, error) {
|
||||
count, err := db.Count(ctx, filter)
|
||||
if err != nil {
|
||||
log.Warn("refundOrderMod Count failed", log.E(err), log.Any("filter", filter))
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
|
||||
// SumRefundOrderMoney 按同一 filter 汇总退款金额(单位:分)。
|
||||
// 走聚合而不是把 money 累加在业务层,是为了统计整个查询结果而不只是当前这一页。
|
||||
func SumRefundOrderMoney(ctx context.Context, filter bson.M) (int64, error) {
|
||||
var ret []struct {
|
||||
TotalMoney int64 `bson:"totalMoney"`
|
||||
}
|
||||
pipeline := []bson.M{
|
||||
{"$match": filter},
|
||||
{"$group": bson.M{
|
||||
"_id": nil,
|
||||
"totalMoney": bson.M{"$sum": "$" + FN.RefundOrder.Money},
|
||||
}},
|
||||
}
|
||||
if err := db.Aggregate(ctx, &ret, pipeline); err != nil {
|
||||
log.Warn("refundOrderMod SumMoney failed", log.E(err), log.Any("filter", filter))
|
||||
return 0, err
|
||||
}
|
||||
if len(ret) == 0 {
|
||||
return 0, nil // 没有命中任何记录
|
||||
}
|
||||
return ret[0].TotalMoney, nil
|
||||
}
|
||||
|
||||
func DelOneRefundOrder(ctx context.Context, filter bson.M) (int64, error) {
|
||||
count, err := db.DeleteOne(ctx, filter)
|
||||
if err != nil {
|
||||
log.Warn("refundOrderMod DeleteOne failed", log.E(err), log.Any("filter", filter))
|
||||
return 0, err
|
||||
}
|
||||
return count, nil
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package refundOrderMod
|
||||
|
||||
type TagRefundOrder struct {
|
||||
ID string
|
||||
AppId string
|
||||
CID string
|
||||
TransNo string
|
||||
IP string
|
||||
DistrictCode string
|
||||
Money string
|
||||
OrderId string
|
||||
UserId string
|
||||
Type string
|
||||
ChannelName string
|
||||
CreatedAt string
|
||||
}
|
||||
|
||||
var FN struct {
|
||||
RefundOrder TagRefundOrder
|
||||
}
|
||||
|
||||
var TN struct {
|
||||
RefundOrder string
|
||||
}
|
||||
|
||||
func init() {
|
||||
FN.RefundOrder.ID = "_id"
|
||||
FN.RefundOrder.AppId = "appId"
|
||||
FN.RefundOrder.CID = "cid"
|
||||
FN.RefundOrder.TransNo = "transNo"
|
||||
FN.RefundOrder.IP = "ip"
|
||||
FN.RefundOrder.DistrictCode = "districtCode"
|
||||
FN.RefundOrder.Money = "money"
|
||||
FN.RefundOrder.OrderId = "orderId"
|
||||
FN.RefundOrder.UserId = "userId"
|
||||
FN.RefundOrder.Type = "type"
|
||||
FN.RefundOrder.ChannelName = "channelName"
|
||||
FN.RefundOrder.CreatedAt = "createdAt"
|
||||
|
||||
TN.RefundOrder = CollRefundOrder
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package refundOrderMod
|
||||
|
||||
import (
|
||||
"stat/common/mongos"
|
||||
"time"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
)
|
||||
|
||||
// 表名
|
||||
const CollRefundOrder = "refund_order"
|
||||
|
||||
var db *mongos.MongoTool
|
||||
|
||||
// RefundOrder 退款订单
|
||||
// 字段取自 log_recharge 的同名字段,按 cid 回填时逐字段拷贝,是下单时点的快照
|
||||
// @table:refund_order
|
||||
type RefundOrder struct {
|
||||
ID primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`
|
||||
AppId int32 `json:"appId" bson:"appId"` // appId
|
||||
CID string `json:"cid" bson:"cid"` // 订单id(来自 log_recharge.cid)
|
||||
TransNo string `json:"transNo" bson:"transNo"` // 请求订单号(pay_server 的 transNo,由前端换取 cid 时一并传入)
|
||||
IP string `json:"ip" bson:"ip"` // 下单用户IP(来自 pay_server 的 recharge_order.userIP,log_recharge 无此字段)
|
||||
DistrictCode string `json:"districtCode" bson:"districtCode"` // 渠道码
|
||||
Money int64 `json:"money" bson:"money"` // 订单金额
|
||||
OrderId string `json:"orderId" bson:"orderId"` // 流水id
|
||||
UserId uint64 `json:"userId" bson:"userId"` // 用户ID
|
||||
Type string `json:"type" bson:"type"` // 充值方式
|
||||
ChannelName string `json:"channelName" bson:"channelName"` // 支付渠道名字
|
||||
CreatedAt time.Time `json:"createdAt" bson:"createdAt"` // 原订单创建时间(取自 log_recharge.createdAt)
|
||||
}
|
||||
|
||||
type RefundOrderList []*RefundOrder
|
||||
@@ -104,6 +104,7 @@ func registerRouter(router *gin.Engine) {
|
||||
appcategoryRouter(rootRouter) // app分类
|
||||
userInviteRouter(rootRouter) // 兼容
|
||||
advitiseV2Router(rootRouter) // 广告管理V2(素材/位置/绑定)
|
||||
refundOrderRouter(rootRouter) // 退款订单管理
|
||||
}
|
||||
|
||||
// 双端下载落地页(无需登陆鉴权)
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"stat/api/refundOrderApi"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func refundOrderRouter(router *gin.RouterGroup) {
|
||||
refund := router.Group("/refundOrder")
|
||||
{
|
||||
refund.POST("/list", refundOrderApi.List)
|
||||
refund.POST("/add", refundOrderApi.Add)
|
||||
refund.POST("/batchAdd", refundOrderApi.BatchAdd)
|
||||
refund.POST("/del", refundOrderApi.Del)
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func InfoList(ctx context.Context, req *proto.AdvitiseInfoListReq) (*proto.Advit
|
||||
return nil, err
|
||||
}
|
||||
opt := &options.FindOptions{}
|
||||
opt.SetSort(bson.D{{Key: advitiseInfoMod.FN.AdvitiseInfo.CreatedAt, Value: -1}})
|
||||
opt.SetSort(infoListOrder(req.OrderBy))
|
||||
opt.SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
opt.SetLimit(req.PageSize)
|
||||
list, err := advitiseInfoMod.FindManyAdvitiseInfos(ctx, filter, opt)
|
||||
@@ -43,6 +43,39 @@ func InfoList(ctx context.Context, req *proto.AdvitiseInfoListReq) (*proto.Advit
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// 列表排序方式
|
||||
const (
|
||||
OrderBySortAsc = "sortAsc" // 排序值从小到大(默认)
|
||||
OrderBySortDesc = "sortDesc" // 排序值从大到小
|
||||
OrderByNewest = "newest" // 按创建时间,最新在前
|
||||
)
|
||||
|
||||
// infoListOrder 把排序方式翻成 mongo 的 sort 文档。
|
||||
// 末尾一律追加 _id 作为最后一级,sort/createdAt 相同的文档顺序才是确定的,
|
||||
// 否则 skip/limit 翻页会出现重复或漏数据。
|
||||
func infoListOrder(orderBy string) bson.D {
|
||||
fn := advitiseInfoMod.FN.AdvitiseInfo
|
||||
switch orderBy {
|
||||
case OrderBySortDesc:
|
||||
return bson.D{
|
||||
{Key: fn.Sort, Value: -1},
|
||||
{Key: fn.CreatedAt, Value: -1},
|
||||
{Key: fn.Id, Value: -1},
|
||||
}
|
||||
case OrderByNewest:
|
||||
return bson.D{
|
||||
{Key: fn.CreatedAt, Value: -1},
|
||||
{Key: fn.Id, Value: -1},
|
||||
}
|
||||
default: // OrderBySortAsc 及未传值
|
||||
return bson.D{
|
||||
{Key: fn.Sort, Value: 1},
|
||||
{Key: fn.CreatedAt, Value: -1},
|
||||
{Key: fn.Id, Value: -1},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func InfoAdd(ctx context.Context, req *proto.AddAdvitiseInfoReq) error {
|
||||
now := time.Now()
|
||||
doc := &advitiseInfoMod.AdvitiseInfo{
|
||||
@@ -60,6 +93,7 @@ func InfoAdd(ctx context.Context, req *proto.AddAdvitiseInfoReq) error {
|
||||
RandomNames: emptyIfNil(req.RandomNames),
|
||||
Link: req.Link,
|
||||
Status: req.Status,
|
||||
Sort: req.Sort,
|
||||
StartTime: req.StartTime,
|
||||
EndTime: req.EndTime,
|
||||
Remark: req.Remark,
|
||||
@@ -113,6 +147,9 @@ func InfoUpdate(ctx context.Context, req *proto.UpdateAdvitiseInfoReq) error {
|
||||
if req.Status != nil {
|
||||
set[advitiseInfoMod.FN.AdvitiseInfo.Status] = *req.Status
|
||||
}
|
||||
if req.Sort != nil {
|
||||
set[advitiseInfoMod.FN.AdvitiseInfo.Sort] = *req.Sort
|
||||
}
|
||||
if req.StartTime != nil {
|
||||
set[advitiseInfoMod.FN.AdvitiseInfo.StartTime] = *req.StartTime
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ func (s *AgentNode) AccumulateChildNodes(ruleMap RuleMap) error {
|
||||
for _, childStat := range childAgent.Stats {
|
||||
hit := false
|
||||
for _, parentStat := range found.Stats {
|
||||
if parentStat.SumDate.Equal(childStat.SumDate) {
|
||||
if parentStat.AppId == childStat.AppId && parentStat.SumDate.Equal(childStat.SumDate) {
|
||||
hit = true
|
||||
AccumulateChildAgentChannelStatToParent(parentStat, childStat)
|
||||
}
|
||||
|
||||
+22
-2
@@ -94,7 +94,7 @@ func DailyBotReportTask(ctx context.Context, bot *tgbot.TgBot, appId int32, repo
|
||||
//text := report.DisplayDayHourText(appName, report)
|
||||
//_ = bot.SendBroadcastMessage(text, "", tgbot.FileTypeNone)
|
||||
|
||||
text, title, desc := report.DisplayDayHourTextWithTitle(appName, historyReport)
|
||||
text, title, desc := report.DisplayDayHourTextWithTitle(appName, historyReport, lastWeekHourStat(ctx, appId, reportTime))
|
||||
et := tgbot.NewEntityList(title, desc, []*tgbot.Entity{
|
||||
{
|
||||
Content: text,
|
||||
@@ -144,7 +144,7 @@ func DailyBotReportInfo(ctx context.Context, appId int32, reportTime time.Time)
|
||||
appNameMap, _ := appInfoMod.GetAllAppIdNameMap(ctx)
|
||||
appName := appNameMap[appId]
|
||||
if statg.GlobalEndpoints.GetBotStatEndpoints().ExistsByAppId(appId) {
|
||||
text, title, desc = report.DisplayDayHourTextWithTitle(appName, historyReport)
|
||||
text, title, desc = report.DisplayDayHourTextWithTitle(appName, historyReport, lastWeekHourStat(ctx, appId, reportTime))
|
||||
}
|
||||
log.Error("DailyBotReportTask end", log.Any("appid", appId),
|
||||
log.Any("time", reportTime))
|
||||
@@ -347,3 +347,23 @@ func BotDailyHourStat(ctx context.Context, appId int32, reportTime time.Time, sa
|
||||
}
|
||||
return report, nil
|
||||
}
|
||||
|
||||
// lastWeekHourStat 取上周当日同一小时的统计记录,给汇报里的"环比"做基准。
|
||||
// 只查不补算: 现算一份七天前的数据要把当天的原始流水全量聚合一遍,代价高,
|
||||
// 而且历史原始数据可能已经被清理,算出来反而是错的。查不到就返回 nil,
|
||||
// 展示层会把那一行标成"无数据"。
|
||||
func lastWeekHourStat(ctx context.Context, appId int32, reportTime time.Time) *botdailyhourstat.BotHourStat {
|
||||
filter := bson.M{
|
||||
botreport.FN.BotReport.AppId: appId,
|
||||
botreport.FN.BotReport.SumDate: reportTime.AddDate(0, 0, -7),
|
||||
}
|
||||
report, err := botdailyhourstat.FindOneBotHourStatEx(ctx, filter)
|
||||
if err != nil {
|
||||
if err != mongo.ErrNoDocuments {
|
||||
log.Warn("lastWeekHourStat failed", log.E(err),
|
||||
log.Any("appId", appId), log.Any("sumDate", reportTime))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return report
|
||||
}
|
||||
|
||||
@@ -491,7 +491,7 @@ func DailyStat(ctx context.Context, cmd *tgbot.Command, moveBackDays int) error
|
||||
return tgbot.NewUserError(cmd, "服务器错误:"+err.Error())
|
||||
}
|
||||
}
|
||||
text, title, desc := report.DisplayDayHourTextWithTitle(appInfo.Name, historyReport)
|
||||
text, title, desc := report.DisplayDayHourTextWithTitle(appInfo.Name, historyReport, lastWeekHourStat(ctx, appId, reportTime))
|
||||
et := tgbot.NewEntityList(title, desc, []*tgbot.Entity{
|
||||
{
|
||||
Content: text,
|
||||
|
||||
@@ -197,7 +197,7 @@ func ChannelSettlementCalc(ctx context.Context, req *proto.SettlementCalcReq) (*
|
||||
for _, parent := range ParentList {
|
||||
parent.CorrectValidData()
|
||||
for _, child := range ChildList {
|
||||
if child.ParentChannel == parent.Channel && child.SumDate.Equal(parent.SumDate) {
|
||||
if child.ParentChannel == parent.Channel && child.AppId == parent.AppId && child.SumDate.Equal(parent.SumDate) {
|
||||
channelmod.ChannelStatSummation(parent, child.CorrectValidData()) // 把子渠道的数据累加到父渠道上面
|
||||
}
|
||||
}
|
||||
@@ -324,7 +324,7 @@ func SearchChannelSettlementStat2(ctx context.Context, req *proto.ChannelStatSet
|
||||
for _, parent := range ParentsChannelStatList {
|
||||
parent.CorrectValidData()
|
||||
for _, child := range ChildChannelStatList {
|
||||
if child.ParentChannel == parent.Channel && child.SumDate.Equal(parent.SumDate) {
|
||||
if child.ParentChannel == parent.Channel && child.AppId == parent.AppId && child.SumDate.Equal(parent.SumDate) {
|
||||
channelmod.ChannelStatSummation(parent, child) // 把子渠道的数据累加到父渠道上面
|
||||
}
|
||||
}
|
||||
@@ -854,7 +854,7 @@ func AddSettlementInfo2(ctx context.Context, req *proto.SettlementInfoAdd2, accT
|
||||
for _, parent := range ParentList {
|
||||
parent.CorrectValidData()
|
||||
for _, child := range ChildList {
|
||||
if child.ParentChannel == parent.Channel && child.SumDate.Equal(parent.SumDate) {
|
||||
if child.ParentChannel == parent.Channel && child.AppId == parent.AppId && child.SumDate.Equal(parent.SumDate) {
|
||||
channelmod.ChannelStatSummation(parent, child) // 把子渠道的数据累加到父渠道上面
|
||||
}
|
||||
}
|
||||
@@ -1138,7 +1138,7 @@ func DetailSettlementInfo(ctx context.Context, req proto.SettlementInfoDetail) (
|
||||
// 把子级记录汇总到一级记录上面
|
||||
for _, parent := range ParentsChannelStatList {
|
||||
for _, child := range ChildsChannelStatList {
|
||||
if child.ParentChannel == parent.Channel && child.SumDate.Equal(parent.SumDate) {
|
||||
if child.ParentChannel == parent.Channel && child.AppId == parent.AppId && child.SumDate.Equal(parent.SumDate) {
|
||||
channelmod.ChannelStatSummation(parent, child) // 把子渠道的数据累加到父渠道上面
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ func SearchChannelStat(ctx context.Context, req *proto.ChannelStatSearch) (*prot
|
||||
channelmod.ChannelStatSummation(&resp.TotalInfo, item)
|
||||
if req.WithChild != nil && *req.WithChild {
|
||||
for _, v := range ChannelStatList {
|
||||
if item.ParentChannel == v.Channel && v.SumDate.Equal(item.SumDate) {
|
||||
if item.ParentChannel == v.Channel && item.AppId == v.AppId && v.SumDate.Equal(item.SumDate) {
|
||||
channelmod.ChannelStatSummation(v, item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package crossSer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"stat/api/proto"
|
||||
@@ -20,6 +21,11 @@ import (
|
||||
"github.com/vicanso/go-charts/v2"
|
||||
)
|
||||
|
||||
// ErrNoRankData 排名数据为空。空数据集不能交给 go-charts 渲染: 坐标轴范围会退化成
|
||||
// math.MinInt64 与 NaN,产出一张刻度为 -9,223,372,036,854,776,000 的坏图。
|
||||
// 常见原因是 innerServers 未配置,跨服循环零次执行。
|
||||
var ErrNoRankData = errors.New("排名数据为空,无法生成图表(请检查 innerServers 配置)")
|
||||
|
||||
var Colors = []drawing.Color{
|
||||
drawing.ColorFromHex("0780cf"),
|
||||
drawing.ColorFromHex("765005"),
|
||||
@@ -353,6 +359,11 @@ func GetRankPhoto(reportTime time.Time, title string, getValue FuncGetStatValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
// 空数据集不能进入渲染: go-charts 会把坐标轴范围退化成 MinInt64/NaN,
|
||||
// 产出一张刻度为 -9,223,372,036,854,776,000 的坏图并照常发到群里。
|
||||
if len(statList) == 0 {
|
||||
return "", nil, ErrNoRankData
|
||||
}
|
||||
// sort
|
||||
sort.Sort(statList)
|
||||
// draw chart
|
||||
@@ -481,6 +492,10 @@ func GetRankDetail(reportTime time.Time, title string, getValue FuncGetStatValue
|
||||
}
|
||||
}
|
||||
}
|
||||
// 同 GetRankPhoto: 空数据集会渲染出坐标轴为 MinInt64/NaN 的坏图
|
||||
if len(statList) == 0 {
|
||||
return "", ErrNoRankData
|
||||
}
|
||||
// sort
|
||||
sort.Sort(statList)
|
||||
// draw chart
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package refundOrderSer
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"stat/api/proto"
|
||||
"stat/models/rechargemod"
|
||||
"stat/models/refundOrderMod"
|
||||
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
"go.mongodb.org/mongo-driver/bson/primitive"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
)
|
||||
|
||||
// 只回填付款成功的订单。status=3 为付款成功,与 models/rechargemod/stat.go 中
|
||||
// 各统计口径一致;付款失败(status=2)的订单不存在退款场景。
|
||||
const rechargeStatusSuccess = 3
|
||||
|
||||
// List 退款订单查询,支持 appId / userId / cid / channelName / districtCode 任意组合
|
||||
func List(ctx context.Context, req *proto.RefundOrderListReq) (*proto.RefundOrderListResp, error) {
|
||||
resp := &proto.RefundOrderListResp{
|
||||
Total: 0,
|
||||
List: make(refundOrderMod.RefundOrderList, 0),
|
||||
}
|
||||
filter := bson.M{}
|
||||
if req.AppId != nil {
|
||||
filter[refundOrderMod.FN.RefundOrder.AppId] = *req.AppId
|
||||
}
|
||||
if req.UserId != nil {
|
||||
filter[refundOrderMod.FN.RefundOrder.UserId] = *req.UserId
|
||||
}
|
||||
if req.CID != nil && *req.CID != "" {
|
||||
filter[refundOrderMod.FN.RefundOrder.CID] = *req.CID
|
||||
}
|
||||
if req.TransNo != nil && *req.TransNo != "" {
|
||||
filter[refundOrderMod.FN.RefundOrder.TransNo] = *req.TransNo
|
||||
}
|
||||
if req.IP != nil && *req.IP != "" {
|
||||
filter[refundOrderMod.FN.RefundOrder.IP] = *req.IP
|
||||
}
|
||||
if req.ChannelName != nil && *req.ChannelName != "" {
|
||||
filter[refundOrderMod.FN.RefundOrder.ChannelName] = *req.ChannelName
|
||||
}
|
||||
if req.DistrictCode != nil && *req.DistrictCode != "" {
|
||||
filter[refundOrderMod.FN.RefundOrder.DistrictCode] = *req.DistrictCode
|
||||
}
|
||||
|
||||
total, err := refundOrderMod.CountRefundOrders(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if total == 0 {
|
||||
return resp, nil // 没有命中,汇总和列表都不用再查
|
||||
}
|
||||
// 汇总整个筛选结果的退款金额,与分页无关
|
||||
totalMoney, err := refundOrderMod.SumRefundOrderMoney(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
opt := &options.FindOptions{}
|
||||
opt.SetSort(bson.D{{Key: refundOrderMod.FN.RefundOrder.CreatedAt, Value: -1}})
|
||||
opt.SetSkip((req.PageNum - 1) * req.PageSize)
|
||||
opt.SetLimit(req.PageSize)
|
||||
list, err := refundOrderMod.FindManyRefundOrders(ctx, filter, opt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Total = total
|
||||
resp.TotalMoney = totalMoney
|
||||
resp.List = list
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Add 按单个 cid 回填
|
||||
func Add(ctx context.Context, req *proto.AddRefundOrderReq) (*proto.AddRefundOrderResp, error) {
|
||||
return addItems(ctx, []proto.RefundOrderItem{{CID: req.CID, TransNo: req.TransNo, IP: req.IP}})
|
||||
}
|
||||
|
||||
// BatchAdd 批量回填
|
||||
func BatchAdd(ctx context.Context, req *proto.BatchAddRefundOrderReq) (*proto.AddRefundOrderResp, error) {
|
||||
return addItems(ctx, req.Items)
|
||||
}
|
||||
|
||||
// addItems 逐个 cid 查询 log_recharge 的成功订单,按 (appId, orderId) 判重后落库。
|
||||
// transNo 不参与查询,只随记录一起写入用于追溯。
|
||||
func addItems(ctx context.Context, items []proto.RefundOrderItem) (*proto.AddRefundOrderResp, error) {
|
||||
resp := &proto.AddRefundOrderResp{
|
||||
Details: make([]proto.RefundCidStat, 0, len(items)),
|
||||
}
|
||||
seen := make(map[string]bool, len(items)) // 同一次请求里 cid 去重
|
||||
for _, item := range items {
|
||||
if item.CID == "" || seen[item.CID] {
|
||||
continue
|
||||
}
|
||||
seen[item.CID] = true
|
||||
|
||||
stat, err := addOneCid(ctx, item.CID, item.TransNo, item.IP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("回填订单[%s]失败: %w", item.CID, err)
|
||||
}
|
||||
resp.Matched += stat.Matched
|
||||
resp.Inserted += stat.Inserted
|
||||
resp.Skipped += stat.Skipped
|
||||
resp.Details = append(resp.Details, *stat)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func addOneCid(ctx context.Context, cid, transNo, ip string) (*proto.RefundCidStat, error) {
|
||||
stat := &proto.RefundCidStat{CID: cid, TransNo: transNo}
|
||||
|
||||
// cid 唯一标识一笔订单,只回填付款成功的那条
|
||||
src, err := rechargemod.FindOneRecharge(ctx, bson.M{
|
||||
"cid": cid,
|
||||
"status": rechargeStatusSuccess,
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, mongo.ErrNoDocuments) {
|
||||
return stat, nil // 该 cid 没有对应的成功订单,matched 记 0,不算失败
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
stat.Matched = 1
|
||||
|
||||
// 按 (appId, orderId) 判重,已存在则跳过
|
||||
exist, err := refundOrderMod.FindOneRefundOrder(ctx, bson.M{
|
||||
refundOrderMod.FN.RefundOrder.AppId: src.AppId,
|
||||
refundOrderMod.FN.RefundOrder.OrderId: src.OrderId,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist != nil && !exist.ID.IsZero() {
|
||||
stat.Skipped = 1
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
doc := &refundOrderMod.RefundOrder{
|
||||
AppId: src.AppId,
|
||||
CID: src.CID,
|
||||
TransNo: transNo,
|
||||
IP: ip,
|
||||
DistrictCode: src.DistrictCode,
|
||||
Money: src.Money,
|
||||
OrderId: src.OrderId,
|
||||
UserId: src.UserId,
|
||||
Type: src.Type,
|
||||
ChannelName: src.ChannelName,
|
||||
CreatedAt: src.CreatedAt,
|
||||
}
|
||||
if err := refundOrderMod.InsertOneRefundOrder(ctx, doc); err != nil {
|
||||
// 并发下两个请求可能同时通过上面的判重检查,由唯一索引兜底。
|
||||
// 撞上即视为重复添加,与"重复跳过不报错"的口径一致。
|
||||
if refundOrderMod.IsDuplicateKey(err) {
|
||||
stat.Skipped = 1
|
||||
return stat, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
stat.Inserted = 1
|
||||
return stat, nil
|
||||
}
|
||||
|
||||
// Del 按 _id 删除单条
|
||||
func Del(ctx context.Context, req *proto.DelRefundOrderReq) error {
|
||||
oid, err := primitive.ObjectIDFromHex(req.Id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("id 格式错误: %w", err)
|
||||
}
|
||||
count, err := refundOrderMod.DelOneRefundOrder(ctx, bson.M{
|
||||
refundOrderMod.FN.RefundOrder.ID: oid,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if count == 0 {
|
||||
return fmt.Errorf("记录不存在")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"stat/statg"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/xxjwxc/public/myglobal/snowflake"
|
||||
)
|
||||
|
||||
@@ -51,7 +52,54 @@ func GenerateConfigFile(origin, target, configFile, DC, PC string) ([]byte, stri
|
||||
if err != nil {
|
||||
output, _ = os.ReadFile("conf/" + configFile)
|
||||
}
|
||||
return output, configFile
|
||||
return uniqueProfileIdentity(output, configFile), configFile
|
||||
}
|
||||
|
||||
// profileIDNamespace 标识符派生的固定命名空间,保证同一模板每次生成的值一致
|
||||
var profileIDNamespace = uuid.NewSHA1(uuid.NameSpaceURL, []byte("stat_center/mobileconfig"))
|
||||
|
||||
var (
|
||||
rePayloadIdent = regexp.MustCompile(`(<key>PayloadIdentifier</key>[ \t]*\r?\n[ \t]*<string>)([^<]*)(</string>)`)
|
||||
rePayloadUUID = regexp.MustCompile(`(<key>PayloadUUID</key>[ \t]*\r?\n[ \t]*<string>)([^<]*)(</string>)`)
|
||||
reUUIDValue = regexp.MustCompile(`[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}`)
|
||||
)
|
||||
|
||||
// uniqueProfileIdentity 把 mobileconfig 里的 PayloadIdentifier/PayloadUUID 改写成按 configFile 派生的值。
|
||||
// conf 下各 app 的模板是互相复制出来的,标识符完全相同,而 iOS 按顶层 PayloadIdentifier 判重:
|
||||
// 相同即替换已安装的描述文件,用户装了第二个 app 第一个就没了。
|
||||
// 按 configFile 派生可保证各 app 互不覆盖;同一 app 多次下载得到相同标识符,
|
||||
// 重复安装只覆盖自己,不会在桌面堆出一排图标。
|
||||
func uniqueProfileIdentity(data []byte, configFile string) []byte {
|
||||
if len(data) == 0 {
|
||||
return data
|
||||
}
|
||||
derive := func(i int) string {
|
||||
return uuid.NewSHA1(profileIDNamespace, []byte(fmt.Sprintf("%s#%d", configFile, i))).String()
|
||||
}
|
||||
|
||||
n := 0
|
||||
data = rePayloadUUID.ReplaceAllFunc(data, func(m []byte) []byte {
|
||||
g := rePayloadUUID.FindSubmatch(m)
|
||||
v := derive(n)
|
||||
n++
|
||||
return []byte(string(g[1]) + v + string(g[3]))
|
||||
})
|
||||
|
||||
// 第 i 个 PayloadIdentifier 与第 i 个 PayloadUUID 共用派生值,保留原前缀(luli. / com.apple.webClip.managed.)
|
||||
i := 0
|
||||
data = rePayloadIdent.ReplaceAllFunc(data, func(m []byte) []byte {
|
||||
g := rePayloadIdent.FindSubmatch(m)
|
||||
v := derive(i)
|
||||
i++
|
||||
val := g[2]
|
||||
if reUUIDValue.Match(val) {
|
||||
val = reUUIDValue.ReplaceAll(val, []byte(v))
|
||||
} else {
|
||||
val = []byte(string(val) + "." + v)
|
||||
}
|
||||
return []byte(string(g[1]) + string(val) + string(g[3]))
|
||||
})
|
||||
return data
|
||||
}
|
||||
|
||||
func GetAppConfig(appId int32) (origin, target, config string) {
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
package versionSer
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var reAllIdent = regexp.MustCompile(`<key>Payload(?:Identifier|UUID)</key>[ \t]*\r?\n[ \t]*<string>([^<]*)</string>`)
|
||||
|
||||
func collectIdentity(t *testing.T, data []byte) []string {
|
||||
t.Helper()
|
||||
var out []string
|
||||
for _, m := range reAllIdent.FindAllSubmatch(data, -1) {
|
||||
out = append(out, string(m[1]))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// TestUniqueProfileIdentityMatchesTemplates 确认 uniqueProfileIdentity 的派生结果与 conf 下
|
||||
// 已改好的模板完全一致:两边用同一套规则,部署后不会再变一次标识符。
|
||||
func TestUniqueProfileIdentityMatchesTemplates(t *testing.T) {
|
||||
files, err := filepath.Glob("../../conf/*.mobileconfig")
|
||||
if err != nil || len(files) == 0 {
|
||||
t.Fatalf("找不到 mobileconfig 模板: %v", err)
|
||||
}
|
||||
|
||||
seen := make(map[string]string)
|
||||
for _, path := range files {
|
||||
name := filepath.Base(path)
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("%s 读取失败: %v", name, err)
|
||||
}
|
||||
|
||||
want := collectIdentity(t, raw)
|
||||
if len(want) == 0 {
|
||||
t.Fatalf("%s 没有解析到任何标识符", name)
|
||||
}
|
||||
got := collectIdentity(t, uniqueProfileIdentity(raw, name))
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("%s 标识符数量不一致: got %d want %d", name, len(got), len(want))
|
||||
}
|
||||
for i := range want {
|
||||
if got[i] != want[i] {
|
||||
t.Errorf("%s 第 %d 个标识符不一致:\n got %s\n want %s", name, i, got[i], want[i])
|
||||
}
|
||||
}
|
||||
|
||||
// 跨文件判重:顶层 PayloadIdentifier 相同会让 iOS 覆盖安装
|
||||
for _, v := range got {
|
||||
if prev, ok := seen[v]; ok {
|
||||
t.Errorf("标识符 %s 同时出现在 %s 和 %s,iOS 上会互相覆盖", v, prev, name)
|
||||
}
|
||||
seen[v] = name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUniqueProfileIdentityIsStable 同一模板多次生成必须得到相同标识符,
|
||||
// 否则用户每次下载都会在桌面多出一个图标。
|
||||
func TestUniqueProfileIdentityIsStable(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../conf/hgmj.mobileconfig")
|
||||
if err != nil {
|
||||
t.Skipf("模板不存在: %v", err)
|
||||
}
|
||||
a := collectIdentity(t, uniqueProfileIdentity(raw, "hgmj.mobileconfig"))
|
||||
b := collectIdentity(t, uniqueProfileIdentity(raw, "hgmj.mobileconfig"))
|
||||
for i := range a {
|
||||
if a[i] != b[i] {
|
||||
t.Fatalf("第 %d 个标识符不稳定: %s != %s", i, a[i], b[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestUniqueProfileIdentityDiffersByConfigFile 不同 configFile 必须派生出不同标识符。
|
||||
func TestUniqueProfileIdentityDiffersByConfigFile(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../conf/hgmj.mobileconfig")
|
||||
if err != nil {
|
||||
t.Skipf("模板不存在: %v", err)
|
||||
}
|
||||
a := collectIdentity(t, uniqueProfileIdentity(raw, "hgmj.mobileconfig"))
|
||||
b := collectIdentity(t, uniqueProfileIdentity(raw, "newapp.mobileconfig"))
|
||||
for i := range a {
|
||||
if a[i] == b[i] {
|
||||
t.Fatalf("不同 configFile 派生出相同标识符: %s", a[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
downloadinfo "stat/models/downloadinfo"
|
||||
"stat/models/versionMod"
|
||||
"stat/service/cacheSer"
|
||||
"stat/service/cpcDownloadCfgSer"
|
||||
"stat/service/crossSer"
|
||||
"stat/statg"
|
||||
"strings"
|
||||
@@ -189,6 +190,18 @@ func PageButtonClick(ctx context.Context, userAgent string, peerIP string, req *
|
||||
return
|
||||
}
|
||||
|
||||
// 双端下载配置里给该渠道配了 Android 下载地址,就用它覆盖 link 并直接返回。
|
||||
// link 也改: 只读 data.link 的老客户端(useAppDownload.ts)才能用上渠道包;
|
||||
// 客户端取值是 links[index] || links[0],单元素数组任意下标都能回落。
|
||||
// 必须在批量打包逻辑之前返回: 下面那段会按默认域名重算并覆盖 link。
|
||||
if req.Terminal == "android" && channelCode.DC != "" {
|
||||
if packUrl := channelApkDownUrl(ctx, req.AppId, channelCode.DC); packUrl != "" {
|
||||
resp.ChannelApkAndDownUrl = packUrl
|
||||
resp.Link = []string{packUrl}
|
||||
return resp, nil
|
||||
}
|
||||
}
|
||||
|
||||
// 新增批量打包逻辑
|
||||
if req.Terminal == "android" && len(data.Md5) > 0 {
|
||||
randTime := int64(60*60) + int64(rand.Intn(60*60))
|
||||
@@ -273,6 +286,22 @@ func PageButtonClick(ctx context.Context, userAgent string, peerIP string, req *
|
||||
return
|
||||
}
|
||||
|
||||
// channelApkDownUrl 取双端下载配置里该渠道的 Android 下载地址,只用于落地页按钮。
|
||||
// 配置里有一部分填的是落地页地址(形如 https://xxx?dc=blt0001)而不是 apk 包,
|
||||
// 落地页拿到它会再跳一次落地页,所以带 dc 参数的一律当没配,返回空串让调用方走默认下载链接。
|
||||
// 不能直接改 cpcDownloadCfgSer.GetDownloadUrl: /download/{appId}/{channel} 这条双端路由
|
||||
// 依赖它跳落地页,过滤掉会把那个功能弄坏。
|
||||
func channelApkDownUrl(ctx context.Context, appId int32, dc string) string {
|
||||
url := cpcDownloadCfgSer.GetDownloadUrl(ctx, appId, dc)
|
||||
if url == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.Contains(url, "?dc=") || strings.Contains(url, "&dc=") {
|
||||
return ""
|
||||
}
|
||||
return url
|
||||
}
|
||||
|
||||
// PageButtonList 落地页获取下载按钮
|
||||
func PageButtonList(ctx context.Context, isIos bool, peerIP string, req *proto.PageButtonListReq) (resp *proto.PageButtonListResp, err error) {
|
||||
resp = &proto.PageButtonListResp{
|
||||
@@ -377,5 +406,10 @@ func PageButtonList(ctx context.Context, isIos bool, peerIP string, req *proto.P
|
||||
resp.HasChannelApk = true
|
||||
}
|
||||
|
||||
// 双端下载配置里给该渠道配了 Android 下载地址就一并返回,落地页优先用它
|
||||
if !isIos && channelCode.DC != "" {
|
||||
resp.ChannelApkAndDownUrl = channelApkDownUrl(ctx, req.AppId, channelCode.DC)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -539,6 +539,7 @@ func ChannelSettlementUpdateByHour() error {
|
||||
|
||||
statYesterdayList, _ := channelmod.FindManyChannelStatYestoday(ctx, yesterdayFilter)
|
||||
|
||||
var succeed, failed int
|
||||
for _, channelStatInfo := range statList {
|
||||
channelStatInfoYesterday := &channelmod.ChannelStatYesterday{
|
||||
// base
|
||||
@@ -591,12 +592,27 @@ func ChannelSettlementUpdateByHour() error {
|
||||
}
|
||||
}
|
||||
|
||||
err := channelmod.InsertOneChannelStatYestoday(ctx, channelStatInfoYesterday)
|
||||
if err != nil {
|
||||
log.Error("ChannelSettlementMethod InsertOneChannelStatYestoday", log.E(err))
|
||||
return err
|
||||
// 单条失败只记录并继续,不能因为一个渠道写失败就丢掉后面所有渠道
|
||||
if err := channelmod.UpsertOneChannelStatYestoday(ctx, channelStatInfoYesterday); err != nil {
|
||||
failed++
|
||||
log.Error("ChannelSettlementUpdateByHour UpsertOneChannelStatYestoday",
|
||||
log.E(err),
|
||||
log.Any("channel", channelStatInfoYesterday.Channel),
|
||||
log.Any("appId", channelStatInfoYesterday.AppId),
|
||||
log.Any("sumDate", channelStatInfoYesterday.SumDate))
|
||||
continue
|
||||
}
|
||||
succeed++
|
||||
}
|
||||
|
||||
log.Info("ChannelSettlementUpdateByHour done",
|
||||
log.Any("sumDate", nowHour),
|
||||
log.Any("total", len(statList)),
|
||||
log.Any("succeed", succeed),
|
||||
log.Any("failed", failed))
|
||||
|
||||
if failed > 0 {
|
||||
return fmt.Errorf("渠道同时段对比数据写入失败 %d 条(成功 %d 条)", failed, succeed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -90,6 +90,13 @@ func Start() {
|
||||
statutils.ServerMon.Run1(statminute.RunStat, defaultStatDate)
|
||||
})
|
||||
|
||||
// 渠道同时段对比数据(channel_stat_yestoday)
|
||||
// 必须严格整点一次: 记录按 sumDate=当前整点 作为唯一键的一部分,
|
||||
// 跑在 5 分钟粒度上会在同一小时内反复覆盖同一批记录,没有意义且白耗资源。
|
||||
_, _ = skdCron.AddFunc("0 0 */1 * * ?", func() {
|
||||
statutils.ServerMon.Run(statminute.ChannelSettlementUpdateByHour)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// 应用点击统计
|
||||
|
||||
Reference in New Issue
Block a user