npm package: @seaverse/assets-sdk
Description: SeaVerse Assets API SDK - Unified multi-tenant asset and credit management service
Author: SeaVerse Team
Latest version: 0.0.4

# @seaverse/assets-sdk

SeaVerse Asset Hub SDK - 统一的多租户积分管理服务（4 池系统）

## 概述

Asset Hub SDK 提供了完整的积分管理功能，支持 4 池积分系统（daily/event/monthly/permanent）和传统的积分账户查询。所有应用程序（包括 SeaVerse 平台本身）都使用相同的 SDK 路由和多租户架构。

**主要 API 端点:**
- `/sdk/v1/credits/detail` - 获取 4 池积分详情（推荐）
- `/sdk/v1/credits/account` - 获取积分账户信息（已弃用）
- `/sdk/v1/credits/transactions` - 查询交易历史

### 核心概念

#### 应用 ID
每个应用程序都有唯一的 `app_id`:
- **SeaVerse 平台**: `appId = "seaverse"` (官方平台应用,如 ops-frontend、meta-product)
- **第三方应用**: `appId = "game-abc123"`、`appId = "social-xyz789"` 等

#### 多租户隔离
- 积分账户按 `app_id` 隔离
- 每个应用都有自己的积分池和交易历史
- 同一用户在不同应用中可以有不同的积分账户

## 安装

```bash
npm install @seaverse/assets-sdk
# 或
pnpm add @seaverse/assets-sdk
```

## 功能特性

- **4 池积分系统**: 支持 daily/event/monthly/permanent 四种积分池
- **过期时间追踪**: 每个积分池都有独立的过期时间戳
- **消费优先级**: Daily → Event → Monthly → Permanent
- 查询用户积分账户信息,支持多租户
- 列出交易历史,支持筛选和分页
- TypeScript 支持,包含完整类型定义
- 只读 API(积分消费/发放由平台后端处理)

## 快速开始

```typescript
import { AssetsClient } from '@seaverse/assets-sdk';

// 为 SeaVerse 平台初始化客户端
const client = new AssetsClient({
  appId: 'seaverse',
  token: 'your-bearer-token'
});

// 或为第三方应用初始化
const gameClient = new AssetsClient({
  appId: 'game-abc123',
  token: 'your-bearer-token'
});

// 获取积分详情（推荐使用 - 4 池系统）
const detail = await client.getCreditDetail();
console.log(`总余额: ${detail.total_balance} 积分`);

// 显示各个积分池的详情
detail.pools.forEach(pool => {
  const expiryLabel = pool.expires_at === 0
    ? '永久有效'
    : `${Math.floor((pool.expires_at - Date.now()) / 86400000)} 天后过期`;
  console.log(`${pool.type}: ${pool.balance} (${expiryLabel})`);
});

// 列出最近的交易
const result = await client.listTransactions({ page_size: 10 });
console.log(`找到 ${result.transactions.length} 笔交易`);
console.log(`有更多: ${result.has_more}`);
```

## 4 池积分系统

Asset Hub SDK 使用 4 池积分系统，每种池有不同的过期规则和用途：

### 积分池类型

| 池类型 | 说明 | 过期规则 |
|--------|------|----------|
| `daily` | 每日积分 | 次日 00:00 UTC 过期 |
| `event` | 活动积分 | 活动截止时间过期 |
| `monthly` | 每月积分 | 最后发放时间 + 30天过期 |
| `permanent` | 通用积分 | 永久有效 |

### 消费优先级

当用户消费积分时，系统按以下优先级扣除：

```
Daily (每日) → Event (活动) → Monthly (每月) → Permanent (通用)
```

这确保先消耗即将过期的积分，最大化用户的积分价值。

### 使用示例

```typescript
const detail = await client.getCreditDetail();

// 计算总余额
console.log(`总计: ${detail.total_balance} 积分`);

// 遍历各个积分池
for (const pool of detail.pools) {
  const remaining = pool.expires_at === 0
    ? Infinity
    : pool.expires_at - Date.now();

  if (remaining === Infinity) {
    console.log(`${pool.type}: ${pool.balance} 积分 (永久有效)`);
  } else {
    const days = Math.floor(remaining / 86400000);
    const hours = Math.floor((remaining % 86400000) / 3600000);
    console.log(`${pool.type}: ${pool.balance} 积分 (${days}天${hours}小时后过期)`);
  }
}

// 检查是否有即将过期的积分
const expiringPools = detail.pools.filter(pool => {
  if (pool.expires_at === 0) return false;
  const daysLeft = (pool.expires_at - Date.now()) / 86400000;
  return daysLeft < 3; // 3天内过期
});

if (expiringPools.length > 0) {
  console.log('⚠️ 有积分即将过期，请尽快使用！');
  expiringPools.forEach(pool => {
    console.log(`  - ${pool.type}: ${pool.balance} 积分`);
  });
}
```

## API 参考

### AssetsClient

#### 构造函数

```typescript
new AssetsClient(options: AssetsClientOptions)
```

**参数:**
- `appId` (string, 必需): 用于多租户隔离的应用 ID
  - `'seaverse'` 用于 SeaVerse 平台
  - `'game-abc123'` 用于第三方应用
- `token` (string, 必需): 用于身份验证的 Bearer token
- `baseURL` (string, 可选): Asset Hub API 的基础 URL
  - 默认: `'https://wallet.seaverse.ai'`
  - 生产环境: `'https://wallet.sg.seaverse.dev'`
- `timeout` (number, 可选): 请求超时时间(毫秒),默认 30000

#### 方法

##### getCreditDetail()

获取积分详情（4 池系统）- **推荐使用**

返回包含 4 种积分池的详细信息，每个池都有余额和过期时间戳。

**API 端点:** `GET /sdk/v1/credits/detail`

**多租户行为:**
- 积分池属于 `appId` 指定的应用
- 每个应用都有独立的 4 池系统
- 池数据按 `app_id` 隔离

**请求头:**
- `Authorization: Bearer <token>` - 必需
- `X-App-ID: <app_id>` - 必需

```typescript
async getCreditDetail(): Promise<CreditDetailResponse>
```

**返回:**
```typescript
{
  total_balance: string           // 所有池的总余额（字符串保证精度）
  pools: [                        // 积分池数组（仅包含余额 > 0 的池）
    {
      type: 'daily' | 'event' | 'monthly' | 'permanent'
      balance: string             // 该池的余额（字符串保证精度）
      expires_at: number          // 过期时间戳（毫秒）
                                  // 0 = 永久有效
                                  // > 0 = 具体过期时间
    }
  ]
}
```

**积分池类型:**
- `daily` - 每日积分（次日 00:00 UTC 过期）
- `event` - 活动积分（活动截止时间过期）
- `monthly` - 每月积分（最后发放时间 + 30天过期）
- `permanent` - 通用积分（永久有效）

**响应特点:**
- 仅返回 `balance > 0` 的积分池
- `expires_at` 为毫秒时间戳（0 表示永久有效）
- 前端可根据时间戳计算剩余时间

**建议刷新频率:** 30 秒

**示例:**
```typescript
const detail = await client.getCreditDetail();
console.log(`总余额: ${detail.total_balance}`);

detail.pools.forEach(pool => {
  const label = pool.expires_at === 0
    ? '永久有效'
    : `${Math.floor((pool.expires_at - Date.now()) / 86400000)} 天后过期`;
  console.log(`${pool.type}: ${pool.balance} (${label})`);
});

// 输出示例:
// 总余额: 5240
// daily: 150 (0 天后过期)
// event: 500 (5 天后过期)
// monthly: 800 (28 天后过期)
// permanent: 3790 (永久有效)
```

##### getCreditAccount()

> ⚠️ **已弃用**: 推荐使用 `getCreditDetail()` 来获取新的 4 池积分系统信息。

获取已认证用户在指定应用中的积分账户信息。

**API 端点:** `GET /sdk/v1/credits/account`

**多租户行为:**
- 积分账户属于 `appId` 指定的应用
- 每个应用都有隔离的积分池
- 账户数据按 `app_id` 隔离

**请求头:**
- `Authorization: Bearer <token>` - 必需
- `X-App-ID: <app_id>` - 必需

```typescript
async getCreditAccount(): Promise<CreditAccount>
```

**返回:**
```typescript
{
  id: string
  user_id: string
  app_id: string | null           // 应用 ID (平台用户为 null)
  balance: number                  // 当前可用余额
  total_earned: number             // 累计获得
  total_spent: number              // 累计消费
  frozen_balance: number           // 冻结余额(不可用)
  last_transaction_id?: string
  status: 'active' | 'suspended' | 'frozen'
  status_reason?: string           // 仅在非活跃状态时存在
  created_at: string               // UTC 时间戳
  updated_at: string               // UTC 时间戳
  last_activity_at?: string        // UTC 时间戳
}
```

**账户状态:**
- `active` - 正常，可用
- `suspended` - 已暂停（请联系支持）
- `frozen` - 已冻结（风险控制）

**注意事项:**
- 某些字段可能为 `null` 或 `undefined`，请做好空值处理
- 数字字段可能以字符串形式返回，SDK 会自动处理类型转换
- 建议使用可选链操作符 `?.` 访问可选字段

**示例:**
```typescript
const account = await client.getCreditAccount();
console.log(`余额: ${account.balance ?? 0}`);
console.log(`应用: ${account.app_id ?? 'N/A'}`);
console.log(`状态: ${account.status}`);

// 安全访问可选字段
if (account.last_activity_at) {
  console.log(`最后活动: ${account.last_activity_at}`);
}
```

##### listTransactions()

列出指定应用的积分交易，支持筛选和分页。

**API 端点:** `GET /sdk/v1/credits/transactions`

**多租户行为:**
- 交易按 `appId` 指定的应用筛选
- 仅返回已认证用户在此应用中的交易
- 交易历史按 `app_id` 隔离

**请求头:**
- `Authorization: Bearer <token>` - 必需
- `X-App-ID: <app_id>` - 必需

```typescript
async listTransactions(request?: ListTransactionsRequest): Promise<TransactionListResponse>
```

**请求参数:**
- `type` (可选): 按交易类型筛选
  - `'earn'` - 积分获得
  - `'spend'` - 积分扣除
  - `'freeze'` - 冻结积分
  - `'unfreeze'` - 解冻积分
  - `'refund'` - 退款返还
  - `'adjust'` - 管理员调整
- `source` (可选): 按交易来源筛选(例如 `'api_call'`、`'purchase'`)
- `status` (可选): 按状态筛选
  - `'pending'` - 处理中
  - `'completed'` - 已完成
  - `'failed'` - 失败
  - `'cancelled'` - 已取消
- `start_date` (可选): 开始日期筛选(RFC3339 格式,例如 `'2024-01-01T00:00:00Z'`)
- `end_date` (可选): 结束日期筛选(RFC3339 格式,例如 `'2024-12-31T23:59:59Z'`)
- `page` (可选): 页码(从 1 开始,默认: 1)
- `page_size` (可选): 每页条数(最小: 1,最大: 50,默认: 20)

**返回:**
```typescript
{
  transactions: CreditTransaction[]
  page: number
  page_size: number
  has_more: boolean
}
```

**CreditTransaction:**
```typescript
{
  id: string
  user_id: string
  app_id: string | null              // 应用 ID (平台用户为 null)
  type: 'spend' | 'earn' | 'refund' | 'adjust' | 'freeze' | 'unfreeze'
  amount: number                     // 负数表示扣除,正数表示增加
  balance_before: number
  balance_after: number
  source: string                     // 交易来源标识符
  source_id?: string                 // 关联的来源 ID(订单/任务/请求 ID)
  description: string
  status: 'pending' | 'completed' | 'failed' | 'cancelled'
  status_reason?: string             // 仅在非已完成状态时存在
  metadata?: Record<string, any>     // 自定义元数据
  created_at: string                 // UTC 时间戳
  completed_at?: string              // UTC 时间戳
}
```

**示例:**
```typescript
// 获取最近的交易
const result = await client.listTransactions({ page_size: 20 });

// 按类型筛选
const spending = await client.listTransactions({
  type: 'spend',
  page: 1,
  page_size: 10
});

// 按日期范围筛选
const recent = await client.listTransactions({
  start_date: '2024-01-01T00:00:00Z',
  end_date: '2024-12-31T23:59:59Z',
  status: 'completed'
});

// 分页
const page2 = await client.listTransactions({
  page: 2,
  page_size: 20
});
if (page2.has_more) {
  // 有更多结果可用
}
```

##### setToken()

更新用于身份验证的 bearer token。

当 token 过期或需要刷新时使用此方法。

```typescript
setToken(token: string): void
```

**示例:**
```typescript
client.setToken('new-bearer-token');
```

##### setBaseURL()

更新 API 的基础 URL。

使用此方法在不同环境(开发/测试/生产)之间切换。

```typescript
setBaseURL(baseURL: string): void
```

**示例:**
```typescript
client.setBaseURL('https://wallet.seaverse.ai');
```

##### setAppId()

更新用于多租户隔离的应用 ID。

使用此方法在运行时在不同应用之间切换。

```typescript
setAppId(appId: string): void
```

**示例:**
```typescript
// 切换到 SeaVerse 平台
client.setAppId('seaverse');

// 切换到第三方应用
client.setAppId('game-abc123');
```

## 错误处理

SDK 会为 API 错误抛出 `AssetsAPIError`:

```typescript
import { AssetsAPIError } from '@seaverse/assets-sdk';

try {
  const account = await client.getCreditAccount();
} catch (error) {
  if (error instanceof AssetsAPIError) {
    console.error(`API 错误 [${error.code}]: ${error.message}`);
    console.error('响应:', error.response);
  } else {
    console.error('意外错误:', error);
  }
}
```

**常见错误码:**
- `400` - 错误的请求(无效参数)
- `401` - 未授权(无效或过期的 token)
- `403` - 禁止访问
- `500` - 内部服务器错误

## 环境配置

不同环境有不同的基础 URL:

```typescript
// 开发/测试环境(默认)
const client = new AssetsClient({
  appId: 'seaverse',
  token: devToken
  // baseURL 默认为 'https://wallet.seaverse.ai'
});

// 生产环境（推荐）
const client = new AssetsClient({
  appId: 'seaverse',
  baseURL: 'https://wallet.sg.seaverse.dev',
  token: prodToken
});

// 本地开发
const client = new AssetsClient({
  appId: 'seaverse',
  baseURL: 'http://localhost:8080',
  token: localToken
});
```

**环境说明:**
- **开发/测试**: `https://wallet.seaverse.ai` (SDK 默认)
- **生产**: `https://wallet.sg.seaverse.dev` (推荐用于生产环境)
- **本地**: `http://localhost:8080` (本地调试)

## 多租户使用示例

### SeaVerse 平台应用

```typescript
// 官方 SeaVerse 平台 (ops-frontend、meta-product 等)
const platformClient = new AssetsClient({
  appId: 'seaverse',
  token: userToken
});

const account = await platformClient.getCreditAccount();
console.log(`平台余额: ${account.balance}`);
```

### 第三方游戏应用

```typescript
// 拥有自己积分池的第三方游戏
const gameClient = new AssetsClient({
  appId: 'game-abc123',
  token: userToken
});

const gameAccount = await gameClient.getCreditAccount();
console.log(`游戏余额: ${gameAccount.balance}`);

// 同一用户,不同应用 - 隔离的积分账户
// gameAccount.balance 可能与 platformClient 的余额不同
```

### 在应用之间切换

```typescript
const client = new AssetsClient({
  appId: 'seaverse',
  token: userToken
});

// 查询 SeaVerse 平台账户
const platformAccount = await client.getCreditAccount();

// 切换到游戏应用
client.setAppId('game-abc123');

// 查询同一用户的游戏账户
const gameAccount = await client.getCreditAccount();

// 两个账户都属于同一用户,但是隔离的
```

## 积分单位

- 1 积分 = 1 单位
- 所有金额都表示为整数(无小数)

## 身份验证

- Token 通过 SeaVerse auth-service 获取
- `Authorization` header 中需要 Bearer token
- 默认 token 有效期: 10 天
- 所需范围: `account`

## 安全性

### 域名白名单(推荐)
对于生产应用,注册允许的域名以防止未经授权的使用:
- `Origin` header 会根据应用的白名单进行验证
- 防止 `app_id` 被盗用和滥用

### 速率限制
- 按 `app_id` 应用速率限制
- 防止滥用

## 故障排除

### 常见问题

**1. `Cannot read properties of undefined` 错误**

如果遇到类似 `Cannot read properties of undefined (reading 'toLocaleString')` 的错误，说明 API 返回的某些字段为 `undefined` 或 `null`。

**解决方案：**
```typescript
// ✅ 推荐：使用空值合并运算符
const balance = account.balance ?? 0;
const totalEarned = account.total_earned ?? 0;

// ✅ 推荐：使用可选链
const lastActivity = account.last_activity_at ?? 'N/A';

// ✅ 推荐：显示前检查值是否存在
if (account.balance !== null && account.balance !== undefined) {
  console.log(`余额: ${account.balance.toLocaleString()}`);
}
```

**2. CORS 错误**

如果在浏览器中遇到 CORS 错误，请确保：
- 你的域名已添加到应用的白名单中
- `X-App-ID` header 正确设置
- 使用正确的 baseURL

**3. 401 未授权错误**

**可能原因：**
- Token 已过期（默认有效期 10 天）
- Token 格式不正确
- Token 没有正确的 scope（需要 `account` scope）

**解决方案：**
```typescript
// 重新获取 token
const newToken = await authService.getToken();
client.setToken(newToken);
```

**4. 数据类型不匹配**

API 可能返回字符串格式的数字，请确保你的代码能够处理：

```typescript
// ✅ 安全的类型转换
const balance = typeof account.balance === 'string'
  ? parseFloat(account.balance)
  : account.balance;

// ✅ 使用 SDK 提供的类型检查
const formatNumber = (value: string | number | null | undefined): number => {
  if (value === null || value === undefined) return 0;
  const num = typeof value === 'string' ? parseFloat(value) : value;
  return isNaN(num) ? 0 : num;
};
```

## 许可证

MIT