feat: 优化账户管理和订单功能
- 重置密码后自动跳转到登录页,登录页添加重置密码入口 - 移除默认账户相关逻辑和UI - 移除前端API Key自动获取提示文案(后端保留自动获取逻辑) - 修复SELL订单takerAmount计算精度问题,使用精确计算避免精度丢失 - 修复导入账户页面白屏问题(缺少Alert组件导入) - 持仓出售支持百分比模式,使用原始精度数据计算,支持小数百分比 - 优化订单金额计算逻辑,确保与SDK一致
This commit is contained in:
@@ -3,10 +3,12 @@ package com.wrbug.polymarketbot.controller
|
||||
import com.wrbug.polymarketbot.dto.*
|
||||
import com.wrbug.polymarketbot.enums.ErrorCode
|
||||
import com.wrbug.polymarketbot.service.AccountService
|
||||
import com.wrbug.polymarketbot.util.toSafeBigDecimal
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.web.bind.annotation.*
|
||||
import java.math.BigDecimal
|
||||
|
||||
/**
|
||||
* 账户管理控制器
|
||||
@@ -16,9 +18,9 @@ import org.springframework.web.bind.annotation.*
|
||||
class AccountController(
|
||||
private val accountService: AccountService
|
||||
) {
|
||||
|
||||
|
||||
private val logger = LoggerFactory.getLogger(AccountController::class.java)
|
||||
|
||||
|
||||
/**
|
||||
* 通过私钥导入账户
|
||||
*/
|
||||
@@ -32,7 +34,7 @@ class AccountController(
|
||||
if (request.walletAddress.isBlank()) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_WALLET_ADDRESS_EMPTY))
|
||||
}
|
||||
|
||||
|
||||
val result = accountService.importAccount(request)
|
||||
result.fold(
|
||||
onSuccess = { account ->
|
||||
@@ -41,7 +43,13 @@ class AccountController(
|
||||
onFailure = { e ->
|
||||
logger.error("导入账户失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
|
||||
is IllegalArgumentException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.PARAM_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_IMPORT_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
@@ -51,7 +59,7 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_IMPORT_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 更新账户信息
|
||||
*/
|
||||
@@ -66,7 +74,13 @@ class AccountController(
|
||||
onFailure = { e ->
|
||||
logger.error("更新账户失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
|
||||
is IllegalArgumentException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.PARAM_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_UPDATE_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
@@ -76,7 +90,7 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_UPDATE_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除账户
|
||||
*/
|
||||
@@ -91,8 +105,20 @@ class AccountController(
|
||||
onFailure = { e ->
|
||||
logger.error("删除账户失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
|
||||
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message))
|
||||
is IllegalArgumentException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.PARAM_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
is IllegalStateException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.BUSINESS_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DELETE_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
@@ -102,7 +128,7 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DELETE_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询账户列表
|
||||
*/
|
||||
@@ -124,7 +150,7 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_LIST_FETCH_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询账户详情
|
||||
*/
|
||||
@@ -139,8 +165,19 @@ class AccountController(
|
||||
onFailure = { e ->
|
||||
logger.error("查询账户详情失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DETAIL_FETCH_FAILED, e.message))
|
||||
is IllegalArgumentException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.PARAM_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
else -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.SERVER_ACCOUNT_DETAIL_FETCH_FAILED,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -149,7 +186,7 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DETAIL_FETCH_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询账户余额
|
||||
*/
|
||||
@@ -164,8 +201,19 @@ class AccountController(
|
||||
onFailure = { e ->
|
||||
logger.error("查询账户余额失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_BALANCE_FETCH_FAILED, e.message))
|
||||
is IllegalArgumentException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.PARAM_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
else -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.SERVER_ACCOUNT_BALANCE_FETCH_FAILED,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -174,32 +222,7 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_BALANCE_FETCH_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认账户
|
||||
*/
|
||||
@PostMapping("/set-default")
|
||||
fun setDefaultAccount(@RequestBody request: SetDefaultAccountRequest): ResponseEntity<ApiResponse<Unit>> {
|
||||
return try {
|
||||
val result = accountService.setDefaultAccount(request.accountId)
|
||||
result.fold(
|
||||
onSuccess = {
|
||||
ResponseEntity.ok(ApiResponse.success(Unit))
|
||||
},
|
||||
onFailure = { e ->
|
||||
logger.error("设置默认账户失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DEFAULT_SET_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error("设置默认账户异常: ${e.message}", e)
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_DEFAULT_SET_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 查询所有账户的仓位列表
|
||||
*/
|
||||
@@ -221,7 +244,7 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_POSITIONS_FETCH_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 卖出仓位
|
||||
*/
|
||||
@@ -242,13 +265,25 @@ class AccountController(
|
||||
if (request.orderType !in listOf("MARKET", "LIMIT")) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ORDER_TYPE_MUST_BE_MARKET_OR_LIMIT))
|
||||
}
|
||||
if (request.quantity.isBlank()) {
|
||||
// 如果传了 percent,不需要校验 quantity;如果没传 percent,必须提供 quantity
|
||||
if (request.percent.isNullOrBlank() && request.quantity.isNullOrBlank()) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_QUANTITY_EMPTY))
|
||||
}
|
||||
// 如果传了 percent,验证百分比值必须在 0-100 之间(支持小数)
|
||||
if (!request.percent.isNullOrBlank()) {
|
||||
try {
|
||||
val percent = request.percent.toSafeBigDecimal()
|
||||
if (percent <= BigDecimal.ZERO || percent > BigDecimal.valueOf(100)) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "卖出百分比必须在 0-100 之间"))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, "卖出百分比格式不正确: ${e.message}"))
|
||||
}
|
||||
}
|
||||
if (request.orderType == "LIMIT" && (request.price == null || request.price.isBlank())) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_PRICE_EMPTY))
|
||||
}
|
||||
|
||||
|
||||
val result = runBlocking { accountService.sellPosition(request) }
|
||||
result.fold(
|
||||
onSuccess = { response ->
|
||||
@@ -257,9 +292,26 @@ class AccountController(
|
||||
onFailure = { e ->
|
||||
logger.error("创建卖出订单失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
|
||||
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message))
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_ORDER_CREATE_FAILED, e.message))
|
||||
is IllegalArgumentException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.PARAM_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
is IllegalStateException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.BUSINESS_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
else -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.SERVER_ACCOUNT_ORDER_CREATE_FAILED,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -268,7 +320,7 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_ORDER_CREATE_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取可赎回仓位统计
|
||||
*/
|
||||
@@ -283,8 +335,19 @@ class AccountController(
|
||||
onFailure = { e ->
|
||||
logger.error("获取可赎回仓位统计失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "获取可赎回仓位统计失败: ${e.message}"))
|
||||
is IllegalArgumentException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.PARAM_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
else -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.SERVER_ERROR,
|
||||
"获取可赎回仓位统计失败: ${e.message}"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -293,7 +356,7 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ERROR, "获取可赎回仓位统计失败: ${e.message}"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 赎回仓位
|
||||
*/
|
||||
@@ -304,7 +367,7 @@ class AccountController(
|
||||
if (request.positions.isEmpty()) {
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_REDEEM_POSITIONS_EMPTY))
|
||||
}
|
||||
|
||||
|
||||
// 验证每个仓位项
|
||||
for (item in request.positions) {
|
||||
if (item.accountId <= 0) {
|
||||
@@ -317,7 +380,7 @@ class AccountController(
|
||||
return ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_INDEX_SETS_INVALID))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
val result = runBlocking { accountService.redeemPositions(request) }
|
||||
result.fold(
|
||||
onSuccess = { response ->
|
||||
@@ -326,9 +389,26 @@ class AccountController(
|
||||
onFailure = { e ->
|
||||
logger.error("赎回仓位失败: ${e.message}", e)
|
||||
when (e) {
|
||||
is IllegalArgumentException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.PARAM_ERROR, e.message))
|
||||
is IllegalStateException -> ResponseEntity.ok(ApiResponse.error(ErrorCode.BUSINESS_ERROR, e.message))
|
||||
else -> ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_REDEEM_POSITIONS_FAILED, e.message))
|
||||
is IllegalArgumentException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.PARAM_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
is IllegalStateException -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.BUSINESS_ERROR,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
|
||||
else -> ResponseEntity.ok(
|
||||
ApiResponse.error(
|
||||
ErrorCode.SERVER_ACCOUNT_REDEEM_POSITIONS_FAILED,
|
||||
e.message
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -337,6 +417,6 @@ class AccountController(
|
||||
ResponseEntity.ok(ApiResponse.error(ErrorCode.SERVER_ACCOUNT_REDEEM_POSITIONS_FAILED, e.message))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ data class AccountImportRequest(
|
||||
val privateKey: String, // 私钥(前端加密后传输)
|
||||
val walletAddress: String, // 钱包地址(前端从私钥推导,用于验证)
|
||||
val accountName: String? = null,
|
||||
val isDefault: Boolean = false,
|
||||
val isEnabled: Boolean = true // 是否启用(用于订单推送等功能的开关)
|
||||
)
|
||||
|
||||
@@ -17,7 +16,6 @@ data class AccountImportRequest(
|
||||
data class AccountUpdateRequest(
|
||||
val accountId: Long,
|
||||
val accountName: String? = null,
|
||||
val isDefault: Boolean? = null,
|
||||
val isEnabled: Boolean? = null // 是否启用(用于订单推送等功能的开关)
|
||||
)
|
||||
|
||||
@@ -32,21 +30,14 @@ data class AccountDeleteRequest(
|
||||
* 账户详情请求
|
||||
*/
|
||||
data class AccountDetailRequest(
|
||||
val accountId: Long? = null // 不提供则返回默认账户
|
||||
val accountId: Long? = null // 账户ID(必需)
|
||||
)
|
||||
|
||||
/**
|
||||
* 账户余额请求
|
||||
*/
|
||||
data class AccountBalanceRequest(
|
||||
val accountId: Long? = null // 不提供则查询默认账户
|
||||
)
|
||||
|
||||
/**
|
||||
* 设置默认账户请求
|
||||
*/
|
||||
data class SetDefaultAccountRequest(
|
||||
val accountId: Long
|
||||
val accountId: Long? = null // 账户ID(必需)
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -57,7 +48,6 @@ data class AccountDto(
|
||||
val walletAddress: String,
|
||||
val proxyAddress: String, // Polymarket 代理钱包地址
|
||||
val accountName: String?,
|
||||
val isDefault: Boolean,
|
||||
val isEnabled: Boolean, // 是否启用(用于订单推送等功能的开关)
|
||||
val apiKeyConfigured: Boolean, // API Key 是否已配置(不返回实际 Key)
|
||||
val apiSecretConfigured: Boolean, // API Secret 是否已配置
|
||||
@@ -114,7 +104,8 @@ data class AccountPositionDto(
|
||||
val marketIcon: String?, // 市场图标 URL
|
||||
val side: String, // 结果名称(如 "YES", "NO", "Pakistan" 等)
|
||||
val outcomeIndex: Int? = null, // 结果索引(0, 1, 2...),用于计算 tokenId
|
||||
val quantity: String,
|
||||
val quantity: String, // 显示用的数量(可能被截位)
|
||||
val originalQuantity: String? = null, // 原始数量(保留完整精度,用于100%出售)
|
||||
val avgPrice: String,
|
||||
val currentPrice: String,
|
||||
val currentValue: String,
|
||||
@@ -146,7 +137,8 @@ data class PositionSellRequest(
|
||||
val side: String, // 结果名称(如 "YES", "NO", "Pakistan" 等)(必需)
|
||||
val outcomeIndex: Int? = null, // 结果索引(0, 1, 2...),用于计算 tokenId(推荐提供)
|
||||
val orderType: String, // 订单类型:MARKET(市价)或 LIMIT(限价)(必需)
|
||||
val quantity: String, // 卖出数量(必需,BigDecimal字符串)
|
||||
val quantity: String? = null, // 卖出数量(可选,BigDecimal字符串,手动输入时使用)
|
||||
val percent: String? = null, // 卖出百分比(可选,BigDecimal字符串,支持小数,0-100之间,选择百分比按钮时使用)
|
||||
val price: String? = null // 限价价格(限价订单必需,市价订单不需要)
|
||||
)
|
||||
|
||||
|
||||
@@ -83,15 +83,7 @@ class AccountService(
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 如果设置为默认账户,取消其他账户的默认状态
|
||||
if (request.isDefault) {
|
||||
accountRepository.findByIsDefaultTrue()?.let { defaultAccount ->
|
||||
val updated = defaultAccount.copy(isDefault = false, updatedAt = System.currentTimeMillis())
|
||||
accountRepository.save(updated)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 获取代理地址(必须成功,否则导入失败)
|
||||
// 5. 获取代理地址(必须成功,否则导入失败)
|
||||
val proxyAddress = runBlocking {
|
||||
val proxyResult = blockchainService.getProxyAddress(request.walletAddress)
|
||||
if (proxyResult.isSuccess) {
|
||||
@@ -123,7 +115,7 @@ class AccountService(
|
||||
apiSecret = encryptedApiSecret, // 存储加密后的 API Secret
|
||||
apiPassphrase = encryptedApiPassphrase, // 存储加密后的 API Passphrase
|
||||
accountName = request.accountName,
|
||||
isDefault = request.isDefault,
|
||||
isDefault = false, // 不再支持默认账户
|
||||
isEnabled = request.isEnabled,
|
||||
createdAt = System.currentTimeMillis(),
|
||||
updatedAt = System.currentTimeMillis()
|
||||
@@ -153,21 +145,12 @@ class AccountService(
|
||||
// 更新账户名称
|
||||
val updatedAccountName = request.accountName ?: account.accountName
|
||||
|
||||
// 如果设置为默认账户,取消其他账户的默认状态
|
||||
val updatedIsDefault = request.isDefault ?: account.isDefault
|
||||
if (updatedIsDefault && !account.isDefault) {
|
||||
accountRepository.findByIsDefaultTrue()?.let { defaultAccount ->
|
||||
val updated = defaultAccount.copy(isDefault = false, updatedAt = System.currentTimeMillis())
|
||||
accountRepository.save(updated)
|
||||
}
|
||||
}
|
||||
|
||||
// 更新启用状态
|
||||
val updatedIsEnabled = request.isEnabled ?: account.isEnabled
|
||||
|
||||
val updated = account.copy(
|
||||
accountName = updatedAccountName,
|
||||
isDefault = updatedIsDefault,
|
||||
isDefault = account.isDefault, // 保持原值,不再支持修改
|
||||
isEnabled = updatedIsEnabled,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
@@ -196,22 +179,6 @@ class AccountService(
|
||||
// 注意:不再检查活跃订单,允许用户删除有活跃订单的账户
|
||||
// 前端会显示确认提示框,由用户决定是否删除
|
||||
|
||||
// 如果删除的是默认账户,需要先设置其他账户为默认
|
||||
if (account.isDefault) {
|
||||
val otherAccounts = accountRepository.findAllByOrderByCreatedAtAsc()
|
||||
.filter { it.id != accountId }
|
||||
|
||||
if (otherAccounts.isNotEmpty()) {
|
||||
val newDefault = otherAccounts.first().copy(
|
||||
isDefault = true,
|
||||
updatedAt = System.currentTimeMillis()
|
||||
)
|
||||
accountRepository.save(newDefault)
|
||||
} else {
|
||||
return Result.failure(IllegalStateException("不能删除最后一个账户"))
|
||||
}
|
||||
}
|
||||
|
||||
accountRepository.delete(account)
|
||||
|
||||
// 刷新订单推送订阅(账户删除时)
|
||||
@@ -249,13 +216,12 @@ class AccountService(
|
||||
*/
|
||||
fun getAccountDetail(accountId: Long?): Result<AccountDto> {
|
||||
return try {
|
||||
val account = if (accountId != null) {
|
||||
accountRepository.findById(accountId).orElse(null)
|
||||
} else {
|
||||
accountRepository.findByIsDefaultTrue()
|
||||
if (accountId == null) {
|
||||
return Result.failure(IllegalArgumentException("账户ID不能为空"))
|
||||
}
|
||||
|
||||
account ?: return Result.failure(IllegalArgumentException("账户不存在"))
|
||||
|
||||
val account = accountRepository.findById(accountId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("账户不存在"))
|
||||
|
||||
Result.success(toDto(account))
|
||||
} catch (e: Exception) {
|
||||
@@ -270,13 +236,12 @@ class AccountService(
|
||||
*/
|
||||
fun getAccountBalance(accountId: Long?): Result<AccountBalanceResponse> {
|
||||
return try {
|
||||
val account = if (accountId != null) {
|
||||
accountRepository.findById(accountId).orElse(null)
|
||||
} else {
|
||||
accountRepository.findByIsDefaultTrue()
|
||||
if (accountId == null) {
|
||||
return Result.failure(IllegalArgumentException("账户ID不能为空"))
|
||||
}
|
||||
|
||||
account ?: return Result.failure(IllegalArgumentException("账户不存在"))
|
||||
|
||||
val account = accountRepository.findById(accountId).orElse(null)
|
||||
?: return Result.failure(IllegalArgumentException("账户不存在"))
|
||||
|
||||
// 检查代理地址是否存在
|
||||
if (account.proxyAddress.isBlank()) {
|
||||
@@ -352,34 +317,6 @@ class AccountService(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置默认账户
|
||||
*/
|
||||
@Transactional
|
||||
fun setDefaultAccount(accountId: Long): Result<Unit> {
|
||||
return try {
|
||||
val account = accountRepository.findById(accountId)
|
||||
.orElse(null) ?: return Result.failure(IllegalArgumentException("账户不存在"))
|
||||
|
||||
// 取消其他账户的默认状态
|
||||
accountRepository.findByIsDefaultTrue()?.let { defaultAccount ->
|
||||
if (defaultAccount.id != account.id) {
|
||||
val updated = defaultAccount.copy(isDefault = false, updatedAt = System.currentTimeMillis())
|
||||
accountRepository.save(updated)
|
||||
}
|
||||
}
|
||||
|
||||
// 设置当前账户为默认
|
||||
val updated = account.copy(isDefault = true, updatedAt = System.currentTimeMillis())
|
||||
accountRepository.save(updated)
|
||||
|
||||
Result.success(Unit)
|
||||
} catch (e: Exception) {
|
||||
logger.error("设置默认账户失败", e)
|
||||
Result.failure(e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为 DTO
|
||||
* 包含交易统计数据(总订单数、总盈亏、活跃订单数、已完成订单数、持仓数量)
|
||||
@@ -392,7 +329,6 @@ class AccountService(
|
||||
walletAddress = account.walletAddress,
|
||||
proxyAddress = account.proxyAddress,
|
||||
accountName = account.accountName,
|
||||
isDefault = account.isDefault,
|
||||
isEnabled = account.isEnabled,
|
||||
apiKeyConfigured = account.apiKey != null,
|
||||
apiSecretConfigured = account.apiSecret != null,
|
||||
@@ -643,6 +579,16 @@ class AccountService(
|
||||
// 使用 eq 方法判断值是否等于 0
|
||||
val isCurrent = !currentValue.eq(BigDecimal.ZERO) && !curPrice.eq(BigDecimal.ZERO)
|
||||
|
||||
// 将 Double 转换为精确的 BigDecimal,保留完整精度
|
||||
val sizeDecimal = pos.size?.let {
|
||||
BigDecimal.valueOf(it) // 使用 BigDecimal.valueOf 保留 Double 的完整精度
|
||||
} ?: BigDecimal.ZERO
|
||||
|
||||
// 显示用的数量(保留4位小数,用于显示)
|
||||
val displayQuantity = sizeDecimal.setScale(4, java.math.RoundingMode.DOWN).toPlainString()
|
||||
// 原始数量(保留完整精度,用于100%出售)
|
||||
val originalQuantity = sizeDecimal.toPlainString()
|
||||
|
||||
val positionDto = AccountPositionDto(
|
||||
accountId = account.id!!,
|
||||
accountName = account.accountName,
|
||||
@@ -654,7 +600,8 @@ class AccountService(
|
||||
marketIcon = pos.icon, // 市场图标
|
||||
side = pos.outcome ?: "",
|
||||
outcomeIndex = pos.outcomeIndex, // 添加 outcomeIndex
|
||||
quantity = pos.size?.toString() ?: "0",
|
||||
quantity = displayQuantity, // 显示用的数量
|
||||
originalQuantity = originalQuantity, // 原始数量(完整精度)
|
||||
avgPrice = pos.avgPrice?.toString() ?: "0",
|
||||
currentPrice = pos.curPrice?.toString() ?: "0",
|
||||
currentValue = pos.currentValue?.toString() ?: "0",
|
||||
@@ -710,9 +657,33 @@ class AccountService(
|
||||
return Result.failure(IllegalStateException("账户未配置API凭证,无法创建订单"))
|
||||
}
|
||||
|
||||
// 2. 验证仓位是否存在且数量足够
|
||||
// 2. 验证参数:percent 和 quantity 至少提供一个
|
||||
if (request.percent.isNullOrBlank() && request.quantity.isNullOrBlank()) {
|
||||
return Result.failure(IllegalArgumentException("必须提供卖出数量(quantity)或卖出百分比(percent)"))
|
||||
}
|
||||
|
||||
if (!request.percent.isNullOrBlank() && !request.quantity.isNullOrBlank()) {
|
||||
return Result.failure(IllegalArgumentException("不能同时提供卖出数量(quantity)和卖出百分比(percent)"))
|
||||
}
|
||||
|
||||
// 验证百分比值(如果提供了)
|
||||
val percentDecimal = if (!request.percent.isNullOrBlank()) {
|
||||
try {
|
||||
val percent = request.percent!!.toSafeBigDecimal()
|
||||
if (percent <= BigDecimal.ZERO || percent > BigDecimal.valueOf(100)) {
|
||||
return Result.failure(IllegalArgumentException("卖出百分比必须在 0-100 之间"))
|
||||
}
|
||||
percent
|
||||
} catch (e: Exception) {
|
||||
return Result.failure(IllegalArgumentException("卖出百分比格式不正确: ${e.message}"))
|
||||
}
|
||||
} else {
|
||||
null
|
||||
}
|
||||
|
||||
// 3. 验证仓位是否存在并获取原始数量
|
||||
val positionsResult = getAllPositions()
|
||||
positionsResult.fold(
|
||||
val (position, originalQuantity) = positionsResult.fold(
|
||||
onSuccess = { positionListResponse ->
|
||||
val position = positionListResponse.currentPositions.find {
|
||||
it.accountId == request.accountId &&
|
||||
@@ -724,23 +695,49 @@ class AccountService(
|
||||
return Result.failure(IllegalArgumentException("仓位不存在"))
|
||||
}
|
||||
|
||||
val positionQuantity = position.quantity.toSafeBigDecimal()
|
||||
val sellQuantity = request.quantity.toSafeBigDecimal()
|
||||
|
||||
if (sellQuantity <= BigDecimal.ZERO) {
|
||||
return Result.failure(IllegalArgumentException("卖出数量必须大于0"))
|
||||
}
|
||||
|
||||
if (sellQuantity > positionQuantity) {
|
||||
return Result.failure(IllegalArgumentException("卖出数量不能超过持仓数量"))
|
||||
// 获取原始数量:如果有 originalQuantity 使用它,否则从 API 重新获取
|
||||
val originalQty = if (position.originalQuantity != null) {
|
||||
position.originalQuantity.toSafeBigDecimal()
|
||||
} else {
|
||||
// 如果没有 originalQuantity,从区块链服务重新获取原始数据
|
||||
val blockchainPositionsResult = blockchainService.getPositions(account.proxyAddress)
|
||||
if (blockchainPositionsResult.isSuccess) {
|
||||
val blockchainPos = blockchainPositionsResult.getOrNull()?.find {
|
||||
it.conditionId == request.marketId && it.outcome == request.side
|
||||
}
|
||||
blockchainPos?.size?.let { BigDecimal.valueOf(it) } ?: position.quantity.toSafeBigDecimal()
|
||||
} else {
|
||||
position.quantity.toSafeBigDecimal()
|
||||
}
|
||||
}
|
||||
|
||||
Pair(position, originalQty)
|
||||
},
|
||||
onFailure = { e ->
|
||||
return Result.failure(Exception("查询仓位失败: ${e.message}"))
|
||||
}
|
||||
)
|
||||
) ?: return Result.failure(IllegalArgumentException("仓位不存在"))
|
||||
|
||||
// 3. 获取 tokenId(从 conditionId 和 outcomeIndex 计算)
|
||||
// 4. 计算实际卖出数量
|
||||
val sellQuantity = if (percentDecimal != null) {
|
||||
// 使用百分比计算:原始数量 * 百分比 / 100
|
||||
originalQuantity.multiply(percentDecimal)
|
||||
.divide(BigDecimal.valueOf(100), 8, java.math.RoundingMode.DOWN)
|
||||
} else {
|
||||
// 使用手动输入的数量
|
||||
request.quantity!!.toSafeBigDecimal()
|
||||
}
|
||||
|
||||
// 5. 验证卖出数量
|
||||
if (sellQuantity <= BigDecimal.ZERO) {
|
||||
return Result.failure(IllegalArgumentException("卖出数量必须大于0"))
|
||||
}
|
||||
|
||||
if (sellQuantity > originalQuantity) {
|
||||
return Result.failure(IllegalArgumentException("卖出数量不能超过持仓数量"))
|
||||
}
|
||||
|
||||
// 6. 获取 tokenId(从 conditionId 和 outcomeIndex 计算)
|
||||
// 需要先获取 tokenId,以便后续通过 CLOB API 获取三元及以上市场的价格
|
||||
// 优先使用 outcomeIndex,如果没有则尝试从 side 推断(仅支持 YES/NO)
|
||||
val tokenIdResult = if (request.outcomeIndex != null) {
|
||||
@@ -762,12 +759,12 @@ class AccountService(
|
||||
logger.warn("无法获取 tokenId,将使用 market 参数: conditionId=${request.marketId}, side=${request.side}, outcomeIndex=${request.outcomeIndex}, error=${tokenIdResult.exceptionOrNull()?.message}")
|
||||
}
|
||||
|
||||
// 4. 验证 tokenId
|
||||
// 7. 验证 tokenId
|
||||
if (tokenId == null) {
|
||||
return Result.failure(IllegalStateException("无法获取 tokenId,无法创建订单。请确保已配置 Ethereum RPC URL 或提供 outcomeIndex 参数"))
|
||||
}
|
||||
|
||||
// 5. 确定卖出价格
|
||||
// 8. 确定卖出价格
|
||||
// 市价单:从订单表获取最优价(通过 tokenId 获取对应 outcome 的订单表)
|
||||
// - 市价卖单:从订单表获取 bestBid(最高买入价),然后减去 SELL_PRICE_ADJUSTMENT
|
||||
// - 市价买单:从订单表获取 bestAsk(最低卖出价),然后加上 BUY_PRICE_ADJUSTMENT
|
||||
@@ -788,13 +785,13 @@ class AccountService(
|
||||
request.price ?: return Result.failure(IllegalArgumentException("限价订单必须提供价格"))
|
||||
}
|
||||
|
||||
// 6. 验证价格
|
||||
// 9. 验证价格
|
||||
val priceDecimal = sellPrice.toSafeBigDecimal()
|
||||
if (priceDecimal <= BigDecimal.ZERO) {
|
||||
return Result.failure(IllegalArgumentException("价格必须大于0"))
|
||||
}
|
||||
|
||||
// 7. 确定订单类型和过期时间
|
||||
// 10. 确定订单类型和过期时间
|
||||
// 根据官方文档:
|
||||
// - GTC (Good-Til-Cancelled): expiration 必须为 "0"
|
||||
// - GTD (Good-Til-Date): expiration 为具体的 Unix 时间戳(秒)
|
||||
@@ -813,7 +810,7 @@ class AccountService(
|
||||
// 7. 解密私钥
|
||||
val decryptedPrivateKey = decryptPrivateKey(account)
|
||||
|
||||
// 8. 创建并签名订单
|
||||
// 11. 创建并签名订单(使用计算后的卖出数量)
|
||||
val signedOrder = try {
|
||||
orderSigningService.createAndSignOrder(
|
||||
privateKey = decryptedPrivateKey,
|
||||
@@ -821,7 +818,7 @@ class AccountService(
|
||||
tokenId = tokenId,
|
||||
side = "SELL",
|
||||
price = sellPrice,
|
||||
size = request.quantity,
|
||||
size = sellQuantity.toPlainString(), // 使用计算后的卖出数量
|
||||
signatureType = 2, // Browser Wallet(与正确订单数据一致)
|
||||
nonce = "0",
|
||||
feeRateBps = "0",
|
||||
@@ -832,7 +829,7 @@ class AccountService(
|
||||
return Result.failure(Exception("创建并签名订单失败: ${e.message}"))
|
||||
}
|
||||
|
||||
// 8. 构建订单请求
|
||||
// 12. 构建订单请求
|
||||
|
||||
val newOrderRequest = com.wrbug.polymarketbot.api.NewOrderRequest(
|
||||
order = signedOrder,
|
||||
@@ -841,7 +838,7 @@ class AccountService(
|
||||
deferExec = false
|
||||
)
|
||||
|
||||
// 9. 解密 API 凭证并使用账户的API凭证创建订单
|
||||
// 13. 解密 API 凭证并使用账户的API凭证创建订单
|
||||
val apiSecret = try {
|
||||
decryptApiSecret(account)
|
||||
} catch (e: Exception) {
|
||||
@@ -874,7 +871,7 @@ class AccountService(
|
||||
marketId = request.marketId,
|
||||
side = request.side,
|
||||
orderType = request.orderType,
|
||||
quantity = request.quantity,
|
||||
quantity = sellQuantity.toPlainString(), // 使用计算后的卖出数量
|
||||
price = if (request.orderType == "LIMIT") sellPrice else null,
|
||||
status = "pending", // 订单状态需要从响应中获取
|
||||
createdAt = System.currentTimeMillis()
|
||||
|
||||
@@ -102,15 +102,18 @@ class OrderSigningService {
|
||||
} else {
|
||||
// SELL: makerAmount = size (shares), takerAmount = price * size (USDC)
|
||||
// makerAmount 是 shares 数量,最多 4 位小数
|
||||
// takerAmount 是 USDC 金额,最多 2 位小数
|
||||
// takerAmount 是 USDC 金额,需要精确计算,不进行舍入(保留足够精度以转换为 wei)
|
||||
val rawMakerAmt = roundDown(sizeDecimal, roundConfig.size)
|
||||
var rawTakerAmt = rawMakerAmt.multiply(roundedPrice)
|
||||
// takerAmount = price * size,使用精确计算,不进行舍入
|
||||
// 直接使用精确计算结果转换为 wei(6 位小数),让 parseUnits 处理精度
|
||||
val rawTakerAmt = rawMakerAmt.multiply(roundedPrice)
|
||||
|
||||
// 确保 makerAmount 精度(shares,最多 4 位小数)
|
||||
val finalMakerAmt = roundDown(rawMakerAmt, TAKER_AMOUNT_DECIMALS)
|
||||
|
||||
// 确保 takerAmount 精度(USDC,最多 2 位小数)
|
||||
rawTakerAmt = roundDown(rawTakerAmt, MAKER_AMOUNT_DECIMALS)
|
||||
// takerAmount 不进行舍入,直接使用精确计算结果转换为 wei
|
||||
// 这样可以保留足够的精度,避免精度丢失导致的错误
|
||||
// parseUnits 会将 BigDecimal 转换为 wei(6 位小数),自动处理精度
|
||||
|
||||
// 转换为 wei(6 位小数)
|
||||
val makerAmount = parseUnits(finalMakerAmt, COLLATERAL_TOKEN_DECIMALS)
|
||||
@@ -319,10 +322,13 @@ class OrderSigningService {
|
||||
|
||||
/**
|
||||
* 将 BigDecimal 转换为 wei(指定小数位数)
|
||||
* 使用精确计算,不进行舍入,直接截断到指定小数位数
|
||||
*/
|
||||
private fun parseUnits(value: BigDecimal, decimals: Int): BigInteger {
|
||||
// 先设置精度到指定小数位数(向下截断,不四舍五入)
|
||||
val scaledValue = value.setScale(decimals, RoundingMode.DOWN)
|
||||
val multiplier = BigInteger.TEN.pow(decimals)
|
||||
return value.multiply(BigDecimal(multiplier)).toBigInteger()
|
||||
return scaledValue.multiply(BigDecimal(multiplier)).toBigInteger()
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -74,7 +74,6 @@ const AccountDetail: React.FC = () => {
|
||||
const updateData: any = {
|
||||
accountId: account.id,
|
||||
accountName: values.accountName || undefined,
|
||||
isDefault: values.isDefault || false
|
||||
}
|
||||
|
||||
// 只有非空字符串才更新 API 凭证
|
||||
@@ -163,8 +162,7 @@ const AccountDetail: React.FC = () => {
|
||||
accountName: account.accountName || '',
|
||||
apiKey: '', // 不显示实际值,留空表示不修改
|
||||
apiSecret: '', // 不显示实际值,留空表示不修改
|
||||
apiPassphrase: '', // 不显示实际值,留空表示不修改
|
||||
isDefault: account.isDefault || false
|
||||
apiPassphrase: '' // 不显示实际值,留空表示不修改
|
||||
})
|
||||
}}
|
||||
size={isMobile ? 'middle' : 'large'}
|
||||
@@ -203,11 +201,6 @@ const AccountDetail: React.FC = () => {
|
||||
{account.walletAddress}
|
||||
</span>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="默认账户">
|
||||
<Tag color={account.isDefault ? 'gold' : 'default'}>
|
||||
{account.isDefault ? '是' : '否'}
|
||||
</Tag>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="账户余额">
|
||||
{balanceLoading ? (
|
||||
<Spin size="small" />
|
||||
@@ -378,13 +371,6 @@ const AccountDetail: React.FC = () => {
|
||||
<Input.Password placeholder="留空表示不修改" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="isDefault"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>设为默认账户</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import { Card, Form, Input, Button, message, Typography, Space, Alert, Checkbox } from 'antd'
|
||||
import { Card, Form, Input, Button, message, Typography, Space } from 'antd'
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import { useMediaQuery } from 'react-responsive'
|
||||
@@ -37,8 +37,7 @@ const AccountEdit: React.FC = () => {
|
||||
|
||||
// 设置表单初始值
|
||||
form.setFieldsValue({
|
||||
accountName: accountData.accountName || '',
|
||||
isDefault: accountData.isDefault || false
|
||||
accountName: accountData.accountName || ''
|
||||
})
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '获取账户详情失败')
|
||||
@@ -55,8 +54,7 @@ const AccountEdit: React.FC = () => {
|
||||
// 构建更新请求
|
||||
const updateData: any = {
|
||||
accountId: Number(accountId),
|
||||
accountName: values.accountName || undefined,
|
||||
isDefault: values.isDefault || false
|
||||
accountName: values.accountName || undefined
|
||||
}
|
||||
|
||||
await updateAccount(updateData)
|
||||
@@ -119,20 +117,6 @@ const AccountEdit: React.FC = () => {
|
||||
<Input placeholder="账户名称(可选)" />
|
||||
</Form.Item>
|
||||
|
||||
<Alert
|
||||
message="API Key 管理"
|
||||
description="API Key 由系统自动管理,无需手动更新。如需重新获取 API Key,请删除并重新导入账户。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="isDefault"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>设为默认账户</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Form, Input, Button, message, Typography, Radio, Space, Alert, Checkbox } from 'antd'
|
||||
import { Card, Form, Input, Button, message, Typography, Radio, Space, Alert } from 'antd'
|
||||
import { ArrowLeftOutlined } from '@ant-design/icons'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import {
|
||||
@@ -135,8 +135,7 @@ const AccountImport: React.FC = () => {
|
||||
await importAccount({
|
||||
privateKey: privateKey,
|
||||
walletAddress: walletAddress,
|
||||
accountName: values.accountName,
|
||||
isDefault: values.isDefault || false
|
||||
accountName: values.accountName
|
||||
})
|
||||
|
||||
message.success('导入账户成功')
|
||||
@@ -302,20 +301,6 @@ const AccountImport: React.FC = () => {
|
||||
<Input placeholder="可选,用于标识账户" />
|
||||
</Form.Item>
|
||||
|
||||
<Alert
|
||||
message="API Key 自动获取"
|
||||
description="系统将自动从 Polymarket 获取或创建 API Key,无需手动输入。"
|
||||
type="info"
|
||||
showIcon
|
||||
style={{ marginBottom: '24px' }}
|
||||
/>
|
||||
|
||||
<Form.Item
|
||||
name="isDefault"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>设为默认账户</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Checkbox, Alert } from 'antd'
|
||||
import { Card, Table, Button, Space, Tag, Popconfirm, message, Typography, Spin, Modal, Descriptions, Divider, Form, Input, Alert } from 'antd'
|
||||
import { PlusOutlined, ReloadOutlined, EditOutlined, CopyOutlined } from '@ant-design/icons'
|
||||
import { useAccountStore } from '../store/accountStore'
|
||||
import type { Account } from '../types'
|
||||
@@ -154,8 +154,7 @@ const AccountList: React.FC = () => {
|
||||
accountName: accountDetail.accountName || '',
|
||||
apiKey: '', // 不显示实际值,留空表示不修改
|
||||
apiSecret: '', // 不显示实际值,留空表示不修改
|
||||
apiPassphrase: '', // 不显示实际值,留空表示不修改
|
||||
isDefault: accountDetail.isDefault || false
|
||||
apiPassphrase: '' // 不显示实际值,留空表示不修改
|
||||
})
|
||||
} catch (error: any) {
|
||||
console.error('打开编辑失败:', error)
|
||||
@@ -173,8 +172,7 @@ const AccountList: React.FC = () => {
|
||||
// 构建更新请求,空字符串转换为 undefined(不修改)
|
||||
const updateData: any = {
|
||||
accountId: editAccount.id,
|
||||
accountName: values.accountName || undefined,
|
||||
isDefault: values.isDefault || false
|
||||
accountName: values.accountName || undefined
|
||||
}
|
||||
|
||||
// 只有非空字符串才更新 API 凭证
|
||||
@@ -816,13 +814,6 @@ const AccountList: React.FC = () => {
|
||||
<Input.Password placeholder="留空表示不修改" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="isDefault"
|
||||
valuePropName="checked"
|
||||
>
|
||||
<Checkbox>设为默认账户</Checkbox>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Space style={{ width: '100%', justifyContent: 'flex-end' }}>
|
||||
<Button
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useNavigate, Link } from 'react-router-dom'
|
||||
import { Card, Form, Input, Button, message, Typography } from 'antd'
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
@@ -95,6 +95,11 @@ const Login: React.FC = () => {
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
<Form.Item style={{ marginBottom: 0, textAlign: 'right' }}>
|
||||
<Link to="/reset-password" style={{ fontSize: isMobile ? '14px' : '13px' }}>
|
||||
忘记密码?重置密码
|
||||
</Link>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
@@ -30,6 +30,7 @@ const PositionList: React.FC = () => {
|
||||
const [orderType, setOrderType] = useState<'MARKET' | 'LIMIT'>('LIMIT')
|
||||
const [sellQuantity, setSellQuantity] = useState<string>('')
|
||||
const [limitPrice, setLimitPrice] = useState<string>('')
|
||||
const [selectedPercent, setSelectedPercent] = useState<string | null>(null) // 记录选择的百分比(字符串格式)
|
||||
const [form] = Form.useForm()
|
||||
const [submitting, setSubmitting] = useState(false)
|
||||
const [wsConnected, setWsConnected] = useState(false)
|
||||
@@ -361,6 +362,7 @@ const PositionList: React.FC = () => {
|
||||
setOrderType('LIMIT')
|
||||
setSellQuantity('')
|
||||
setLimitPrice('')
|
||||
setSelectedPercent(null) // 重置百分比选择
|
||||
form.resetFields()
|
||||
|
||||
// 加载市场价格
|
||||
@@ -382,6 +384,9 @@ const PositionList: React.FC = () => {
|
||||
// 处理数量快捷按钮
|
||||
const handleQuantityQuickSelect = (percent: number) => {
|
||||
if (!selectedPosition) return
|
||||
// 记录选择的百分比(转为字符串,避免精度问题)
|
||||
setSelectedPercent(percent.toString())
|
||||
// 计算显示用的数量(用于预览,使用显示数量即可)
|
||||
const quantity = parseFloat(selectedPosition.quantity)
|
||||
const sellQty = (quantity * percent / 100).toFixed(4)
|
||||
setSellQuantity(sellQty)
|
||||
@@ -440,7 +445,12 @@ const PositionList: React.FC = () => {
|
||||
side: selectedPosition.side,
|
||||
outcomeIndex: selectedPosition.outcomeIndex, // 传递 outcomeIndex
|
||||
orderType: orderType,
|
||||
quantity: sellQuantity,
|
||||
// 如果选择了百分比,只传递百分比,不传 quantity
|
||||
// 如果手动输入,只传递 quantity,不传 percent
|
||||
...(selectedPercent != null
|
||||
? { percent: selectedPercent }
|
||||
: { quantity: sellQuantity }
|
||||
),
|
||||
price: orderType === 'LIMIT' ? limitPrice : undefined
|
||||
}
|
||||
|
||||
@@ -452,6 +462,7 @@ const PositionList: React.FC = () => {
|
||||
// 重置表单
|
||||
setSellQuantity('')
|
||||
setLimitPrice('')
|
||||
setSelectedPercent(null) // 重置百分比选择
|
||||
form.resetFields()
|
||||
// 仓位列表会通过WebSocket自动更新
|
||||
} else {
|
||||
@@ -1371,6 +1382,8 @@ const PositionList: React.FC = () => {
|
||||
onChange={(e) => {
|
||||
const newQuantity = e.target.value
|
||||
setSellQuantity(newQuantity)
|
||||
// 用户手动输入时,清除百分比选择
|
||||
setSelectedPercent(null)
|
||||
if (newQuantity) {
|
||||
const price = getCurrentSellPrice()
|
||||
calculatePnl(newQuantity, price)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Card, Form, Input, Button, message, Typography, Alert, Progress } from 'antd'
|
||||
import { LockOutlined, KeyOutlined, UserOutlined } from '@ant-design/icons'
|
||||
import { apiService } from '../services/api'
|
||||
@@ -52,7 +51,6 @@ const getPasswordStrengthInfo = (strength: number): { text: string; color: strin
|
||||
}
|
||||
|
||||
const ResetPassword: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const isMobile = useMediaQuery({ maxWidth: 768 })
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [passwordStrength, setPasswordStrength] = useState(0)
|
||||
@@ -77,11 +75,11 @@ const ResetPassword: React.FC = () => {
|
||||
newPassword: values.newPassword
|
||||
})
|
||||
if (response.data.code === 0) {
|
||||
message.success('密码重置成功,请登录')
|
||||
// 延迟跳转到登录页,让用户看到成功提示
|
||||
message.success('密码重置成功', 1)
|
||||
// 使用 window.location.href 强制跳转到登录页,确保跳转成功
|
||||
setTimeout(() => {
|
||||
navigate('/login', { replace: true })
|
||||
}, 1000)
|
||||
window.location.href = '/login'
|
||||
}, 500)
|
||||
} else {
|
||||
message.error(response.data.msg || '密码重置失败')
|
||||
}
|
||||
|
||||
@@ -184,12 +184,6 @@ export const apiService = {
|
||||
balance: (data: { accountId?: number }) =>
|
||||
apiClient.post<ApiResponse<any>>('/copy-trading/accounts/balance', data),
|
||||
|
||||
/**
|
||||
* 设置默认账户
|
||||
*/
|
||||
setDefault: (data: { accountId: number }) =>
|
||||
apiClient.post<ApiResponse<void>>('/copy-trading/accounts/set-default', data),
|
||||
|
||||
/**
|
||||
* 查询所有账户的仓位列表
|
||||
*/
|
||||
|
||||
@@ -15,7 +15,6 @@ export interface Account {
|
||||
walletAddress: string
|
||||
proxyAddress: string // Polymarket 代理钱包地址
|
||||
accountName?: string
|
||||
isDefault: boolean
|
||||
isEnabled?: boolean // 是否启用
|
||||
apiKeyConfigured: boolean
|
||||
apiSecretConfigured: boolean
|
||||
@@ -43,7 +42,6 @@ export interface AccountImportRequest {
|
||||
privateKey: string
|
||||
walletAddress: string
|
||||
accountName?: string
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,7 +50,6 @@ export interface AccountImportRequest {
|
||||
export interface AccountUpdateRequest {
|
||||
accountId: number
|
||||
accountName?: string
|
||||
isDefault?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,7 +284,8 @@ export interface AccountPosition {
|
||||
marketIcon?: string // 市场图标 URL
|
||||
side: string // 结果名称(如 "YES", "NO", "Pakistan" 等)
|
||||
outcomeIndex?: number // 结果索引(0, 1, 2...),用于计算 tokenId
|
||||
quantity: string
|
||||
quantity: string // 显示用的数量(可能被截位)
|
||||
originalQuantity?: string // 原始数量(保留完整精度,用于100%出售)
|
||||
avgPrice: string
|
||||
currentPrice: string
|
||||
currentValue: string
|
||||
@@ -319,7 +317,8 @@ export interface PositionSellRequest {
|
||||
side: string // 结果名称(如 "YES", "NO", "Pakistan" 等)
|
||||
outcomeIndex?: number // 结果索引(0, 1, 2...),用于计算 tokenId(推荐提供)
|
||||
orderType: 'MARKET' | 'LIMIT'
|
||||
quantity: string
|
||||
quantity?: string // 卖出数量(可选,手动输入时使用)
|
||||
percent?: string // 卖出百分比(可选,BigDecimal字符串,支持小数,0-100之间,选择百分比按钮时使用)
|
||||
price?: string // 限价订单必需
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user