Files
hfb_sys/backend/cmd/rebate-backfill-rental/main.go
T

229 lines
7.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// rebate-backfill-rental 为「仲裁按实际消耗结算」的租赁订单补发邀请返利。
//
// 背景:仲裁结案历史路径未触发邀请返利;且订单保留原预收金额快照,
// 不能直接用 order.rent_amount_cent - owner_rent_amount_cent 计算。
// 本脚本按仲裁后的实际平台毛利(优先取已结算 checkout 的平台费,
// 否则取 actual_pure_coin_amount_cent - 仲裁入账给号主的金额)补发。
//
// 幂等:复用 invitation_rebate_ledgers 的 (source, order_id, trade_role) 唯一键。
//
// 用法(默认预演):
//
// go run ./cmd/rebate-backfill-rental -order-no RO20261017002241506,RO20261012185753383
// go run ./cmd/rebate-backfill-rental -apply -order-no RO20261017002241506,RO20261012185753383
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/pkg/money"
"gorm.io/gorm"
)
type candidate struct {
Order model.RentalOrder
ActualProfitCent int64
ActualProfitSource string
}
func main() {
apply := flag.Bool("apply", false, "实际执行补发(默认仅预演)")
orderNos := flag.String("order-no", "", "需要补发的租赁订单号,多个用逗号分隔")
flag.Parse()
orderNoList := splitOrderNos(*orderNos)
if len(orderNoList) == 0 {
log.Fatal("必须通过 -order-no 指定订单号,避免误补全站历史订单")
}
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, orderNoList)
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 int
var paidCent int64
for _, row := range rows {
rateAt := time.Now()
if row.Order.SettledAt != nil {
rateAt = *row.Order.SettledAt
}
fmt.Printf("[%s] %s 号主=%d 原毛利=%s 实际平台毛利=%s(%s)计提时刻=%s\n",
mode, row.Order.OrderNo, row.Order.OwnerID,
money.FormatWithSymbol(row.Order.RentAmountCent-row.Order.OwnerRentAmountCent),
money.FormatWithSymbol(row.ActualProfitCent), row.ActualProfitSource,
rateAt.Format("2006-01-02 15:04:05"))
if !*apply {
continue
}
amountCent, err := backfillOne(db, row, rateAt)
if err != nil {
log.Printf("补发失败 %s: %v", row.Order.OrderNo, err)
continue
}
if amountCent <= 0 {
fmt.Printf("[跳过] %s 未产生返利(无生效邀请 / 邀请人未实名 / 返利已关闭 / 已存在记录)\n", row.Order.OrderNo)
continue
}
paidCount++
paidCent += amountCent
fmt.Printf("[补发] %s 返利 %s 已入邀请人钱包\n", row.Order.OrderNo, money.FormatWithSymbol(amountCent))
}
if *apply {
fmt.Printf("完成:补发 %d 单,合计 %s。\n", paidCount, money.FormatWithSymbol(paidCent))
}
}
func splitOrderNos(raw string) []string {
parts := strings.Split(raw, ",")
result := make([]string, 0, len(parts))
for _, part := range parts {
if value := strings.TrimSpace(part); value != "" {
result = append(result, value)
}
}
return result
}
func loadCandidates(db *gorm.DB, orderNos []string) ([]candidate, error) {
var orders []model.RentalOrder
if err := db.Where("order_no IN ?", orderNos).Order("id ASC").Find(&orders).Error; err != nil {
return nil, err
}
result := make([]candidate, 0, len(orders))
for _, order := range orders {
if order.Status != "completed" || order.SettlementStatus != "arbitrated" {
continue
}
var rebateCount int64
if err := db.Model(&model.InvitationRebateLedger{}).
Where("source = ? AND order_id = ? AND trade_role = 'seller'", invitation.RebateSourceRentalOrder, order.ID).
Count(&rebateCount).Error; err != nil {
return nil, err
}
if rebateCount > 0 {
continue
}
profitCent, source, err := actualProfitCent(db, order)
if err != nil {
return nil, err
}
if profitCent <= 0 {
continue
}
result = append(result, candidate{
Order: order,
ActualProfitCent: profitCent,
ActualProfitSource: source,
})
}
return result, nil
}
// actualProfitCent 返回仲裁后的实际平台毛利。
// 已结算 checkout 的平台费最准确;没有 checkout 时用实际纯币收入减去仲裁入账号主的金额。
func actualProfitCent(db *gorm.DB, order model.RentalOrder) (int64, string, error) {
var checkout model.OrderCheckout
checkoutErr := db.Where("order_id = ? AND status = ?", order.ID, "settled").
Order("id DESC").First(&checkout).Error
if checkoutErr == nil && checkout.PlatformFeeCent > 0 {
return checkout.PlatformFeeCent, "settled_checkout.platform_fee_cent", nil
}
if checkoutErr != nil && checkoutErr != gorm.ErrRecordNotFound {
return 0, "", checkoutErr
}
var ownerIncomeCent int64
if err := db.Model(&model.WalletLedger{}).
Where("order_id = ? AND biz_type = ? AND direction = 'in' AND balance_type = 'available'", order.ID, "arbitration_owner_income").
Select("COALESCE(SUM(amount_cent), 0)").Scan(&ownerIncomeCent).Error; err != nil {
return 0, "", err
}
if order.ActualPureCoinAmountCent <= 0 || ownerIncomeCent <= 0 {
return 0, "", nil
}
return order.ActualPureCoinAmountCent - ownerIncomeCent, "actual_pure_coin - arbitration_owner_income", nil
}
func backfillOne(db *gorm.DB, row candidate, rateAt time.Time) (int64, error) {
var amountCent int64
err := db.Transaction(func(tx *gorm.DB) error {
// 事务内重新读取,避免预演后订单状态或金额发生变化。
var order model.RentalOrder
if err := tx.First(&order, row.Order.ID).Error; err != nil {
return err
}
if order.Status != "completed" || order.SettlementStatus != "arbitrated" {
return nil
}
profitCent, _, err := actualProfitCent(tx, order)
if err != nil {
return err
}
if profitCent <= 0 {
return nil
}
options := invitation.OrderRebateOptions{
GrossProfitCent: &profitCent,
RateReferenceAt: rateAt,
SettledAt: time.Now(),
}
before := int64(0)
_ = tx.Model(&model.InvitationRebateLedger{}).
Where("source = ? AND order_id = ? AND trade_role = 'seller'", invitation.RebateSourceRentalOrder, order.ID).
Select("COALESCE(SUM(rebate_amount_cent), 0)").Scan(&before).Error
if err := invitation.ProcessOrderInvitationRebateWithOptionsTx(tx, order.ID, options); err != nil {
return err
}
var after int64
if err := tx.Model(&model.InvitationRebateLedger{}).
Where("source = ? AND order_id = ? AND trade_role = 'seller'", invitation.RebateSourceRentalOrder, order.ID).
Select("COALESCE(SUM(rebate_amount_cent), 0)").Scan(&after).Error; err != nil {
return err
}
amountCent = after - before
return nil
})
return amountCent, err
}