Files
observer/server/biz/service/payment_test.go
T

249 lines
8.2 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 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_orderlicense 只留到期时间(此处验证第二次下单的订单快照)
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 必须落 NULLUNIQUE 约束下多个 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)
}
}
}