fix: gate research pool trial creation
This commit is contained in:
+3
@@ -6,6 +6,7 @@ import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolAlreadyE
|
||||
import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolConfirmRequiredException
|
||||
import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolDuplicateTrialConfigException
|
||||
import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolNotFoundException
|
||||
import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolResearchCandidateNotReadyException
|
||||
import com.wrbug.polymarketbot.service.copytrading.leaderpool.LeaderPoolService
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.context.MessageSource
|
||||
@@ -146,6 +147,7 @@ class LeaderPoolController(
|
||||
is LeaderPoolAlreadyExistsException -> ErrorCode.LEADER_POOL_ALREADY_EXISTS
|
||||
is LeaderPoolDuplicateTrialConfigException -> ErrorCode.LEADER_POOL_DUPLICATE_TRIAL_CONFIG
|
||||
is LeaderPoolConfirmRequiredException -> ErrorCode.LEADER_POOL_CONFIRM_REQUIRED
|
||||
is LeaderPoolResearchCandidateNotReadyException -> ErrorCode.LEADER_RESEARCH_CANDIDATE_NOT_READY
|
||||
is IllegalArgumentException -> when (e.message) {
|
||||
"账户不存在" -> ErrorCode.ACCOUNT_NOT_FOUND
|
||||
"Leader 不存在" -> ErrorCode.LEADER_NOT_FOUND
|
||||
@@ -166,6 +168,7 @@ class LeaderPoolController(
|
||||
errorCode == ErrorCode.LEADER_POOL_ALREADY_EXISTS ||
|
||||
errorCode == ErrorCode.LEADER_POOL_DUPLICATE_TRIAL_CONFIG ||
|
||||
errorCode == ErrorCode.LEADER_POOL_CONFIRM_REQUIRED ||
|
||||
errorCode == ErrorCode.LEADER_RESEARCH_CANDIDATE_NOT_READY ||
|
||||
errorCode == ErrorCode.ACCOUNT_NOT_FOUND ||
|
||||
errorCode == ErrorCode.LEADER_NOT_FOUND
|
||||
}
|
||||
|
||||
+4
@@ -7,3 +7,7 @@ class LeaderPoolAlreadyExistsException(message: String = "Leader 已在池子中
|
||||
class LeaderPoolDuplicateTrialConfigException(message: String = "该账户已存在此 Leader 的跟单配置") : RuntimeException(message)
|
||||
|
||||
class LeaderPoolConfirmRequiredException(message: String = "立即启用试跟配置需要显式确认") : RuntimeException(message)
|
||||
|
||||
class LeaderPoolResearchCandidateNotReadyException(
|
||||
message: String = "研究候选尚未进入试跟建议状态"
|
||||
) : RuntimeException(message)
|
||||
|
||||
+10
@@ -5,6 +5,7 @@ import com.wrbug.polymarketbot.entity.CopyTrading
|
||||
import com.wrbug.polymarketbot.entity.Leader
|
||||
import com.wrbug.polymarketbot.entity.LeaderPool
|
||||
import com.wrbug.polymarketbot.enums.LeaderPoolStatus
|
||||
import com.wrbug.polymarketbot.enums.LeaderResearchState
|
||||
import com.wrbug.polymarketbot.repository.AccountRepository
|
||||
import com.wrbug.polymarketbot.repository.CopyTradingRepository
|
||||
import com.wrbug.polymarketbot.repository.LeaderPoolRepository
|
||||
@@ -207,6 +208,15 @@ class LeaderPoolService(
|
||||
}
|
||||
|
||||
return try {
|
||||
if (pool.researchCandidateId != null && pool.researchState != LeaderResearchState.TRIAL_READY) {
|
||||
logger.warn(
|
||||
"拒绝从未试跟建议的研究候选创建 Leader 池试跟配置: poolId={}, leaderId={}, researchState={}",
|
||||
pool.id,
|
||||
pool.leaderId,
|
||||
pool.researchState
|
||||
)
|
||||
return Result.failure(LeaderPoolResearchCandidateNotReadyException())
|
||||
}
|
||||
if (request.enableImmediately && !request.confirm) {
|
||||
logger.warn("拒绝未确认的立即启用试跟配置: poolId={}, leaderId={}", pool.id, pool.leaderId)
|
||||
return Result.failure(LeaderPoolConfirmRequiredException())
|
||||
|
||||
+20
-1
@@ -12,6 +12,7 @@ import com.wrbug.polymarketbot.entity.CopyTrading
|
||||
import com.wrbug.polymarketbot.entity.Leader
|
||||
import com.wrbug.polymarketbot.entity.LeaderPool
|
||||
import com.wrbug.polymarketbot.enums.LeaderPoolStatus
|
||||
import com.wrbug.polymarketbot.enums.LeaderResearchState
|
||||
import com.wrbug.polymarketbot.repository.AccountRepository
|
||||
import com.wrbug.polymarketbot.repository.CopyTradingRepository
|
||||
import com.wrbug.polymarketbot.repository.LeaderPoolRepository
|
||||
@@ -180,6 +181,20 @@ class LeaderPoolServiceTest {
|
||||
assertEquals(LeaderPoolStatus.TRIAL, poolCaptor.value.status)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `research pool item must be trial ready before creating trial config`() {
|
||||
Mockito.`when`(leaderPoolRepository.findById(10L)).thenReturn(
|
||||
Optional.of(pool(researchCandidateId = 99, researchState = LeaderResearchState.PAPER))
|
||||
)
|
||||
|
||||
val result = service.createTrialConfig(LeaderPoolCreateTrialConfigRequest(poolId = 10, accountId = 2))
|
||||
|
||||
assertTrue(result.isFailure)
|
||||
assertTrue(result.exceptionOrNull() is LeaderPoolResearchCandidateNotReadyException)
|
||||
Mockito.verify(accountRepository, Mockito.never()).findById(2L)
|
||||
Mockito.verify(copyTradingService, Mockito.never()).createCopyTrading(anyCreateRequest())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `create trial failure leaves pool status unchanged`() {
|
||||
Mockito.`when`(leaderPoolRepository.findById(10L)).thenReturn(Optional.of(pool()))
|
||||
@@ -261,11 +276,15 @@ class LeaderPoolServiceTest {
|
||||
private fun pool(
|
||||
id: Long = 10,
|
||||
leaderId: Long = 1,
|
||||
status: LeaderPoolStatus = LeaderPoolStatus.CANDIDATE
|
||||
status: LeaderPoolStatus = LeaderPoolStatus.CANDIDATE,
|
||||
researchCandidateId: Long? = null,
|
||||
researchState: LeaderResearchState? = null
|
||||
) = LeaderPool(
|
||||
id = id,
|
||||
leaderId = leaderId,
|
||||
status = status,
|
||||
researchCandidateId = researchCandidateId,
|
||||
researchState = researchState,
|
||||
suggestedFixedAmount = BigDecimal("1"),
|
||||
suggestedMaxDailyOrders = 10,
|
||||
suggestedMaxDailyLoss = BigDecimal("5"),
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"createTrial": "Create Trial Config",
|
||||
"trialConfirmTitle": "This creates a disabled FIXED small trial config",
|
||||
"trialConfirmDesc": "Confirm fixed amount, max daily orders, max daily loss, price range, and max position before submitting.",
|
||||
"trialRequiresResearchReady": "Research candidates must be trial-ready before a trial config can be created.",
|
||||
"account": "Account",
|
||||
"selectAccount": "Please select an account",
|
||||
"noAccounts": "No accounts yet",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"createTrial": "创建试跟配置",
|
||||
"trialConfirmTitle": "将创建默认禁用的小额 FIXED 跟单配置",
|
||||
"trialConfirmDesc": "提交前请确认固定金额、每日最大单数、每日最大亏损、价格区间和最大持仓。",
|
||||
"trialRequiresResearchReady": "研究候选进入建议试跟后才能创建试跟配置",
|
||||
"account": "账户",
|
||||
"selectAccount": "请选择账户",
|
||||
"noAccounts": "暂无账户,请先添加账户",
|
||||
|
||||
@@ -619,6 +619,7 @@
|
||||
"createTrial": "創建試跟配置",
|
||||
"trialConfirmTitle": "將創建默認禁用的小額 FIXED 跟單配置",
|
||||
"trialConfirmDesc": "提交前請確認固定金額、每日最大單數、每日最大虧損、價格區間和最大持倉。",
|
||||
"trialRequiresResearchReady": "研究候選進入建議試跟後才能創建試跟配置",
|
||||
"account": "賬戶",
|
||||
"selectAccount": "請選擇賬戶",
|
||||
"noAccounts": "暫無賬戶,請先添加賬戶",
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
Statistic,
|
||||
Table,
|
||||
Tag,
|
||||
Tooltip,
|
||||
Typography,
|
||||
message
|
||||
} from 'antd'
|
||||
@@ -45,6 +46,9 @@ const formatDate = (timestamp?: number) => {
|
||||
return dayjs(timestamp).format('YYYY-MM-DD HH:mm')
|
||||
}
|
||||
|
||||
const canCreateTrialConfig = (item: LeaderPoolItem) =>
|
||||
!item.researchCandidateId || item.researchState === 'TRIAL_READY'
|
||||
|
||||
const LeaderPool: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const navigate = useNavigate()
|
||||
@@ -333,36 +337,46 @@ const LeaderPool: React.FC = () => {
|
||||
key: 'actions',
|
||||
fixed: 'right' as const,
|
||||
width: 320,
|
||||
render: (_: unknown, item: LeaderPoolItem) => (
|
||||
<Space wrap size={4}>
|
||||
<Button size="small" icon={<EyeOutlined />} onClick={() => window.open(item.profileUrl, '_blank', 'noopener,noreferrer')}>
|
||||
Profile
|
||||
</Button>
|
||||
<Button size="small" onClick={() => openStatusModal(item)}>
|
||||
{t('leaderPool.updateStatus')}
|
||||
</Button>
|
||||
<Button size="small" onClick={() => openPlanModal(item)}>
|
||||
{t('leaderPool.editPlan')}
|
||||
</Button>
|
||||
render: (_: unknown, item: LeaderPoolItem) => {
|
||||
const trialAllowed = canCreateTrialConfig(item)
|
||||
const trialButton = (
|
||||
<Button
|
||||
size="small"
|
||||
type="primary"
|
||||
loading={creatingMap[item.id]}
|
||||
disabled={creatingMap[item.id]}
|
||||
disabled={creatingMap[item.id] || !trialAllowed}
|
||||
onClick={() => openTrialModal(item)}
|
||||
>
|
||||
{t('leaderPool.createTrial')}
|
||||
</Button>
|
||||
<Popconfirm
|
||||
title={t('leaderPool.removeConfirm')}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
onConfirm={() => handleRemove(item)}
|
||||
>
|
||||
<Button size="small" danger>{t('common.delete')}</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
)
|
||||
return (
|
||||
<Space wrap size={4}>
|
||||
<Button size="small" icon={<EyeOutlined />} onClick={() => window.open(item.profileUrl, '_blank', 'noopener,noreferrer')}>
|
||||
Profile
|
||||
</Button>
|
||||
<Button size="small" onClick={() => openStatusModal(item)}>
|
||||
{t('leaderPool.updateStatus')}
|
||||
</Button>
|
||||
<Button size="small" onClick={() => openPlanModal(item)}>
|
||||
{t('leaderPool.editPlan')}
|
||||
</Button>
|
||||
{trialAllowed ? trialButton : (
|
||||
<Tooltip title={t('leaderPool.trialRequiresResearchReady')}>
|
||||
<span>{trialButton}</span>
|
||||
</Tooltip>
|
||||
)}
|
||||
<Popconfirm
|
||||
title={t('leaderPool.removeConfirm')}
|
||||
okText={t('common.confirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
onConfirm={() => handleRemove(item)}
|
||||
>
|
||||
<Button size="small" danger>{t('common.delete')}</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
)
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user