package service import ( "archive/zip" "encoding/json" "fmt" "io" "net/http" "os" "path/filepath" "ssq-desk/internal/storage/repository" "time" ) // PackageService 离线数据包服务 type PackageService struct { sqliteRepo repository.SsqRepository } // NewPackageService 创建数据包服务 func NewPackageService() (*PackageService, error) { repo, err := repository.NewSQLiteSsqRepository() if err != nil { return nil, err } return &PackageService{ sqliteRepo: repo, }, nil } // PackageInfo 数据包信息 type PackageInfo struct { Version string `json:"version"` // 数据包版本 TotalCount int `json:"total_count"` // 数据总数 LatestIssue string `json:"latest_issue"` // 最新期号 PackageSize int64 `json:"package_size"` // 包大小 DownloadURL string `json:"download_url"` // 下载地址 ReleaseDate string `json:"release_date"` // 发布日期 CheckSum string `json:"checksum"` // 校验和 } // PackageDownloadResult 数据包下载结果 type PackageDownloadResult struct { FilePath string `json:"file_path"` FileSize int64 `json:"file_size"` Duration string `json:"duration"` // 下载耗时 } // ImportResult 导入结果 type ImportResult struct { ImportedCount int `json:"imported_count"` // 导入数量 UpdatedCount int `json:"updated_count"` // 更新数量 ErrorCount int `json:"error_count"` // 错误数量 Duration string `json:"duration"` // 导入耗时 } // DownloadPackage 下载数据包 func (s *PackageService) DownloadPackage(downloadURL string, progressCallback func(int64, int64)) (*PackageDownloadResult, error) { startTime := time.Now() // 创建下载目录 appDataDir, err := os.UserConfigDir() if err != nil { return nil, fmt.Errorf("获取用户配置目录失败: %v", err) } downloadDir := filepath.Join(appDataDir, "ssq-desk", "packages") if err := os.MkdirAll(downloadDir, 0755); err != nil { return nil, fmt.Errorf("创建下载目录失败: %v", err) } // 生成文件名 filename := filepath.Base(downloadURL) if filename == "" || filename == "." { filename = fmt.Sprintf("ssq-data-%s.zip", time.Now().Format("20060102-150405")) } filePath := filepath.Join(downloadDir, filename) // 创建文件 out, err := os.Create(filePath) if err != nil { return nil, fmt.Errorf("创建文件失败: %v", err) } defer out.Close() // 发起 HTTP 请求 resp, err := http.Get(downloadURL) if err != nil { return nil, fmt.Errorf("下载失败: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("下载失败: HTTP %d", resp.StatusCode) } // 获取文件大小 totalSize := resp.ContentLength // 复制数据并显示进度 var written int64 buffer := make([]byte, 32*1024) // 32KB buffer for { nr, er := resp.Body.Read(buffer) if nr > 0 { nw, ew := out.Write(buffer[0:nr]) if nw < 0 || nr < nw { nw = 0 if ew == nil { ew = fmt.Errorf("无效写入结果") } } written += int64(nw) if ew != nil { err = ew break } if nr != nw { err = io.ErrShortWrite break } // 调用进度回调 if progressCallback != nil { progressCallback(written, totalSize) } } if er != nil { if er != io.EOF { err = er } break } } if err != nil { os.Remove(filePath) return nil, fmt.Errorf("写入文件失败: %v", err) } // 获取文件信息 fileInfo, err := out.Stat() if err != nil { return nil, fmt.Errorf("获取文件信息失败: %v", err) } duration := time.Since(startTime).String() return &PackageDownloadResult{ FilePath: filePath, FileSize: fileInfo.Size(), Duration: duration, }, nil } // ImportPackage 导入数据包 func (s *PackageService) ImportPackage(packagePath string) (*ImportResult, error) { startTime := time.Now() result := &ImportResult{} // 检查文件是否存在 if _, err := os.Stat(packagePath); os.IsNotExist(err) { return nil, fmt.Errorf("数据包文件不存在: %s", packagePath) } // 打开 ZIP 文件 zipReader, err := zip.OpenReader(packagePath) if err != nil { return nil, fmt.Errorf("打开数据包失败: %v", err) } defer zipReader.Close() // 查找数据文件(JSON 格式) var dataFile *zip.File for _, file := range zipReader.File { if filepath.Ext(file.Name) == ".json" { dataFile = file break } } if dataFile == nil { return nil, fmt.Errorf("数据包中未找到 JSON 数据文件") } // 读取数据文件 rc, err := dataFile.Open() if err != nil { return nil, fmt.Errorf("打开数据文件失败: %v", err) } defer rc.Close() // 解析 JSON var histories []map[string]interface{} decoder := json.NewDecoder(rc) if err := decoder.Decode(&histories); err != nil { return nil, fmt.Errorf("解析数据文件失败: %v", err) } // 导入数据 // TODO: 实际导入逻辑需要根据数据包格式实现 // 这里只是框架代码,需要将 JSON 数据转换为 SsqHistory 并插入数据库 for range histories { // 转换为 SsqHistory 结构(这里需要根据实际数据格式转换) // 简化处理:直接创建记录 // 实际应用中需要根据数据包格式解析 result.ImportedCount++ } // TODO: 实际导入逻辑需要根据数据包格式实现 // 这里只是框架代码 result.Duration = time.Since(startTime).String() return result, nil } // CheckPackageUpdate 检查数据包更新 func (s *PackageService) CheckPackageUpdate(remoteURL string) (*PackageInfo, error) { // 发起 HTTP 请求获取数据包信息 resp, err := http.Get(remoteURL) if err != nil { return nil, fmt.Errorf("获取数据包信息失败: %v", err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("获取数据包信息失败: HTTP %d", resp.StatusCode) } // 解析 JSON var info PackageInfo decoder := json.NewDecoder(resp.Body) if err := decoder.Decode(&info); err != nil { return nil, fmt.Errorf("解析数据包信息失败: %v", err) } // 获取本地最新期号 localLatestIssue, err := s.sqliteRepo.GetLatestIssue() if err != nil { return nil, fmt.Errorf("获取本地最新期号失败: %v", err) } // 比较是否需要更新 // 如果远程最新期号大于本地,则需要更新 if info.LatestIssue > localLatestIssue { return &info, nil } return nil, nil // 不需要更新 } // ListLocalPackages 列出本地数据包 func (s *PackageService) ListLocalPackages() ([]string, error) { appDataDir, err := os.UserConfigDir() if err != nil { return nil, fmt.Errorf("获取用户配置目录失败: %v", err) } packageDir := filepath.Join(appDataDir, "ssq-desk", "packages") // 检查目录是否存在 if _, err := os.Stat(packageDir); os.IsNotExist(err) { return []string{}, nil } files, err := os.ReadDir(packageDir) if err != nil { return nil, fmt.Errorf("读取数据包目录失败: %v", err) } var packages []string for _, file := range files { if filepath.Ext(file.Name()) == ".zip" { packages = append(packages, filepath.Join(packageDir, file.Name())) } } return packages, nil }