560 lines
22 KiB
Go
560 lines
22 KiB
Go
package smoke_test
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
_ "github.com/go-sql-driver/mysql"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
)
|
|
|
|
const (
|
|
bagGiftAppCode = "lalu"
|
|
bagGiftJWTSecret = "dev-shared-secret"
|
|
|
|
bagGiftSenderID = int64(920001)
|
|
bagGiftTargetID = int64(920002)
|
|
|
|
bagGiftID = "bag_smoke_gift"
|
|
bagGiftResourceCode = "bag_smoke_gift_resource"
|
|
bagGiftEntitlement = "bag-smoke-entitlement"
|
|
bagGiftPrice = int64(10)
|
|
bagGiftHeatValue = int64(10)
|
|
bagGiftSendCount = int64(2)
|
|
)
|
|
|
|
func TestBagGiftLocalHTTPFlow(t *testing.T) {
|
|
if os.Getenv("BAG_GIFT_REAL_HTTP_FLOW_TEST") != "1" {
|
|
t.Skip("set BAG_GIFT_REAL_HTTP_FLOW_TEST=1 to run the local bag gift HTTP flow")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
|
defer cancel()
|
|
|
|
db, err := sql.Open("mysql", bagGiftEnvOr("BAG_GIFT_MYSQL_DSN", "hyapp:hyapp@tcp(127.0.0.1:23306)/?parseTime=true&charset=utf8mb4&loc=UTC&multiStatements=true"))
|
|
if err != nil {
|
|
t.Fatalf("open mysql failed: %v", err)
|
|
}
|
|
defer db.Close()
|
|
if err := db.PingContext(ctx); err != nil {
|
|
t.Fatalf("mysql ping failed: %v", err)
|
|
}
|
|
|
|
flow := &bagGiftHTTPFlow{
|
|
db: db,
|
|
http: &http.Client{Timeout: 15 * time.Second},
|
|
gateway: strings.TrimRight(bagGiftEnvOr("BAG_GIFT_GATEWAY", "http://127.0.0.1:13000"), "/"),
|
|
startMS: time.Now().UTC().UnixMilli(),
|
|
}
|
|
regionID, err := flow.lookupUSRegion(ctx)
|
|
if err != nil {
|
|
t.Fatalf("lookup region failed: %v", err)
|
|
}
|
|
if err := flow.cleanup(ctx); err != nil {
|
|
t.Fatalf("cleanup failed: %v", err)
|
|
}
|
|
if err := flow.seedUsers(ctx, regionID); err != nil {
|
|
t.Fatalf("seed users failed: %v", err)
|
|
}
|
|
if err := flow.seedBagGift(ctx); err != nil {
|
|
t.Fatalf("seed bag gift failed: %v", err)
|
|
}
|
|
if err := flow.assertMyGiftResourceVisible(ctx); err != nil {
|
|
t.Fatalf("my gift resource assertion failed: %v", err)
|
|
}
|
|
roomID, err := flow.createRoomAndJoin(ctx)
|
|
if err != nil {
|
|
t.Fatalf("create room and join failed: %v", err)
|
|
}
|
|
if err := flow.assertBagPanelVisible(ctx, roomID); err != nil {
|
|
t.Fatalf("bag gift panel assertion failed: %v", err)
|
|
}
|
|
|
|
commandID := bagGiftCommandID("send")
|
|
sendResp, err := flow.sendBagGift(ctx, roomID, commandID)
|
|
if err != nil {
|
|
t.Fatalf("send bag gift failed: %v", err)
|
|
}
|
|
if strings.TrimSpace(sendResp.BillingReceiptID) == "" {
|
|
t.Fatalf("send response must include billing_receipt_id: %+v", sendResp)
|
|
}
|
|
if sendResp.RoomHeat < bagGiftPrice*bagGiftSendCount {
|
|
t.Fatalf("room_heat must include bag gift value, got=%d want>=%d resp=%+v", sendResp.RoomHeat, bagGiftPrice*bagGiftSendCount, sendResp)
|
|
}
|
|
if err := flow.assertWalletFacts(ctx, commandID, sendResp.BillingReceiptID); err != nil {
|
|
t.Fatalf("wallet facts mismatch: %v", err)
|
|
}
|
|
if err := flow.assertRoomFacts(ctx, roomID, commandID); err != nil {
|
|
t.Fatalf("room facts mismatch: %v", err)
|
|
}
|
|
|
|
t.Logf("bag gift HTTP smoke passed: gateway=%s room_id=%s command_id=%s receipt=%s", flow.gateway, roomID, commandID, sendResp.BillingReceiptID)
|
|
}
|
|
|
|
type bagGiftHTTPFlow struct {
|
|
db *sql.DB
|
|
http *http.Client
|
|
gateway string
|
|
startMS int64
|
|
}
|
|
|
|
type bagGiftEnvelope struct {
|
|
Code string `json:"code"`
|
|
Message string `json:"message"`
|
|
RequestID string `json:"request_id"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
|
|
type bagGiftCreateRoomData struct {
|
|
Room struct {
|
|
RoomID string `json:"room_id"`
|
|
} `json:"room"`
|
|
}
|
|
|
|
type bagGiftSendData struct {
|
|
BillingReceiptID string `json:"billing_receipt_id"`
|
|
RoomHeat int64 `json:"room_heat"`
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) lookupUSRegion(ctx context.Context) (int64, error) {
|
|
var regionID int64
|
|
err := f.db.QueryRowContext(ctx, `
|
|
SELECT r.region_id
|
|
FROM hyapp_user.regions r
|
|
JOIN hyapp_user.region_countries rc ON rc.app_code = r.app_code AND rc.region_id = r.region_id
|
|
WHERE r.app_code = ? AND rc.country_code = 'US' AND r.status = 'active' AND rc.status = 'active'
|
|
ORDER BY r.region_id
|
|
LIMIT 1`, bagGiftAppCode).Scan(®ionID)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("lookup US region: %w", err)
|
|
}
|
|
return regionID, nil
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) cleanup(ctx context.Context) error {
|
|
// 这个 smoke 使用固定用户和固定资源,运行前先把本用例自己产生的房间、钱包流水和权益恢复到确定状态。
|
|
// 业务行为仍然只通过 gateway HTTP 发起;这里的 SQL 只负责隔离测试数据,避免旧房间 owner 唯一约束和旧权益库存影响结果。
|
|
queries := []struct {
|
|
sql string
|
|
args []any
|
|
}{
|
|
{`DELETE FROM hyapp_room.room_background_images WHERE app_code = ? AND room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?))`, []any{bagGiftAppCode, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}},
|
|
{`DELETE FROM hyapp_room.room_snapshots WHERE app_code = ? AND room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?))`, []any{bagGiftAppCode, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}},
|
|
{`DELETE FROM hyapp_room.room_command_log WHERE app_code = ? AND (command_id LIKE 'bag-gift-smoke:%' OR room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?)))`, []any{bagGiftAppCode, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}},
|
|
{`DELETE FROM hyapp_room.room_outbox WHERE app_code = ? AND room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?))`, []any{bagGiftAppCode, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}},
|
|
{`DELETE FROM hyapp_room.room_user_gift_stats WHERE app_code = ? AND room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?))`, []any{bagGiftAppCode, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}},
|
|
{`DELETE FROM hyapp_room.room_user_presence WHERE app_code = ? AND (user_id IN (?, ?) OR room_id IN (SELECT room_id FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?)))`, []any{bagGiftAppCode, bagGiftSenderID, bagGiftTargetID, bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}},
|
|
{`DELETE FROM hyapp_room.rooms WHERE app_code = ? AND owner_user_id IN (?, ?)`, []any{bagGiftAppCode, bagGiftSenderID, bagGiftTargetID}},
|
|
{`DELETE FROM hyapp_wallet.wallet_entries WHERE app_code = ? AND transaction_id IN (SELECT transaction_id FROM hyapp_wallet.wallet_transactions WHERE app_code = ? AND command_id LIKE 'bag-gift-smoke:%')`, []any{bagGiftAppCode, bagGiftAppCode}},
|
|
{`DELETE FROM hyapp_wallet.wallet_outbox WHERE app_code = ? AND command_id LIKE 'bag-gift-smoke:%'`, []any{bagGiftAppCode}},
|
|
{`DELETE FROM hyapp_wallet.wallet_transactions WHERE app_code = ? AND command_id LIKE 'bag-gift-smoke:%'`, []any{bagGiftAppCode}},
|
|
}
|
|
for _, query := range queries {
|
|
if _, err := f.db.ExecContext(ctx, query.sql, query.args...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) seedUsers(ctx context.Context, regionID int64) error {
|
|
if _, err := f.db.ExecContext(ctx, `
|
|
DELETE FROM hyapp_user.user_display_user_ids
|
|
WHERE app_code = ? AND (user_id IN (?, ?) OR display_user_id IN ('920001', '920002'))`,
|
|
bagGiftAppCode, bagGiftSenderID, bagGiftTargetID,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
nowMS := time.Now().UTC().UnixMilli()
|
|
users := []struct {
|
|
id int64
|
|
name string
|
|
avatar string
|
|
}{
|
|
{bagGiftSenderID, "Bag Smoke Sender", "https://cdn.example.com/bag-smoke-sender.png"},
|
|
{bagGiftTargetID, "Bag Smoke Target", "https://cdn.example.com/bag-smoke-target.png"},
|
|
}
|
|
for _, user := range users {
|
|
displayID := strconv.FormatInt(user.id, 10)
|
|
if _, err := f.db.ExecContext(ctx, `
|
|
INSERT INTO hyapp_user.users (
|
|
app_code, user_id, default_display_user_id, current_display_user_id, current_display_user_id_kind,
|
|
username, gender, country, region_id, avatar, profile_completed, profile_completed_at_ms,
|
|
onboarding_status, status, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, ?, 'default', ?, 'unknown', 'US', ?, ?, 1, ?, 'completed', 'active', ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
current_display_user_id = VALUES(current_display_user_id),
|
|
current_display_user_id_kind = VALUES(current_display_user_id_kind),
|
|
username = VALUES(username),
|
|
country = VALUES(country),
|
|
region_id = VALUES(region_id),
|
|
avatar = VALUES(avatar),
|
|
profile_completed = VALUES(profile_completed),
|
|
profile_completed_at_ms = VALUES(profile_completed_at_ms),
|
|
onboarding_status = VALUES(onboarding_status),
|
|
status = VALUES(status),
|
|
updated_at_ms = VALUES(updated_at_ms)`,
|
|
bagGiftAppCode, user.id, displayID, displayID, user.name, regionID, user.avatar, nowMS, nowMS, nowMS,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if _, err := f.db.ExecContext(ctx, `
|
|
INSERT INTO hyapp_user.user_display_user_ids (
|
|
app_code, display_user_id, user_id, display_user_id_kind, status,
|
|
assigned_at_ms, activated_at_ms, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, 'default', 'active', ?, ?, ?, ?)`,
|
|
bagGiftAppCode, displayID, user.id, nowMS, nowMS, nowMS, nowMS,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) seedBagGift(ctx context.Context) error {
|
|
nowMS := time.Now().UTC().UnixMilli()
|
|
if _, err := f.db.ExecContext(ctx, `
|
|
INSERT INTO hyapp_wallet.resources (
|
|
app_code, resource_code, resource_type, name, status, grantable, manager_grant_enabled,
|
|
grant_strategy, wallet_asset_type, wallet_asset_amount, price_type, coin_price, gift_point_amount,
|
|
usage_scope_json, asset_url, preview_url, animation_url, metadata_json, sort_order,
|
|
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, 'gift', 'Bag Smoke Gift', 'active', TRUE, TRUE,
|
|
'increase_quantity', '', 0, 'coin', ?, 0, JSON_ARRAY('gift_panel'),
|
|
'https://cdn.example.com/bag-smoke-gift.png', 'https://cdn.example.com/bag-smoke-gift-preview.png',
|
|
'https://cdn.example.com/bag-smoke-gift-animation.svga', JSON_OBJECT(), 0, 0, 0, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
resource_type = 'gift',
|
|
name = VALUES(name),
|
|
status = 'active',
|
|
grantable = TRUE,
|
|
manager_grant_enabled = TRUE,
|
|
grant_strategy = 'increase_quantity',
|
|
price_type = 'coin',
|
|
coin_price = VALUES(coin_price),
|
|
usage_scope_json = VALUES(usage_scope_json),
|
|
asset_url = VALUES(asset_url),
|
|
preview_url = VALUES(preview_url),
|
|
animation_url = VALUES(animation_url),
|
|
updated_at_ms = VALUES(updated_at_ms)`,
|
|
bagGiftAppCode, bagGiftResourceCode, bagGiftPrice, nowMS, nowMS,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
|
|
var resourceID int64
|
|
if err := f.db.QueryRowContext(ctx, `
|
|
SELECT resource_id FROM hyapp_wallet.resources WHERE app_code = ? AND resource_code = ?`,
|
|
bagGiftAppCode, bagGiftResourceCode,
|
|
).Scan(&resourceID); err != nil {
|
|
return err
|
|
}
|
|
|
|
if _, err := f.db.ExecContext(ctx, `
|
|
INSERT INTO hyapp_wallet.gift_configs (
|
|
app_code, gift_id, resource_id, status, name, sort_order, presentation_json,
|
|
gift_type_code, effective_from_ms, effective_to_ms, effect_types_json,
|
|
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, 'active', 'Bag Smoke Gift', 1, CAST(? AS JSON), 'normal', 0, 0, JSON_ARRAY(), 0, 0, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
resource_id = VALUES(resource_id),
|
|
status = 'active',
|
|
name = VALUES(name),
|
|
sort_order = VALUES(sort_order),
|
|
presentation_json = VALUES(presentation_json),
|
|
gift_type_code = 'normal',
|
|
effective_from_ms = 0,
|
|
effective_to_ms = 0,
|
|
effect_types_json = JSON_ARRAY(),
|
|
updated_at_ms = VALUES(updated_at_ms)`,
|
|
bagGiftAppCode, bagGiftID, resourceID, `{"preview_url":"https://cdn.example.com/bag-smoke-gift-preview.png","animation_url":"https://cdn.example.com/bag-smoke-gift-animation.svga"}`, nowMS, nowMS,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if _, err := f.db.ExecContext(ctx, `
|
|
INSERT INTO hyapp_wallet.gift_config_regions (app_code, gift_id, region_id, created_at_ms, updated_at_ms)
|
|
VALUES (?, ?, 0, ?, ?)
|
|
ON DUPLICATE KEY UPDATE updated_at_ms = VALUES(updated_at_ms)`,
|
|
bagGiftAppCode, bagGiftID, nowMS, nowMS,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if _, err := f.db.ExecContext(ctx, `
|
|
INSERT INTO hyapp_wallet.wallet_gift_prices (
|
|
app_code, gift_id, price_version, status, charge_asset_type, coin_price,
|
|
gift_point_amount, heat_value, effective_at_ms, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, 'bag-smoke-v1', 'active', 'COIN', ?, 0, ?, 0, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
status = 'active',
|
|
charge_asset_type = 'COIN',
|
|
coin_price = VALUES(coin_price),
|
|
gift_point_amount = 0,
|
|
heat_value = VALUES(heat_value),
|
|
effective_at_ms = 0,
|
|
updated_at_ms = VALUES(updated_at_ms)`,
|
|
bagGiftAppCode, bagGiftID, bagGiftPrice, bagGiftHeatValue, nowMS, nowMS,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if _, err := f.db.ExecContext(ctx, `
|
|
INSERT INTO hyapp_wallet.wallet_accounts (
|
|
app_code, user_id, asset_type, available_amount, frozen_amount, version, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, 'COIN', 0, 0, 1, ?, ?)
|
|
ON DUPLICATE KEY UPDATE available_amount = 0, frozen_amount = 0, version = version + 1, updated_at_ms = VALUES(updated_at_ms)`,
|
|
bagGiftAppCode, bagGiftSenderID, nowMS, nowMS,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
if _, err := f.db.ExecContext(ctx, `
|
|
INSERT INTO hyapp_wallet.user_resource_entitlements (
|
|
app_code, entitlement_id, user_id, resource_id, status, quantity, remaining_quantity,
|
|
effective_at_ms, expires_at_ms, source_grant_id, created_at_ms, updated_at_ms
|
|
) VALUES (?, ?, ?, ?, 'active', 3, 3, 0, 0, 'bag-smoke-grant', ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
user_id = VALUES(user_id),
|
|
resource_id = VALUES(resource_id),
|
|
status = 'active',
|
|
quantity = 3,
|
|
remaining_quantity = 3,
|
|
effective_at_ms = 0,
|
|
expires_at_ms = 0,
|
|
source_grant_id = VALUES(source_grant_id),
|
|
updated_at_ms = VALUES(updated_at_ms)`,
|
|
bagGiftAppCode, bagGiftEntitlement, bagGiftSenderID, resourceID, nowMS, nowMS,
|
|
); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) createRoomAndJoin(ctx context.Context) (string, error) {
|
|
var createData bagGiftCreateRoomData
|
|
if err := f.request(ctx, http.MethodPost, "/api/v1/rooms/create", bagGiftSenderID, map[string]any{
|
|
"command_id": bagGiftCommandID("create-room"),
|
|
"seat_count": 10,
|
|
"mode": "voice",
|
|
"room_name": "Bag Gift Smoke Room",
|
|
"room_avatar": "https://cdn.example.com/bag-gift-smoke-room.png",
|
|
"room_description": "bag gift smoke",
|
|
}, &createData); err != nil {
|
|
return "", err
|
|
}
|
|
if strings.TrimSpace(createData.Room.RoomID) == "" {
|
|
return "", fmt.Errorf("create room response missing room.room_id")
|
|
}
|
|
for _, userID := range []int64{bagGiftSenderID, bagGiftTargetID} {
|
|
if err := f.request(ctx, http.MethodPost, "/api/v1/rooms/join", userID, map[string]any{
|
|
"room_id": createData.Room.RoomID,
|
|
"command_id": bagGiftCommandID(fmt.Sprintf("join-%d", userID)),
|
|
"role": "audience",
|
|
}, nil); err != nil {
|
|
return "", fmt.Errorf("join user %d: %w", userID, err)
|
|
}
|
|
}
|
|
return createData.Room.RoomID, nil
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) assertMyGiftResourceVisible(ctx context.Context) error {
|
|
var raw json.RawMessage
|
|
if err := f.request(ctx, http.MethodGet, "/api/v1/users/me/resources?resource_type=gift", bagGiftSenderID, nil, &raw); err != nil {
|
|
return err
|
|
}
|
|
body := string(raw)
|
|
if !strings.Contains(body, bagGiftEntitlement) || !strings.Contains(body, bagGiftResourceCode) {
|
|
return fmt.Errorf("my resources does not contain bag gift entitlement/resource data=%s", body)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) assertBagPanelVisible(ctx context.Context, roomID string) error {
|
|
var raw json.RawMessage
|
|
if err := f.request(ctx, http.MethodGet, "/api/v1/rooms/"+url.PathEscape(roomID)+"/gift-panel", bagGiftSenderID, nil, &raw); err != nil {
|
|
return err
|
|
}
|
|
body := string(raw)
|
|
if !strings.Contains(body, bagGiftID) {
|
|
return fmt.Errorf("gift panel does not contain gift_id=%s data=%s", bagGiftID, body)
|
|
}
|
|
if !strings.Contains(body, bagGiftEntitlement) || !strings.Contains(body, `"source":"bag"`) {
|
|
return fmt.Errorf("gift panel does not contain bag entitlement/source data=%s", body)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) sendBagGift(ctx context.Context, roomID string, commandID string) (bagGiftSendData, error) {
|
|
var data bagGiftSendData
|
|
err := f.request(ctx, http.MethodPost, "/api/v1/rooms/gift/send", bagGiftSenderID, map[string]any{
|
|
"room_id": roomID,
|
|
"command_id": commandID,
|
|
"target_type": "user",
|
|
"target_user_ids": []int64{bagGiftTargetID},
|
|
"gift_id": bagGiftID,
|
|
"gift_count": bagGiftSendCount,
|
|
"source": "bag",
|
|
"entitlement_id": bagGiftEntitlement,
|
|
}, &data)
|
|
return data, err
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) assertWalletFacts(ctx context.Context, commandID string, billingReceiptID string) error {
|
|
var remaining int64
|
|
if err := f.db.QueryRowContext(ctx, `
|
|
SELECT remaining_quantity
|
|
FROM hyapp_wallet.user_resource_entitlements
|
|
WHERE app_code = ? AND entitlement_id = ?`,
|
|
bagGiftAppCode, bagGiftEntitlement,
|
|
).Scan(&remaining); err != nil {
|
|
return err
|
|
}
|
|
if remaining != 1 {
|
|
return fmt.Errorf("remaining_quantity=%d, want 1", remaining)
|
|
}
|
|
|
|
var transactionID, bizType, status, chargeSource, chargeAssetType, gotBillingReceiptID string
|
|
var coinSpent, heatValue int64
|
|
if err := f.db.QueryRowContext(ctx, `
|
|
SELECT transaction_id, biz_type, status,
|
|
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_source')), ''),
|
|
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.charge_asset_type')), ''),
|
|
COALESCE(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.billing_receipt_id')), ''),
|
|
COALESCE(JSON_EXTRACT(metadata_json, '$.coin_spent'), 0),
|
|
COALESCE(JSON_EXTRACT(metadata_json, '$.heat_value'), 0)
|
|
FROM hyapp_wallet.wallet_transactions
|
|
WHERE app_code = ? AND command_id = ?`,
|
|
bagGiftAppCode, commandID,
|
|
).Scan(&transactionID, &bizType, &status, &chargeSource, &chargeAssetType, &gotBillingReceiptID, &coinSpent, &heatValue); err != nil {
|
|
return err
|
|
}
|
|
// Bag 送礼只是不扣 COIN 账户库存,交易快照里的 coin_spent/heat_value 仍表示礼物原价总值;
|
|
// 房间贡献、麦位热度和主播积分继续复用这份价值口径,不能因为来源是背包就归零。
|
|
if bizType != "gift_debit" || status != "succeeded" || chargeSource != "bag" || chargeAssetType != "BAG" || coinSpent != bagGiftPrice*bagGiftSendCount || heatValue != bagGiftPrice*bagGiftSendCount {
|
|
return fmt.Errorf("wallet transaction mismatch biz_type=%s status=%s source=%s asset=%s coin_spent=%d heat=%d", bizType, status, chargeSource, chargeAssetType, coinSpent, heatValue)
|
|
}
|
|
if gotBillingReceiptID != billingReceiptID {
|
|
return fmt.Errorf("billing receipt mismatch metadata=%s response=%s transaction_id=%s", gotBillingReceiptID, billingReceiptID, transactionID)
|
|
}
|
|
var entryCount int64
|
|
if err := f.db.QueryRowContext(ctx, `
|
|
SELECT COUNT(*) FROM hyapp_wallet.wallet_entries WHERE app_code = ? AND transaction_id = ?`,
|
|
bagGiftAppCode, transactionID,
|
|
).Scan(&entryCount); err != nil {
|
|
return err
|
|
}
|
|
if entryCount != 0 {
|
|
return fmt.Errorf("bag send must not create coin wallet entries, got=%d", entryCount)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) assertRoomFacts(ctx context.Context, roomID string, commandID string) error {
|
|
var commandCount int64
|
|
if err := f.db.QueryRowContext(ctx, `
|
|
SELECT COUNT(*)
|
|
FROM hyapp_room.room_command_log
|
|
WHERE app_code = ? AND room_id = ? AND command_id = ? AND command_type = 'send_gift'`,
|
|
bagGiftAppCode, roomID, commandID,
|
|
).Scan(&commandCount); err != nil {
|
|
return err
|
|
}
|
|
if commandCount != 1 {
|
|
return fmt.Errorf("room_command_log count=%d, want 1", commandCount)
|
|
}
|
|
|
|
var outboxCount int64
|
|
if err := f.db.QueryRowContext(ctx, `
|
|
SELECT COUNT(*)
|
|
FROM hyapp_room.room_outbox
|
|
WHERE app_code = ? AND room_id = ? AND event_type = 'RoomGiftSent' AND created_at_ms >= ?`,
|
|
bagGiftAppCode, roomID, f.startMS,
|
|
).Scan(&outboxCount); err != nil {
|
|
return err
|
|
}
|
|
if outboxCount == 0 {
|
|
return fmt.Errorf("missing RoomGiftSent outbox event")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (f *bagGiftHTTPFlow) request(ctx context.Context, method string, path string, userID int64, body any, out any) error {
|
|
var reader io.Reader
|
|
if body != nil {
|
|
raw, err := json.Marshal(body)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reader = bytes.NewReader(raw)
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, f.gateway+path, reader)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+bagGiftTokenForUser(userID))
|
|
req.Header.Set("X-App-Code", bagGiftAppCode)
|
|
if body != nil {
|
|
req.Header.Set("Content-Type", "application/json")
|
|
}
|
|
resp, err := f.http.Do(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer resp.Body.Close()
|
|
raw, _ := io.ReadAll(resp.Body)
|
|
var envelope bagGiftEnvelope
|
|
if err := json.Unmarshal(raw, &envelope); err != nil {
|
|
return fmt.Errorf("%s %s returned non-envelope status=%d body=%s", method, path, resp.StatusCode, string(raw))
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 || envelope.Code != "OK" {
|
|
return fmt.Errorf("%s %s failed status=%d code=%s message=%s body=%s", method, path, resp.StatusCode, envelope.Code, envelope.Message, string(raw))
|
|
}
|
|
if out == nil {
|
|
return nil
|
|
}
|
|
if rawOut, ok := out.(*json.RawMessage); ok {
|
|
*rawOut = append((*rawOut)[:0], envelope.Data...)
|
|
return nil
|
|
}
|
|
if err := json.Unmarshal(envelope.Data, out); err != nil {
|
|
return fmt.Errorf("decode data for %s %s failed: %w data=%s", method, path, err, string(envelope.Data))
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func bagGiftTokenForUser(userID int64) string {
|
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
|
"user_id": strconv.FormatInt(userID, 10),
|
|
"app_code": bagGiftAppCode,
|
|
"sid": "bag-gift-smoke-" + strconv.FormatInt(userID, 10),
|
|
"profile_completed": true,
|
|
"onboarding_status": "completed",
|
|
"exp": time.Now().Add(2 * time.Hour).Unix(),
|
|
"iat": time.Now().Unix(),
|
|
})
|
|
signed, err := token.SignedString([]byte(bagGiftJWTSecret))
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return signed
|
|
}
|
|
|
|
func bagGiftCommandID(key string) string {
|
|
return fmt.Sprintf("bag-gift-smoke:%s:%d", key, time.Now().UTC().UnixNano())
|
|
}
|
|
|
|
func bagGiftEnvOr(key string, fallback string) string {
|
|
if value := strings.TrimSpace(os.Getenv(key)); value != "" {
|
|
return value
|
|
}
|
|
return fallback
|
|
}
|