# coin_account — 金币 + 现金余额快照

> 模型 `app/models/wallet.py` · 仓库 `app/repositories/wallet.py` · 接口 [wallet-account](../api/wallet/wallet-account.md) · [← 索引](./README.md) · [总览](./OVERVIEW.md)

一用户一行的余额快照,App「资产卡 / 钱包」读它展示。每次余额变动都另写一笔流水(`coin_transaction` / `cash_transaction`)并记 `balance_after`,出问题逐笔回溯。`user_id` 既是主键也是外键(一对一)。详见 [总览 §四 资金模型](./OVERVIEW.md#四资金模型金币--现金--提现三层)。

## 用在哪 / 增删改查
- **C(插入)**:`get_or_create_account` 懒建 —— 用户第一次发生金币动作(签到/任务/看广告/admin 发币/兑换/提现)时建一行空账户(全 0)。从未发生过金币动作的用户**没有这行**(读接口按 0 兜底)。
- **U(更新)**:几乎所有钱相关动作都更新它 —— 发金币(签到/任务/看广告/admin)`coin_balance += `、兑现金 `coin_balance −= / cash_balance_cents += `、发起提现 `cash_balance_cents −= `(带条件原子 UPDATE 防超额)、提现退款 `cash_balance_cents += `。
- **D**:无。
- **R**:`GET /wallet/account`(钱包余额卡)、admin 用户 360 概览。

## 字段
| 列 | 类型 | 约束 / 默认 | 说明(取值 / join) |
|---|---|---|---|
| `user_id` | Integer | **PK + FK→user.id** | 用户(一用户一行);既是主键也是外键 |
| `coin_balance` | Integer | NOT NULL, default 0 | 当前金币余额(个数);= 历次 `coin_transaction.amount` 之和 |
| `cash_balance_cents` | Integer | NOT NULL, default 0 | 当前现金余额(分,**金币兑换账本**);= 历次 `cash_transaction.amount_cents` 之和 |
| `invite_cash_balance_cents` | Integer | NOT NULL, default 0 | 当前**邀请奖励金**余额(分,与现金物理隔离的第二本现金账);= 历次 [`invite_cash_transaction`](./invite_cash_transaction.md)`.amount_cents` 之和。好友比价并下单发奖入账(#113),`source=invite_cash` 提现出账(#121) |
| `total_coin_earned` | Integer | NOT NULL, default 0 | 累计赚取金币(**只增不减**,仅正向 grant 累加),用于"历史总收益"展示 |
| `updated_at` | DateTime(tz) | server_default now(), onupdate now() | 最后更新时间 |

## 关系 / Join Key
- `user_id` → `user.id`(一对一)。
- 与 `coin_transaction` / `cash_transaction` 无外键直连,靠 `user_id` 关联;快照 = 流水累加值,流水的 `balance_after*` 应等于当时的快照余额(对账依据)。

## 索引与约束
- PK `user_id`(同时是 FK→user.id)。

## 注意
- **唯一发金币入口** `wallet.grant_coins`:更新本表 + 写 `coin_transaction`,**不 commit**,由调用方同事务提交(发币与业务记录原子化)。邀请奖励金同款:**唯一变动入口 `wallet.grant_invite_cash`**(更新 `invite_cash_balance_cents` + 写 `invite_cash_transaction`)。
- 扣现金用 `UPDATE ... WHERE <对应余额列> >= amount` 原子条件扣减(按提现 `source` 选 `cash_balance_cents` 或 `invite_cash_balance_cents`),并发/重试不会超额。
