.
This commit is contained in:
281
internal/service/package_service.go
Normal file
281
internal/service/package_service.go
Normal file
@@ -0,0 +1,281 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user