🎯 重构登录逻辑:以人为核心的三种出战模式
核心变更: - 单枪匹马 (solo) - 人类单独出战 - 组队团战 (team) - 人类 +N 龙虾 - 独当一面 (agent_only) - 龙虾单独出征 后端: - users/views.py: 支持多选 agent_ids - 新增 mode_names 映射 - 错误提示优化 前端: - 新模式选择 UI(带图标和说明) - 多选龙虾复选框 - 实时显示已选龙虾数量 - 选中模式高亮显示 测试: - test_new_login.py: 完整测试三种模式 - 绑定第二只龙虾(龙虾监控 🦞) 结果: ✅ 单枪匹马 - 1 个人类座位 ✅ 组队团战 - 1+N 个座位(人类 + 龙虾) ✅ 独当一面 - N 个龙虾座位
This commit is contained in:
92
backend/test_new_login.py
Normal file
92
backend/test_new_login.py
Normal file
@@ -0,0 +1,92 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
测试新登录逻辑(单枪匹马/组队团战/独当一面)
|
||||
"""
|
||||
|
||||
import requests
|
||||
|
||||
API_BASE = 'http://localhost:8000/api/v1'
|
||||
|
||||
def test_new_login():
|
||||
print("="*60)
|
||||
print("🎯 测试新登录逻辑")
|
||||
print("="*60)
|
||||
|
||||
# 1. 单枪匹马
|
||||
print("\n🥷 测试 1: 单枪匹马(仅人类)")
|
||||
res = requests.post(f'{API_BASE}/auth/login/', json={
|
||||
'username': 'test',
|
||||
'password': 'test123',
|
||||
'mode': 'solo'
|
||||
})
|
||||
if res.status_code == 200:
|
||||
data = res.json()
|
||||
print(f"✅ 登录成功")
|
||||
print(f" 模式:{data['mode_name']}")
|
||||
print(f" 会话数:{len(data['sessions'])}")
|
||||
for s in data['sessions']:
|
||||
print(f" - {s['session_type']}: {s['nickname']} ({s['emoji']})")
|
||||
else:
|
||||
print(f"❌ 登录失败:{res.json()}")
|
||||
|
||||
# 2. 组队团战
|
||||
print("\n🛡️ 测试 2: 组队团战(人类 +2 龙虾)")
|
||||
res = requests.post(f'{API_BASE}/auth/login/', json={
|
||||
'username': 'test',
|
||||
'password': 'test123',
|
||||
'mode': 'team',
|
||||
'agent_ids': ['flying_hero', 'lobster_monitor']
|
||||
})
|
||||
if res.status_code == 200:
|
||||
data = res.json()
|
||||
print(f"✅ 登录成功")
|
||||
print(f" 模式:{data['mode_name']}")
|
||||
print(f" 会话数:{len(data['sessions'])}")
|
||||
for s in data['sessions']:
|
||||
emoji = s.get('emoji', '🤖')
|
||||
print(f" - {s['session_type']}: {s['nickname']} ({emoji})")
|
||||
else:
|
||||
print(f"❌ 登录失败:{res.json()}")
|
||||
|
||||
# 3. 独当一面
|
||||
print("\n⚔️ 测试 3: 独当一面(仅龙虾)")
|
||||
res = requests.post(f'{API_BASE}/auth/login/', json={
|
||||
'username': 'test',
|
||||
'password': 'test123',
|
||||
'mode': 'agent_only',
|
||||
'agent_ids': ['flying_hero']
|
||||
})
|
||||
if res.status_code == 200:
|
||||
data = res.json()
|
||||
print(f"✅ 登录成功")
|
||||
print(f" 模式:{data['mode_name']}")
|
||||
print(f" 会话数:{len(data['sessions'])}")
|
||||
for s in data['sessions']:
|
||||
emoji = s.get('emoji', '🤖')
|
||||
print(f" - {s['session_type']}: {s['nickname']} ({emoji})")
|
||||
else:
|
||||
print(f"❌ 登录失败:{res.json()}")
|
||||
|
||||
# 4. 错误测试 - 组队但没选龙虾
|
||||
print("\n❌ 测试 4: 组队但没选龙虾(应该失败)")
|
||||
res = requests.post(f'{API_BASE}/auth/login/', json={
|
||||
'username': 'test',
|
||||
'password': 'test123',
|
||||
'mode': 'team',
|
||||
'agent_ids': []
|
||||
})
|
||||
if res.status_code == 400:
|
||||
print(f"✅ 正确失败:{res.json()['error']}")
|
||||
else:
|
||||
print(f"❌ 应该失败但成功了:{res.json()}")
|
||||
|
||||
print("\n" + "="*60)
|
||||
print("✅ 新登录逻辑测试完成!")
|
||||
print("="*60)
|
||||
print("\n📊 三种模式:")
|
||||
print("1. 🥷 单枪匹马 - 人类单独出战")
|
||||
print("2. 🛡️ 组队团战 - 人类 +N 龙虾")
|
||||
print("3. ⚔️ 独当一面 - 龙虾单独出征")
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_new_login()
|
||||
@@ -8,11 +8,17 @@ User = get_user_model()
|
||||
class LoginSerializer(serializers.Serializer):
|
||||
username = serializers.CharField()
|
||||
password = serializers.CharField()
|
||||
login_mode = serializers.ChoiceField(
|
||||
choices=['human_only', 'agent_only', 'both'],
|
||||
default='human_only'
|
||||
mode = serializers.ChoiceField(
|
||||
choices=['solo', 'team', 'agent_only'],
|
||||
default='solo',
|
||||
help_text='solo=单枪匹马,team=组队团战,agent_only=独当一面'
|
||||
)
|
||||
agent_ids = serializers.ListField(
|
||||
child=serializers.CharField(),
|
||||
required=False,
|
||||
default=list,
|
||||
help_text='选择的龙虾 ID 列表(team 或 agent_only 模式)'
|
||||
)
|
||||
selected_agent_id = serializers.CharField(required=False, allow_blank=True)
|
||||
|
||||
|
||||
class LoginView(views.APIView):
|
||||
@@ -23,8 +29,8 @@ class LoginView(views.APIView):
|
||||
|
||||
username = serializer.validated_data['username']
|
||||
password = serializer.validated_data['password']
|
||||
login_mode = serializer.validated_data.get('login_mode', 'human_only')
|
||||
selected_agent_id = serializer.validated_data.get('selected_agent_id')
|
||||
mode = serializer.validated_data.get('mode', 'solo')
|
||||
agent_ids = serializer.validated_data.get('agent_ids', [])
|
||||
|
||||
user = authenticate(username=username, password=password)
|
||||
if not user:
|
||||
@@ -40,8 +46,8 @@ class LoginView(views.APIView):
|
||||
# 构建会话信息
|
||||
sessions = []
|
||||
|
||||
if login_mode in ['human_only', 'both']:
|
||||
# 人类身份
|
||||
if mode in ['solo', 'team']:
|
||||
# 人类身份(单枪匹马 或 组队团战)
|
||||
sessions.append({
|
||||
'session_type': 'human',
|
||||
'nickname': user.username,
|
||||
@@ -49,10 +55,17 @@ class LoginView(views.APIView):
|
||||
'user_id': user.id
|
||||
})
|
||||
|
||||
if login_mode in ['agent_only', 'both']:
|
||||
# 龙虾身份
|
||||
if selected_agent_id:
|
||||
agent = user.get_linked_agent(selected_agent_id)
|
||||
if mode in ['team', 'agent_only']:
|
||||
# 龙虾身份(组队团战 或 独当一面)
|
||||
if not agent_ids:
|
||||
return Response(
|
||||
{'error': '组队或独当一面模式需要选择至少一只龙虾'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 添加所有选择的龙虾
|
||||
for agent_id in agent_ids:
|
||||
agent = user.get_linked_agent(agent_id)
|
||||
if agent:
|
||||
sessions.append({
|
||||
'session_type': 'agent',
|
||||
@@ -64,10 +77,17 @@ class LoginView(views.APIView):
|
||||
})
|
||||
else:
|
||||
return Response(
|
||||
{'error': f'未找到绑定的龙虾:{selected_agent_id}'},
|
||||
{'error': f'未找到绑定的龙虾:{agent_id}'},
|
||||
status=status.HTTP_400_BAD_REQUEST
|
||||
)
|
||||
|
||||
# 模式名称映射
|
||||
mode_names = {
|
||||
'solo': '单枪匹马',
|
||||
'team': '组队团战',
|
||||
'agent_only': '独当一面'
|
||||
}
|
||||
|
||||
return Response({
|
||||
'token': token,
|
||||
'user': {
|
||||
@@ -77,7 +97,8 @@ class LoginView(views.APIView):
|
||||
'linked_agents': user.linked_agents
|
||||
},
|
||||
'sessions': sessions,
|
||||
'login_mode': login_mode
|
||||
'mode': mode,
|
||||
'mode_name': mode_names.get(mode, mode)
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ axios.interceptors.request.use(config => {
|
||||
function LoginPage() {
|
||||
const [username, setUsername] = useState('test');
|
||||
const [password, setPassword] = useState('test123');
|
||||
const [loginMode, setLoginMode] = useState('human_only');
|
||||
const [mode, setMode] = useState('solo');
|
||||
const [agents, setAgents] = useState([]);
|
||||
const [selectedAgent, setSelectedAgent] = useState('');
|
||||
const [selectedAgents, setSelectedAgents] = useState([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
// 扫描本机龙虾
|
||||
@@ -28,9 +28,6 @@ function LoginPage() {
|
||||
try {
|
||||
const res = await axios.get(`${API_BASE}/user/scan-local-agents/`);
|
||||
setAgents(res.data.agents || []);
|
||||
if (res.data.agents?.length > 0) {
|
||||
setSelectedAgent(res.data.agents[0].agent_id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('扫描龙虾失败:', error);
|
||||
}
|
||||
@@ -42,24 +39,32 @@ function LoginPage() {
|
||||
const payload = {
|
||||
username,
|
||||
password,
|
||||
login_mode: loginMode
|
||||
mode
|
||||
};
|
||||
|
||||
if (loginMode !== 'human_only' && selectedAgent) {
|
||||
payload.selected_agent_id = selectedAgent;
|
||||
if (mode !== 'solo' && selectedAgents.length > 0) {
|
||||
payload.agent_ids = selectedAgents;
|
||||
}
|
||||
|
||||
const res = await axios.post(`${API_BASE}/auth/login/`, payload);
|
||||
localStorage.setItem('token', res.data.token);
|
||||
localStorage.setItem('user', JSON.stringify(res.data.user));
|
||||
localStorage.setItem('sessions', JSON.stringify(res.data.sessions));
|
||||
localStorage.setItem('login_mode', res.data.login_mode);
|
||||
localStorage.setItem('mode', res.data.mode);
|
||||
navigate('/meetings');
|
||||
} catch (error) {
|
||||
alert('登录失败:' + (error.response?.data?.detail || error.response?.data?.error || error.message));
|
||||
}
|
||||
};
|
||||
|
||||
const toggleAgent = (agentId) => {
|
||||
setSelectedAgents(prev =>
|
||||
prev.includes(agentId)
|
||||
? prev.filter(id => id !== agentId)
|
||||
: [...prev, agentId]
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={styles.center}>
|
||||
<div style={styles.card}>
|
||||
@@ -68,64 +73,71 @@ function LoginPage() {
|
||||
<input type="text" placeholder="用户名" value={username} onChange={e => setUsername(e.target.value)} style={styles.input} required />
|
||||
<input type="password" placeholder="密码" value={password} onChange={e => setPassword(e.target.value)} style={styles.input} required />
|
||||
|
||||
{/* 身份模式选择 */}
|
||||
{/* 出战模式选择 */}
|
||||
<div style={{margin: '15px 0'}}>
|
||||
<label style={{display: 'block', marginBottom: '10px', fontWeight: '600'}}>登录身份:</label>
|
||||
<label style={{display: 'block', marginBottom: '8px', cursor: 'pointer'}}>
|
||||
<label style={{display: 'block', marginBottom: '10px', fontWeight: '600'}}>🎯 出战模式:</label>
|
||||
<label style={{display: 'block', marginBottom: '12px', cursor: 'pointer', padding: '10px', background: mode === 'solo' ? '#e7f3ff' : 'white', borderRadius: '8px', border: '1px solid #2196f3'}}>
|
||||
<input
|
||||
type="radio"
|
||||
name="loginMode"
|
||||
value="human_only"
|
||||
checked={loginMode === 'human_only'}
|
||||
onChange={e => setLoginMode(e.target.value)}
|
||||
name="mode"
|
||||
value="solo"
|
||||
checked={mode === 'solo'}
|
||||
onChange={e => setMode(e.target.value)}
|
||||
/>
|
||||
{' '}👤 人类身份(纯人类参会)
|
||||
{' '}🥷 <strong>单枪匹马</strong>(人类单独出战,不带龙虾)
|
||||
</label>
|
||||
<label style={{display: 'block', marginBottom: '8px', cursor: 'pointer'}}>
|
||||
<label style={{display: 'block', marginBottom: '12px', cursor: 'pointer', padding: '10px', background: mode === 'team' ? '#e7f3ff' : 'white', borderRadius: '8px', border: '1px solid #2196f3'}}>
|
||||
<input
|
||||
type="radio"
|
||||
name="loginMode"
|
||||
name="mode"
|
||||
value="team"
|
||||
checked={mode === 'team'}
|
||||
onChange={e => setMode(e.target.value)}
|
||||
/>
|
||||
{' '}🛡️ <strong>组队团战</strong>(人类 + N 只龙虾一起出战)
|
||||
</label>
|
||||
<label style={{display: 'block', marginBottom: '12px', cursor: 'pointer', padding: '10px', background: mode === 'agent_only' ? '#e7f3ff' : 'white', borderRadius: '8px', border: '1px solid #2196f3'}}>
|
||||
<input
|
||||
type="radio"
|
||||
name="mode"
|
||||
value="agent_only"
|
||||
checked={loginMode === 'agent_only'}
|
||||
onChange={e => setLoginMode(e.target.value)}
|
||||
checked={mode === 'agent_only'}
|
||||
onChange={e => setMode(e.target.value)}
|
||||
/>
|
||||
{' '}🦞 龙虾身份(Agent 参会)
|
||||
</label>
|
||||
<label style={{display: 'block', marginBottom: '8px', cursor: 'pointer'}}>
|
||||
<input
|
||||
type="radio"
|
||||
name="loginMode"
|
||||
value="both"
|
||||
checked={loginMode === 'both'}
|
||||
onChange={e => setLoginMode(e.target.value)}
|
||||
/>
|
||||
{' '}👤+ 双重身份(人类 + 龙虾)
|
||||
{' '}⚔️ <strong>独当一面</strong>(龙虾单独出征,人类不上场)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{/* 龙虾选择 */}
|
||||
{loginMode !== 'human_only' && (
|
||||
<div style={{margin: '15px 0'}}>
|
||||
<label style={{display: 'block', marginBottom: '8px', fontWeight: '600'}}>选择龙虾:</label>
|
||||
<select
|
||||
value={selectedAgent}
|
||||
onChange={e => setSelectedAgent(e.target.value)}
|
||||
style={styles.input}
|
||||
>
|
||||
{agents.length === 0 ? (
|
||||
<option value="">未找到龙虾</option>
|
||||
) : (
|
||||
agents.map(a => (
|
||||
<option key={a.agent_id} value={a.agent_id}>
|
||||
{a.agent_id} ({a.instance_name})
|
||||
</option>
|
||||
))
|
||||
)}
|
||||
</select>
|
||||
{/* 龙虾选择(组队或独当一面) */}
|
||||
{mode !== 'solo' && (
|
||||
<div style={{margin: '15px 0', padding: '15px', background: '#f9f9f9', borderRadius: '8px'}}>
|
||||
<label style={{display: 'block', marginBottom: '10px', fontWeight: '600'}}>🦐 选择龙虾队友:</label>
|
||||
{agents.length === 0 ? (
|
||||
<p style={{color: '#999', fontSize: '14px'}}>未找到可用龙虾</p>
|
||||
) : (
|
||||
agents.map(a => (
|
||||
<label key={a.agent_id} style={{display: 'flex', alignItems: 'center', marginBottom: '8px', cursor: 'pointer'}}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedAgents.includes(a.agent_id)}
|
||||
onChange={() => toggleAgent(a.agent_id)}
|
||||
style={{marginRight: '10px'}}
|
||||
/>
|
||||
<span style={{fontSize: '16px', marginRight: '8px'}}>{a.agent_emoji || '🤖'}</span>
|
||||
<span>{a.agent_id}</span>
|
||||
<span style={{color: '#999', fontSize: '12px', marginLeft: '8px'}}>({a.instance_name})</span>
|
||||
</label>
|
||||
))
|
||||
)}
|
||||
{selectedAgents.length > 0 && (
|
||||
<p style={{marginTop: '10px', color: '#2196f3', fontWeight: '600'}}>
|
||||
已选 {selectedAgents.length} 只龙虾队友 🦸
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button type="submit" style={styles.btn}>登录</button>
|
||||
<button type="submit" style={styles.btn}>🚀 登录出征</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user