refactor: 重构项目结构

- 将 Python 代码移至 analyzer/ 目录(含 venv)
- 拆分 Go 服务器代码为模块化结构:
  - config/: 配置加载
  - model/: 请求/响应模型
  - service/: 业务逻辑
  - handler/: API处理器
- 添加 .gitignore 文件
- 删除旧的独立脚本文件
This commit is contained in:
clz
2026-01-07 23:26:32 +08:00
parent b15922a027
commit c40a118a3d
23 changed files with 2060 additions and 557 deletions

72
server/handler/review.go Normal file
View File

@@ -0,0 +1,72 @@
package handler
import (
"net/http"
"os"
"path/filepath"
"strings"
"github.com/gin-gonic/gin"
"billai-server/config"
"billai-server/model"
"billai-server/service"
)
// Review 获取需要复核的记录
func Review(c *gin.Context) {
// 获取文件名参数
fileName := c.Query("file")
if fileName == "" {
c.JSON(http.StatusBadRequest, model.ReviewResponse{
Result: false,
Message: "请提供文件名参数 (file)",
})
return
}
// 构建文件路径
outputDirAbs := config.ResolvePath(config.Global.OutputDir)
filePath := filepath.Join(outputDirAbs, fileName)
// 检查文件是否存在
if _, err := os.Stat(filePath); os.IsNotExist(err) {
c.JSON(http.StatusNotFound, model.ReviewResponse{
Result: false,
Message: "文件不存在: " + fileName,
})
return
}
// 判断文件格式
format := "csv"
if strings.HasSuffix(fileName, ".json") {
format = "json"
}
// 提取需要复核的记录
records := service.ExtractNeedsReview(filePath, format)
// 统计高低优先级数量
highCount := 0
lowCount := 0
for _, r := range records {
if r.ReviewLevel == "HIGH" {
highCount++
} else if r.ReviewLevel == "LOW" {
lowCount++
}
}
c.JSON(http.StatusOK, model.ReviewResponse{
Result: true,
Message: "获取成功",
Data: &model.ReviewData{
Total: len(records),
High: highCount,
Low: lowCount,
Records: records,
},
})
}

119
server/handler/upload.go Normal file
View File

@@ -0,0 +1,119 @@
package handler
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"billai-server/config"
"billai-server/model"
)
// Upload 处理账单上传和清理请求
func Upload(c *gin.Context) {
// 1. 获取上传的文件
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, model.UploadResponse{
Result: false,
Message: "请上传账单文件 (参数名: file)",
})
return
}
defer file.Close()
// 2. 解析请求参数
var req model.UploadRequest
c.ShouldBind(&req)
if req.Format == "" {
req.Format = "csv"
}
// 3. 保存上传的文件
timestamp := time.Now().Format("20060102_150405")
inputFileName := fmt.Sprintf("%s_%s", timestamp, header.Filename)
uploadDirAbs := config.ResolvePath(config.Global.UploadDir)
inputPath := filepath.Join(uploadDirAbs, inputFileName)
dst, err := os.Create(inputPath)
if err != nil {
c.JSON(http.StatusInternalServerError, model.UploadResponse{
Result: false,
Message: "保存文件失败: " + err.Error(),
})
return
}
defer dst.Close()
io.Copy(dst, file)
// 4. 构建输出文件路径
baseName := strings.TrimSuffix(header.Filename, filepath.Ext(header.Filename))
outputExt := ".csv"
if req.Format == "json" {
outputExt = ".json"
}
outputFileName := fmt.Sprintf("%s_%s_cleaned%s", timestamp, baseName, outputExt)
outputDirAbs := config.ResolvePath(config.Global.OutputDir)
outputPath := filepath.Join(outputDirAbs, outputFileName)
// 5. 构建命令参数
cleanScriptAbs := config.ResolvePath(config.Global.CleanScript)
args := []string{cleanScriptAbs, inputPath, outputPath}
if req.Year != "" {
args = append(args, "--year", req.Year)
}
if req.Month != "" {
args = append(args, "--month", req.Month)
}
if req.Start != "" {
args = append(args, "--start", req.Start)
}
if req.End != "" {
args = append(args, "--end", req.End)
}
if req.Format != "" {
args = append(args, "--format", req.Format)
}
// 6. 执行 Python 脚本
pythonPathAbs := config.ResolvePath(config.Global.PythonPath)
cmd := exec.Command(pythonPathAbs, args...)
cmd.Dir = config.Global.ProjectRoot
output, err := cmd.CombinedOutput()
outputStr := string(output)
if err != nil {
c.JSON(http.StatusInternalServerError, model.UploadResponse{
Result: false,
Message: "处理失败: " + err.Error(),
})
return
}
// 7. 检测账单类型
billType := ""
if strings.Contains(outputStr, "支付宝") {
billType = "alipay"
} else if strings.Contains(outputStr, "微信") {
billType = "wechat"
}
// 8. 返回成功响应
c.JSON(http.StatusOK, model.UploadResponse{
Result: true,
Message: "处理成功",
Data: &model.UploadData{
BillType: billType,
FileURL: fmt.Sprintf("/download/%s", outputFileName),
FileName: outputFileName,
},
})
}