Compare commits

...

2 Commits

Author SHA1 Message Date
shanshanzhong147 0a897419a5 修复(#86): 排除永久订阅回归促销误判
Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 01:44:11 -07:00
shanshanzhong147 c90edac630 修复(#73): 修正促销迁移表结构 — subscribe_promo 加 quantity + 索引优化
Build docker and publish / build (20.15.1) (push) Failing after 8m28s
Build docker and publish / build (20.15.1) (pull_request) Successful in 7m59s
- subscribe_promo 新增 quantity 列,唯一键改为 (subscribe_id, quantity, promo_rule_id)
- promo_rule 索引合并为 idx_enabled_priority_deleted (enabled, deleted_at, priority DESC)

Co-authored-by: multica-agent <github@multica.ai>
2026-05-27 01:10:31 -07:00
3 changed files with 74 additions and 5 deletions
@@ -11,19 +11,19 @@ CREATE TABLE IF NOT EXISTS `promo_rule` (
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`deleted_at` DATETIME DEFAULT NULL COMMENT '软删除时间',
PRIMARY KEY (`id`),
KEY `idx_enabled_priority` (`enabled`, `priority` DESC),
KEY `idx_deleted_at` (`deleted_at`)
KEY `idx_enabled_priority_deleted` (`enabled`, `deleted_at`, `priority` DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='促销规则表';
CREATE TABLE IF NOT EXISTS `subscribe_promo` (
`id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`subscribe_id` BIGINT UNSIGNED NOT NULL COMMENT '套餐规格 ID',
`quantity` INT NOT NULL DEFAULT 0 COMMENT '购买数量',
`promo_rule_id` BIGINT UNSIGNED NOT NULL COMMENT '促销规则 ID',
`promo_price` BIGINT NOT NULL DEFAULT 0 COMMENT '该规格在此规则下的优惠价(分)',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_subscribe_rule` (`subscribe_id`, `promo_rule_id`),
UNIQUE KEY `uk_subscribe_qty_rule` (`subscribe_id`, `quantity`, `promo_rule_id`),
KEY `idx_promo_rule_id` (`promo_rule_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='规格促销价表';
+20 -2
View File
@@ -11,6 +11,7 @@ import (
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type PromoResult struct {
@@ -148,6 +149,12 @@ func evaluateInactiveUserPromo(
err := db.WithContext(ctx).
Model(&user.Subscribe{}).
Where("user_id = ?", userID).
Order(clause.OrderBy{
Expression: clause.Expr{
SQL: "CASE WHEN expire_time = ? THEN 0 ELSE 1 END",
Vars: []interface{}{permanentSubscribeExpireTime()},
},
}).
Order("expire_time DESC").
Limit(1).
Take(&lastSub).Error
@@ -158,8 +165,19 @@ func evaluateInactiveUserPromo(
return false, time.Time{}, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query promo inactive user subscription failed")
}
threshold := now.AddDate(0, -params.InactiveMonths, 0)
return lastSub.ExpireTime.Before(threshold) || lastSub.ExpireTime.Equal(threshold), ruleExpiresAt, nil
return isInactivePromoEligible(lastSub.ExpireTime, now, params.InactiveMonths), ruleExpiresAt, nil
}
func isInactivePromoEligible(expireTime time.Time, now time.Time, inactiveMonths int) bool {
if expireTime.Equal(permanentSubscribeExpireTime()) {
return false
}
threshold := now.AddDate(0, -inactiveMonths, 0)
return expireTime.Before(threshold) || expireTime.Equal(threshold)
}
func permanentSubscribeExpireTime() time.Time {
return time.UnixMilli(0)
}
func promoRuleExpiresAt(rule *promo.RuleWithPrice) time.Time {
@@ -0,0 +1,51 @@
package common
import (
"testing"
"time"
)
func TestIsInactivePromoEligible(t *testing.T) {
now := time.Date(2026, time.May, 27, 12, 0, 0, 0, time.UTC)
tests := []struct {
name string
expireAt time.Time
want bool
}{
{
name: "expired before inactive threshold is eligible",
expireAt: now.AddDate(0, -4, 0),
want: true,
},
{
name: "expired exactly at inactive threshold is eligible",
expireAt: now.AddDate(0, -3, 0),
want: true,
},
{
name: "recently expired subscription is not eligible",
expireAt: now.AddDate(0, -2, 0),
want: false,
},
{
name: "active future subscription is not eligible",
expireAt: now.Add(time.Hour),
want: false,
},
{
name: "permanent subscription marker is not eligible",
expireAt: time.UnixMilli(0),
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := isInactivePromoEligible(tt.expireAt, now, 3)
if got != tt.want {
t.Fatalf("isInactivePromoEligible() = %v, want %v", got, tt.want)
}
})
}
}