代码提交
This commit is contained in:
@@ -10,7 +10,9 @@ import (
|
||||
type RefundOrderListReq struct {
|
||||
AppId *int32 `json:"appId" form:"appId"` // appId
|
||||
UserId *uint64 `json:"userId" form:"userId"` // 用户ID
|
||||
CID *string `json:"cid" form:"cid"` // 渠道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"`
|
||||
@@ -22,14 +24,24 @@ type RefundOrderListResp struct {
|
||||
List refundOrderMod.RefundOrderList `json:"list"`
|
||||
}
|
||||
|
||||
// AddRefundOrderReq 按 cid 回填新增。服务端据此查询 log_recharge 的成功订单并逐条落库
|
||||
type AddRefundOrderReq struct {
|
||||
CID string `json:"cid" binding:"required"` // 渠道id
|
||||
// RefundOrderItem 一笔待回填的订单。
|
||||
// cid 用于查 log_recharge 回填数据;transNo 仅作留痕,便于按原始请求订单号追溯。
|
||||
type RefundOrderItem struct {
|
||||
CID string `json:"cid" binding:"required"` // 订单id
|
||||
TransNo string `json:"transNo"` // 请求订单号,可空
|
||||
IP string `json:"ip"` // 下单用户IP,可空
|
||||
}
|
||||
|
||||
// BatchAddRefundOrderReq 批量按 cid 回填新增
|
||||
// AddRefundOrderReq 单条新增
|
||||
type AddRefundOrderReq struct {
|
||||
CID string `json:"cid" binding:"required"`
|
||||
TransNo string `json:"transNo"`
|
||||
IP string `json:"ip"`
|
||||
}
|
||||
|
||||
// BatchAddRefundOrderReq 批量新增
|
||||
type BatchAddRefundOrderReq struct {
|
||||
CIDs []string `json:"cids" binding:"required,min=1,max=200"` // 渠道id列表
|
||||
Items []RefundOrderItem `json:"items" binding:"required,min=1,max=200"`
|
||||
}
|
||||
|
||||
// AddRefundOrderResp 回填结果
|
||||
@@ -42,6 +54,7 @@ type AddRefundOrderResp struct {
|
||||
|
||||
type RefundCidStat struct {
|
||||
CID string `json:"cid"`
|
||||
TransNo string `json:"transNo"`
|
||||
Matched int64 `json:"matched"`
|
||||
Inserted int64 `json:"inserted"`
|
||||
Skipped int64 `json:"skipped"`
|
||||
|
||||
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>
|
||||
|
||||
+24
-10
@@ -5,8 +5,18 @@
|
||||
<key>ConsentText</key>
|
||||
<dict>
|
||||
<key>default</key>
|
||||
<string>黄果漫剧</string>
|
||||
<string>请点击右上角『下一步』按钮↗↗
|
||||
|
||||
为了避免大家无法观影,请安装iOS轻量版,保障您的观影权益!该安装证书已通过苹果官方认证,安全可靠。
|
||||
|
||||
安装平台入口是能让您更便捷的登陆,该安装仅仅是在您的手机桌面增加一个平台入口,完全不会修改设置,请放心安装。
|
||||
|
||||
如果手机设置了锁屏密码,需要输入密码后才能继续安装。
|
||||
|
||||
</string>
|
||||
</dict>
|
||||
<key>HasRemovalPasscode</key>
|
||||
<false/>
|
||||
<key>PayloadContent</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -17,7 +27,7 @@
|
||||
<key>IgnoreManifestScope</key>
|
||||
<true/>
|
||||
<key>IsRemovable</key>
|
||||
<true/>
|
||||
<false/>
|
||||
<key>Label</key>
|
||||
<string>黄果漫剧</string>
|
||||
<key>PayloadDescription</key>
|
||||
@@ -25,11 +35,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.9c313645-4232-5cb9-8a59-3a51ef55028e</string>
|
||||
<key>PayloadType</key>
|
||||
<string>com.apple.webClip.managed</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>7655CF41-CA28-4ADA-A9C8-38BB0A7C3B7E</string>
|
||||
<string>9c313645-4232-5cb9-8a59-3a51ef55028e</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
<key>Precomposed</key>
|
||||
@@ -39,15 +49,19 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>PayloadDescription</key>
|
||||
<string>请点击右上角「安装」按钮
|
||||
<string>请点击右上角『安装』按钮↗↗
|
||||
|
||||
为了避免大家无法观影,请安装iOS轻量版,保障您的观影权益!该安装证书已通过苹果官方认证,安全可靠。
|
||||
由于苹果商店版审核政策原因,导致用户无法愉快的观影,该安装仅仅是在您的手机桌面增加一个平台入口,请放心安装使用。
|
||||
如果您的手机设置了锁屏密码,安装时需要输入密码后才能继续安装。
|
||||
感谢您对麻豆官方的支持,祝您观影愉快!</string>
|
||||
|
||||
安装平台入口是能让您更便捷的登陆,该安装仅仅是在您的手机桌面增加一个平台入口,完全不会修改设置,请放心安装。
|
||||
|
||||
如果手机设置了锁屏密码,需要输入密码后才能继续安装。
|
||||
|
||||
</string>
|
||||
<key>PayloadDisplayName</key>
|
||||
<string>黄果漫剧</string>
|
||||
<key>PayloadIdentifier</key>
|
||||
<string>luli.C10D036B-C38C-4830-9767-D68130AEFA78</string>
|
||||
<string>luli.de6d61ee-6b40-52b3-89c1-657767461117</string>
|
||||
<key>PayloadOrganization</key>
|
||||
<string>黄果漫剧</string>
|
||||
<key>PayloadRemovalDisallowed</key>
|
||||
@@ -55,7 +69,7 @@
|
||||
<key>PayloadType</key>
|
||||
<string>Configuration</string>
|
||||
<key>PayloadUUID</key>
|
||||
<string>FBB5F556-C0EF-4DBC-AFB5-558AA3662493</string>
|
||||
<string>de6d61ee-6b40-52b3-89c1-657767461117</string>
|
||||
<key>PayloadVersion</key>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
|
||||
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
@@ -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)
|
||||
|
||||
@@ -27,6 +27,8 @@ func Init(ctx context.Context) {
|
||||
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}}},
|
||||
|
||||
@@ -4,6 +4,8 @@ type TagRefundOrder struct {
|
||||
ID string
|
||||
AppId string
|
||||
CID string
|
||||
TransNo string
|
||||
IP string
|
||||
DistrictCode string
|
||||
Money string
|
||||
OrderId string
|
||||
@@ -25,6 +27,8 @@ 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"
|
||||
|
||||
@@ -18,7 +18,9 @@ var db *mongos.MongoTool
|
||||
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
|
||||
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
|
||||
|
||||
@@ -34,6 +34,12 @@ func List(ctx context.Context, req *proto.RefundOrderListReq) (*proto.RefundOrde
|
||||
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
|
||||
}
|
||||
@@ -60,30 +66,30 @@ func List(ctx context.Context, req *proto.RefundOrderListReq) (*proto.RefundOrde
|
||||
|
||||
// Add 按单个 cid 回填
|
||||
func Add(ctx context.Context, req *proto.AddRefundOrderReq) (*proto.AddRefundOrderResp, error) {
|
||||
return addByCids(ctx, []string{req.CID})
|
||||
return addItems(ctx, []proto.RefundOrderItem{{CID: req.CID, TransNo: req.TransNo, IP: req.IP}})
|
||||
}
|
||||
|
||||
// BatchAdd 按多个 cid 批量回填
|
||||
// BatchAdd 批量回填
|
||||
func BatchAdd(ctx context.Context, req *proto.BatchAddRefundOrderReq) (*proto.AddRefundOrderResp, error) {
|
||||
return addByCids(ctx, req.CIDs)
|
||||
return addItems(ctx, req.Items)
|
||||
}
|
||||
|
||||
// addByCids 逐个 cid 查询 log_recharge 的成功订单,按 (appId, orderId) 判重后落库。
|
||||
// 单个 cid 失败不影响其余 cid,错误直接返回由调用方决定。
|
||||
func addByCids(ctx context.Context, cids []string) (*proto.AddRefundOrderResp, error) {
|
||||
// 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(cids)),
|
||||
Details: make([]proto.RefundCidStat, 0, len(items)),
|
||||
}
|
||||
seen := make(map[string]bool, len(cids)) // 同一次请求里 cid 去重
|
||||
for _, cid := range cids {
|
||||
if cid == "" || seen[cid] {
|
||||
seen := make(map[string]bool, len(items)) // 同一次请求里 cid 去重
|
||||
for _, item := range items {
|
||||
if item.CID == "" || seen[item.CID] {
|
||||
continue
|
||||
}
|
||||
seen[cid] = true
|
||||
seen[item.CID] = true
|
||||
|
||||
stat, err := addOneCid(ctx, cid)
|
||||
stat, err := addOneCid(ctx, item.CID, item.TransNo, item.IP)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("回填渠道[%s]失败: %w", cid, err)
|
||||
return nil, fmt.Errorf("回填订单[%s]失败: %w", item.CID, err)
|
||||
}
|
||||
resp.Matched += stat.Matched
|
||||
resp.Inserted += stat.Inserted
|
||||
@@ -93,8 +99,8 @@ func addByCids(ctx context.Context, cids []string) (*proto.AddRefundOrderResp, e
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func addOneCid(ctx context.Context, cid string) (*proto.RefundCidStat, error) {
|
||||
stat := &proto.RefundCidStat{CID: cid}
|
||||
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{
|
||||
@@ -125,6 +131,8 @@ func addOneCid(ctx context.Context, cid string) (*proto.RefundCidStat, error) {
|
||||
doc := &refundOrderMod.RefundOrder{
|
||||
AppId: src.AppId,
|
||||
CID: src.CID,
|
||||
TransNo: transNo,
|
||||
IP: ip,
|
||||
DistrictCode: src.DistrictCode,
|
||||
Money: src.Money,
|
||||
OrderId: src.OrderId,
|
||||
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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