Files
qitongxue-api/internal/logic/admin_order_test.go
2026-09-29 10:57:01 +08:00

537 lines
21 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.
package logic
import (
"context"
"fmt"
"os"
"sync"
"testing"
"time"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/os/gtime"
v1 "tool-api/api/admin/v1"
"tool-api/internal/consts"
"tool-api/internal/model/entity"
)
// ensureTestSchema 保证 member_orders 的退款相关列已存在。
// 说明:GoFrame 的 Update(Data(map)) 会**按表结构过滤掉未知列**(静默丢弃),
// 故运行期迁移 Migrate() 必须先执行,service_revoked / refunded_at 才会真正落库。
var migrateOnce sync.Once
func ensureTestSchema() {
migrateOnce.Do(func() { Migrate(context.Background()) })
}
// ============================================================================
// 订单退款 / 回收所购服务 · 集成测试(真实库,自建 fixture 并清理)
//
// 覆盖:会员套餐回收(减天数)、次数包回收(扣到 0 不为负)、不回收(权益保留)、
// 幂等(第二次调用不重复回收)、列表关键字命中。
// ============================================================================
func newTag(prefix string) string { return fmt.Sprintf("%s%d", prefix, time.Now().UnixNano()) }
func insertRefundUser(t *testing.T, tag, levelKey string, expire *gtime.Time) int64 {
t.Helper()
now := gtime.Now()
id, err := g.Model(consts.TableUsers).Data(g.Map{
"openid": "refund_" + tag,
"username": "refund_" + tag,
"nickname": "退款测试" + tag,
"level_key": levelKey,
"level_expire_at": expire, // nil → 写 NULL(永久)
"status": 1,
"created_at": now,
"updated_at": now,
}).InsertAndGetId()
if err != nil {
t.Fatalf("插入测试用户失败: %v", err)
}
return id
}
func insertDeliveredOrder(t *testing.T, userId int64, tag string, orderType int, fields g.Map) string {
t.Helper()
no := "TREF" + tag
now := gtime.Now()
data := g.Map{
"out_trade_no": no,
"wx_order_id": "wx_" + no,
"user_id": userId,
"openid": "refund_" + tag,
"order_type": orderType,
"plan_key": "",
"level_key": "",
"product_id": "test_product",
"price_cents": 1000,
"origin_price_cents": 1000,
"paid_price_cents": 1000,
"discount_cents": 0,
"promo_kind": 0,
"status": consts.OrderStatusDelivered,
"pay_channel": consts.PayChannelWx,
"created_at": now,
"updated_at": now,
"paid_at": now,
"delivered_at": now,
}
for k, v := range fields {
data[k] = v
}
if _, err := g.Model(consts.TableMemberOrders).Data(data).Insert(); err != nil {
t.Fatalf("插入测试订单失败: %v", err)
}
return no
}
func cleanupRefundFixture(t *testing.T, uid int64, orderNos ...string) {
t.Helper()
for _, no := range orderNos {
if _, err := g.Model(consts.TableMemberOrders).Where("out_trade_no", no).Delete(); err != nil {
t.Logf("[cleanup] 删除订单 %s 失败: %v", no, err)
}
}
g.Model(consts.TableUserToolQuota).Where("user_id", uid).Delete()
g.Model(consts.TableUsers).Where("id", uid).Delete()
}
// TestAdminOrderRefund_RevokeMemberService 会员套餐退款回收:减去本单天数 + 幂等。
func TestAdminOrderRefund_RevokeMemberService(t *testing.T) {
ensureTestSchema()
ctx := context.Background()
tag := newTag("rm")
uid := insertRefundUser(t, tag, "v2", gtime.Now().AddDate(0, 0, 60))
no := insertDeliveredOrder(t, uid, tag, consts.OrderTypeMember, g.Map{
"plan_key": "vip-month", "level_key": "v2", "duration_days": 30,
})
defer cleanupRefundFixture(t, uid, no)
// 读回基线(读回存在已知时区偏移,故一律用「读回 vs 读回」比较,偏移自然抵消)
baseU, _ := g.Model(consts.TableUsers).Where("id", uid).One()
baseline := baseU["level_expire_at"].GTime()
res, err := AdminOrderRefund(ctx, no, true)
if err != nil {
t.Fatalf("退款失败: %v", err)
}
if !res.Revoked || !res.ServiceRevoked {
t.Fatalf("期望 revoke=true 且执行退款,得 %+v", res)
}
row, _ := g.Model(consts.TableMemberOrders).Where("out_trade_no", no).One()
if row["status"].Int() != consts.OrderStatusRefunded {
t.Fatalf("订单状态应为已退款(2),得 %d", row["status"].Int())
}
if row["service_revoked"].Int() != 1 {
t.Fatalf("service_revoked 应为 1,得 %d", row["service_revoked"].Int())
}
if row["refunded_at"].GTime() == nil || row["refunded_at"].GTime().IsZero() {
t.Fatalf("refunded_at 应已写入")
}
u, _ := g.Model(consts.TableUsers).Where("id", uid).One()
newExpire := u["level_expire_at"].GTime()
want := baseline.AddDate(0, 0, -30)
if newExpire == nil {
t.Fatalf("回收后期望仍为有限期(非 NULL)")
}
if diff := newExpire.Time.Sub(want.Time); diff < -2*time.Minute || diff > 2*time.Minute {
t.Fatalf("到期时间应回退 30 天:期望≈%s,得 %s", want.String(), newExpire.String())
}
if u["level_key"].String() != "v2" {
t.Fatalf("回收后仍未到期,等级不应降级,得 %q", u["level_key"].String())
}
// 幂等:再次退款返回成功但不重复回收
res2, err := AdminOrderRefund(ctx, no, true)
if err != nil {
t.Fatalf("幂等退款失败: %v", err)
}
if !res2.AlreadyRefunded || res2.Revoked {
t.Fatalf("第二次应为幂等命中,得 %+v", res2)
}
u2, _ := g.Model(consts.TableUsers).Where("id", uid).One()
if !u2["level_expire_at"].GTime().Time.Equal(newExpire.Time) {
t.Fatalf("幂等调用不应重复回收:%s → %s", newExpire.String(), u2["level_expire_at"].String())
}
}
// TestAdminOrderRefund_RevokeMemberExpiredDowngrade 回收后已到期 → 期限置当前时刻并回落默认等级。
func TestAdminOrderRefund_RevokeMemberExpiredDowngrade(t *testing.T) {
ensureTestSchema()
ctx := context.Background()
tag := newTag("rd")
uid := insertRefundUser(t, tag, "v2", gtime.Now().AddDate(0, 0, 10))
no := insertDeliveredOrder(t, uid, tag, consts.OrderTypeMember, g.Map{
"plan_key": "vip-month", "level_key": "v2", "duration_days": 30,
})
defer cleanupRefundFixture(t, uid, no)
if _, err := AdminOrderRefund(ctx, no, true); err != nil {
t.Fatalf("退款失败: %v", err)
}
u, _ := g.Model(consts.TableUsers).Where("id", uid).One()
expire := u["level_expire_at"].GTime()
if expire == nil {
t.Fatalf("回收后期限不应为 NULL(不得写成永久)")
}
if expire.After(gtime.Now()) {
t.Fatalf("回收后应已到期,得 %s", expire.String())
}
// 等级回落经 EffectiveLevelKey 收敛(不直接改写 level_key,与自然到期口径一致)
eff := EffectiveLevelKey(&entity.Users{LevelKey: u["level_key"].String(), LevelExpireAt: expire})
if eff != consts.DefaultLevelKey {
t.Fatalf("回收后已到期,生效等级应回落默认,得 %q", eff)
}
}
// TestAdminOrderRefund_RevokeQuotaClamp 次数包退款回收:扣到 0 为止,不为负。
func TestAdminOrderRefund_RevokeQuotaClamp(t *testing.T) {
ensureTestSchema()
ctx := context.Background()
tag := newTag("rq")
toolKey := "refund_test_tool_" + tag
uid := insertRefundUser(t, tag, consts.DefaultLevelKey, nil)
no1 := insertDeliveredOrder(t, uid, tag+"a", consts.OrderTypeQuota, g.Map{
"pack_key": "pack5", "tool_key": toolKey, "times": 5, "duration_days": 365,
})
no2 := insertDeliveredOrder(t, uid, tag+"b", consts.OrderTypeQuota, g.Map{
"pack_key": "pack10", "tool_key": toolKey, "times": 10, "duration_days": 365,
})
defer cleanupRefundFixture(t, uid, no1, no2)
now := gtime.Now()
if _, err := g.Model(consts.TableUserToolQuota).Data(g.Map{
"user_id": uid, "tool_key": toolKey, "times_left": 8, "total_bought": 8,
"expire_at": now.AddDate(0, 0, 365), "created_at": now, "updated_at": now,
}).Insert(); err != nil {
t.Fatalf("插入额度失败: %v", err)
}
// 第一次:8 - 5 = 3
if _, err := AdminOrderRefund(ctx, no1, true); err != nil {
t.Fatalf("退款失败: %v", err)
}
q, _ := g.Model(consts.TableUserToolQuota).Where("user_id", uid).Where("tool_key", toolKey).One()
if q["times_left"].Int() != 3 {
t.Fatalf("次数回收后应剩 3,得 %d", q["times_left"].Int())
}
// 第二次:3 - 10 → 夹到 0(不为负)
if _, err := AdminOrderRefund(ctx, no2, true); err != nil {
t.Fatalf("退款失败: %v", err)
}
q2, _ := g.Model(consts.TableUserToolQuota).Where("user_id", uid).Where("tool_key", toolKey).One()
if q2["times_left"].Int() != 0 {
t.Fatalf("次数应扣到 0 为止,得 %d", q2["times_left"].Int())
}
if q2["total_bought"].Int() < q2["times_left"].Int() {
t.Fatalf("total_bought 不应小于 times_left:%d < %d", q2["total_bought"].Int(), q2["times_left"].Int())
}
}
// TestAdminOrderRefund_PermanentMemberRevoked 永久会员(level_expire_at IS NULL)退款回收:
// 用户裁定(2026-09-23)—— 管理员勾选「取消服务」时,永久授权**同样真取消、置为已过期**。
//
// 关键反转点(相对旧行为「保留服务」):
// 1. level_expire_at 不再为 NULL,且 ≈ 当前时刻(由 DB 侧 NOW() 写入,读写同源、无 8h 时区偏差);
// 2. EffectiveLevelKey 随即回落默认等级(降级生效);
// 3. level_key 字段本身**保留原值**(失效判定交给 EffectiveLevelKey,不为永久会员加特例清空)。
func TestAdminOrderRefund_PermanentMemberRevoked(t *testing.T) {
ensureTestSchema()
ctx := context.Background()
tag := newTag("pm")
uid := insertRefundUser(t, tag, "v2", nil) // level_expire_at = NULL → 永久
no := insertDeliveredOrder(t, uid, tag, consts.OrderTypeMember, g.Map{
"plan_key": "vip-month", "level_key": "v2", "duration_days": 30,
})
defer cleanupRefundFixture(t, uid, no)
before := gtime.Now()
res, err := AdminOrderRefund(ctx, no, true)
if err != nil {
t.Fatalf("退款失败: %v", err)
}
if !res.Revoked || !res.ServiceRevoked {
t.Fatalf("期望 revoke=true 且确已回收永久授权,得 %+v", res)
}
row, _ := g.Model(consts.TableMemberOrders).Where("out_trade_no", no).One()
if row["status"].Int() != consts.OrderStatusRefunded {
t.Fatalf("订单状态应为已退款(2),得 %d", row["status"].Int())
}
if row["service_revoked"].Int() != 1 {
t.Fatalf("service_revoked 应为 1,得 %d", row["service_revoked"].Int())
}
u, _ := g.Model(consts.TableUsers).Where("id", uid).One()
expire := u["level_expire_at"].GTime()
// 关键反转点 1:永久(NULL)已被置为「当前时刻」,不再为 NULL
if expire == nil || expire.IsZero() {
t.Fatalf("永久会员退款回收后 level_expire_at 不应再为 NULL")
}
// DB 侧 NOW() 写入 + 读侧同源,容许 ±2 分钟
if d := expire.Time.Sub(before.Time); d < -2*time.Minute || d > 2*time.Minute {
t.Fatalf("level_expire_at 应≈当前时刻:期望≈%s,得 %s", before.String(), expire.String())
}
// 关键反转点 3:level_key 字段本身保留原值,不清空
if u["level_key"].String() != "v2" {
t.Fatalf("level_key 字段应保留原值 v2,得 %q", u["level_key"].String())
}
// 关键反转点 2:生效等级已回落默认(降级生效)。
// EffectiveLevelKey 内部以严格 `<` 判过期;而取消时刻由 DB 侧 NOW() 写入,因秒级截断与
// DB/进程时钟的亚秒偏差,读回值可能恰落在「当前秒」甚至略晚。故先等待进程时钟越过该时刻,
// 再验证降级已生效(<= ~2s,确定性,不受 DB 时钟偏差影响)。
if wait := time.Until(expire.Time.Add(time.Second)); wait > 0 {
time.Sleep(wait)
}
if eff := EffectiveLevelKey(&entity.Users{LevelKey: u["level_key"].String(), LevelExpireAt: expire}); eff != consts.DefaultLevelKey {
t.Fatalf("永久会员被回收后生效等级应回落默认,得 %q", eff)
}
}
// TestAdminOrderRefund_RevokeButNothingToReclaim 勾选「取消服务」但用户当前无生效会员(已过期):
// service_revoked 按**实际回收结果**记录 → 0(列表不显示「已回收」,避免误导管理员)。
func TestAdminOrderRefund_RevokeButNothingToReclaim(t *testing.T) {
ensureTestSchema()
ctx := context.Background()
tag := newTag("rn")
// level_key=v2 但 level_expire_at 早于当前时刻 → 当前无生效会员(已过期)
uid := insertRefundUser(t, tag, "v2", gtime.Now().AddDate(0, 0, -1))
no := insertDeliveredOrder(t, uid, tag, consts.OrderTypeMember, g.Map{
"plan_key": "vip-month", "level_key": "v2", "duration_days": 30,
})
defer cleanupRefundFixture(t, uid, no)
res, err := AdminOrderRefund(ctx, no, true) // 明确勾选回收
if err != nil {
t.Fatalf("退款失败: %v", err)
}
if !res.Revoked {
t.Fatalf("订单应被标记退款,得 %+v", res)
}
if res.ServiceRevoked {
t.Fatalf("用户当前无生效会员,service_revoked 应为 false,得 %+v", res)
}
row, _ := g.Model(consts.TableMemberOrders).Where("out_trade_no", no).One()
if row["status"].Int() != consts.OrderStatusRefunded {
t.Fatalf("订单状态应为已退款(2),得 %d", row["status"].Int())
}
if row["service_revoked"].Int() != 0 {
t.Fatalf("无可回收的生效授权时 service_revoked 应为 0,得 %d", row["service_revoked"].Int())
}
// 权益本就无效,退款回收后仍为「已过期」(不产生新的生效会员)
u, _ := g.Model(consts.TableUsers).Where("id", uid).One()
eff := EffectiveLevelKey(&entity.Users{
LevelKey: u["level_key"].String(),
LevelExpireAt: u["level_expire_at"].GTime(),
})
if eff != consts.DefaultLevelKey {
t.Fatalf("已过期会员退款回收后不应变为生效,得 %q", eff)
}
}
// TestAdminOrderRefund_NewWritesNoTZ01Drift 钉住「新代码不得引入 TZ-01」:
// 退款流程新引入的 datetime 写入(订单 refunded_at / updated_at、额度 updated_at、永久会员 level_expire_at)
// 一律走库内 NOW(),库读回后应≈当前墙钟(±5s),而**不是**早 8h。
func TestAdminOrderRefund_NewWritesNoTZ01Drift(t *testing.T) {
ensureTestSchema()
ctx := context.Background()
// ---- (1) 会员单退款:订单 refunded_at / updated_at ≈ 墙钟 ----
tag := newTag("tzm")
uid := insertRefundUser(t, tag, "v2", gtime.Now().AddDate(0, 0, 60))
no := insertDeliveredOrder(t, uid, tag, consts.OrderTypeMember, g.Map{
"plan_key": "vip-month", "level_key": "v2", "duration_days": 30,
})
defer cleanupRefundFixture(t, uid, no)
before := gtime.Now()
if _, err := AdminOrderRefund(ctx, no, true); err != nil {
t.Fatalf("退款失败: %v", err)
}
row, _ := g.Model(consts.TableMemberOrders).Where("out_trade_no", no).One()
refunded := row["refunded_at"].GTime()
if refunded == nil || refunded.IsZero() {
t.Fatalf("refunded_at 应写入且非零")
}
if d := refunded.Time.Sub(before.Time); d < -5*time.Second || d > 5*time.Second {
t.Fatalf("refunded_at 应≈墙钟(±5s),得 %s(疑似 TZ-01 8h 偏差)", refunded.String())
}
if d := row["updated_at"].GTime().Time.Sub(before.Time); d < -5*time.Second || d > 5*time.Second {
t.Fatalf("订单 updated_at 应≈墙钟(±5s),得 %s(疑似 TZ-01 8h 偏差)", row["updated_at"].String())
}
// ---- (2) 永久会员退款:level_expire_at 非 NULL 且不晚于库内 NOW() ----
tag2 := newTag("tzp")
uid2 := insertRefundUser(t, tag2, "v2", nil) // 永久
no2 := insertDeliveredOrder(t, uid2, tag2, consts.OrderTypeMember, g.Map{
"plan_key": "vip-month", "level_key": "v2", "duration_days": 30,
})
defer cleanupRefundFixture(t, uid2, no2)
before2 := gtime.Now()
if _, err := AdminOrderRefund(ctx, no2, true); err != nil {
t.Fatalf("退款失败: %v", err)
}
u2, _ := g.Model(consts.TableUsers).Where("id", uid2).One()
exp := u2["level_expire_at"].GTime()
if exp == nil || exp.IsZero() {
t.Fatalf("永久会员回收后 level_expire_at 不应为 NULL")
}
// 库内判定「不晚于 NOW()」:比 Go 侧严格 `<` 更稳,规避秒级边界。
late, _ := g.DB().GetValue(ctx,
"SELECT (level_expire_at <= NOW()) FROM "+consts.TableUsers+" WHERE id = ?", uid2)
if !late.Bool() {
t.Fatalf("永久会员回收后 level_expire_at 应不晚于库内 NOW(),得 %s", exp.String())
}
if d := exp.Time.Sub(before2.Time); d < -5*time.Second || d > 5*time.Second {
t.Fatalf("level_expire_at 应≈墙钟(±5s),得 %s(疑似 TZ-01 8h 偏差)", exp.String())
}
// ---- (3) 次数包退款:额度 updated_at ≈ 墙钟 ----
tag3 := newTag("tzq")
toolKey := "tz_tool_" + tag3
uid3 := insertRefundUser(t, tag3, consts.DefaultLevelKey, nil)
no3 := insertDeliveredOrder(t, uid3, tag3, consts.OrderTypeQuota, g.Map{
"pack_key": "pack5", "tool_key": toolKey, "times": 5, "duration_days": 365,
})
defer cleanupRefundFixture(t, uid3, no3)
tn := gtime.Now()
if _, err := g.Model(consts.TableUserToolQuota).Data(g.Map{
"user_id": uid3, "tool_key": toolKey, "times_left": 8, "total_bought": 8,
"expire_at": tn.AddDate(0, 0, 365), "created_at": tn, "updated_at": tn,
}).Insert(); err != nil {
t.Fatalf("插入额度失败: %v", err)
}
beforeQ := gtime.Now()
if _, err := AdminOrderRefund(ctx, no3, true); err != nil {
t.Fatalf("退款失败: %v", err)
}
q, _ := g.Model(consts.TableUserToolQuota).Where("user_id", uid3).Where("tool_key", toolKey).One()
if d := q["updated_at"].GTime().Time.Sub(beforeQ.Time); d < -5*time.Second || d > 5*time.Second {
t.Fatalf("额度 updated_at 应≈墙钟(±5s),得 %s(疑似 TZ-01 8h 偏差)", q["updated_at"].String())
}
}
// TestAdminOrderRefund_NoRevokeKeepsService 不回收服务:仅改状态,权益保留。
func TestAdminOrderRefund_NoRevokeKeepsService(t *testing.T) {
ensureTestSchema()
ctx := context.Background()
tag := newTag("nr")
uid := insertRefundUser(t, tag, "v2", gtime.Now().AddDate(0, 0, 60))
no := insertDeliveredOrder(t, uid, tag, consts.OrderTypeMember, g.Map{
"plan_key": "vip-month", "level_key": "v2", "duration_days": 30,
})
defer cleanupRefundFixture(t, uid, no)
baseU, _ := g.Model(consts.TableUsers).Where("id", uid).One()
baseline := baseU["level_expire_at"].GTime()
res, err := AdminOrderRefund(ctx, no, false)
if err != nil {
t.Fatalf("退款失败: %v", err)
}
if !res.Revoked || res.ServiceRevoked {
t.Fatalf("不应回收服务,得 %+v", res)
}
row, _ := g.Model(consts.TableMemberOrders).Where("out_trade_no", no).One()
if row["status"].Int() != consts.OrderStatusRefunded || row["service_revoked"].Int() != 0 {
t.Fatalf("状态/service_revoked 不符:%d/%d", row["status"].Int(), row["service_revoked"].Int())
}
u, _ := g.Model(consts.TableUsers).Where("id", uid).One()
if !u["level_expire_at"].GTime().Time.Equal(baseline.Time) {
t.Fatalf("不回收时会员期限应保持不变:%s → %s", baseline.String(), u["level_expire_at"].String())
}
}
// TestAdminOrderRefund_StatusGuard 非已发货订单退款被拒(可读业务错误,4015)。
func TestAdminOrderRefund_StatusGuard(t *testing.T) {
ensureTestSchema()
ctx := context.Background()
tag := newTag("sg")
uid := insertRefundUser(t, tag, consts.DefaultLevelKey, nil)
no := insertDeliveredOrder(t, uid, tag, consts.OrderTypeMember, g.Map{
"plan_key": "vip-month", "level_key": "v2", "duration_days": 30,
"status": consts.OrderStatusPending,
})
defer cleanupRefundFixture(t, uid, no)
if _, err := AdminOrderRefund(ctx, no, true); err == nil {
t.Fatalf("待支付订单退款应被拒绝")
}
}
// TestAdminOrderList_Keyword 列表关键字命中订单号,且返回商品名与用户信息。
func TestAdminOrderList_Keyword(t *testing.T) {
ctx := context.Background()
tag := newTag("ls")
uid := insertRefundUser(t, tag, "v2", gtime.Now().AddDate(0, 0, 60))
no := insertDeliveredOrder(t, uid, tag, consts.OrderTypeMember, g.Map{
"plan_key": "vip-month", "level_key": "v2", "duration_days": 30,
})
defer cleanupRefundFixture(t, uid, no)
res, err := AdminOrderList(ctx, &v1.OrderListReq{Keyword: no})
if err != nil {
t.Fatalf("列表失败: %v", err)
}
if res.Total < 1 {
t.Fatalf("按订单号应至少命中 1 条,得 %d", res.Total)
}
found := false
for _, it := range res.List {
if it.OutTradeNo == no {
found = true
if it.Nickname == "" || it.UserId != uid {
t.Fatalf("应带归属用户信息,得 %+v", it)
}
if it.StatusText == "" {
t.Fatalf("应带状态文案")
}
}
}
if !found {
t.Fatalf("未在列表中找到目标订单 %s", no)
}
}
// ============================================================================
// 供 curl 实测用的可选 fixture(默认不运行;设 SEED_REFUND=1 时插入并保留)
// ============================================================================
func TestAdminOrderSeedForCurl(t *testing.T) {
if os.Getenv("SEED_REFUND") != "1" {
t.Skip("未设置 SEED_REFUND=1,跳过 curl fixture 播种")
}
tag := "curl" + fmt.Sprintf("%d", time.Now().UnixNano())
uid := insertRefundUser(t, tag, "v2", gtime.Now().AddDate(0, 0, 60))
no := insertDeliveredOrder(t, uid, tag, consts.OrderTypeMember, g.Map{
"plan_key": "vip-month", "level_key": "v2", "duration_days": 30,
})
fmt.Printf("SEED_REFUND uid=%d out_trade_no=%s\n", uid, no)
}
// TestAdminOrderCleanupForCurl 清理 curl fixture(设 CLEAN_REFUND=1 + REFUND_ORDER_NO/REFUND_UID)。
func TestAdminOrderCleanupForCurl(t *testing.T) {
if os.Getenv("CLEAN_REFUND") != "1" {
t.Skip("未设置 CLEAN_REFUND=1,跳过 curl fixture 清理")
}
no := os.Getenv("REFUND_ORDER_NO")
uid := os.Getenv("REFUND_UID")
if no != "" {
if _, err := g.Model(consts.TableMemberOrders).Where("out_trade_no", no).Delete(); err != nil {
t.Logf("删除订单失败: %v", err)
}
}
if uid != "" {
g.Model(consts.TableUserToolQuota).Where("user_id", uid).Delete()
g.Model(consts.TableUsers).Where("id", uid).Delete()
}
fmt.Printf("CLEAN_REFUND done no=%s uid=%s\n", no, uid)
}