249 lines
8.2 KiB
Go
249 lines
8.2 KiB
Go
package service
|
||
|
||
// 服务层白盒测试:验证支付回调落授权核心链路(无真实商户,绕过 SDK 调用)。
|
||
// 运行方式: cd server && GF_GCFG_FILE=biz/service/testdata/config.yml go test ./biz/service/
|
||
|
||
import (
|
||
"context"
|
||
"strconv"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/gogf/gf/v2/database/gdb"
|
||
"github.com/gogf/gf/v2/frame/g"
|
||
"github.com/gogf/gf/v2/os/gtime"
|
||
|
||
"observer-server/biz/consts"
|
||
"observer-server/biz/dao"
|
||
"observer-server/biz/model/dto"
|
||
"observer-server/biz/model/entity"
|
||
"observer-server/common"
|
||
)
|
||
|
||
func ctx() context.Context {
|
||
return context.Background()
|
||
}
|
||
|
||
// uniquePhone / uniqueOrderId 每次运行生成唯一值,避免测试库残留数据冲突
|
||
func uniquePhone() string {
|
||
return "138" + strconv.FormatInt(time.Now().UnixNano()%100000000, 10)
|
||
}
|
||
|
||
func uniqueOrderId(prefix string) string {
|
||
return prefix + strconv.FormatInt(time.Now().UnixNano()%100000000, 10)
|
||
}
|
||
|
||
// insertOrder 直插一条 created 订单(模拟下单链路产出)
|
||
func insertOrder(t *testing.T, o *entity.PaymentOrder) {
|
||
t.Helper()
|
||
err := g.DB().Transaction(ctx(), func(ctx context.Context, tx gdb.TX) error {
|
||
return dao.PaymentOrder.InsertOneInTx(ctx, tx, o)
|
||
})
|
||
if err != nil {
|
||
t.Fatalf("insert order: %v", err)
|
||
}
|
||
}
|
||
|
||
// TestPersistPaid 回调落授权:created → paid + 授权生效,重复回调幂等
|
||
func TestPersistPaid(t *testing.T) {
|
||
phone := uniquePhone()
|
||
orderId := uniqueOrderId("O-test-persist-")
|
||
wxTrade := uniqueOrderId("wx-trade-")
|
||
insertOrder(t, &entity.PaymentOrder{
|
||
OrderId: orderId,
|
||
PhoneNum: phone,
|
||
PlanId: "week",
|
||
Channel: consts.ChannelWechat,
|
||
AmountCents: 5600,
|
||
Status: consts.OrderStatusCreated,
|
||
CreatedAt: gtime.Now(),
|
||
})
|
||
|
||
if err := Payment.persistPaid(ctx(), orderId, wxTrade, consts.ChannelWechat, 0); err != nil {
|
||
t.Fatalf("persistPaid: %v", err)
|
||
}
|
||
|
||
// 订单已支付
|
||
o, err := dao.PaymentOrder.GetByPk(ctx(), orderId)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if o.Status != consts.OrderStatusPaid || o.WxTradeNo == nil || *o.WxTradeNo != wxTrade {
|
||
t.Fatalf("order not paid: status=%s wx=%v", o.Status, o.WxTradeNo)
|
||
}
|
||
|
||
// 授权生效:week = 今天 0 点 + 7 天
|
||
lic, err := dao.License.GetByPhone(ctx(), phone)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if lic == nil || lic.ExpiresAt == nil {
|
||
t.Fatal("license not granted")
|
||
}
|
||
want := time.Date(time.Now().Year(), time.Now().Month(), time.Now().Day(), 0, 0, 0, 0, time.Local).AddDate(0, 0, 7)
|
||
if !lic.ExpiresAt.Time.Equal(want) {
|
||
t.Fatalf("expiresAt = %s, want %s", lic.ExpiresAt.Time, want)
|
||
}
|
||
|
||
// 授权查询接口返回 active
|
||
licRes, err := License.Get(common.WithPhone(ctx(), phone), &dto.LicenseReq{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !licRes.Active {
|
||
t.Fatalf("license res = %+v", licRes)
|
||
}
|
||
|
||
// confirm 返回 paid
|
||
conf, err := Order.Confirm(ctx(), &dto.ConfirmOrderReq{OrderId: orderId})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if conf.Status != "paid" {
|
||
t.Fatalf("confirm status = %s, want paid", conf.Status)
|
||
}
|
||
|
||
// 重复回调幂等:不报错、不重复累加授权
|
||
before := lic.ExpiresAt
|
||
if err := Payment.persistPaid(ctx(), orderId, wxTrade, consts.ChannelWechat, 0); err != nil {
|
||
t.Fatalf("repeat persistPaid: %v", err)
|
||
}
|
||
lic2, _ := dao.License.GetByPhone(ctx(), phone)
|
||
if !lic2.ExpiresAt.Time.Equal(before.Time) {
|
||
t.Fatalf("idempotency broken: %s -> %s", before, lic2.ExpiresAt)
|
||
}
|
||
}
|
||
|
||
// TestPersistPaidRenewal 续费叠加:未过期顺延(base=现有到期),已过期从当前时间起算
|
||
func TestPersistPaidRenewal(t *testing.T) {
|
||
phone := uniquePhone()
|
||
order1 := uniqueOrderId("O-test-renew-1-")
|
||
insertOrder(t, &entity.PaymentOrder{
|
||
OrderId: order1,
|
||
PhoneNum: phone,
|
||
PlanId: "week",
|
||
Channel: consts.ChannelAlipay,
|
||
AmountCents: 5600,
|
||
Status: consts.OrderStatusCreated,
|
||
CreatedAt: gtime.Now(),
|
||
})
|
||
if err := Payment.persistPaid(ctx(), order1, uniqueOrderId("ali-trade-"), consts.ChannelAlipay, 5600); err != nil {
|
||
t.Fatalf("persistPaid 1: %v", err)
|
||
}
|
||
lic, _ := dao.License.GetByPhone(ctx(), phone)
|
||
|
||
// 第二次购买 month:base = 现有到期(未过期),30 天叠加
|
||
order2 := uniqueOrderId("O-test-renew-2-")
|
||
insertOrder(t, &entity.PaymentOrder{
|
||
OrderId: order2,
|
||
PhoneNum: phone,
|
||
PlanId: "month",
|
||
Channel: consts.ChannelWechat,
|
||
AmountCents: 18000,
|
||
Status: consts.OrderStatusCreated,
|
||
CreatedAt: gtime.Now(),
|
||
})
|
||
if err := Payment.persistPaid(ctx(), order2, uniqueOrderId("wx-trade-"), consts.ChannelWechat, 0); err != nil {
|
||
t.Fatalf("persistPaid 2: %v", err)
|
||
}
|
||
lic2, _ := dao.License.GetByPhone(ctx(), phone)
|
||
want := lic.ExpiresAt.Time.AddDate(0, 0, 30)
|
||
if !lic2.ExpiresAt.Time.Equal(want) {
|
||
t.Fatalf("renewal expiresAt = %s, want %s (base %s)", lic2.ExpiresAt, want, lic.ExpiresAt)
|
||
}
|
||
// 购买的套餐记录在 payment_order,license 只留到期时间(此处验证第二次下单的订单快照)
|
||
o2, _ := dao.PaymentOrder.GetByPk(ctx(), order2)
|
||
if o2 == nil || o2.PlanId != "month" {
|
||
t.Fatalf("order plan snapshot = %v, want month", o2)
|
||
}
|
||
|
||
// 支付宝回调金额不匹配 → 拒绝
|
||
order3 := uniqueOrderId("O-test-renew-3-")
|
||
insertOrder(t, &entity.PaymentOrder{
|
||
OrderId: order3,
|
||
PhoneNum: uniquePhone(),
|
||
PlanId: "day",
|
||
Channel: consts.ChannelAlipay,
|
||
AmountCents: 1000,
|
||
Status: consts.OrderStatusCreated,
|
||
CreatedAt: gtime.Now(),
|
||
})
|
||
if err := Payment.persistPaid(ctx(), order3, uniqueOrderId("ali-trade-"), consts.ChannelAlipay, 999); err == nil {
|
||
t.Fatal("amount mismatch should fail")
|
||
}
|
||
}
|
||
|
||
// TestCreateSequence 订单创建串行序列:惰性关闭超时订单 + 2 小时内复用 created 订单
|
||
func TestCreateSequence(t *testing.T) {
|
||
phone := uniquePhone()
|
||
orderStale := uniqueOrderId("O-test-stale-")
|
||
orderRecent := uniqueOrderId("O-test-recent-")
|
||
// 3 小时前的 stale 订单 + 1 分钟前的有效订单
|
||
now := gtime.Now()
|
||
insertOrder(t, &entity.PaymentOrder{
|
||
OrderId: orderStale, PhoneNum: phone, PlanId: "day", Channel: consts.ChannelWechat,
|
||
AmountCents: 1000, Status: consts.OrderStatusCreated,
|
||
CreatedAt: gtime.New(now.Time.Add(-3 * time.Hour)),
|
||
})
|
||
insertOrder(t, &entity.PaymentOrder{
|
||
OrderId: orderRecent, PhoneNum: phone, PlanId: "day", Channel: consts.ChannelWechat,
|
||
AmountCents: 1000, Status: consts.OrderStatusCreated,
|
||
CreatedAt: gtime.New(now.Time.Add(-1 * time.Minute)),
|
||
})
|
||
|
||
// 复现 Create 的串行事务序列
|
||
var recent *entity.PaymentOrder
|
||
err := common.Serial().Submit(ctx(), func() error {
|
||
return g.DB().Transaction(ctx(), func(ctx context.Context, tx gdb.TX) error {
|
||
window := gtime.New(now.Time.Add(-consts.PendingOrderWindowSeconds * time.Second))
|
||
if err := dao.PaymentOrder.CloseStaleCreatedInTx(ctx, tx, phone, window); err != nil {
|
||
return err
|
||
}
|
||
var err error
|
||
recent, err = dao.PaymentOrder.GetRecentCreatedInTx(ctx, tx, phone, window)
|
||
return err
|
||
})
|
||
})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
stale, _ := dao.PaymentOrder.GetByPk(ctx(), orderStale)
|
||
if stale.Status != consts.OrderStatusClosed {
|
||
t.Fatalf("stale order status = %s, want closed", stale.Status)
|
||
}
|
||
if recent == nil || recent.OrderId != orderRecent {
|
||
t.Fatalf("recent = %+v, want %s", recent, orderRecent)
|
||
}
|
||
}
|
||
|
||
// TestTradeNoNullable 未支付订单 trade_no 必须落 NULL:UNIQUE 约束下多个 created 订单(同渠道不同账号)可共存
|
||
func TestTradeNoNullable(t *testing.T) {
|
||
for _, channel := range []string{consts.ChannelAlipay, consts.ChannelAlipay, consts.ChannelWechat, consts.ChannelWechat} {
|
||
insertOrder(t, &entity.PaymentOrder{
|
||
OrderId: uniqueOrderId("O-test-trade-"), PhoneNum: uniquePhone(), PlanId: "day", Channel: channel,
|
||
AmountCents: 1000, Status: consts.OrderStatusCreated, CreatedAt: gtime.Now(),
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestCalcExpiresAt 自然日语义:day 当天 24 点失效、week/month 自生效日起累计
|
||
func TestCalcExpiresAt(t *testing.T) {
|
||
base := time.Date(2026, 8, 22, 15, 30, 0, 0, time.Local)
|
||
start := time.Date(2026, 8, 22, 0, 0, 0, 0, time.Local)
|
||
cases := []struct {
|
||
days int
|
||
want time.Time
|
||
}{
|
||
{1, start.AddDate(0, 0, 1)}, // 当天 24:00
|
||
{7, start.AddDate(0, 0, 7)}, // 第 7 天 24:00
|
||
{30, start.AddDate(0, 0, 30)},
|
||
}
|
||
for _, c := range cases {
|
||
got := License.CalcExpiresAt(base, c.days)
|
||
if !got.Time.Equal(c.want) {
|
||
t.Fatalf("CalcExpiresAt(%d) = %s, want %s", c.days, got, c.want)
|
||
}
|
||
}
|
||
}
|