282 lines
7.0 KiB
Go
282 lines
7.0 KiB
Go
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
|
||
}
|