fix(invitation): 线下提号接入邀请返利并补发历史漏发,返利比例文案改读配置
- 线下提号(admin_pickups)不走订单结账流程,此前完全不发邀请返利;现于 Complete() 内按号主利润对半基数发放卖家侧返利 - 迁移 000086:invitation_rebate_ledgers 增加 source 列,唯一键调整为 (source, order_id, trade_role),支持提号来源且保持幂等 - 新增 cmd/rebate-backfill 幂等补发脚本:默认预演,加 -apply 才写库 - 返利阶段文案不再写死 60%/30%/15%,改为按本次实际生效比例生成(钱包流水备注同步修正) - 前台邀请页阶段标签/横幅/空态文案、移动端个人中心入口文案均改读接口配置,并修掉合法 0% 比例被 || 覆盖成 60% 的问题 - 新增单测:提号返利发放与幂等、cycleLabel 跟随后台配置
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
// rebate-backfill 为「线下提号」补发邀请返利(一次性运维脚本,可安全重复执行)。
|
||||
//
|
||||
// 背景:线下提号(admin_pickups)不走正常订单结账流程,历史上未接入邀请返利结算,
|
||||
// 导致已完成提号单的号主邀请人拿不到分成。代码侧已在 pickup.Complete 内接入,
|
||||
// 本脚本用于补齐「接入前已完成」的历史提号单;仍在进行中的提号单会在完成时自动发放。
|
||||
//
|
||||
// 幂等:写入 (source='pickup', order_id, trade_role='seller') 唯一键,
|
||||
// 且候选集已排除已有提号返利记录的单据,重复执行不会重复发放。
|
||||
//
|
||||
// 口径:单边基数 = 提号利润对半(profit_amount_cent / 2),与租赁订单卖家侧一致;
|
||||
// 比例阶梯按提号完成时刻距今(被邀请人实名生效)的档位计算。
|
||||
//
|
||||
// 用法(默认预演,不写库):
|
||||
//
|
||||
// go run ./cmd/rebate-backfill # 预演,只列待补发单据
|
||||
// go run ./cmd/rebate-backfill -apply # 实际补发
|
||||
// go run ./cmd/rebate-backfill -apply -pickup-no PK20261011113459355
|
||||
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/invitation"
|
||||
"hfb_sys/backend/internal/modules/pickup"
|
||||
"hfb_sys/backend/pkg/money"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type candidate struct {
|
||||
ID uint64
|
||||
PickupNo string
|
||||
OwnerID uint64
|
||||
ProfitAmountCent int64
|
||||
CompletedAt *time.Time
|
||||
ListingSiteID *uint64
|
||||
}
|
||||
|
||||
func main() {
|
||||
apply := flag.Bool("apply", false, "实际执行补发(默认仅预演)")
|
||||
pickupNo := flag.String("pickup-no", "", "仅处理指定提号单号")
|
||||
limit := flag.Int("limit", 0, "最多处理条数,0 表示不限制")
|
||||
flag.Parse()
|
||||
|
||||
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, *pickupNo, *limit)
|
||||
if err != nil {
|
||||
log.Fatalf("查询待补发提号单失败: %v", err)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
fmt.Println("没有需要补发的提号单:已完成提号单均已覆盖或不存在生效邀请关系。")
|
||||
return
|
||||
}
|
||||
|
||||
mode := "预演"
|
||||
if *apply {
|
||||
mode = "实际补发"
|
||||
}
|
||||
fmt.Printf("模式=%s,候选提号单=%d\n", mode, len(rows))
|
||||
|
||||
var paidCount, skippedCount int
|
||||
var paidCent int64
|
||||
for _, row := range rows {
|
||||
completedAt := time.Now()
|
||||
if row.CompletedAt != nil {
|
||||
completedAt = *row.CompletedAt
|
||||
}
|
||||
siteID := uint64(0)
|
||||
if row.ListingSiteID != nil {
|
||||
siteID = *row.ListingSiteID
|
||||
}
|
||||
|
||||
if !*apply {
|
||||
fmt.Printf("[预演] %s 号主=%d 利润=%s 单边基数=%s 完成于=%s\n",
|
||||
row.PickupNo, row.OwnerID,
|
||||
money.FormatWithSymbol(row.ProfitAmountCent),
|
||||
money.FormatWithSymbol(row.ProfitAmountCent/2),
|
||||
completedAt.Format("2006-01-02 15:04:05"))
|
||||
continue
|
||||
}
|
||||
|
||||
amountCent, err := backfillOne(db, row.ID, siteID, completedAt)
|
||||
if err != nil {
|
||||
log.Printf("补发失败 %s: %v", row.PickupNo, err)
|
||||
continue
|
||||
}
|
||||
if amountCent <= 0 {
|
||||
skippedCount++
|
||||
fmt.Printf("[跳过] %s 未产生返利(无生效邀请 / 邀请人未实名 / 返利已关闭 / 已存在记录)\n", row.PickupNo)
|
||||
continue
|
||||
}
|
||||
paidCount++
|
||||
paidCent += amountCent
|
||||
fmt.Printf("[补发] %s 返利 %s 已入邀请人钱包\n", row.PickupNo, money.FormatWithSymbol(amountCent))
|
||||
}
|
||||
|
||||
if *apply {
|
||||
fmt.Printf("完成:补发 %d 单,合计 %s;跳过 %d 单。\n",
|
||||
paidCount, money.FormatWithSymbol(paidCent), skippedCount)
|
||||
}
|
||||
}
|
||||
|
||||
func loadCandidates(db *gorm.DB, pickupNo string, limit int) ([]candidate, error) {
|
||||
query := db.Table("admin_pickups AS p").
|
||||
Select(`p.id, p.pickup_no, p.owner_id, p.profit_amount_cent, p.completed_at,
|
||||
l.distribution_site_id AS listing_site_id`).
|
||||
Joins("JOIN user_invitations i ON i.invitee_id = p.owner_id AND i.status = ? AND i.inviter_id <> p.owner_id", invitation.StatusActive).
|
||||
Joins("LEFT JOIN rental_listings l ON l.id = p.listing_id").
|
||||
Where("p.status = ?", pickup.StatusCompleted).
|
||||
Where("p.profit_amount_cent > 0").
|
||||
Where(`NOT EXISTS (
|
||||
SELECT 1 FROM invitation_rebate_ledgers r
|
||||
WHERE r.source = ? AND r.order_id = p.id AND r.trade_role = 'seller'
|
||||
)`, invitation.RebateSourcePickup).
|
||||
Order("p.completed_at ASC")
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// backfillOne 在独立事务内为单张提号单补发返利并返回实际发放金额(分)。
|
||||
func backfillOne(db *gorm.DB, pickupID uint64, siteID uint64, completedAt time.Time) (int64, error) {
|
||||
var amountCent 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 {
|
||||
return nil
|
||||
}
|
||||
amount, err := invitation.AwardSellerSideRebateTx(tx, invitation.SellerSideRebateInput{
|
||||
Source: invitation.RebateSourcePickup,
|
||||
SourceID: record.ID,
|
||||
SourceNo: record.PickupNo,
|
||||
SellerID: record.OwnerID,
|
||||
GrossBasisCent: record.ProfitAmountCent,
|
||||
BasisCent: record.ProfitAmountCent / 2,
|
||||
SiteID: siteID,
|
||||
RateReferenceAt: completedAt,
|
||||
SettledAt: time.Now(),
|
||||
RemarkPrefix: "邀请卖家提号返利",
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
amountCent = amount
|
||||
return nil
|
||||
})
|
||||
return amountCent, err
|
||||
}
|
||||
Reference in New Issue
Block a user