跳到主要内容

理财服务设计文档 (Earn Service Design)

版本:v4.2 最后更新:2026-01-08

本文档详细描述 AXBlade 理财服务的完整后端实现方案,包含智能合约、数据库、API 和后台服务。


一、后端服务设计

1.1 目录结构

src/
├── services/earn/
│ ├── mod.rs # EarnService 主体 (~1050行)
│ ├── models.rs # 数据模型和类型
│ └── settlement.rs # 结算调度器
├── api/
│ ├── handlers/earn.rs # API 处理器
│ └── routes/mod.rs # 路由配置
└── main.rs # 服务初始化

1.2 EarnService 结构

pub struct EarnService {
pool: PgPool,
contract: Option<EarnContract<SignerMiddleware<Provider<Http>, LocalWallet>>>,
provider: Option<Arc<Provider<Http>>>,
signer: Option<LocalWallet>,
contract_address: String,
chain_id: u64,
}

1.3 核心方法

impl EarnService {
// 初始化
pub fn new(pool: PgPool) -> Self;
pub async fn with_contract(pool, rpc_url, contract_address, private_key, chain_id) -> Result<Self>;

// 产品查询 (公开)
pub async fn list_products(&self, status, page, page_size) -> Result<ProductListResponse>;
pub async fn get_product(&self, product_id: &str) -> Result<ProductDetail>;
pub async fn get_historical_performance(&self, limit: i32) -> Result<Vec<HistoricalPerformance>>;

// 用户申购 (需认证)
pub async fn get_user_subscriptions(&self, user_address: &str) -> Result<Vec<UserSubscriptionDetail>>;
pub async fn prepare_subscribe(&self, user_address, product_id, amount) -> Result<PrepareSubscribeResponse>;

// EIP-712 签名 (内部)
fn sign_subscribe(&self, user, product_id, amount, deadline) -> Result<String>;

// 管理操作 (需Admin)
pub async fn create_product(&self, req, creator_address, chain_product_id) -> Result<EarnProduct>;
pub async fn update_product_status(&self, product_id, new_status) -> Result<EarnProduct>;
pub async fn get_product_subscriptions(&self, product_id, query) -> Result<AdminSubscriptionListResponse>;

// 事件处理
pub async fn handle_subscribed_event(&self, event: SubscribedEvent) -> Result<()>;
pub async fn handle_settled_event(&self, event: SettledEvent) -> Result<()>;
pub async fn handle_claimed_event(&self, event: ClaimedEvent) -> Result<()>;

// 事件监听
pub async fn start_event_listener(self: Arc<Self>);
async fn poll_events(&self, from_block: u64) -> Result<u64>;
}

1.4 合约 ABI 绑定

abigen!(
EarnContract,
r#"[
function productCount() external view returns (uint256)
function createProduct(string name, uint256 annualRateBps, uint256 durationDays, uint256 totalQuota, uint256 minAmount, uint256 maxAmountPerUser, uint256 subscribeStartTime, uint256 subscribeEndTime) external returns (uint256)
function openSubscription(uint256 productId) external
function activateProduct(uint256 productId) external
function settleProduct(uint256 productId) external
function subscribe(uint256 productId, uint256 amount, uint256 deadline, bytes signature) external
function claim(uint256 productId) external
function emergencyClaim(uint256 productId) external
function getProductConfig(uint256 productId) external view returns (string name, uint256 annualRateBps, uint256 durationDays, uint256 periodRateBps, uint256 totalQuota, uint256 minAmount, uint256 maxAmountPerUser)
function getProductTiming(uint256 productId) external view returns (uint256 subscribeStartTime, uint256 subscribeEndTime, uint256 settleTime, uint256 actualSettleTime)
function getProductStats(uint256 productId) external view returns (uint256 subscribedAmount, uint256 totalInterestPaid, uint256 participantCount, uint8 status, address creator)
function getSubscription(uint256 productId, address user) external view returns (uint256 amount, uint256 expectedReturn, uint256 actualReturn, uint256 subscribedAt, bool claimed)
function balanceOf(address account, uint256 id) external view returns (uint256)
function domainSeparator() external view returns (bytes32)
event ProductCreated(uint256 indexed productId, string name, uint256 annualRateBps, uint256 durationDays, uint256 totalQuota)
event ProductStatusChanged(uint256 indexed productId, uint8 oldStatus, uint8 newStatus)
event Subscribed(uint256 indexed productId, address indexed user, uint256 amount, uint256 expectedReturn)
event ProductSettled(uint256 indexed productId, uint256 totalPrincipal, uint256 totalInterest)
event Claimed(uint256 indexed productId, address indexed user, uint256 principal, uint256 interest)
]"#
);

1.5 事件监听机制

  • 轮询间隔: 12秒
  • 批量扫描: 最多1000个区块
  • 错误退避: 指数退避,最大5分钟
  • 状态持久化: block_sync_state 表 (event_type = 'earn_events')

1.6 结算调度器

// settlement.rs
pub async fn start_settlement_scheduler(service: Arc<EarnService>) {
// 每分钟检查一次
// 1. 申购结束 → 调用 activateProduct
// 2. 到期时间到达 → 调用 settleProduct
}

二、数据库设计

2.1 迁移文件

文件路径: migrations/0018_earn_service.sql

2.2 表结构

earn_products (理财产品表)

CREATE TABLE earn_products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
chain_product_id BIGINT UNIQUE NOT NULL,
contract_address VARCHAR(42) NOT NULL,
name VARCHAR(100) NOT NULL,
description TEXT,
annual_rate_bps INTEGER NOT NULL,
duration_days INTEGER NOT NULL,
period_rate_bps INTEGER NOT NULL,
total_quota DECIMAL(36,18) NOT NULL,
min_amount DECIMAL(36,18) NOT NULL,
max_amount_per_user DECIMAL(36,18) NOT NULL,
subscribed_amount DECIMAL(36,18) DEFAULT 0,
subscribe_start_time TIMESTAMPTZ NOT NULL,
subscribe_end_time TIMESTAMPTZ NOT NULL,
settle_time TIMESTAMPTZ NOT NULL,
status earn_product_status DEFAULT 'created',
subscriber_count INTEGER DEFAULT 0,
total_interest_paid DECIMAL(36,18) DEFAULT 0,
creator_address VARCHAR(42) NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);

earn_subscriptions (申购记录表)

CREATE TABLE earn_subscriptions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES earn_products(id),
chain_product_id BIGINT NOT NULL,
user_address VARCHAR(42) NOT NULL,
amount DECIMAL(36,18) NOT NULL,
nft_amount DECIMAL(36,18) NOT NULL,
expected_return DECIMAL(36,18) NOT NULL,
actual_return DECIMAL(36,18),
nft_status earn_nft_status DEFAULT 'active',
subscribed_at TIMESTAMPTZ DEFAULT NOW(),
settled_at TIMESTAMPTZ,
claimed_at TIMESTAMPTZ,
subscribe_tx_hash VARCHAR(66),
claim_tx_hash VARCHAR(66),
claimed BOOLEAN DEFAULT FALSE,
UNIQUE(product_id, user_address)
);

earn_settlements (结算记录表)

CREATE TABLE earn_settlements (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
product_id UUID REFERENCES earn_products(id),
chain_product_id BIGINT NOT NULL,
total_principal DECIMAL(36,18) NOT NULL,
total_interest DECIMAL(36,18) NOT NULL,
settled_count INTEGER NOT NULL,
tx_hash VARCHAR(66),
block_number BIGINT,
settled_at TIMESTAMPTZ DEFAULT NOW()
);

earn_subscribe_signatures (签名防重放表)

CREATE TABLE earn_subscribe_signatures (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_address VARCHAR(42) NOT NULL,
product_id UUID REFERENCES earn_products(id),
chain_product_id BIGINT NOT NULL,
amount DECIMAL(36,18) NOT NULL,
deadline BIGINT NOT NULL,
signature VARCHAR(132) UNIQUE NOT NULL,
used BOOLEAN DEFAULT FALSE,
used_at TIMESTAMPTZ,
used_tx_hash VARCHAR(66),
created_at TIMESTAMPTZ DEFAULT NOW()
);

2.3 枚举类型

CREATE TYPE earn_product_status AS ENUM (
'created', 'subscribing', 'active', 'settling', 'ended'
);

CREATE TYPE earn_nft_status AS ENUM (
'created', 'active', 'matured', 'redeemed'
);

三、配置项

3.1 Testnet 环境变量 (Sepolia)

# Earn Contract (v3.1,使用平台 USDT)
EARN_CONTRACT_ADDRESS=0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864

# 后端签名私钥
BACKEND_SIGNER_PRIVATE_KEY=0x117340e28aa95a23046640e34ed703c66af6607d62525bd82c60e2bc118e7904

# RPC 配置
RPC_URL=https://sepolia-rollup.arbitrum.io/rpc
CHAIN_ID=421614

# Admin API Key
ADMIN_API_KEY=axblade-sepolia-admin-key-2026

3.2 Mainnet 环境变量 (待配置)

# Earn Contract
EARN_CONTRACT_ADDRESS=<待部署>

# RPC 配置
RPC_URL=https://arb1.arbitrum.io/rpc
CHAIN_ID=42161

3.3 AppState 集成

pub struct AppState {
// ... 现有字段
pub earn_service: Arc<EarnService>,
}

四、完整产品创建流程

4.1 流程概述

创建理财产品需要完成以下步骤:

1. 链上创建产品 (createProduct)

2. 等待申购开始时间

3. 链上激活产品 (openSubscription) ← 关键步骤!

4. 添加到数据库

5. 验证 API 可用

重要: 产品创建后状态为 Created (0),必须调用 openSubscription 才能变为 Subscribing (1),用户才能申购。

4.2 步骤详解

步骤1: 链上创建产品

# 设置参数
DEPLOYER_KEY="0x70cedc96483841f7fdf593e122f28d10271d5222f6e9b6ed804a01c249c074ff"
CONTRACT="0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864" # v3.1 合约
RPC="https://sepolia-rollup.arbitrum.io/rpc"

# 计算时间 (使用区块时间,非本地时间)
BLOCK_TIME=$(cast block latest --rpc-url $RPC | grep timestamp | awk '{print $2}')
SUBSCRIBE_START=$((BLOCK_TIME + 60)) # 1分钟后开始
SUBSCRIBE_END=$((BLOCK_TIME + 3600)) # 1小时后结束

# v3: productId 作为第一个参数, durationSeconds 替代 durationDays
PRODUCT_ID=100 # 自定义产品ID

# 创建产品 (v3: 使用 durationSeconds)
cast send $CONTRACT \
"createProduct(uint256,string,uint256,uint256,uint256,uint256,uint256,uint256,uint256)" \
$PRODUCT_ID \
"产品名称 - 500% APY" \
50000 \ # maxAnnualRateBps (500% = 50000, 实际利率 0% ~ 500%)
600 \ # durationSeconds (600秒 = 10分钟)
50000000000 \ # totalQuota (50000 USDT, 6位小数)
1000000 \ # minAmount (1 USDT)
5000000000 \ # maxAmountPerUser (5000 USDT)
$SUBSCRIBE_START \ # subscribeStartTime
$SUBSCRIBE_END \ # subscribeEndTime
--private-key $DEPLOYER_KEY \
--rpc-url $RPC

参数说明:

参数类型说明示例
productIduint256自定义产品ID100, 1001, etc
namestring产品名称"AXBlade Strategy"
maxAnnualRateBpsuint256v3: 最大年化收益率 (BPS)50000 = 500%
durationSecondsuint256v3: 产品期限 (秒)600 = 10分钟, 86400 = 1天
totalQuotauint256总额度 (6位小数)50000000000 = 50000 USDT
minAmountuint256最小申购1000000 = 1 USDT
maxAmountPerUseruint256个人限额5000000000 = 5000 USDT
subscribeStartTimeuint256申购开始时间戳必须 > 当前区块时间
subscribeEndTimeuint256申购结束时间戳必须 > subscribeStartTime

步骤2: 等待申购开始时间

# 检查当前区块时间
cast block latest --rpc-url $RPC | grep timestamp

# 检查产品时间配置
cast call $CONTRACT "getProductTiming(uint256)(uint256,uint256,uint256,uint256)" $PRODUCT_ID --rpc-url $RPC

注意: 必须等到 block.timestamp >= subscribeStartTime 才能执行下一步

步骤3: 激活产品 (关键!)

# 调用 openSubscription 将状态从 Created(0) 改为 Subscribing(1)
cast send $CONTRACT \
"openSubscription(uint256)" $PRODUCT_ID \
--private-key $DEPLOYER_KEY \
--rpc-url $RPC

验证状态:

# 检查产品状态 (最后一个值应为 1)
cast call $CONTRACT "getProductStats(uint256)(uint256,uint256,uint256,uint8)" $PRODUCT_ID --rpc-url $RPC

状态枚举:

状态说明
0Created已创建,不可申购
1Subscribing申购中,用户可申购
2Active进行中,申购已关闭
3Settling结算中
4Settled已结算,可领取
5Cancelled已取消

步骤4: 添加到数据库

INSERT INTO earn_products (
chain_product_id,
contract_address,
name,
description,
annual_rate_bps,
duration_days,
period_rate_bps,
total_quota,
min_amount,
max_amount_per_user,
subscribed_amount,
subscribe_start_time,
subscribe_end_time,
settle_time,
status,
subscriber_count,
total_interest_paid,
creator_address
) VALUES (
$PRODUCT_ID,
'0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864',
'产品名称 - 500% APY',
'产品描述',
50000, -- max_annual_rate_bps (v3)
1, -- duration_days (数据库保留,合约用 durationSeconds)
137, -- max_period_rate_bps (v3: 自动计算)
50000.000000, -- total_quota
1.000000, -- min_amount
5000.000000, -- max_amount_per_user
0, -- subscribed_amount
to_timestamp($SUBSCRIBE_START),
to_timestamp($SUBSCRIBE_END),
to_timestamp($SUBSCRIBE_END + 600), -- v3: settle_time = subscribeEnd + durationSeconds
'subscribing', -- status
0,
0,
'0x6538469807e019E05c9ec4Bd158b12afB1DA50F3'
);

步骤5: 验证 API

# 检查产品列表
curl -s "https://api.8a27.xyz/api/v1/earn/products" | jq '.products[] | select(.chain_product_id == $PRODUCT_ID)'

# 测试申购签名
curl -X POST "https://api.8a27.xyz/api/v1/earn/subscribe/prepare" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $JWT_TOKEN" \
-d '{"product_id":"$UUID","amount":"100.00"}'

4.3 常见问题

Q1: 用户申购报错 "Not subscribing"

原因: 产品状态仍为 Created (0),未调用 openSubscription

解决:

# 1. 检查产品状态
cast call $CONTRACT "getProductStats(uint256)(uint256,uint256,uint256,uint8)" $PRODUCT_ID --rpc-url $RPC

# 2. 如果状态为 0,调用 openSubscription
cast send $CONTRACT "openSubscription(uint256)" $PRODUCT_ID --private-key $KEY --rpc-url $RPC

Q2: openSubscription 调用失败

可能原因:

  1. 当前区块时间 < subscribeStartTime → 等待时间到达
  2. 调用者没有 OPERATOR_ROLE → 使用正确的私钥
  3. 产品状态不是 Created → 检查当前状态

五、资金取出/存回流程 (v2/v3)

5.1 流程概述

v2/v3 版本支持在产品 Active 状态下取出本金进行外部投资,结算前需存回本金+利息。

v3 新增: depositReturns 支持动态利息 (0% ~ maxPeriodRateBps)

申购结束 → activateProduct() → Active 状态

withdrawPrincipal() ← 取出本金到 Treasury

[外部投资获得收益]

depositReturns() ← 存入本金+利息

settleProduct() → Settled 状态

用户 claim()

5.2 函数说明

withdrawPrincipal(productId)

功能: 将产品本金从合约转到 Treasury 地址

权限: OPERATOR_ROLE

前置条件:

  • 产品状态必须是 Active
  • 本金未被取出 (principalWithdrawn[productId] == false)
  • 有申购金额 (subscribedAmount > 0)

执行结果:

  • subscribedAmount 转账到 Treasury
  • 设置 principalWithdrawn[productId] = true
  • 触发 PrincipalWithdrawn 事件

depositReturns(productId, actualTotalInterest) [v3]

功能: 从 Treasury 转入本金+实际利息到合约

权限: OPERATOR_ROLE

前置条件:

  • 产品状态必须是 Active
  • 本金已被取出 (principalWithdrawn[productId] == true)
  • 尚未调用过 (returnsDeposited[productId] == false)
  • Treasury 必须已 approve 合约足够额度
  • actualTotalInterest &lt;= maxInterest (利息不能超过上限)

v3 动态利率:

maxInterest = 本金 × maxPeriodRateBps ÷ 10000
actualTotalInterest 可以是 0 到 maxInterest 之间的任意值
总额 = 本金 + actualTotalInterest

执行结果:

  • 记录 actualInterestDeposited[productId] = actualTotalInterest
  • 设置 returnsDeposited[productId] = true
  • 从 Treasury transferFrom 本金+利息到合约
  • 触发 ReturnsDeposited 事件

5.3 操作示例

步骤1: 激活产品(申购期结束后)

# 配置 (v3.1 合约)
CONTRACT="0xA40b4b726980C02F6558fcA5C0a52FaA2E5A0864"
RPC="https://sepolia-rollup.arbitrum.io/rpc"
DEPLOYER_KEY="0x70cedc96483841f7fdf593e122f28d10271d5222f6e9b6ed804a01c249c074ff"
PRODUCT_ID=100

# 激活产品
cast send $CONTRACT "activateProduct(uint256)" $PRODUCT_ID \
--private-key $DEPLOYER_KEY --rpc-url $RPC

步骤2: 取出本金到 Treasury

# 取出本金
cast send $CONTRACT "withdrawPrincipal(uint256)" $PRODUCT_ID \
--private-key $DEPLOYER_KEY --rpc-url $RPC

# 验证取款状态
cast call $CONTRACT "principalWithdrawn(uint256)" $PRODUCT_ID --rpc-url $RPC
# 返回: 0x01 (true)

步骤3: 计算需存入金额

# 获取申购金额
SUBSCRIBED=$(cast call $CONTRACT "productStats(uint256)(uint256,uint256,uint256,uint8,address)" $PRODUCT_ID --rpc-url $RPC | head -1)
echo "本金: $SUBSCRIBED"

# 获取最大期间利率 (v3)
MAX_PERIOD_RATE=$(cast call $CONTRACT "productConfigs(uint256)(string,uint256,uint256,uint256,uint256,uint256,uint256)" $PRODUCT_ID --rpc-url $RPC | sed -n '4p')
echo "最大期间利率 (BPS): $MAX_PERIOD_RATE"

# v3: 实际利息由运营方决定 (0 到 maxInterest)
# maxInterest = 本金 × maxPeriodRateBps ÷ 10000
# 实际利息可以是 0, maxInterest/2, maxInterest, 或任何中间值

计算示例:

项目数值
本金60,000,000 (60 USDT)
maxPeriodRateBps82 (0.82%)
最大利息60,000,000 × 82 ÷ 10000 = 492,000 (0.492 USDT)
实际利息 (v3)运营方指定: 0 ~ 492,000
总计需存入本金 + 实际利息

步骤4: Treasury 授权合约

USDT="0x572E474C3Cf364D085760784F938A1Aa397a8B9b"
TREASURY_KEY="0x70cedc96483841f7fdf593e122f28d10271d5222f6e9b6ed804a01c249c074ff"
AMOUNT=60492000 # 本金 + 利息

# 授权 Earn 合约可以花费 USDT
cast send $USDT "approve(address,uint256)" $CONTRACT $AMOUNT \
--private-key $TREASURY_KEY --rpc-url $RPC

步骤5: 存入本金+利息 (v3: 指定实际利息)

# v3: 指定实际利息金额
ACTUAL_INTEREST=492000 # 可以是 0 到 maxInterest 之间的任意值

# 存入 returns (v3: 新增第二个参数)
cast send $CONTRACT "depositReturns(uint256,uint256)" $PRODUCT_ID $ACTUAL_INTEREST \
--private-key $DEPLOYER_KEY --rpc-url $RPC

v3 动态利率示例:

# 场景1: 全额利息 (收益达到预期)
cast send $CONTRACT "depositReturns(uint256,uint256)" $PRODUCT_ID 492000 ...

# 场景2: 半额利息 (收益一般)
cast send $CONTRACT "depositReturns(uint256,uint256)" $PRODUCT_ID 246000 ...

# 场景3: 零利息 (无收益/亏损)
cast send $CONTRACT "depositReturns(uint256,uint256)" $PRODUCT_ID 0 ...

步骤6: 结算产品

# 结算 (到期时间到达后)
cast send $CONTRACT "settleProduct(uint256)" $PRODUCT_ID \
--private-key $DEPLOYER_KEY --rpc-url $RPC

5.4 注意事项

  1. 授权要求: depositReturns 使用 transferFrom,Treasury 必须先 approve
  2. 顺序依赖: 必须先 withdrawPrincipal 才能 depositReturns
  3. 不可逆操作: 一旦取出本金,必须存回才能结算
  4. v3 动态利息: 运营方指定 actualTotalInterest (0 ~ maxInterest)
  5. 利息上限: actualTotalInterest 不能超过 maxPeriodRateBps 计算的上限
  6. 比例分配: 用户 claim 时按申购比例分配实际利息
  7. 如果未取款: 如果没有调用 withdrawPrincipal,可直接调用 settleProduct(使用最大利率,利息会从 Treasury 自动转入)

5.5 事件监听

event PrincipalWithdrawn(uint256 indexed productId, uint256 amount, address to);
event ReturnsDeposited(uint256 indexed productId, uint256 principal, uint256 interest);

六、前端集成指南

6.1 申购流程

// 1. 获取 EIP-712 Domain
const domain = await fetch('/api/v1/earn/domain').then(r => r.json());

// 2. 登录获取 JWT
const nonce = await fetch(`/api/v1/auth/nonce/${address}`).then(r => r.json());
const signature = await signer.signTypedData(nonce.typed_data.domain, ...);
const { token } = await fetch('/api/v1/auth/login', { ... }).then(r => r.json());

// 3. 准备申购
const prepareRes = await fetch('/api/v1/earn/subscribe/prepare', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ product_id, amount })
}).then(r => r.json());

// 4. 调用合约申购
const tx = await earnContract.subscribe(
prepareRes.chain_product_id,
prepareRes.amount,
prepareRes.deadline,
prepareRes.signature
);

6.2 合约 ABI (关键函数)

[
"function subscribe(uint256 productId, uint256 amount, uint256 deadline, bytes signature) external",
"function claim(uint256 productId) external",
"function balanceOf(address account, uint256 id) external view returns (uint256)",
"function getSubscription(uint256 productId, address user) external view returns (uint256 amount, uint256 expectedReturn, uint256 actualReturn, uint256 subscribedAt, bool claimed)"
]

七、参考文件

文件说明
src/services/earn/mod.rsEarnService 主体实现
src/services/earn/models.rs数据模型定义
src/services/earn/settlement.rs结算调度器
src/api/handlers/earn.rsAPI 处理器 (含 EIP-712 Domain 端点)
migrations/0018_earn_service.sql数据库迁移
contracts/src/AXBladeEarn.sol智能合约源码
contracts/script/Deploy.s.sol部署脚本 (含 DeployEarnOnly)

八、变更日志

日期版本变更内容
2026-01-07v2.0初始文档,完成 Phase 1-4
2026-01-07v2.1新增 EIP-712 Domain API 端点
2026-01-07v2.2重新部署合约使用平台 USDT,完成 Phase 5
2026-01-07v2.3部署到 Sepolia 服务器,完成端到端测试
2026-01-07v2.4切换到主合约 (0xCB97...),更新 8a27.xyz 测试环境配置
2026-01-07v2.5新增完整产品创建流程文档
2026-01-07v3.0合约 v2 升级: 自定义 ProductID、资金取出/存回、多次申购支持
2026-01-07v3.1新增资金取出/存回流程详细文档
2026-01-08v4.0合约 v3 升级: durationSeconds、动态APR (0%~max%)、actualTotalInterest 参数
2026-01-08v4.1文档更新: 强调链上 openSubscription() 的重要性,添加 "Not subscribing" 错误解决方案
2026-01-08v4.2合约 v3.1 升级: 修复短期产品利息计算bug,移除 maxPeriodRateBps 字段,直接计算利息避免中间值截断