diff --git a/src/api/admin/awsService.js b/src/api/admin/awsService.js
index bd40ad3..d6f2d28 100644
--- a/src/api/admin/awsService.js
+++ b/src/api/admin/awsService.js
@@ -1,6 +1,13 @@
import { http2 } from '@/utils/request.js'
const base = '/api/v1/admin/server/aws_service'
+const runtimeBase = `${base}/instances/runtime`
+
+export const createAwsIdempotencyKey = (prefix = 'aws') => {
+ const random = globalThis.crypto?.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`
+ return `${prefix}-${random}`.slice(0, 128)
+}
+const withKey = (key) => ({ headers: { 'Idempotency-Key': key || createAwsIdempotencyKey() } })
export const getAwsServiceList = (params) => http2.get(`${base}/list`, { params })
export const getAwsServiceDetail = (id) => http2.get(`${base}/detail`, { params: { id } })
@@ -15,5 +22,35 @@ export const createAwsGoods = (data) => http2.post(`${base}/goods/create`, data)
export const updateAwsGoods = (data) => http2.post(`${base}/goods/update`, data)
export const deleteAwsGoods = (id) => http2.delete(`${base}/goods/delete`, { params: { id } })
-export const getAwsInstanceList = (params) => http2.get(`${base}/instances/list`, { params })
-export const retryAwsInstance = (instanceId) => http2.post(`${base}/instances/retry`, null, { params: { instance_id: instanceId } })
+export const getAwsInstanceDetail = (identifier) => http2.get(`${base}/instances/detail`, {
+ params: typeof identifier === 'object' ? identifier : { instance_id: identifier }
+})
+export const retryAwsInstance = (instanceId, note = '') => http2.post(`${base}/instances/retry`, { instance_id: instanceId, note })
+export const resyncAwsInstance = (instanceId, note = '') => http2.post(`${base}/instances/resync`, { instance_id: instanceId, note })
+export const resolveAwsInstance = (instanceId, resolution, note = '') => http2.post(`${base}/instances/resolve`, { instance_id: instanceId, resolution, note })
+
+export const getAdminAwsRuntimeDetails = instanceId => http2.get(`${runtimeBase}/details`, { params: { instance_id: instanceId } })
+export const refreshAdminAwsRuntimeDetails = (instanceId, key) => http2.post(`${runtimeBase}/details/refresh`, { instance_id: instanceId }, withKey(key))
+export const getAdminAwsLoginInfo = instanceId => http2.post(`${runtimeBase}/login_info`, { instance_id: instanceId })
+export const resetAdminAwsPassword = (instanceId, key) => http2.post(`${runtimeBase}/password/reset`, { instance_id: instanceId }, withKey(key))
+export const getAdminAwsMonitoring = instanceId => http2.get(`${runtimeBase}/monitoring`, { params: { instance_id: instanceId } })
+export const getAdminAwsSecurityGroup = instanceId => http2.get(`${runtimeBase}/security_group`, { params: { instance_id: instanceId } })
+export const addAdminAwsSecurityGroupRule = (instanceId, rule, key) => http2.post(`${runtimeBase}/security_group/rules/add`, { instance_id: instanceId, rule }, withKey(key))
+export const deleteAdminAwsSecurityGroupRule = (instanceId, rule, key) => http2.post(`${runtimeBase}/security_group/rules/delete`, { instance_id: instanceId, rule }, withKey(key))
+export const getAdminAwsVolumes = instanceId => http2.get(`${runtimeBase}/volumes`, { params: { instance_id: instanceId } })
+export const createAdminAwsVolume = (data, key) => http2.post(`${runtimeBase}/volumes/create`, data, withKey(key))
+export const modifyAdminAwsVolume = (data, key) => http2.post(`${runtimeBase}/volumes/modify`, data, withKey(key))
+export const attachAdminAwsVolume = (data, key) => http2.post(`${runtimeBase}/volumes/attach`, data, withKey(key))
+export const detachAdminAwsVolume = (data, key) => http2.post(`${runtimeBase}/volumes/detach`, data, withKey(key))
+export const deleteAdminAwsVolume = (data, key) => http2.post(`${runtimeBase}/volumes/delete`, data, withKey(key))
+export const getAdminAwsEip = instanceId => http2.get(`${runtimeBase}/eip`, { params: { instance_id: instanceId } })
+export const replaceAdminAwsEip = (data, key) => http2.post(`${runtimeBase}/eip/replace`, data, withKey(key))
+export const disassociateAdminAwsEip = (instanceId, key) => http2.post(`${runtimeBase}/eip/disassociate`, { instance_id: instanceId }, withKey(key))
+export const releaseAdminAwsEip = (instanceId, key) => http2.post(`${runtimeBase}/eip/release`, { instance_id: instanceId }, withKey(key))
+export const updateAdminAwsEipReverseDns = (data, key) => http2.post(`${runtimeBase}/eip/reverse_dns`, data, withKey(key))
+export const getAdminAwsOperations = (instanceId, params = {}) => http2.get(`${runtimeBase}/operations`, { params: { instance_id: instanceId, ...params } })
+export const getAdminAwsOperationDetail = operationId => http2.get(`${runtimeBase}/operations/detail`, { params: { operation_id: operationId } })
+export const retryAdminAwsOperation = (operationId, note = '', key) => http2.post(`${runtimeBase}/operations/retry`, { operation_id: operationId, note }, withKey(key))
+export const resyncAdminAwsOperation = operationId => http2.post(`${runtimeBase}/operations/resync`, null, { params: { operation_id: operationId } })
+export const resolveAdminAwsOperation = (operationId, note = '') => http2.post(`${runtimeBase}/operations/resolve`, { operation_id: operationId, note })
+export const rejectAdminAwsOperation = (operationId, note = '') => http2.post(`${runtimeBase}/operations/reject`, { operation_id: operationId, note })
diff --git a/src/api/admin/userVm.js b/src/api/admin/userVm.js
index 5527691..0cc3d8c 100644
--- a/src/api/admin/userVm.js
+++ b/src/api/admin/userVm.js
@@ -114,6 +114,16 @@ export const renewUserGoods = (data) => http2.post(`${GOODS_BASE}/renew`, fd(dat
export const getUserVmMetricsHistory = (params) => http2.get(`${BASE}/metrics_history`, { params })
export const getUserVmTrafficHourly = (params) => http2.get(`${BASE}/traffic_hourly`, { params })
+// ========== 虚拟机专用操作日志 ==========
+const OPERATION_RECORD_BASE = '/api/v1/admin/host_service/point/vm/operation_record'
+export const getVmOperationRecordList = (params) => http2.get(`${OPERATION_RECORD_BASE}/list`, { params })
+export const getVmOperationRecordDetail = (id) => http2.get(`${OPERATION_RECORD_BASE}/detail`, { params: { id } })
+export const refreshVmOperationRecord = (id) => http2.post(
+ `${OPERATION_RECORD_BASE}/refresh`,
+ fd({ id }),
+ { headers: { 'Content-Type': 'multipart/form-data' } }
+)
+
// ========== 流量策略 ==========
// 测试未通过(接口新增,待联调)
export const getUserVmTrafficPolicy = (params) => http2.get(`${BASE}/traffic_policy`, { params })
diff --git a/src/components/admin/ProductGoodsTreeSelector.vue b/src/components/admin/ProductGoodsTreeSelector.vue
new file mode 100644
index 0000000..f98affa
--- /dev/null
+++ b/src/components/admin/ProductGoodsTreeSelector.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+ {{ data.name }}
+ {{ data.type==='group'?'商品组':'商品' }}
+ #{{ data.id }}
+
+
+
+
+
+ 当前选择
+
+ {{ selected.type==='group'?'商品组':'商品' }}
+ {{ selected.name }}
+ #{{ selected.id }}
+
+ 尚未选择
+
+
+ 取消
+ 确定选择
+
+
+
+
+
+
+
diff --git a/src/config/menus.js b/src/config/menus.js
index e2eff65..17fa551 100644
--- a/src/config/menus.js
+++ b/src/config/menus.js
@@ -186,8 +186,7 @@ export const menus = [
icon: 'Cloudy',
children: [
{ path: '/aws/services', title: '服务配置' },
- { path: '/aws/goods', title: '商品绑定' },
- { path: '/aws/instances', title: '履约实例' }
+ { path: '/aws/goods', title: '商品绑定' }
]
},
{
diff --git a/src/router/index.js b/src/router/index.js
index 27c992c..7d2a034 100644
--- a/src/router/index.js
+++ b/src/router/index.js
@@ -689,12 +689,6 @@ const routes = [
name: 'AwsGoods',
component: () => import('../views/aws/AwsGoods.vue'),
meta: { title: '商品绑定' }
- },
- {
- path: 'instances',
- name: 'AwsInstances',
- component: () => import('../views/aws/AwsInstances.vue'),
- meta: { title: '履约实例' }
}
]
},
diff --git a/src/views/aws/AwsGoods.vue b/src/views/aws/AwsGoods.vue
index 01d099c..3f04634 100644
--- a/src/views/aws/AwsGoods.vue
+++ b/src/views/aws/AwsGoods.vue
@@ -10,52 +10,113 @@
EIP加密系统盘Agent
+ {{cap}}全部关闭
{{row.enabled?'启用':'停用'}}
编辑删除
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+ 选择
+
+
+
+
+
+
+
+
+
+
+
+
+
+
绑定 EIP加密系统盘安装 Agent启用绑定
+ 登录凭据密码重置监控安全组数据卷读取
取消保存
+
diff --git a/src/views/aws/AwsInstanceDetail.vue b/src/views/aws/AwsInstanceDetail.vue
new file mode 100644
index 0000000..c71ce1a
--- /dev/null
+++ b/src/views/aws/AwsInstanceDetail.vue
@@ -0,0 +1,132 @@
+
+
+
+
+
+
+ #{{instance.userGoodsId}}
+ #{{instance.orderId}}
+ #{{instance.serviceId}}
+ {{statusText(instance.provisionStatus)}}
+ {{instance.region||'-'}}{{instance.placementId||'-'}}{{instance.remoteAwsInstanceId||instance.remoteInstanceId||'-'}}{{instance.pendingAction||'-'}}
+
+
+
+
+
+
+ {{instance.reviewStatus||'-'}}{{statusText(instance.previousProvisionStatus)}}{{instance.remoteTaskId||'-'}}{{instance.retryCount||0}}{{instance.lastErrorCode||'-'}}{{instance.failureStage||'-'}}{{instance.lastError||'-'}}{{instance.reviewNote||'-'}}
+ 创建请求
{{pretty(baseDetail?.request)}}
+
+
+
+
+
+ 实时详情
{{pretty(runtimeDetails)}}监控聚合
{{pretty(monitoring)}}
+
+
+
+
+ {{portText(row)}}删除
+ {{pretty(eip)}}
+
+
+
+
+ 扩容/修改挂载卸载删除
+
+
+
+ 运行期异步操作刷新
+ {{operationText(row.status)}}详情同步重试确认解决驳回
+
+
+
+
+
+
+
+
+
{{user.username||'未知用户'}}
{{(user.methods||[]).length}} 种登录方式
+
+ {{credentialType(method)}}
{{method.credential?.available===false?'不可用':'可用'}}
+
+
+ 登录密码
{{revealed[`${ui}-${mi}-p`]?method.credential.password:mask(method.credential.password)}}
{{revealed[`${ui}-${mi}-p`]?'隐藏密码':'查看密码'}}复制密码
+ SSH 私钥
{{method.credential?.key_name||'EC2 Key Pair'}}
下载私钥复制私钥
+
+
+
+
+
+
- 取消确认
+
取消提交
+
{{pretty(currentOperation)}}
+
+
+
+
+
+
diff --git a/src/views/aws/AwsInstances.vue b/src/views/aws/AwsInstances.vue
deleted file mode 100644
index 0329e6f..0000000
--- a/src/views/aws/AwsInstances.vue
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
重置
-
-
- {{row.userGoodsId}}
- {{row.orderId}}
- {{row.serviceId}}
-
-
- {{statusText(row.provisionStatus)}}
-
-
-
- {{formatTime(row.lastSyncedAt)}}
- 详情重试
-
-
-
- {{current.id}}{{statusText(current.provisionStatus)}}{{current.remoteAwsInstanceId||'-'}}{{current.remoteTaskId||'-'}}{{current.lastErrorCode||'-'}}{{formatTime(current.nextRetryAt)}}{{current.lastError||'-'}}资源快照
{{snapshotText}}
-
-
-
-
-
diff --git a/src/views/aws/AwsRuntimeVisualization.vue b/src/views/aws/AwsRuntimeVisualization.vue
new file mode 100644
index 0000000..0e75dca
--- /dev/null
+++ b/src/views/aws/AwsRuntimeVisualization.vue
@@ -0,0 +1,41 @@
+
+
+
+
实例状态{{ stateText(instance.state || instance.provision_status) }}{{ instance.instance_id || '-' }}
+
公网 / 私网 IP{{ instance.public_ip_address || '-' }}{{ instance.private_ip_address || '-' }}
+
规格 / 可用区{{ instance.instance_type || '-' }}{{ instance.availability_zone || '-' }}
+
Agent{{ agent.status === 'online' ? '在线' : agent.status || '未连接' }}{{ agent.os || '-' }} · {{ agent.version || '-' }}
+
+
+
实例与费用{{ time(instance.synced_at) }}{{ r[0] }}{{ r[1] || '-' }}
本月{{ money(instance.month_cost) }}
预计本月{{ money(instance.projected_month_cost) }}
完整月{{ money(instance.full_month_cost) }}
+
流量与资源{{ limit.measurement_source === 'agent' ? 'Agent 计量' : 'AWS 计量' }}{{ bytes(limit.current_usage_bytes) }}/ {{ bytes(limit.limit_bytes) }}
{{ details.security_groups?.length || 0 }} 安全组{{ details.storage?.volumes?.length || 0 }} 数据卷{{ details.network?.elastic_ips?.length || 0 }} EIP
+
+
+ CPU{{ percent(metrics.cpu_percent) }}内存{{ percent(ratio(metrics.memory_used_bytes,metrics.memory_total_bytes)) }}{{ bytes(metrics.memory_used_bytes) }} / {{ bytes(metrics.memory_total_bytes) }}磁盘{{ percent(ratio(metrics.disk_used_bytes,metrics.disk_total_bytes)) }}{{ bytes(metrics.disk_used_bytes) }} / {{ bytes(metrics.disk_total_bytes) }}运行时长{{ duration(metrics.uptime_seconds) }}
+ 网络吞吐趋势{{ monitoring.traffic?.bucket_seconds || 60 }} 秒聚合
+
+
+
+
+
+
+
+
diff --git a/src/views/product/UserGoodsDetail.vue b/src/views/product/UserGoodsDetail.vue
index 8aad036..c22f3da 100644
--- a/src/views/product/UserGoodsDetail.vue
+++ b/src/views/product/UserGoodsDetail.vue
@@ -210,6 +210,8 @@
+
+
@@ -336,6 +338,7 @@ import { extractApiError } from '@/utils/kvmErrorUtil'
import VmSelectorPopup from '@/components/admin/VmSelectorPopup.vue'
import KvmServiceSelector from '@/components/admin/KvmServiceSelector.vue'
import UserVmDetail from '@/views/user-vm/UserVmDetail.vue'
+import AwsUserGoodsDetail from './components/AwsUserGoodsDetail.vue'
import dayjs from 'dayjs'
const route = useRoute()
@@ -345,6 +348,7 @@ const goodsId = computed(() => parseInt(route.params.id) || 0)
const loading = ref(false)
const refreshing = ref(false)
const vmDetailRef = ref(null)
+const awsDetailRef = ref(null)
const submitLoading = ref(false)
const detail = ref(null)
const detailExpanded = ref(false)
@@ -387,6 +391,10 @@ const expireStatusClass = computed(() => {
const goBack = () => router.push('/user-goods/list')
const currentTag = computed(() => (detail.value?.tag || detail.value?.good?.tag || '').toLowerCase())
+const isAwsGoods = computed(() => {
+ const tag = currentTag.value.replace(/\s+/g, '')
+ return tag === 'aws' || tag.includes('aws云服务器') || tag.includes('aws服务器')
+})
// 是否已删除(后端返回 deleteAt 字段,有值即已删除)
const isDeleted = computed(() => !!(detail.value?.deleteAt || detail.value?.DeleteAt || detail.value?.deleted_at))
@@ -458,6 +466,10 @@ const refreshDetail = async () => {
await nextTick()
await vmDetailRef.value?.refresh?.()
}
+ if (isAwsGoods.value && !isDeleted.value) {
+ await nextTick()
+ await awsDetailRef.value?.refresh?.()
+ }
} finally {
refreshing.value = false
}
diff --git a/src/views/product/components/AwsUserGoodsDetail.vue b/src/views/product/components/AwsUserGoodsDetail.vue
new file mode 100644
index 0000000..e6a1e98
--- /dev/null
+++ b/src/views/product/components/AwsUserGoodsDetail.vue
@@ -0,0 +1,202 @@
+
+
+
+
+
+
+
+ 重新加载
+
+
+
+
+
+
+
+
+
+
+
+ {{ resource.name || 'AWS 云服务器' }}
+ {{ statusText(provisionStatus) }}
+ 正在{{ actionText(pendingAction) }}
+
+
{{ remoteInstanceId || '等待 AWS 实例创建' }}
+
+
+
+
+ 服务 #{{ instance.serviceId || instance.service_id }}
+
+ 履约记录 #{{ instance.id }}
+
+
+
+
+
+
+ Region / 可用区{{ instance.region || resource.region || '-' }}{{ resource.availability_zone || instance.placementId || instance.placement_id || '-' }}
+
+
+
+ 实例规格{{ resource.instance_type || requestSnapshot.instance_type || '-' }}{{ resource.image_id || requestSnapshot.image_id || '镜像信息待同步' }}
+
+
+
+ 公网 IP{{ resource.public_ip_address || resource.public_ip || '-' }}私网 {{ resource.private_ip_address || resource.private_ip || '-' }}
+
+
+
+ 最近同步{{ formatTime(instance.lastSyncedAt || instance.last_synced_at || resource.synced_at) }}重试 {{ instance.retryCount ?? instance.retry_count ?? 0 }} 次
+
+
+
+
+ {{ statusText(provisionStatus) }}
+ {{ resource.state || resource.provision_status || '-' }}
+ {{ pendingAction ? actionText(pendingAction) : '-' }}
+ {{ reviewText(instance.reviewStatus || instance.review_status) }}
+ {{ instance.remoteTaskId || instance.remote_task_id || resource.last_task_id || '-' }}
+ {{ formatTime(instance.CreatedAt || instance.createdAt || instance.created_at) }}
+ {{ formatTime(instance.finishedAt || instance.finished_at) }}
+
+
+
+
实例管理履约审核、运行概览、网络安全、存储与操作中心
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/views/user-vm/UserVmDetail.vue b/src/views/user-vm/UserVmDetail.vue
index 89fdceb..c683908 100644
--- a/src/views/user-vm/UserVmDetail.vue
+++ b/src/views/user-vm/UserVmDetail.vue
@@ -564,6 +564,9 @@
+
+
+
@@ -1215,6 +1218,7 @@ import UserSelector from '@/components/UserSelector/index.vue'
import UserVmSecurityGroupSelector from '@/components/admin/UserVmSecurityGroupSelector.vue'
import UserVmVolumeSelector from '@/components/admin/UserVmVolumeSelector.vue'
import UserVmNetworkSelector from '@/components/admin/UserVmNetworkSelector.vue'
+import VmOperationRecords from './components/VmOperationRecords.vue'
import dayjs from 'dayjs'
import * as echarts from 'echarts'
diff --git a/src/views/user-vm/components/VmOperationRecords.vue b/src/views/user-vm/components/VmOperationRecords.vue
new file mode 100644
index 0000000..a2819ad
--- /dev/null
+++ b/src/views/user-vm/components/VmOperationRecords.vue
@@ -0,0 +1,70 @@
+
+
+
+
+
+
+
+
+
+
+
+ 查询刷新
+
+
+
+ {{ operationLabel(row.operation_type) }}{{ row.note || row.operation_type || '-' }}
+ {{ operatorLabel(row.operator_type) }}#{{ row.operator_id || '-' }}
+ {{ statusLabel(row.status) }}
+
+ {{ formatTime(row.started_at) }}
+ {{ duration(row) }}
+
+ 详情核对状态
+
+
+
+
+
+ #{{ detail.id }}{{ statusLabel(detail.status) }}
+ {{ operationLabel(detail.operation_type) }}{{ operatorLabel(detail.operator_type) }} #{{ detail.operator_id || '-' }}
+ #{{ detail.user_good_id || '-' }}#{{ detail.vm_id || '-' }}
+ #{{ detail.service_id || '-' }} / #{{ detail.host_id || '-' }}{{ detail.task_id || '-' }}
+ {{ detail.method || '-' }} {{ detail.route || '-' }}
+ {{ detail.note || '-' }}{{ detail.error_message || '-' }}
+ {{ formatTime(detail.started_at) }}{{ formatTime(detail.finished_at) }}
+
+ 请求快照
{{ pretty(detail.request_snapshot) }}结果快照
{{ pretty(detail.result_snapshot) }}
+
+
+
+
+
+
+
diff --git a/默认模块.openapi.json b/默认模块.openapi.json
index 10d90e9..bc9b325 100644
--- a/默认模块.openapi.json
+++ b/默认模块.openapi.json
@@ -24029,7 +24029,7 @@
},
"/api/v1/admin/good/user_vm/migrate": {
"post": {
- "summary": "迁移虚拟机(更换宿主机)",
+ "summary": "迁移虚拟机(更换宿主机,不保留数据)",
"deprecated": false,
"description": "",
"tags": [