Files
ssq-desk/internal/service/package_service.go
2026-01-14 14:17:38 +08:00

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

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
}