79 lines
3.1 KiB
Go
79 lines
3.1 KiB
Go
package logic
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/gtime"
|
||
|
||
"tool-api/internal/consts"
|
||
)
|
||
|
||
// ============================================================================
|
||
// T06 · 优惠域时区错位回归(真库 IT,IT_DB 门控)
|
||
//
|
||
// 背景:读侧恒定走 gtime.StrToTime(恒 Local,与 DSN 无关),写入走驱动 loc(UTC),
|
||
// 于是「Go 侧读回值」比真实早 8h —— 任何 `readback.Before/After(now)` 的过期判定都会错。
|
||
// 唯一正确治法:把 expire_at / valid_to 的过期判定挪到 **SQL 侧**(绑定 gtime 参数,
|
||
// 与写入同源),绝不在 Go 侧比较读回时间。
|
||
//
|
||
// 本用例即 T06 验收:
|
||
// 断言① expire_at = now+30min 的免单券应**放行**(可占用);
|
||
// 断言② expire_at = now-1min 的免单券应**拒绝**(4011)。
|
||
//
|
||
// 修复前:断言①因读回早 8h 被判「已过期」而拒绝 → 本用例失败(即 T06 的失败证据)。
|
||
//
|
||
// 运行:$env:IT_DB=1; go test ./internal/logic/ -run TestOrderCouponExpireSqlSide -count=1 -v
|
||
// ============================================================================
|
||
|
||
// insertProbeCoupon 插入一张探测免单券(指定 expire_at),返回券 id。
|
||
func insertProbeCoupon(ctx context.Context, userId int64, expireAt *gtime.Time) (int64, error) {
|
||
return g.Model(consts.TableUserCoupons).Data(g.Map{
|
||
"coupon_id": 0,
|
||
"user_id": userId,
|
||
"coupon_title": "TZ 探测免单券",
|
||
"coupon_type": consts.CouponTypeFreebie,
|
||
"kind": consts.CouponKindFreebie,
|
||
"status": consts.UserCouponStatusUnused,
|
||
"source": consts.UserCouponSourceAdmin,
|
||
"expire_at": expireAt,
|
||
"created_at": gtime.Now(),
|
||
"updated_at": gtime.Now(),
|
||
}).InsertAndGetId()
|
||
}
|
||
|
||
// TestOrderCouponExpireSqlSide 券过期判定 SQL 侧归一(T06 验收)。
|
||
func TestOrderCouponExpireSqlSide(t *testing.T) {
|
||
ctx := requireOrderIT(t)
|
||
p := setupOrderProbe(ctx, t)
|
||
defer p.cleanup(ctx)
|
||
|
||
res := createMemberOrder(ctx, t, p)
|
||
|
||
// 断言①:expire_at = now+30min → 应放行(可占用)
|
||
freshId, err := insertProbeCoupon(ctx, p.userId, gtime.Now().Add(30*time.Minute))
|
||
if err != nil {
|
||
t.Fatalf("insert fresh coupon: %v", err)
|
||
}
|
||
if _, err = applyInTx(ctx, t, p.userId, res.OutTradeNo, "", freshId); err != nil {
|
||
t.Fatalf("断言①失败:expire_at=now+30min 应放行,实得 err=%v (code=%d)", err, codeOf(err))
|
||
}
|
||
// 清除优惠(释放刚占用的券),不影响下一断言
|
||
if _, err = applyInTx(ctx, t, p.userId, res.OutTradeNo, "", 0); err != nil {
|
||
t.Fatalf("清除优惠失败: %v", err)
|
||
}
|
||
|
||
// 断言②:expire_at = now-1min → 应拒绝 4011
|
||
staleId, err := insertProbeCoupon(ctx, p.userId, gtime.Now().Add(-1*time.Minute))
|
||
if err != nil {
|
||
t.Fatalf("insert stale coupon: %v", err)
|
||
}
|
||
if _, err = applyInTx(ctx, t, p.userId, res.OutTradeNo, "", staleId); err == nil {
|
||
t.Fatalf("断言②失败:expire_at=now-1min 应被拒绝,实得放行")
|
||
} else if codeOf(err) != consts.CodeCouponUnavailable {
|
||
t.Fatalf("断言②失败:过期券应返 4011,实得 code=%d err=%v", codeOf(err), err)
|
||
}
|
||
}
|