跳到主要内容

用户投资记录表 (User Investments)

user_investments 表记录用户对各期理财产品的申购和持有情况,是理财系统的核心业务表之一。

投资状态枚举

系统定义了以下投资状态类型:

CREATE TYPE investment_status AS ENUM (
'holding', -- 持有中:投资已生效,正在计息
'redeemed', -- 已赎回:已到期并完成收益结算
'failed' -- 失败:申购失败或异常状态
);

表结构

完整的 user_investments 表定义:

CREATE TABLE user_investments (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL, -- 用户ID
product_id INT REFERENCES financial_products(id),
invested_amount NUMERIC(20, 6) NOT NULL, -- 投资本金
expected_interest NUMERIC(20, 6) NOT NULL, -- 预期利息 (计算得出)
actual_interest NUMERIC(20, 6) DEFAULT 0, -- 实际派息

status investment_status DEFAULT 'holding',
invested_at TIMESTAMPTZ DEFAULT NOW(), -- 投资时间
redeemed_at TIMESTAMPTZ -- 自动赎回/到期时间
);

字段详解

核心字段

字段类型约束说明
idSERIALPRIMARY KEY投资记录唯一标识(自增主键)
user_idINTNOT NULL用户 ID,关联用户表
product_idINTREFERENCES financial_products(id)理财产品 ID,外键关联 financial_products

金额字段

字段类型约束说明
invested_amountNUMERIC(20, 6)NOT NULL用户实际投资的本金金额(USDT)
例:1000.000000 表示投资 1000 U
expected_interestNUMERIC(20, 6)NOT NULL预期收益金额,根据产品利率计算
公式:invested_amount × product.period_yield
actual_interestNUMERIC(20, 6)DEFAULT 0实际派发的利息金额
结算时更新,通常等于 expected_interest

利息计算示例:如果投资 1000 U 到年化 150%、期限 7 天的产品(当期利率 2.88%),则预期利息 = 1000 × 0.0288 = 28.8 U

状态和时间

字段类型约束说明
statusinvestment_statusDEFAULT 'holding'投资状态,见上方状态枚举说明
invested_atTIMESTAMPTZDEFAULT NOW()申购成功时间(起息时间)
redeemed_atTIMESTAMPTZ-赎回时间,到期自动结算时填充

投资生命周期

用户投资记录的典型状态流转:

申购 → holding (持有中) → redeemed (已赎回)

failed (失败)
  1. holding(持有中):用户成功申购后,资金锁定,开始计息
  2. redeemed(已赎回):到达产品 settlement_time,系统自动结算收益并返还本息
  3. failed(失败):申购过程中出现异常(如余额不足、产品售罄等)

示例数据

示例 1:用户 ID 1001 投资第 1 期理财

-- 假设用户投资 5000 U 到第 1 期理财产品
-- 第 1 期:年化 150%,7 天期,当期利率 2.88%
INSERT INTO user_investments (
user_id,
product_id,
invested_amount,
expected_interest,
status,
invested_at
) VALUES (
1001,
1, -- 第 1 期产品
5000.000000, -- 投资 5000 U
144.000000, -- 预期利息: 5000 × 0.0288 = 144 U
'holding',
'2026-01-10 10:30:00+08'
);

示例 2:多个用户投资同一产品

-- 用户 1002 投资 10000 U(达到个人限额)
INSERT INTO user_investments (
user_id, product_id, invested_amount, expected_interest, status, invested_at
) VALUES (
1002, 1, 10000.000000, 288.000000, 'holding', '2026-01-10 11:00:00+08'
);

-- 用户 1003 投资 100 U(最低起购额)
INSERT INTO user_investments (
user_id, product_id, invested_amount, expected_interest, status, invested_at
) VALUES (
1003, 1, 100.000000, 2.880000, 'holding', '2026-01-10 11:15:00+08'
);

示例 3:已结算的投资记录

-- 用户 1004 的投资已到期结算
INSERT INTO user_investments (
user_id,
product_id,
invested_amount,
expected_interest,
actual_interest,
status,
invested_at,
redeemed_at
) VALUES (
1004,
1,
3000.000000, -- 投资 3000 U
86.400000, -- 预期利息 86.4 U
86.400000, -- 实际派息 86.4 U
'redeemed',
'2026-01-10 12:00:00+08',
'2026-01-17 10:00:00+08' -- 7天后到期
);

查询示例

1. 查询用户的所有投资记录

SELECT 
ui.id,
fp.period_name,
ui.invested_amount,
ui.expected_interest,
ui.actual_interest,
ui.status,
ui.invested_at,
ui.redeemed_at
FROM user_investments ui
JOIN financial_products fp ON ui.product_id = fp.id
WHERE ui.user_id = 1001
ORDER BY ui.invested_at DESC;

2. 查询用户当前持有的投资

SELECT 
fp.period_name,
fp.period_number,
ui.invested_amount,
ui.expected_interest,
fp.settlement_time,
EXTRACT(DAY FROM (fp.settlement_time - NOW())) AS days_to_settle
FROM user_investments ui
JOIN financial_products fp ON ui.product_id = fp.id
WHERE ui.user_id = 1001
AND ui.status = 'holding'
ORDER BY fp.settlement_time ASC;

3. 统计某期产品的投资情况

-- 统计第 1 期产品的投资人数和金额
SELECT
COUNT(DISTINCT user_id) AS total_investors,
COUNT(*) AS total_orders,
SUM(invested_amount) AS total_invested,
SUM(expected_interest) AS total_expected_interest,
AVG(invested_amount) AS avg_invested_per_order
FROM user_investments
WHERE product_id = 1
AND status != 'failed';

4. 查询即将到期需要结算的投资

-- 查询未来 1 天内需要结算的投资记录
SELECT
ui.id,
ui.user_id,
ui.product_id,
fp.period_name,
ui.invested_amount,
ui.expected_interest,
fp.settlement_time
FROM user_investments ui
JOIN financial_products fp ON ui.product_id = fp.id
WHERE ui.status = 'holding'
AND fp.settlement_time <= NOW() + INTERVAL '1 day'
AND fp.settlement_time > NOW()
ORDER BY fp.settlement_time ASC;

5. 计算用户的总收益

-- 计算用户 1001 的累计收益(已结算的)
SELECT
user_id,
COUNT(*) AS total_investments,
SUM(invested_amount) AS total_principal,
SUM(actual_interest) AS total_earned,
ROUND((SUM(actual_interest) / SUM(invested_amount) * 100), 2) AS roi_percentage
FROM user_investments
WHERE user_id = 1001
AND status = 'redeemed'
GROUP BY user_id;

6. 检查用户对某产品的累计投资额

-- 检查用户 1001 对第 1 期产品的累计投资(用于限额控制)
SELECT
user_id,
product_id,
SUM(invested_amount) AS total_invested_in_product
FROM user_investments
WHERE user_id = 1001
AND product_id = 1
AND status IN ('holding', 'redeemed') -- 排除失败的
GROUP BY user_id, product_id;

业务逻辑

1. 申购流程

用户申购理财产品时的处理逻辑:

-- 1. 开启事务
BEGIN;

-- 2. 锁定产品行,检查额度
SELECT * FROM financial_products
WHERE id = $product_id
AND status = 'active'
FOR UPDATE;

-- 3. 检查用户累计投资是否超限
SELECT COALESCE(SUM(invested_amount), 0) AS user_total
FROM user_investments
WHERE user_id = $user_id
AND product_id = $product_id
AND status IN ('holding', 'redeemed');

-- 4. 验证投资金额
-- - 单笔金额 >= individual_min_amount
-- - user_total + $amount <= individual_max_amount
-- - current_amount + $amount <= total_capacity

-- 5. 创建投资记录
INSERT INTO user_investments (
user_id,
product_id,
invested_amount,
expected_interest,
status
) VALUES (
$user_id,
$product_id,
$amount,
$amount * (SELECT period_yield FROM financial_products WHERE id = $product_id),
'holding'
) RETURNING id;

-- 6. 更新产品已售金额
UPDATE financial_products
SET current_amount = current_amount + $amount,
updated_at = NOW()
WHERE id = $product_id;

-- 7. 扣除用户余额(在 account_transactions 中记录)
-- ... 用户资金操作 ...

COMMIT;

2. 到期结算流程

系统定时任务自动执行的结算逻辑:

-- 1. 找出所有到期的投资记录
WITH due_investments AS (
SELECT ui.id, ui.user_id, ui.invested_amount, ui.expected_interest
FROM user_investments ui
JOIN financial_products fp ON ui.product_id = fp.id
WHERE ui.status = 'holding'
AND fp.settlement_time <= NOW()
)
-- 2. 批量更新为已赎回状态
UPDATE user_investments
SET status = 'redeemed',
actual_interest = expected_interest,
redeemed_at = NOW()
WHERE id IN (SELECT id FROM due_investments);

-- 3. 给每个用户返还本金 + 利息
-- 在 account_transactions 表中创建相应的入账记录
INSERT INTO account_transactions (user_id, amount, type, reference_id)
SELECT
user_id,
invested_amount + expected_interest AS total_return,
'wealth_settlement',
id
FROM due_investments;

3. 数据一致性维护

确保产品的 current_amount 与投资记录一致:

-- 校验第 1 期产品的数据一致性
SELECT
fp.period_name,
fp.current_amount AS product_current_amount,
COALESCE(SUM(ui.invested_amount), 0) AS actual_invested_sum,
fp.current_amount - COALESCE(SUM(ui.invested_amount), 0) AS difference
FROM financial_products fp
LEFT JOIN user_investments ui
ON fp.id = ui.product_id
AND ui.status IN ('holding', 'redeemed')
WHERE fp.id = 1
GROUP BY fp.id, fp.period_name, fp.current_amount;

索引建议

为提高查询性能,建议添加以下索引:

-- 用户ID索引,用于查询用户的所有投资
CREATE INDEX idx_user_investments_user_id ON user_investments(user_id);

-- 产品ID索引,用于查询某产品的所有投资者
CREATE INDEX idx_user_investments_product_id ON user_investments(product_id);

-- 状态索引,用于快速筛选不同状态的投资
CREATE INDEX idx_user_investments_status ON user_investments(status);

-- 投资时间索引,用于按时间查询
CREATE INDEX idx_user_investments_invested_at ON user_investments(invested_at);

-- 复合索引,用于查询用户在某产品的投资
CREATE INDEX idx_user_product ON user_investments(user_id, product_id);

-- 复合索引,用于查询用户的持有中投资
CREATE INDEX idx_user_status ON user_investments(user_id, status);

触发器建议

自动更新产品已售金额

-- 当新增投资记录时,自动更新产品的 current_amount
CREATE OR REPLACE FUNCTION update_product_amount()
RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'INSERT' AND NEW.status != 'failed') THEN
UPDATE financial_products
SET current_amount = current_amount + NEW.invested_amount,
updated_at = NOW()
WHERE id = NEW.product_id;
END IF;

RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trigger_update_product_amount
AFTER INSERT ON user_investments
FOR EACH ROW
EXECUTE FUNCTION update_product_amount();

相关表

注意事项

  1. 外键约束product_id 有外键约束,确保引用的产品存在
  2. 精度控制:金额字段使用 NUMERIC(20, 6) 保证计算精度
  3. 时区处理:所有时间字段使用 TIMESTAMPTZ 类型
  4. 并发安全:申购时必须锁定产品行(FOR UPDATE),防止超售
  5. 状态一致性
    • holding 状态的记录,redeemed_at 应为 NULL
    • redeemed 状态的记录,actual_interest 应该已填充
    • failed 状态的记录不应影响产品的 current_amount
  6. 利息计算expected_interest 应在创建时就计算好,避免后续产品参数变更影响
  7. 批量结算:建议使用定时任务批量处理到期结算,而不是逐条处理