- 实现账户管理改进设计文档中的所有核心功能 - 三科目余额管理 (个人余额、劳动报酬、冻结余额) - 交易状态机 (created → pending → bank_submitted → success/failed/timeout → reversed) - 三键幂等体系 (JZTxId/BankTxId/SourceKey) - 优先级扣款规则 (先个人后劳动) - 在途资金管理 (可用→在途→结转/回退) - 三账对账闭环 (总账 = 银行账 + 在途净额) - 补偿服务域 (超时检测、重试、死信队列) - 虚拟银行模拟器用于业务测试 - 完整的集成测试套件 (133 个测试全部通过) - Docker 容器化部署配置 - 前端 Vue3 + TypeScript 项目结构
320 lines
11 KiB
Rust
320 lines
11 KiB
Rust
//! 账户 API 处理器
|
|
|
|
use axum::{
|
|
extract::{Path, Query, State},
|
|
Json,
|
|
};
|
|
|
|
use crate::api::AppState;
|
|
use crate::application::dto::*;
|
|
use crate::domain::account::{CreatePhysicalAccountRequest, CreateVirtualSubAccountRequest, AccountType};
|
|
use crate::error::Result;
|
|
|
|
/// 创建实体账户
|
|
pub async fn create_physical_account(
|
|
State(state): State<AppState>,
|
|
Json(dto): Json<CreatePhysicalAccountDto>,
|
|
) -> Result<Json<SuccessResponse<PhysicalAccountResponse>>> {
|
|
let service = state.account_service();
|
|
|
|
let request = CreatePhysicalAccountRequest {
|
|
account_no: dto.account_no,
|
|
bank_code: dto.bank_code,
|
|
bank_name: dto.bank_name,
|
|
consistency_mode: dto.consistency_mode,
|
|
outbound_control: dto.outbound_control,
|
|
};
|
|
|
|
let account = service.create_physical_account(request).await?;
|
|
|
|
Ok(Json(SuccessResponse::new(PhysicalAccountResponse {
|
|
id: account.id,
|
|
account_no: account.account_no,
|
|
bank_code: account.bank_code,
|
|
bank_name: account.bank_name,
|
|
consistency_mode: account.consistency_mode,
|
|
outbound_control: account.outbound_control,
|
|
status: account.status,
|
|
created_at: account.created_at,
|
|
personal_balance: None,
|
|
labor_balance: None,
|
|
frozen_balance: None,
|
|
bank_balance: None,
|
|
transit_amount: None,
|
|
available_balance: None,
|
|
})))
|
|
}
|
|
|
|
/// 账户列表查询参数
|
|
#[derive(serde::Deserialize)]
|
|
pub struct ListAccountsQuery {
|
|
pub status: Option<String>,
|
|
pub keyword: Option<String>,
|
|
pub page: Option<u32>,
|
|
pub page_size: Option<u32>,
|
|
}
|
|
|
|
/// 获取实体账户列表
|
|
pub async fn list_physical_accounts(
|
|
State(state): State<AppState>,
|
|
Query(query): Query<ListAccountsQuery>,
|
|
) -> Result<Json<SuccessResponse<PageResponse<PhysicalAccountResponse>>>> {
|
|
let service = state.account_service();
|
|
let ledger_service = state.ledger_service();
|
|
let mut accounts = service.list_physical_accounts().await?;
|
|
|
|
// 筛选:按状态
|
|
if let Some(status_str) = &query.status {
|
|
if let Ok(status) = status_str.parse::<crate::domain::account::AccountStatus>() {
|
|
accounts.retain(|a| a.status == status);
|
|
}
|
|
}
|
|
|
|
// 筛选:按关键词(账号或银行名称)
|
|
if let Some(keyword) = &query.keyword {
|
|
let keyword_lower = keyword.to_lowercase();
|
|
accounts.retain(|a| {
|
|
a.account_no.to_lowercase().contains(&keyword_lower)
|
|
|| a.bank_name.as_ref().map(|n| n.to_lowercase().contains(&keyword_lower)).unwrap_or(false)
|
|
});
|
|
}
|
|
|
|
// 分页
|
|
let page = query.page.unwrap_or(1).max(1);
|
|
let page_size = query.page_size.unwrap_or(10).max(1).min(100);
|
|
let total = accounts.len() as i64;
|
|
let start = ((page - 1) * page_size) as usize;
|
|
let end = (start + page_size as usize).min(accounts.len());
|
|
let paged_accounts = if start < accounts.len() {
|
|
accounts[start..end].to_vec()
|
|
} else {
|
|
Vec::new()
|
|
};
|
|
|
|
// 转换为响应并获取余额信息
|
|
let mut responses = Vec::new();
|
|
for account in paged_accounts {
|
|
// 获取余额信息
|
|
let balance = ledger_service
|
|
.get_balance(account.id, AccountType::Physical)
|
|
.await
|
|
.ok();
|
|
|
|
responses.push(PhysicalAccountResponse {
|
|
id: account.id,
|
|
account_no: account.account_no,
|
|
bank_code: account.bank_code,
|
|
bank_name: account.bank_name,
|
|
consistency_mode: account.consistency_mode,
|
|
outbound_control: account.outbound_control,
|
|
status: account.status,
|
|
created_at: account.created_at,
|
|
personal_balance: balance.as_ref().map(|b| b.personal_balance),
|
|
labor_balance: balance.as_ref().map(|b| b.labor_balance),
|
|
frozen_balance: balance.as_ref().map(|b| b.frozen_balance),
|
|
bank_balance: balance.as_ref().map(|b| b.bank_balance),
|
|
transit_amount: balance.as_ref().map(|b| b.transit_amount),
|
|
available_balance: balance.as_ref().map(|b| b.available_balance),
|
|
});
|
|
}
|
|
|
|
Ok(Json(SuccessResponse::new(PageResponse::new(
|
|
responses,
|
|
total,
|
|
page as i64,
|
|
page_size as i64,
|
|
))))
|
|
}
|
|
|
|
/// 获取实体账户
|
|
pub async fn get_physical_account(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<i64>,
|
|
) -> Result<Json<SuccessResponse<PhysicalAccountResponse>>> {
|
|
let service = state.account_service();
|
|
let ledger_service = state.ledger_service();
|
|
let account = service.get_physical_account(id).await?;
|
|
|
|
// 获取余额信息
|
|
let balance = ledger_service
|
|
.get_balance(id, AccountType::Physical)
|
|
.await
|
|
.ok();
|
|
|
|
Ok(Json(SuccessResponse::new(PhysicalAccountResponse {
|
|
id: account.id,
|
|
account_no: account.account_no,
|
|
bank_code: account.bank_code,
|
|
bank_name: account.bank_name,
|
|
consistency_mode: account.consistency_mode,
|
|
outbound_control: account.outbound_control,
|
|
status: account.status,
|
|
created_at: account.created_at,
|
|
personal_balance: balance.as_ref().map(|b| b.personal_balance),
|
|
labor_balance: balance.as_ref().map(|b| b.labor_balance),
|
|
frozen_balance: balance.as_ref().map(|b| b.frozen_balance),
|
|
bank_balance: balance.as_ref().map(|b| b.bank_balance),
|
|
transit_amount: balance.as_ref().map(|b| b.transit_amount),
|
|
available_balance: balance.as_ref().map(|b| b.available_balance),
|
|
})))
|
|
}
|
|
|
|
/// 冻结实体账户余额
|
|
pub async fn freeze_physical_account(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<i64>,
|
|
Json(dto): Json<FreezeAccountDto>,
|
|
) -> Result<Json<SuccessResponse<String>>> {
|
|
let ledger_service = state.ledger_service();
|
|
ledger_service.freeze_amount(id, AccountType::Physical, dto.amount).await?;
|
|
Ok(Json(SuccessResponse::new(format!("已冻结金额 {}", dto.amount))))
|
|
}
|
|
|
|
/// 解冻实体账户余额
|
|
pub async fn unfreeze_physical_account(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<i64>,
|
|
Json(dto): Json<UnfreezeAccountDto>,
|
|
) -> Result<Json<SuccessResponse<String>>> {
|
|
let ledger_service = state.ledger_service();
|
|
ledger_service.unfreeze_amount(id, AccountType::Physical, dto.amount).await?;
|
|
Ok(Json(SuccessResponse::new(format!("已解冻金额 {}", dto.amount))))
|
|
}
|
|
|
|
/// 创建虚拟子账户
|
|
pub async fn create_sub_account(
|
|
State(state): State<AppState>,
|
|
Json(dto): Json<CreateVirtualSubAccountDto>,
|
|
) -> Result<Json<SuccessResponse<VirtualSubAccountResponse>>> {
|
|
let service = state.account_service();
|
|
|
|
let request = CreateVirtualSubAccountRequest {
|
|
physical_account_id: dto.physical_account_id,
|
|
account_code: dto.account_code,
|
|
account_type: dto.account_type,
|
|
valid_from: dto.valid_from,
|
|
valid_to: dto.valid_to,
|
|
};
|
|
|
|
let account = service.create_virtual_sub_account(request).await?;
|
|
|
|
Ok(Json(SuccessResponse::new(VirtualSubAccountResponse {
|
|
id: account.id,
|
|
physical_account_id: account.physical_account_id,
|
|
account_code: account.account_code,
|
|
account_type: account.account_type,
|
|
valid_from: account.valid_from,
|
|
valid_to: account.valid_to,
|
|
status: account.status,
|
|
balance: None,
|
|
created_at: account.created_at,
|
|
})))
|
|
}
|
|
|
|
/// 获取虚拟子账户
|
|
pub async fn get_sub_account(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<i64>,
|
|
) -> Result<Json<SuccessResponse<VirtualSubAccountResponse>>> {
|
|
let account_service = state.account_service();
|
|
let ledger_service = state.ledger_service();
|
|
|
|
let account = account_service.get_virtual_sub_account(id).await?;
|
|
let balance = ledger_service.get_balance(id, AccountType::Virtual).await?;
|
|
|
|
Ok(Json(SuccessResponse::new(VirtualSubAccountResponse {
|
|
id: account.id,
|
|
physical_account_id: account.physical_account_id,
|
|
account_code: account.account_code,
|
|
account_type: account.account_type,
|
|
valid_from: account.valid_from,
|
|
valid_to: account.valid_to,
|
|
status: account.status,
|
|
balance: Some(BalanceResponse {
|
|
system_balance: balance.system_balance,
|
|
bank_balance: balance.bank_balance,
|
|
available_balance: balance.available_balance,
|
|
frozen_amount: balance.frozen_amount,
|
|
transit_amount: balance.transit_amount,
|
|
}),
|
|
created_at: account.created_at,
|
|
})))
|
|
}
|
|
|
|
/// 获取子账户余额
|
|
pub async fn get_sub_account_balance(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<i64>,
|
|
) -> Result<Json<SuccessResponse<BalanceResponse>>> {
|
|
let ledger_service = state.ledger_service();
|
|
let balance = ledger_service.get_balance(id, AccountType::Virtual).await?;
|
|
|
|
Ok(Json(SuccessResponse::new(BalanceResponse {
|
|
system_balance: balance.system_balance,
|
|
bank_balance: balance.bank_balance,
|
|
available_balance: balance.available_balance,
|
|
frozen_amount: balance.frozen_amount,
|
|
transit_amount: balance.transit_amount,
|
|
})))
|
|
}
|
|
|
|
/// 冻结子账户余额
|
|
pub async fn freeze_sub_account(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<i64>,
|
|
Json(dto): Json<FreezeAccountDto>,
|
|
) -> Result<Json<SuccessResponse<String>>> {
|
|
let ledger_service = state.ledger_service();
|
|
ledger_service.freeze_amount(id, AccountType::Virtual, dto.amount).await?;
|
|
Ok(Json(SuccessResponse::new(format!("已冻结金额 {}", dto.amount))))
|
|
}
|
|
|
|
/// 解冻子账户余额
|
|
pub async fn unfreeze_sub_account(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<i64>,
|
|
Json(dto): Json<UnfreezeAccountDto>,
|
|
) -> Result<Json<SuccessResponse<String>>> {
|
|
let ledger_service = state.ledger_service();
|
|
ledger_service.unfreeze_amount(id, AccountType::Virtual, dto.amount).await?;
|
|
Ok(Json(SuccessResponse::new(format!("已解冻金额 {}", dto.amount))))
|
|
}
|
|
|
|
/// 销户
|
|
pub async fn close_sub_account(
|
|
State(state): State<AppState>,
|
|
Path(id): Path<i64>,
|
|
) -> Result<Json<SuccessResponse<String>>> {
|
|
let service = state.account_service();
|
|
service.close_sub_account(id).await?;
|
|
Ok(Json(SuccessResponse::new("子账户已销户".to_string())))
|
|
}
|
|
|
|
/// 获取实体账户下的子账户列表
|
|
pub async fn list_sub_accounts(
|
|
State(state): State<AppState>,
|
|
Path(physical_account_id): Path<i64>,
|
|
) -> Result<Json<SuccessResponse<Vec<VirtualSubAccountResponse>>>> {
|
|
let service = state.account_service();
|
|
let accounts = service.list_sub_accounts(physical_account_id).await?;
|
|
|
|
let responses: Vec<VirtualSubAccountResponse> = accounts
|
|
.into_iter()
|
|
.map(|a| VirtualSubAccountResponse {
|
|
id: a.id,
|
|
physical_account_id: a.physical_account_id,
|
|
account_code: a.account_code,
|
|
account_type: a.account_type,
|
|
valid_from: a.valid_from,
|
|
valid_to: a.valid_to,
|
|
status: a.status,
|
|
balance: None,
|
|
created_at: a.created_at,
|
|
})
|
|
.collect();
|
|
|
|
Ok(Json(SuccessResponse::new(responses)))
|
|
}
|
|
|
|
|