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) } }