seaislee1209 1e94241587 feat: multi-project per sub-account support
Data model:
- Add IAMUserProject model (sub-account → N projects, each with monitoring toggle)
- Remove old single project_name from IAMUser model
- Update SpendingRecord with per-project granularity

Backend:
- Project CRUD views: list/add/update-toggle/delete/toggle-all
- Create user view auto-adds first project if specified
- Scheduler aggregates spending across all enabled projects per user
- Per-project spending recorded in SpendingRecord + IAMUserProject.current_spending
- Alert details include per-project spending breakdown

Frontend:
- New "项目管理" dialog: add projects from Volcengine dropdown, toggle monitoring per project, remove projects, batch toggle all
- "项目" column in user table showing monitored/total count (clickable)
- BillingView: expandable rows showing per-project spending breakdown
- Create dialog: optional initial project selection
- Removed old single-project select from config dialog

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-19 20:37:38 +08:00

756 lines
26 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""AirGate 核心 API 视图"""
import logging
from datetime import datetime
from decimal import Decimal
from django.db.models import Sum
from rest_framework import status
from rest_framework.decorators import api_view, permission_classes
from rest_framework.response import Response
from utils.crypto import encrypt, decrypt, make_hint
from utils.iam_service import IAMService, ProjectService
from utils.billing_service import BillingService
from utils.volcengine_client import VolcengineAPIError
from .models import VolcAccount, IAMUser, IAMUserProject, GlobalConfig, AlertRecord, SpendingRecord, QuotaAllocation
from .serializers import (
VolcAccountSerializer, VolcAccountCreateSerializer,
IAMUserSerializer, IAMUserCreateSerializer, IAMUserImportSerializer,
IAMUserConfigSerializer,
IAMUserProjectSerializer, IAMUserProjectAddSerializer, IAMUserProjectUpdateSerializer,
QuotaAllocateSerializer, QuotaAllocationSerializer,
GlobalConfigSerializer,
AlertRecordSerializer,
DashboardSerializer,
)
logger = logging.getLogger(__name__)
def _get_volc_account(volc_id=None):
"""获取主账号,解密密钥"""
if volc_id:
account = VolcAccount.objects.get(pk=volc_id)
else:
account = VolcAccount.objects.filter(is_active=True).first()
if not account:
return None, '', ''
ak = decrypt(account.access_key_enc)
sk = decrypt(account.secret_key_enc)
return account, ak, sk
# ==================== Dashboard ====================
@api_view(['GET'])
def dashboard_view(request):
total = IAMUser.objects.count()
active = IAMUser.objects.filter(status=IAMUser.Status.ACTIVE).count()
disabled = IAMUser.objects.filter(status=IAMUser.Status.DISABLED).count()
monitored = IAMUser.objects.filter(monitor_enabled=True).count()
total_spending = IAMUser.objects.aggregate(
total=Sum('consumed_total'))['total'] or Decimal('0')
recent_alerts = AlertRecord.objects.all()[:10]
data = {
'total_users': total,
'active_users': active,
'disabled_users': disabled,
'monitored_users': monitored,
'total_spending': total_spending,
'recent_alerts': AlertRecordSerializer(recent_alerts, many=True).data,
}
return Response(data)
# ==================== Volcengine Account ====================
@api_view(['GET', 'POST'])
def volc_account_view(request):
if request.method == 'GET':
accounts = VolcAccount.objects.all()
return Response(VolcAccountSerializer(accounts, many=True).data)
serializer = VolcAccountCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
d = serializer.validated_data
account = VolcAccount.objects.create(
name=d['name'],
access_key_enc=encrypt(d['access_key']),
secret_key_enc=encrypt(d['secret_key']),
access_key_hint=make_hint(d['access_key']),
)
return Response(VolcAccountSerializer(account).data, status=status.HTTP_201_CREATED)
@api_view(['PUT', 'DELETE'])
def volc_account_detail_view(request, pk):
try:
account = VolcAccount.objects.get(pk=pk)
except VolcAccount.DoesNotExist:
return Response({'error': 'not_found', 'message': '主账号不存在'},
status=status.HTTP_404_NOT_FOUND)
if request.method == 'DELETE':
account.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
# PUT: update
name = request.data.get('name')
if name:
account.name = name
ak = request.data.get('access_key')
sk = request.data.get('secret_key')
if ak:
account.access_key_enc = encrypt(ak)
account.access_key_hint = make_hint(ak)
if sk:
account.secret_key_enc = encrypt(sk)
account.save()
return Response(VolcAccountSerializer(account).data)
@api_view(['POST'])
def volc_account_test_view(request, pk):
"""测试主账号密钥是否有效"""
try:
account = VolcAccount.objects.get(pk=pk)
except VolcAccount.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
ak = decrypt(account.access_key_enc)
sk = decrypt(account.secret_key_enc)
try:
svc = IAMService(ak, sk)
svc.list_users(limit=1)
return Response({'status': 'ok', 'message': '密钥验证成功'})
except VolcengineAPIError as e:
return Response({'status': 'error', 'message': str(e)},
status=status.HTTP_400_BAD_REQUEST)
# ==================== IAM Users ====================
@api_view(['GET'])
def iam_user_list_view(request):
users = IAMUser.objects.select_related('volc_account').all()
status_filter = request.query_params.get('status')
if status_filter:
users = users.filter(status=status_filter)
return Response(IAMUserSerializer(users, many=True).data)
@api_view(['POST'])
def iam_user_sync_view(request):
"""从火山引擎同步所有已有 IAM 用户"""
account, ak, sk = _get_volc_account()
if not account:
return Response({'error': 'no_account', 'message': '请先配置火山主账号'},
status=status.HTTP_400_BAD_REQUEST)
svc = IAMService(ak, sk)
imported = []
offset = 0
while True:
try:
resp = svc.list_users(limit=100, offset=offset)
except VolcengineAPIError as e:
return Response({'error': 'api_error', 'message': str(e)},
status=status.HTTP_502_BAD_GATEWAY)
users = resp.get("Result", {}).get("UserMetadata", [])
if not users:
break
for u in users:
username = u.get("UserName", "")
obj, created = IAMUser.objects.update_or_create(
volc_account=account,
username=username,
defaults={
'display_name': u.get("DisplayName", ""),
'user_id': u.get("UserId", ""),
'email': u.get("Email", ""),
'phone': u.get("MobilePhone", ""),
},
)
if created:
imported.append(username)
# Sync access keys
try:
keys = svc.list_access_keys(username)
obj.access_key_ids = [k["AccessKeyId"] for k in keys]
except Exception:
pass
# Sync login status
try:
profile = svc.get_login_profile(username)
login_allowed = profile.get("Result", {}).get("LoginProfile", {}).get("LoginAllowed", True)
obj.status = IAMUser.Status.ACTIVE if login_allowed else IAMUser.Status.DISABLED
except Exception:
obj.status = IAMUser.Status.UNKNOWN
obj.save()
offset += 100
total = resp.get("Result", {}).get("Total", 0)
if offset >= total:
break
total_count = IAMUser.objects.filter(volc_account=account).count()
return Response({
'message': f'同步完成,共 {total_count} 个用户,新导入 {len(imported)}',
'imported': imported,
'total': total_count,
})
@api_view(['POST'])
def iam_user_create_view(request):
"""在火山引擎创建新的 IAM 子账号并纳入管理"""
serializer = IAMUserCreateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
d = serializer.validated_data
account, ak, sk = _get_volc_account()
if not account:
return Response({'error': 'no_account', 'message': '请先配置火山主账号'},
status=status.HTTP_400_BAD_REQUEST)
svc = IAMService(ak, sk)
# 1. Create user on Volcengine
try:
resp = svc.create_user(
username=d['username'],
display_name=d.get('display_name', ''),
email=d.get('email', ''),
phone=d.get('phone', ''),
)
except VolcengineAPIError as e:
return Response({'error': 'create_failed', 'message': f'创建用户失败: {e}'},
status=status.HTTP_502_BAD_GATEWAY)
volc_user = resp.get("Result", {}).get("User", {})
result_info = {'username': d['username']}
# 2. Create login profile if password provided
password = d.get('password', '')
if password:
try:
svc.create_login_profile(d['username'], password)
result_info['login_enabled'] = True
except VolcengineAPIError as e:
result_info['login_error'] = str(e)
# 3. Create access key
try:
ak_resp = svc.create_access_key(d['username'])
ak_data = ak_resp.get("Result", {}).get("AccessKey", {})
result_info['access_key_id'] = ak_data.get("AccessKeyId", "")
result_info['secret_access_key'] = ak_data.get("SecretAccessKey", "")
result_info['secret_key_warning'] = "SecretAccessKey 仅此一次显示,请立即保存!"
except VolcengineAPIError as e:
result_info['access_key_error'] = str(e)
# 4. Attach basic policies
for policy in ['AccessKeySelfManageAccess']:
try:
svc.attach_user_policy(d['username'], policy, 'System')
except VolcengineAPIError:
pass
# 5. Save to local DB
obj = IAMUser.objects.create(
volc_account=account,
username=d['username'],
display_name=d.get('display_name', ''),
user_id=volc_user.get("UserId", ""),
email=d.get('email', ''),
phone=d.get('phone', ''),
status=IAMUser.Status.ACTIVE,
access_key_ids=[result_info.get('access_key_id', '')] if result_info.get('access_key_id') else [],
)
# 6. Auto-add project if specified
project_name = d.get('project_name', '')
if project_name:
IAMUserProject.objects.create(
iam_user=obj,
project_name=project_name,
monitor_enabled=True,
)
AlertRecord.objects.create(
iam_user=obj,
alert_type=AlertRecord.AlertType.MANUAL,
title=f"创建子账号 {d['username']}",
content=f"操作人: {request.user.username}",
)
return Response({
'message': f"子账号 {d['username']} 创建成功",
'user': IAMUserSerializer(obj).data,
'volcengine': result_info,
}, status=status.HTTP_201_CREATED)
@api_view(['POST'])
def iam_user_import_view(request):
"""导入指定的已有 IAM 用户"""
serializer = IAMUserImportSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
username = serializer.validated_data['username']
account, ak, sk = _get_volc_account()
if not account:
return Response({'error': 'no_account', 'message': '请先配置火山主账号'},
status=status.HTTP_400_BAD_REQUEST)
svc = IAMService(ak, sk)
try:
resp = svc.get_user(username)
except VolcengineAPIError as e:
return Response({'error': 'user_not_found', 'message': f'火山引擎未找到用户: {e}'},
status=status.HTTP_404_NOT_FOUND)
u = resp.get("Result", {}).get("User", {})
obj, created = IAMUser.objects.update_or_create(
volc_account=account,
username=username,
defaults={
'display_name': u.get("DisplayName", ""),
'user_id': u.get("UserId", ""),
'email': u.get("Email", ""),
'phone': u.get("MobilePhone", ""),
},
)
return Response({
'message': '导入成功' if created else '用户已存在,已更新信息',
'user': IAMUserSerializer(obj).data,
})
@api_view(['GET'])
def iam_user_detail_view(request, pk):
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
return Response(IAMUserSerializer(user).data)
@api_view(['PUT'])
def iam_user_update_view(request, pk):
"""更新子账号的本地配置(阈值、开关等)"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
serializer = IAMUserConfigSerializer(data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
for field, value in serializer.validated_data.items():
setattr(user, field, value)
user.save()
return Response(IAMUserSerializer(user).data)
@api_view(['POST'])
def iam_user_disable_view(request, pk):
"""停用子账号"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
account, ak, sk = _get_volc_account(user.volc_account_id)
if not ak:
return Response({'error': 'no_credentials'}, status=status.HTTP_400_BAD_REQUEST)
svc = IAMService(ak, sk)
try:
svc.disable_user(user.username)
user.status = IAMUser.Status.DISABLED
user.save(update_fields=['status'])
AlertRecord.objects.create(
iam_user=user,
alert_type=AlertRecord.AlertType.MANUAL,
title=f"手动停用子账号 {user.username}",
content=f"操作人: {request.user.username}",
)
return Response({'message': f'用户 {user.username} 已停用'})
except VolcengineAPIError as e:
return Response({'error': 'api_error', 'message': str(e)},
status=status.HTTP_502_BAD_GATEWAY)
@api_view(['POST'])
def iam_user_enable_view(request, pk):
"""恢复子账号"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
account, ak, sk = _get_volc_account(user.volc_account_id)
if not ak:
return Response({'error': 'no_credentials'}, status=status.HTTP_400_BAD_REQUEST)
svc = IAMService(ak, sk)
try:
svc.enable_user(user.username)
user.status = IAMUser.Status.ACTIVE
user.save(update_fields=['status'])
AlertRecord.objects.create(
iam_user=user,
alert_type=AlertRecord.AlertType.MANUAL,
title=f"手动恢复子账号 {user.username}",
content=f"操作人: {request.user.username}",
)
return Response({'message': f'用户 {user.username} 已恢复'})
except VolcengineAPIError as e:
return Response({'error': 'api_error', 'message': str(e)},
status=status.HTTP_502_BAD_GATEWAY)
@api_view(['GET'])
def iam_user_policies_view(request, pk):
"""查看子账号的权限策略"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
account, ak, sk = _get_volc_account(user.volc_account_id)
if not ak:
return Response({'error': 'no_credentials'}, status=status.HTTP_400_BAD_REQUEST)
svc = IAMService(ak, sk)
try:
resp = svc.list_attached_user_policies(user.username)
policies = resp.get("Result", {}).get("AttachedPolicyMetadata", [])
return Response({'policies': policies})
except VolcengineAPIError as e:
return Response({'error': 'api_error', 'message': str(e)},
status=status.HTTP_502_BAD_GATEWAY)
@api_view(['POST'])
def iam_user_attach_policy_view(request, pk):
"""给子账号附加权限策略"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
policy_name = request.data.get('policy_name', '')
policy_type = request.data.get('policy_type', 'System')
if not policy_name:
return Response({'error': 'missing_policy_name', 'message': '请指定策略名'},
status=status.HTTP_400_BAD_REQUEST)
account, ak, sk = _get_volc_account(user.volc_account_id)
if not ak:
return Response({'error': 'no_credentials'}, status=status.HTTP_400_BAD_REQUEST)
svc = IAMService(ak, sk)
try:
svc.attach_user_policy(user.username, policy_name, policy_type)
AlertRecord.objects.create(
iam_user=user,
alert_type=AlertRecord.AlertType.MANUAL,
title=f"附加策略 {policy_name}{user.username}",
content=f"操作人: {request.user.username},策略类型: {policy_type}",
)
return Response({'message': f'已附加策略 {policy_name}'})
except VolcengineAPIError as e:
return Response({'error': 'api_error', 'message': str(e)},
status=status.HTTP_502_BAD_GATEWAY)
@api_view(['POST'])
def iam_user_detach_policy_view(request, pk):
"""从子账号移除权限策略"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
policy_name = request.data.get('policy_name', '')
policy_type = request.data.get('policy_type', 'System')
if not policy_name:
return Response({'error': 'missing_policy_name', 'message': '请指定策略名'},
status=status.HTTP_400_BAD_REQUEST)
account, ak, sk = _get_volc_account(user.volc_account_id)
if not ak:
return Response({'error': 'no_credentials'}, status=status.HTTP_400_BAD_REQUEST)
svc = IAMService(ak, sk)
try:
svc.detach_user_policy(user.username, policy_name, policy_type)
AlertRecord.objects.create(
iam_user=user,
alert_type=AlertRecord.AlertType.MANUAL,
title=f"移除策略 {policy_name}{user.username}",
content=f"操作人: {request.user.username},策略类型: {policy_type}",
)
return Response({'message': f'已移除策略 {policy_name}'})
except VolcengineAPIError as e:
return Response({'error': 'api_error', 'message': str(e)},
status=status.HTTP_502_BAD_GATEWAY)
# ==================== IAM User Projects ====================
@api_view(['GET'])
def iam_user_project_list_view(request, pk):
"""查看子账号关联的项目列表"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
projects = user.projects.all()
return Response(IAMUserProjectSerializer(projects, many=True).data)
@api_view(['POST'])
def iam_user_project_add_view(request, pk):
"""给子账号添加关联项目"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
serializer = IAMUserProjectAddSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
d = serializer.validated_data
obj, created = IAMUserProject.objects.get_or_create(
iam_user=user,
project_name=d['project_name'],
defaults={
'display_name': d.get('display_name', ''),
'monitor_enabled': d.get('monitor_enabled', True),
},
)
if not created:
return Response({'error': 'duplicate', 'message': f'项目 {d["project_name"]} 已关联'},
status=status.HTTP_409_CONFLICT)
return Response({
'message': f'已关联项目 {d["project_name"]}',
'project': IAMUserProjectSerializer(obj).data,
}, status=status.HTTP_201_CREATED)
@api_view(['PUT'])
def iam_user_project_update_view(request, pk, pid):
"""更新项目监测开关"""
try:
project = IAMUserProject.objects.get(pk=pid, iam_user_id=pk)
except IAMUserProject.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
serializer = IAMUserProjectUpdateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
project.monitor_enabled = serializer.validated_data['monitor_enabled']
project.save(update_fields=['monitor_enabled'])
return Response(IAMUserProjectSerializer(project).data)
@api_view(['DELETE'])
def iam_user_project_delete_view(request, pk, pid):
"""移除关联项目"""
try:
project = IAMUserProject.objects.get(pk=pid, iam_user_id=pk)
except IAMUserProject.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
name = project.project_name
project.delete()
return Response({'message': f'已移除项目 {name}'})
@api_view(['POST'])
def iam_user_project_toggle_all_view(request, pk):
"""批量切换所有项目的监测开关"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
enable = request.data.get('monitor_enabled', True)
count = user.projects.update(monitor_enabled=enable)
label = '开启' if enable else '关闭'
return Response({'message': f'{label}全部 {count} 个项目的监测'})
# ==================== Quota Allocation ====================
@api_view(['POST'])
def quota_allocate_view(request, pk):
"""额度变更:正数=追加,负数=扣减"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
serializer = QuotaAllocateSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
amount = serializer.validated_data['amount']
note = serializer.validated_data.get('note', '')
new_total = user.allocated_quota + amount
# 扣减保护:总额度不能低于已消费金额
if new_total < user.consumed_total:
return Response({
'error': 'quota_underflow',
'message': (
f'扣减后总额度 ¥{new_total:.2f} 低于已消费 ¥{user.consumed_total:.2f}'
f'最多可扣减 ¥{user.allocated_quota - user.consumed_total:.2f}'
),
}, status=status.HTTP_400_BAD_REQUEST)
user.allocated_quota = new_total
user.triggered_alerts = [] # 额度变更时重置告警状态
user.save(update_fields=['allocated_quota', 'triggered_alerts'])
is_deduct = amount < 0
action_label = f"扣减额度 ¥{abs(amount)}" if is_deduct else f"追加额度 ¥{amount}"
allocation = QuotaAllocation.objects.create(
iam_user=user,
amount=amount,
total_after=user.allocated_quota,
note=note,
created_by=request.user.username,
)
AlertRecord.objects.create(
iam_user=user,
alert_type=AlertRecord.AlertType.MANUAL,
title=f"{action_label}{user.username}",
content=f"操作人: {request.user.username},变更后总额度: ¥{user.allocated_quota},备注: {note}",
)
return Response({
'message': f'{action_label},当前总额度 ¥{user.allocated_quota}',
'user': IAMUserSerializer(user).data,
'allocation': QuotaAllocationSerializer(allocation).data,
})
@api_view(['GET'])
def quota_history_view(request, pk):
"""查看子账号的额度划拨历史"""
try:
user = IAMUser.objects.get(pk=pk)
except IAMUser.DoesNotExist:
return Response({'error': 'not_found'}, status=status.HTTP_404_NOT_FOUND)
allocations = QuotaAllocation.objects.filter(iam_user=user)
return Response(QuotaAllocationSerializer(allocations, many=True).data)
# ==================== Billing ====================
@api_view(['GET'])
def spending_overview_view(request):
"""消费总览"""
bill_period = request.query_params.get('period', datetime.now().strftime("%Y-%m"))
users = IAMUser.objects.all().order_by('-consumed_total')
return Response({
'period': bill_period,
'users': IAMUserSerializer(users, many=True).data,
})
@api_view(['POST'])
def spending_refresh_view(request):
"""手动刷新消费数据"""
from utils.scheduler import check_spending
try:
check_spending()
return Response({'message': '消费数据刷新完成'})
except Exception as e:
return Response({'error': 'refresh_failed', 'message': str(e)},
status=status.HTTP_500_INTERNAL_SERVER_ERROR)
@api_view(['GET'])
def balance_view(request):
"""查询主账号余额"""
account, ak, sk = _get_volc_account()
if not ak:
return Response({'error': 'no_account', 'message': '请先配置火山主账号'},
status=status.HTTP_400_BAD_REQUEST)
try:
billing = BillingService(ak, sk)
result = billing.get_balance()
return Response(result)
except VolcengineAPIError as e:
return Response({'error': 'api_error', 'message': str(e)},
status=status.HTTP_502_BAD_GATEWAY)
# ==================== Global Config ====================
@api_view(['GET', 'PUT'])
def global_config_view(request):
config = GlobalConfig.get_solo()
if request.method == 'GET':
return Response(GlobalConfigSerializer(config).data)
serializer = GlobalConfigSerializer(config, data=request.data, partial=True)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data)
# ==================== Alerts ====================
@api_view(['GET'])
def alert_list_view(request):
alerts = AlertRecord.objects.select_related('iam_user').all()
alert_type = request.query_params.get('type')
if alert_type:
alerts = alerts.filter(alert_type=alert_type)
limit = int(request.query_params.get('limit', 50))
return Response(AlertRecordSerializer(alerts[:limit], many=True).data)
# ==================== Projects ====================
@api_view(['GET'])
def project_list_view(request):
"""从火山引擎拉取项目列表"""
account, ak, sk = _get_volc_account()
if not ak:
return Response({'error': 'no_account', 'message': '请先配置火山主账号'},
status=status.HTTP_400_BAD_REQUEST)
try:
svc = ProjectService(ak, sk)
projects = svc.list_projects()
result = [
{
'name': p.get('ProjectName', ''),
'display_name': p.get('DisplayName', p.get('ProjectName', '')),
'description': p.get('Description', ''),
}
for p in projects
]
return Response(result)
except VolcengineAPIError as e:
return Response({'error': 'api_error', 'message': str(e)},
status=status.HTTP_502_BAD_GATEWAY)