- 提号完成按号主实际结算金额发放积分,使用提号单维度幂等键防重复 - 增加线下提号积分补发脚本,默认预演,支持指定起始时间和单号 - 补充提号完成与积分幂等回归测试
178 lines
5.4 KiB
Go
178 lines
5.4 KiB
Go
// pickup-points-backfill 为「线下提号」补发号主成长积分。
|
||
//
|
||
// 背景:优质号主功能上线前,线下提号完成只做钱包结算和邀请返利,没有发放成长积分。
|
||
// 本脚本只补发指定时间之后、真实号主(排除 admin: 内部账号)的已完成提号单。
|
||
//
|
||
// 幂等:积分流水 biz_key = pickup:<id>:owner:pickup_completed,
|
||
// 候选集已排除已有积分流水的提号单,重复执行不会重复加分。
|
||
//
|
||
// 用法(默认预演,不写库):
|
||
//
|
||
// go run ./cmd/pickup-points-backfill
|
||
// go run ./cmd/pickup-points-backfill -apply
|
||
// go run ./cmd/pickup-points-backfill -apply -pickup-no PK202611050001
|
||
package main
|
||
|
||
import (
|
||
"flag"
|
||
"fmt"
|
||
"log"
|
||
"strings"
|
||
"time"
|
||
|
||
"hfb_sys/backend/internal/config"
|
||
"hfb_sys/backend/internal/database"
|
||
"hfb_sys/backend/internal/logging"
|
||
"hfb_sys/backend/internal/model"
|
||
"hfb_sys/backend/internal/modules/pickup"
|
||
"hfb_sys/backend/internal/modules/rentergrowth"
|
||
"hfb_sys/backend/internal/timeutil"
|
||
"hfb_sys/backend/pkg/money"
|
||
|
||
"gorm.io/gorm"
|
||
"gorm.io/gorm/clause"
|
||
)
|
||
|
||
const defaultSince = "2026-11-04 21:02:24"
|
||
|
||
type candidate struct {
|
||
ID uint64
|
||
PickupNo string
|
||
OwnerID uint64
|
||
OwnerPhone string
|
||
SettleAmountCent int64
|
||
CompletedAt *time.Time
|
||
}
|
||
|
||
func main() {
|
||
apply := flag.Bool("apply", false, "实际执行补发(默认仅预演)")
|
||
sinceText := flag.String("since", defaultSince, "只处理该时间之后完成的提号单,格式 YYYY-MM-DD HH:MM:SS")
|
||
pickupNo := flag.String("pickup-no", "", "仅处理指定提号单号")
|
||
limit := flag.Int("limit", 0, "最多处理条数,0 表示不限制")
|
||
includeInternal := flag.Bool("include-internal", false, "是否包含 admin: 内部账号")
|
||
flag.Parse()
|
||
|
||
since, err := time.ParseInLocation("2006-01-02 15:04:05", strings.TrimSpace(*sinceText), timeutil.ShanghaiLocation())
|
||
if err != nil {
|
||
log.Fatalf("解析 -since 失败: %v", err)
|
||
}
|
||
|
||
cfg := config.Load()
|
||
logger, err := logging.New(cfg.Log)
|
||
if err != nil {
|
||
log.Fatalf("初始化日志失败: %v", err)
|
||
}
|
||
defer func() {
|
||
_ = logger.Sync()
|
||
}()
|
||
|
||
db, err := database.OpenMySQLWithOptions(cfg.MySQLDSN, cfg.Log.Level, logger, database.MySQLOptions{})
|
||
if err != nil {
|
||
log.Fatalf("连接数据库失败: %v", err)
|
||
}
|
||
|
||
rows, err := loadCandidates(db, since, *pickupNo, *limit, *includeInternal)
|
||
if err != nil {
|
||
log.Fatalf("查询待补发提号单失败: %v", err)
|
||
}
|
||
if len(rows) == 0 {
|
||
fmt.Println("没有需要补发积分的提号单。")
|
||
return
|
||
}
|
||
|
||
mode := "预演"
|
||
if *apply {
|
||
mode = "实际补发"
|
||
}
|
||
ownerIDs := make(map[uint64]struct{}, len(rows))
|
||
var basisCent int64
|
||
for _, row := range rows {
|
||
ownerIDs[row.OwnerID] = struct{}{}
|
||
basisCent += row.SettleAmountCent
|
||
}
|
||
fmt.Printf("模式=%s,起点=%s,候选提号单=%d,号主=%d,结算基数=%s\n",
|
||
mode, since.Format("2006-01-02 15:04:05"), len(rows), len(ownerIDs), money.FormatWithSymbol(basisCent))
|
||
|
||
var awardedCount, skippedCount int
|
||
var awardedPoints int64
|
||
awardedOwnerIDs := make(map[uint64]struct{}, len(rows))
|
||
for _, row := range rows {
|
||
if !*apply {
|
||
fmt.Printf("[预演] %s 号主=%d 积分基数=%s\n",
|
||
row.PickupNo, row.OwnerID, money.FormatWithSymbol(row.SettleAmountCent))
|
||
continue
|
||
}
|
||
points, err := backfillOne(db, row.ID)
|
||
if err != nil {
|
||
log.Printf("补发失败 %s: %v", row.PickupNo, err)
|
||
continue
|
||
}
|
||
if points <= 0 {
|
||
skippedCount++
|
||
fmt.Printf("[跳过] %s 未产生积分(积分功能关闭 / 已存在流水 / 金额为 0)\n", row.PickupNo)
|
||
continue
|
||
}
|
||
awardedCount++
|
||
awardedOwnerIDs[row.OwnerID] = struct{}{}
|
||
awardedPoints += points
|
||
fmt.Printf("[补发] %s 号主=%d 积分=+%d\n", row.PickupNo, row.OwnerID, points)
|
||
}
|
||
if *apply {
|
||
fmt.Printf("完成:补发 %d 单 / %d 人,积分 +%d,跳过 %d 单。\n",
|
||
awardedCount, len(awardedOwnerIDs), awardedPoints, skippedCount)
|
||
}
|
||
}
|
||
|
||
func loadCandidates(db *gorm.DB, since time.Time, pickupNo string, limit int, includeInternal bool) ([]candidate, error) {
|
||
query := db.Table("admin_pickups AS p").
|
||
Select("p.id, p.pickup_no, p.owner_id, u.phone AS owner_phone, p.settle_amount_cent, p.completed_at").
|
||
Joins("JOIN users u ON u.id = p.owner_id").
|
||
Where("p.status = ?", pickup.StatusCompleted).
|
||
Where("p.completed_at >= ?", since).
|
||
Where("p.settle_amount_cent > 0").
|
||
Where(`NOT EXISTS (
|
||
SELECT 1 FROM renter_growth_ledger g
|
||
WHERE g.biz_key = CONCAT('pickup:', p.id, ':owner:pickup_completed')
|
||
)`).
|
||
Order("p.completed_at ASC, p.id ASC")
|
||
if !includeInternal {
|
||
query = query.Where("u.phone NOT LIKE ?", "admin:%")
|
||
}
|
||
if trimmed := strings.TrimSpace(pickupNo); trimmed != "" {
|
||
query = query.Where("p.pickup_no = ?", trimmed)
|
||
}
|
||
if limit > 0 {
|
||
query = query.Limit(limit)
|
||
}
|
||
|
||
var rows []candidate
|
||
if err := query.Scan(&rows).Error; err != nil {
|
||
return nil, err
|
||
}
|
||
return rows, nil
|
||
}
|
||
|
||
func backfillOne(db *gorm.DB, pickupID uint64) (int64, error) {
|
||
var points int64
|
||
err := db.Transaction(func(tx *gorm.DB) error {
|
||
var record model.AdminPickup
|
||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&record, pickupID).Error; err != nil {
|
||
return err
|
||
}
|
||
if record.Status != pickup.StatusCompleted || record.SettleAmountCent <= 0 {
|
||
return nil
|
||
}
|
||
var err error
|
||
points, err = rentergrowth.AwardPickupOwnerCompleted(
|
||
tx,
|
||
record.ID,
|
||
record.PickupNo,
|
||
record.OwnerID,
|
||
record.SettleAmountCent,
|
||
rentergrowth.SourcePickupCompleted,
|
||
)
|
||
return err
|
||
})
|
||
return points, err
|
||
}
|