162 lines
4.1 KiB
Go
162 lines
4.1 KiB
Go
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)
|
||
}
|
||
}
|