# PRD 计划执行文档 — 服务管理模块功能升级（服务授权 + 目标配置）

> 版本：1.0 ｜ 日期：2026-08-12 ｜ 分支：troubleshoot-ai-assistant

---

## 执行步骤总览

| 步骤 | 内容 | 优先级 | 预估改动 |
|------|------|--------|----------|
| 1 | 后端：服务管理数据目录 + 目标配置服务 | P0 | 新建 2~3 文件 |
| 2 | 后端：服务管理 API 路由（授权 + 目标配置） | P0 | 修改 1 文件 |
| 3 | 前端：类型定义 + API 封装 | P0 | 新建 3 文件 |
| 4 | 前端：目标配置页面 | P0 | 新建 1 文件 |
| 5 | 前端：服务授权页面补全 | P0 | 修改 1 文件 |
| 6 | 前端：侧边栏菜单 + 路由 | P0 | 修改 2 文件 |
| 7 | 单元测试 + 集成测试 | P1 | 新建测试文件 |
| 8 | 部署 + 验证 | P1 | 修改部署脚本 |

---

## 步骤 1：后端数据目录 + 目标配置服务

### 1.1 新建数据目录

**文件**：`skill/code/web/service_manage_data/`（运行时数据，加入 .gitignore）

```
service_manage_data/
├── targets.json    # 目标配置（服务器密码 Fernet 加密）
└── projects.json   # 项目信息
```

### 1.2 新建路径常量

**修改**：`skill/code/web/utils/paths.py`

```python
# 服务管理数据目录
SERVICE_MANAGE_DATA_DIR = DATA_DIR / "service_manage_data"
SERVICE_MANAGE_DATA_DIR.mkdir(parents=True, exist_ok=True)

MANAGE_TARGETS_FILE = SERVICE_MANAGE_DATA_DIR / "targets.json"
MANAGE_PROJECTS_FILE = SERVICE_MANAGE_DATA_DIR / "projects.json"
```

### 1.3 新建目标配置服务

**文件**：`skill/code/web/services/service_manage/service.py`（或 `services/service_manage.py`）

```python
# -*- coding: utf-8 -*-
"""
service_manage 业务层 — 目标配置 + 项目信息

目标配置：管理员维护对接的目标服务器（如 5.44 统一管理平台）
凭据：密码 Fernet 加密存储（复用 service_monitor.utils.crypto）
"""

def list_targets() -> list:
    """列出全部目标（脱敏：密码→has_password 布尔）"""

def create_target(data: dict) -> dict:
    """新增目标（校验 host/port/name）"""

def update_target(target_id: str, data: dict) -> dict:
    """更新目标"""

def delete_target(target_id: str) -> bool:
    """删除目标"""

def test_connection(data: dict) -> dict:
    """测试 HTTP(S) 连通性（GET /，返回状态码）"""

def save_project(data: dict) -> dict:
    """保存项目信息到 projects.json（密码脱敏后存储）"""

def get_project() -> Optional[dict]:
    """读取项目信息（脱敏）"""
```

**关键点**：
- 目标配置与「服务监测」的目标是两套独立数据（一个用于监测巡检，一个用于授权代理），互不干扰
- 复用 `service_monitor/utils/crypto.py` 的 `encrypt_password` / `decrypt_password`（同包可导入）
- 连通性测试用 `urllib.request` 或 `requests` 做 HTTP(S) GET，不引入 SSH 依赖

---

## 步骤 2：后端 API 路由

**修改**：`skill/code/web/routes/service_manage.py`

### 2.1 权限辅助

```python
def _require_admin_json():
    if 'user' not in session:
        return jsonify({"success": False, "error": {"code": 401, "message": "未登录"}}), 401
    if session.get('user', {}).get('role') != 'admin':
        return jsonify({"success": False, "error": {"code": 403, "message": "权限不足"}}), 403
    return None
```

### 2.2 API 路由清单

```python
# ============ 目标配置（管理员） ============
@bp.route('/api/service-manage/targets', methods=['GET'])
def manage_targets_list():      # 目标列表

@bp.route('/api/service-manage/targets', methods=['POST'])
def manage_targets_create():    # 新增目标

@bp.route('/api/service-manage/targets/<target_id>', methods=['PUT'])
def manage_targets_update(target_id):   # 更新目标

@bp.route('/api/service-manage/targets/<target_id>', methods=['DELETE'])
def manage_targets_delete(target_id):   # 删除目标

@bp.route('/api/service-manage/targets/test', methods=['POST'])
def manage_targets_test():      # 测试连接

# ============ 服务授权（管理员） ============
@bp.route('/api/service-manage/license/download', methods=['GET'])
def license_download():         # 下载激活文件（占位：返回本地模板或代理）

@bp.route('/api/service-manage/license/upload', methods=['POST'])
def license_upload():           # 上传授权文件（暂存 + 轮询状态）

@bp.route('/api/service-manage/license/task-status', methods=['GET'])
def license_task_status():      # 查询授权任务状态

@bp.route('/api/service-manage/project', methods=['GET', 'POST'])
def manage_project():           # 保存/读取项目信息

@bp.route('/api/service-manage/deploy-view/download', methods=['GET'])
def deploy_view_download():     # 下载部署视图（占位）

@bp.route('/api/service-manage/deploy-view/upload', methods=['POST'])
def deploy_view_upload():       # 上传部署视图（暂存）

@bp.route('/api/service-manage/self-check/download', methods=['GET'])
def self_check_download():      # 下载自检文档（占位）

@bp.route('/api/service-manage/self-check/upload', methods=['POST'])
def self_check_upload():        # 上传自检文档（暂存）
```

### 2.3 授权代理模式说明

**阶段一（本迭代）**：服务授权 API 先实现为「本地占位 + 文件暂存」：
- 下载激活/部署视图/自检文档：生成占位文件（或返回 501 提示待对接）
- 上传授权/部署视图/自检文档：暂存到 `service_manage_data/uploads/`，返回成功

**阶段二（后续迭代）**：对接目标服务器代理转发：
- 基于目标配置（targets.json）选择目标服务器
- 用 `requests` 转发到 5.44 的 `/exapi/system/*`，携带 HMAC 签名头
- 任务状态轮询 `licenceTaskStatus`

---

## 步骤 3：前端类型定义 + API 封装

### 3.1 新建类型定义

**文件**：`frontend/src/types/service-manage.ts`

```typescript
// 目标配置
export interface ManageTarget { ... }
export interface CreateManageTargetRequest { ... }
export interface TestManageTargetRequest { ... }

// 服务授权
export interface LicenseTaskStatus { ... }
export interface ProjectInfo { ... }
```

### 3.2 新建 API 封装

**文件**：`frontend/src/api/service-manage/index.ts`

```typescript
import http from '@/utils/http'
import type { ManageTarget, ProjectInfo, LicenseTaskStatus } from '@/types/service-manage'

/** 目标列表 */
export async function getTargets() { ... }

/** 新增目标 */
export async function createTarget(data) { ... }

/** 更新目标 */
export async function updateTarget(id, data) { ... }

/** 删除目标 */
export async function deleteTarget(id) { ... }

/** 测试连接 */
export async function testConnection(data) { ... }

/** 下载激活文件 */
export async function downloadLicense() { ... }

/** 上传授权文件 */
export async function uploadLicense(file) { ... }

/** 查询授权任务状态 */
export async function getLicenseTaskStatus(taskId) { ... }

/** 保存项目信息 */
export async function saveProject(data) { ... }

/** 下载部署视图 */
export async function downloadDeployView() { ... }

/** 下载自检文档 */
export async function downloadSelfCheck() { ... }
```

**文件下载封装**（`frontend/src/utils/download.ts` 或内联）：

```typescript
export async function downloadBlob(url: string, filename: string) {
  const res = await http.get(url, { responseType: 'blob' })
  const blob = new Blob([res.data])
  const link = document.createElement('a')
  link.href = URL.createObjectURL(blob)
  link.download = filename
  link.click()
  URL.revokeObjectURL(link.href)
}
```

---

## 步骤 4：前端目标配置页面

**文件**：`frontend/src/views/service-manage/Targets.vue`（新建）

### 4.1 页面结构

```
┌──────────────────────────────────────────────────────────────┐
│  🎯 目标配置                                    [+ 新增目标] │
├──────────────────────────────────────────────────────────────┤
│  el-table                                                    │
│  ┌──────────┬─────────────┬───────┬──────────┬─────────────┐ │
│  │ 名称     │ 地址        │ 端口  │ 状态     │ 操作        │ │
│  │ 5.44平台 │ 192.168.5.44│ 443   │ ✅ 在线  │ ✏️ 🗑️ 🔗  │ │
│  └──────────┴─────────────┴───────┴──────────┴─────────────┘ │
└──────────────────────────────────────────────────────────────┘

新增/编辑对话框（el-dialog + el-form）
  名称 / 地址 / 端口 / 协议 / 认证方式 / 用户名 / 密码
  [测试连接] [保存] [取消]
```

### 4.2 关键交互

- 列表加载：`onMounted` 调 `getTargets()`
- 新增：打开对话框 → 填表 → 测试连接（可选）→ 保存
- 编辑：行内 `✏️` → 回填表单 → 保存
- 删除：行内 `🗑️` → `ElMessageBox.confirm` → 确认后删除
- 测试连接：表单内「测试连接」按钮 → `testConnection()` → 成功/失败提示

---

## 步骤 5：前端服务授权页面补全

**修改**：`frontend/src/views/service-manage/Authorization.vue`

### 5.1 Step 1 — 申请服务授权

```typescript
// 下载激活文件
async function handleDownloadActivation() {
  const loading = ElLoading.service({ text: '正在生成激活文件...' })
  try {
    await downloadLicense()   // Blob 下载
    ElMessage.success('激活文件下载成功')
  } catch (e) {
    ElMessage.error('下载失败：' + e.message)
  } finally {
    loading.close()
  }
}

// 上传授权文件
async function handleAuthUpload(file: any) {
  const loading = ElLoading.service({ text: '正在上传授权文件...' })
  try {
    const res = await uploadLicense(file.raw)
    if (res.success) {
      ElMessage.success('上传成功，正在处理授权...')
      pollLicenseTask(res.taskId)   // 轮询状态
    }
  } finally {
    loading.close()
  }
  return false
}

// 轮询授权任务状态（5 分钟超时，2s 间隔）
async function pollLicenseTask(taskId: string) {
  const deadline = Date.now() + 5 * 60 * 1000
  while (Date.now() < deadline) {
    const res = await getLicenseTaskStatus(taskId)
    if (res.status === 'completed') { ElMessage.success('授权完成'); return }
    if (res.status === 'failed')    { ElMessage.error('授权失败：' + res.message); return }
    await sleep(2000)
  }
  ElMessage.warning('授权处理超时，请稍后刷新查看')
}
```

### 5.2 Step 2 — 填写项目信息

```typescript
// 保存项目信息
async function handleSaveProject() {
  // 表单校验（必填项）
  const loading = ElLoading.service({ text: '正在保存...' })
  try {
    const res = await saveProject({
      salesPeople: projectForm.projectSale,
      deployer: projectForm.deployer,
      orderTime: formatDateTime(projectForm.orderTime),
      serverPassword: projectForm.serverPassword,
      projectOverview: projectForm.projectOverview,
    })
    if (res.success) ElMessage.success('项目信息保存成功')
  } finally {
    loading.close()
  }
}
```

### 5.3 Step 3/4 — 下载部署视图/自检文档

```typescript
async function handleDownloadDeployView() {
  await downloadDeployView()    // Blob 下载
  ElMessage.success('部署视图下载成功')
}

async function handleDownloadSelfCheck() {
  await downloadSelfCheck()     // Blob 下载
  ElMessage.success('自检文档下载成功')
}
```

### 5.4 上传区域调整

- 授权文件上传：`accept=".zip,.lic,.key,.dat"`，`action` 改为自定义上传（`http-request` 或 `on-change` 手动调 API）
- 部署视图上传：`accept=".xlsx,.xls,.pdf"`
- 自检文档上传：`accept=".docx,.doc,.pdf"`
- 上传后显示文件列表 + 状态标签

---

## 步骤 6：前端侧边栏菜单 + 路由

### 6.1 侧边栏菜单

**修改**：`frontend/src/layouts/ManageLayout.vue`

```typescript
const manageMenus = computed(() => [
  { key: 'authorization', icon: '🔑', label: '服务授权', path: '/service-manage/authorization' },
  { key: 'targets',       icon: '🎯', label: '目标配置', path: '/service-manage/targets' },  // 新增
  { key: 'upgrade',       icon: '🚀', label: '服务升级', path: '/service-manage/upgrade' },
  { key: 'info',          icon: 'ℹ️', label: '服务信息', path: '/service-manage/info' },
  { key: 'logs',          icon: '📝', label: '操作日志', path: '/service-manage/logs' },
])
```

### 6.2 路由

**修改**：`frontend/src/router/index.ts`

```typescript
{
  path: 'targets',
  name: 'ManageTargets',
  component: () => import('@/views/service-manage/Targets.vue'),
  meta: { activeMenu: 'targets', title: '目标配置' }
},
```

---

## 步骤 7：单元测试

**文件**：`skill/code/tests/test_service_manage.py`（新建）

```python
# 目标配置 CRUD 测试
def test_create_target():
    """新增目标 → 列表可见、密码脱敏"""
def test_create_target_invalid():
    """非法主机/端口/空名称 → ValueError"""
def test_update_target():
    """更新名称/端口 → 生效"""
def test_delete_target():
    """删除 → 列表移除"""
def test_password_encrypted():
    """密码 Fernet 加密存储，列表返回 has_password 布尔"""

# 项目信息测试
def test_save_project():
    """保存项目信息 → 可读取，密码脱敏"""
```

---

## 步骤 8：部署 + 验证

1. **前端构建**：`cd frontend && npm run build`
2. **上传清单同步**：修改 `deploy/upload_to_server.py`
   - `FILES_TO_UPLOAD` 增加：`routes/service_manage.py`、`services/service_manage.py`（或包目录）
   - `DIRS_TO_UPLOAD` 增加：`web/service_manage_data/`（若需初始化）
3. **部署**：`! cd deploy && SSH_PASSWORD='***' python upload_to_server.py`
4. **权威验证**：`cd deploy && python verify_deployment.py`
5. **功能验证**：
   - 登录 → 服务管理 → 目标配置 → 新增目标 → 测试连接 → 保存
   - 服务授权 → 4 步流程：下载/上传/保存

---

## 风险与注意事项

| 风险 | 应对 |
|------|------|
| 5.44 授权 API 需要 HMAC 签名，本地无法直接调用 | 本迭代先本地占位实现，代理转发放下一迭代 |
| 上传文件大小限制 2048M | 后端限制文件大小，前端提示 |
| 授权任务处理耗时（轮询 80+ 次） | 前端 2s 间隔轮询 + 5 分钟超时 |
| 目标配置与监测目标数据隔离 | 两套独立 JSON 文件，命名区分 |
| 服务器密码明文传输 | 前后端均用 HTTPS，存储 Fernet 加密 |

---

## 验收标准

1. ✅ 服务管理左侧栏显示「目标配置」菜单，可正常访问
2. ✅ 目标配置支持新增/编辑/删除/测试连接
3. ✅ 服务授权 4 步流程按钮可点击，调用后端 API 有反馈
4. ✅ 项目信息保存后写入后端，密码加密存储
5. ✅ 文件下载返回真实文件（或明确的占位提示）
6. ✅ 单元测试通过（新增用例全绿）
7. ✅ 部署到 5.60 后 verify_deployment.py 通过