按实际纯币金额计算租客优惠与积分
This commit is contained in:
@@ -26,6 +26,7 @@ const (
|
||||
MaxLevelPoints = 1_000_000_000
|
||||
MaxLevelQuotaCent = 100_000_000
|
||||
SourceOrderCompleted = "order_completed"
|
||||
SourceOrderRefunded = "order_refunded"
|
||||
SourceAdminAdjustment = "admin_adjustment"
|
||||
)
|
||||
|
||||
@@ -243,7 +244,7 @@ func CalculateDiscountCent(originalRentCent int64, platformFeeCent int64, discou
|
||||
return discountCent
|
||||
}
|
||||
|
||||
func AwardOrderCompleted(tx *gorm.DB, order *model.RentalOrder, actualRentCent int64, source string) error {
|
||||
func AwardOrderCompleted(tx *gorm.DB, order *model.RentalOrder, actualPureCoinCent int64, source string) error {
|
||||
if tx == nil || order == nil || order.ID == 0 || order.RenterID == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -271,10 +272,12 @@ func AwardOrderCompleted(tx *gorm.DB, order *model.RentalOrder, actualRentCent i
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
order.GrowthPointsBasisCent = maxInt64(actualPureCoinCent, 0)
|
||||
order.GrowthPointsPerYuan = cfg.PointsPerYuan
|
||||
if !cfg.Enabled {
|
||||
return nil
|
||||
}
|
||||
points := PointsForRent(actualRentCent, cfg.PointsPerYuan)
|
||||
points := PointsForPureCoin(actualPureCoinCent, cfg.PointsPerYuan)
|
||||
if points <= 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -293,16 +296,18 @@ func AwardOrderCompleted(tx *gorm.DB, order *model.RentalOrder, actualRentCent i
|
||||
}
|
||||
now := time.Now()
|
||||
ledger := model.RenterGrowthLedger{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &order.ID,
|
||||
Points: points,
|
||||
BeforePoints: beforePoints,
|
||||
AfterPoints: afterPoints,
|
||||
BeforeLevel: beforeLevel.Code,
|
||||
AfterLevel: afterLevel.Code,
|
||||
Source: source,
|
||||
Remark: "租号订单完成发放成长积分",
|
||||
CreatedAt: now,
|
||||
UserID: order.RenterID,
|
||||
OrderID: &order.ID,
|
||||
Points: points,
|
||||
BasisAmountCent: actualPureCoinCent,
|
||||
PointsPerYuan: cfg.PointsPerYuan,
|
||||
BeforePoints: beforePoints,
|
||||
AfterPoints: afterPoints,
|
||||
BeforeLevel: beforeLevel.Code,
|
||||
AfterLevel: afterLevel.Code,
|
||||
Source: source,
|
||||
Remark: "租号订单完成发放成长积分",
|
||||
CreatedAt: now,
|
||||
}
|
||||
if err := tx.Create(&ledger).Error; err != nil {
|
||||
return err
|
||||
@@ -312,14 +317,90 @@ func AwardOrderCompleted(tx *gorm.DB, order *model.RentalOrder, actualRentCent i
|
||||
return nil
|
||||
}
|
||||
|
||||
func PointsForRent(actualRentCent int64, pointsPerYuan int64) int64 {
|
||||
if actualRentCent <= 0 {
|
||||
// RevokeOrderCompleted 在已完成订单被全额人工退款后扣回该单已发放的成长积分。
|
||||
func RevokeOrderCompleted(tx *gorm.DB, order *model.RentalOrder) error {
|
||||
if tx == nil || order == nil || order.ID == 0 || order.RenterID == 0 {
|
||||
return nil
|
||||
}
|
||||
var existing model.RenterGrowthLedger
|
||||
err := tx.Where("order_id = ? AND source = ?", order.ID, SourceOrderRefunded).First(&existing).Error
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
points := order.GrowthPointsAwarded
|
||||
if points <= 0 {
|
||||
var awarded model.RenterGrowthLedger
|
||||
if err := tx.Where("order_id = ? AND source = ?", order.ID, SourceOrderCompleted).First(&awarded).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
points = awarded.Points
|
||||
}
|
||||
if points <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
cfg, err := ConfigForTx(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var user model.User
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&user, order.RenterID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
beforePoints := maxInt64(user.RenterGrowthPoints, 0)
|
||||
revokedPoints := points
|
||||
if revokedPoints > beforePoints {
|
||||
revokedPoints = beforePoints
|
||||
}
|
||||
beforeLevel := LevelForPoints(cfg, beforePoints)
|
||||
afterPoints := beforePoints - revokedPoints
|
||||
afterLevel := LevelForPoints(cfg, afterPoints)
|
||||
if !cfg.Enabled {
|
||||
beforeLevel = LevelRule{Code: DefaultLevelCode, Name: DefaultLevelName}
|
||||
afterLevel = beforeLevel
|
||||
}
|
||||
user.RenterGrowthPoints = afterPoints
|
||||
user.RenterGrowthLevel = afterLevel.Code
|
||||
if err := tx.Save(&user).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
orderID := order.ID
|
||||
return tx.Create(&model.RenterGrowthLedger{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
Points: -revokedPoints,
|
||||
BasisAmountCent: order.GrowthPointsBasisCent,
|
||||
PointsPerYuan: order.GrowthPointsPerYuan,
|
||||
BeforePoints: beforePoints,
|
||||
AfterPoints: afterPoints,
|
||||
BeforeLevel: beforeLevel.Code,
|
||||
AfterLevel: afterLevel.Code,
|
||||
Source: SourceOrderRefunded,
|
||||
Remark: "已完成订单全额退款扣回成长积分",
|
||||
CreatedAt: time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func PointsForPureCoin(actualPureCoinCent int64, pointsPerYuan int64) int64 {
|
||||
if actualPureCoinCent <= 0 {
|
||||
return 0
|
||||
}
|
||||
if pointsPerYuan <= 0 {
|
||||
pointsPerYuan = DefaultPointsPerYuan
|
||||
}
|
||||
return (actualRentCent / 100) * pointsPerYuan
|
||||
return (actualPureCoinCent / 100) * pointsPerYuan
|
||||
}
|
||||
|
||||
// PointsForRent 保留旧调用兼容,积分基数现统一解释为实际纯币金额。
|
||||
func PointsForRent(actualPureCoinCent int64, pointsPerYuan int64) int64 {
|
||||
return PointsForPureCoin(actualPureCoinCent, pointsPerYuan)
|
||||
}
|
||||
|
||||
func LevelForPoints(cfg Config, points int64) LevelRule {
|
||||
|
||||
@@ -159,6 +159,9 @@ func TestAwardOrderCompletedUpdatesUserAndIsIdempotent(t *testing.T) {
|
||||
if order.GrowthPointsAwarded != 20 || order.GrowthPointsAwardedAt == nil {
|
||||
t.Fatalf("order awarded = %d at %v, want 20 with time", order.GrowthPointsAwarded, order.GrowthPointsAwardedAt)
|
||||
}
|
||||
if order.GrowthPointsBasisCent != 2000 || order.GrowthPointsPerYuan != 1 {
|
||||
t.Fatalf("order points basis = %d x %d, want 2000 x 1", order.GrowthPointsBasisCent, order.GrowthPointsPerYuan)
|
||||
}
|
||||
var savedUser model.User
|
||||
if err := db.First(&savedUser, user.ID).Error; err != nil {
|
||||
t.Fatalf("find user failed: %v", err)
|
||||
@@ -186,4 +189,72 @@ func TestAwardOrderCompletedUpdatesUserAndIsIdempotent(t *testing.T) {
|
||||
if ledgerCount != 1 {
|
||||
t.Fatalf("ledger count = %d, want 1", ledgerCount)
|
||||
}
|
||||
|
||||
if err := RevokeOrderCompleted(db, &order); err != nil {
|
||||
t.Fatalf("RevokeOrderCompleted() error = %v", err)
|
||||
}
|
||||
if err := RevokeOrderCompleted(db, &order); err != nil {
|
||||
t.Fatalf("second RevokeOrderCompleted() error = %v", err)
|
||||
}
|
||||
if err := db.First(&savedUser, user.ID).Error; err != nil {
|
||||
t.Fatalf("find user after revoke failed: %v", err)
|
||||
}
|
||||
if savedUser.RenterGrowthPoints != 290 || savedUser.RenterGrowthLevel != DefaultLevelCode {
|
||||
t.Fatalf("user growth after revoke = %d/%s, want 290/%s", savedUser.RenterGrowthPoints, savedUser.RenterGrowthLevel, DefaultLevelCode)
|
||||
}
|
||||
if err := db.Model(&model.RenterGrowthLedger{}).Where("order_id = ?", order.ID).Count(&ledgerCount).Error; err != nil {
|
||||
t.Fatalf("count ledger after revoke failed: %v", err)
|
||||
}
|
||||
if ledgerCount != 2 {
|
||||
t.Fatalf("ledger count after revoke = %d, want 2", ledgerCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRevokeOrderCompletedRecordsIdempotencyWhenUserHasNoPoints(t *testing.T) {
|
||||
db := database.NewTestDB()
|
||||
if err := db.AutoMigrate(
|
||||
&model.User{},
|
||||
&model.RentalOrder{},
|
||||
&model.SystemConfig{},
|
||||
&model.RenterGrowthLedger{},
|
||||
); err != nil {
|
||||
t.Fatalf("migrate failed: %v", err)
|
||||
}
|
||||
user := model.User{Phone: "13800008889", RenterGrowthLevel: DefaultLevelCode}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
t.Fatalf("create user failed: %v", err)
|
||||
}
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "RG202607260002",
|
||||
RenterID: user.ID,
|
||||
GrowthPointsAwarded: 20,
|
||||
GrowthPointsBasisCent: 2000,
|
||||
GrowthPointsPerYuan: 1,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatalf("create order failed: %v", err)
|
||||
}
|
||||
if err := RevokeOrderCompleted(db, &order); err != nil {
|
||||
t.Fatalf("RevokeOrderCompleted() error = %v", err)
|
||||
}
|
||||
if err := db.Model(&model.User{}).Where("id = ?", user.ID).Update("renter_growth_points", 100).Error; err != nil {
|
||||
t.Fatalf("add later points failed: %v", err)
|
||||
}
|
||||
if err := RevokeOrderCompleted(db, &order); err != nil {
|
||||
t.Fatalf("repeat RevokeOrderCompleted() error = %v", err)
|
||||
}
|
||||
var saved model.User
|
||||
if err := db.First(&saved, user.ID).Error; err != nil {
|
||||
t.Fatalf("find user failed: %v", err)
|
||||
}
|
||||
if saved.RenterGrowthPoints != 100 {
|
||||
t.Fatalf("later points after repeated callback = %d, want 100", saved.RenterGrowthPoints)
|
||||
}
|
||||
var ledger model.RenterGrowthLedger
|
||||
if err := db.Where("order_id = ? AND source = ?", order.ID, SourceOrderRefunded).First(&ledger).Error; err != nil {
|
||||
t.Fatalf("find revoke ledger failed: %v", err)
|
||||
}
|
||||
if ledger.Points != 0 {
|
||||
t.Fatalf("revoke ledger points = %d, want 0", ledger.Points)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user