Files
billai/server/service/archive.go
2026-01-26 13:44:22 +08:00

162 lines
4.1 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 (
"fmt"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/yeka/zip"
"golang.org/x/text/encoding/simplifiedchinese"
"golang.org/x/text/transform"
)
// ExtractResult 解压结果
type ExtractResult struct {
ExtractedFiles []string // 解压出的文件路径
BillFile string // 账单文件路径csv 或 xlsx
BillType string // 检测到的账单类型
}
// ExtractZip 解压 ZIP 文件,支持密码
// 返回解压后的账单文件路径
func ExtractZip(zipPath, destDir, password string) (*ExtractResult, error) {
reader, err := zip.OpenReader(zipPath)
if err != nil {
return nil, fmt.Errorf("无法打开 ZIP 文件: %w", err)
}
defer reader.Close()
result := &ExtractResult{
ExtractedFiles: make([]string, 0),
}
timestamp := time.Now().Format("20060102_150405")
for _, file := range reader.File {
// 处理文件名编码(可能是 GBK
fileName := decodeFileName(file.Name)
// 安全检查:防止路径遍历
if strings.Contains(fileName, "..") {
continue
}
// 获取文件扩展名
ext := strings.ToLower(filepath.Ext(fileName))
// 生成安全的目标文件名(避免编码问题)
// 使用时间戳+序号+扩展名的格式
safeFileName := fmt.Sprintf("extracted_%s_%d%s", timestamp, len(result.ExtractedFiles), ext)
destPath := filepath.Join(destDir, safeFileName)
if file.FileInfo().IsDir() {
os.MkdirAll(destPath, 0755)
continue
}
// 确保目录存在
if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil {
return nil, fmt.Errorf("创建目录失败: %w", err)
}
// 设置密码(如果有)
if file.IsEncrypted() {
if password == "" {
return nil, fmt.Errorf("ZIP 文件已加密,请提供密码")
}
file.SetPassword(password)
}
// 打开文件
rc, err := file.Open()
if err != nil {
if file.IsEncrypted() {
return nil, fmt.Errorf("密码错误或无法解密文件")
}
return nil, fmt.Errorf("无法读取文件 %s: %w", fileName, err)
}
// 写入文件
destFile, err := os.Create(destPath)
if err != nil {
rc.Close()
return nil, fmt.Errorf("创建文件失败: %w", err)
}
_, err = io.Copy(destFile, rc)
rc.Close()
destFile.Close()
if err != nil {
return nil, fmt.Errorf("写入文件失败: %w", err)
}
result.ExtractedFiles = append(result.ExtractedFiles, destPath)
// 检测账单文件
if ext == ".csv" || ext == ".xlsx" {
result.BillFile = destPath
// 检测账单类型(从原始文件名检测)
if strings.Contains(fileName, "支付宝") || strings.Contains(strings.ToLower(fileName), "alipay") {
result.BillType = "alipay"
} else if strings.Contains(fileName, "微信") || strings.Contains(strings.ToLower(fileName), "wechat") {
result.BillType = "wechat"
} else if strings.Contains(fileName, "京东") || strings.Contains(strings.ToLower(fileName), "jd") {
result.BillType = "jd"
}
}
}
if result.BillFile == "" {
return nil, fmt.Errorf("ZIP 文件中未找到账单文件(.csv 或 .xlsx")
}
return result, nil
}
// decodeFileName 尝试将 GBK 编码的文件名转换为 UTF-8
func decodeFileName(name string) string {
// 如果文件名只包含 ASCII 字符,直接返回
isAscii := true
for i := 0; i < len(name); i++ {
if name[i] > 127 {
isAscii = false
break
}
}
if isAscii {
return name
}
// 尝试 GBK 解码
// Windows 上创建的 ZIP 文件通常使用 GBK 编码中文文件名
decoded, _, err := transform.String(simplifiedchinese.GBK.NewDecoder(), name)
if err == nil && len(decoded) > 0 {
return decoded
}
return name
}
// IsSupportedArchive 检查文件是否为支持的压缩格式
func IsSupportedArchive(filename string) bool {
lower := strings.ToLower(filename)
return strings.HasSuffix(lower, ".zip")
}
// IsBillFile 检查文件是否为账单文件
func IsBillFile(filename string) bool {
lower := strings.ToLower(filename)
return strings.HasSuffix(lower, ".csv") || strings.HasSuffix(lower, ".xlsx")
}
// CleanupExtractedFiles 清理解压的临时文件
func CleanupExtractedFiles(files []string) {
for _, f := range files {
os.Remove(f)
}
}