Files

197 lines
6.1 KiB
Go

package pickup
import (
"bytes"
"encoding/csv"
"encoding/json"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"hfb_jysh/backend/internal/auditlog"
"hfb_jysh/backend/internal/middleware"
"hfb_jysh/backend/internal/model"
"github.com/gin-gonic/gin"
)
func TestBuildAdminPickupCSVUsesExcelFriendlyEncodingAndEscaping(t *testing.T) {
createdAt := time.Date(2026, 11, 12, 10, 20, 30, 0, time.FixedZone("CST", 8*60*60))
completedAt := createdAt.Add(2 * time.Hour)
items := []PickupDTO{
{
PickupNo: "PK-001",
AccountTitle: "账号,带逗号",
ServerRegion: "国服",
LoginPlatform: "QQ",
ListingNo: "202611120001",
OwnerPhone: "18800000000",
Operator: "运营小张",
AccountSource: AccountSourceExternalPlatformManaged,
SourceChannel: "闲鱼",
Platform: "拼多多",
ThirdPartyOrderNo: "=1+1",
ShopName: `大锤"专营店"`,
Remark: "第一行\n第二行",
OrderAmountCent: 29500,
DepositAmountCent: 35000,
EffectiveProfitAmountCent: 1234,
EffectiveSettleAmountCent: 20000,
SettlementMode: SettlementModePlatformManaged,
OfflineSettlementStatus: OfflineSettlementStatusPending,
Status: StatusCompleted,
CreatedAt: createdAt,
CompletedAt: &completedAt,
CompleteRemark: "已完成",
},
}
data, err := buildAdminPickupCSV(items)
if err != nil {
t.Fatalf("buildAdminPickupCSV() error = %v", err)
}
if !bytes.HasPrefix(data, []byte{0xEF, 0xBB, 0xBF}) {
t.Fatal("CSV 缺少 UTF-8 BOM")
}
records, err := csv.NewReader(bytes.NewReader(data[3:])).ReadAll()
if err != nil {
t.Fatalf("解析 CSV 失败: %v", err)
}
if len(records) != 2 {
t.Fatalf("CSV 行数 = %d, want 2", len(records))
}
if len(records[0]) != len(adminPickupExportHeader) {
t.Fatalf("表头列数 = %d, want %d", len(records[0]), len(adminPickupExportHeader))
}
row := records[1]
if row[1] != "账号,带逗号" {
t.Fatalf("账号标题 = %q", row[1])
}
if row[7] != "外部上传 · 平台代管" {
t.Fatalf("账号来源 = %q", row[7])
}
if row[10] != "'=1+1" {
t.Fatalf("第三方单号未转义公式注入: %q", row[10])
}
if row[11] != `大锤"专营店"` {
t.Fatalf("店铺名称 = %q", row[11])
}
if row[12] != "第一行\n第二行" {
t.Fatalf("提号备注 = %q", row[12])
}
if row[13] != "295.00" || row[14] != "350.00" || row[15] != "12.34" {
t.Fatalf("金额列 = %q/%q/%q", row[13], row[14], row[15])
}
if row[16] != "200.00" {
t.Fatalf("结算金额 = %q", row[16])
}
if row[17] != "已完成" || row[18] != "待线下结算" {
t.Fatalf("状态列 = %q/%q", row[17], row[18])
}
if row[19] != "2026-11-12 10:20:30" || row[20] != "2026-11-12 12:20:30" {
t.Fatalf("时间列 = %q/%q", row[19], row[20])
}
}
func TestServiceExportAdminReusesFiltersAndRecordsAudit(t *testing.T) {
repo, db := newPickupTestRepo(t)
first := seedPickup(t, db, StatusPickingUp)
if err := db.Model(&first).Updates(map[string]any{
"operator": "运营,小张",
"platform": "拼多多",
"third_party_order_no": "=1+1",
"order_amount_cent": 29500,
"deposit_amount_cent": 35000,
}).Error; err != nil {
t.Fatalf("更新第一条提号失败: %v", err)
}
second := first
second.ID = 0
second.PickupNo = "PK-EXPORT-002"
second.Status = StatusCompleted
second.Operator = "运营小李"
second.ThirdPartyOrderNo = "TB-002"
if err := db.Create(&second).Error; err != nil {
t.Fatalf("创建第二条提号失败: %v", err)
}
service := NewService(repo)
data, rowCount, err := service.ExportAdmin(
t.Context(),
AdminPickupQuery{Status: StatusPickingUp},
99,
auditlog.Meta{RequestID: "req-pickup-export"},
)
if err != nil {
t.Fatalf("ExportAdmin() error = %v", err)
}
if rowCount != 1 {
t.Fatalf("导出行数 = %d, want 1", rowCount)
}
records, err := csv.NewReader(bytes.NewReader(data[3:])).ReadAll()
if err != nil {
t.Fatalf("解析导出 CSV 失败: %v", err)
}
if len(records) != 2 {
t.Fatalf("筛选导出 CSV 行数 = %d, want 2", len(records))
}
if records[1][0] != first.PickupNo || records[1][10] != "'=1+1" {
t.Fatalf("筛选导出行 = %#v", records[1])
}
var audit model.AuditLog
if err := db.Where("action = ?", "pickup_export").First(&audit).Error; err != nil {
t.Fatalf("查询导出审计失败: %v", err)
}
var detail map[string]any
if err := json.Unmarshal(audit.Detail, &detail); err != nil {
t.Fatalf("解析导出审计失败: %v", err)
}
if detail["status"] != StatusPickingUp {
t.Fatalf("审计 status = %v, want %s", detail["status"], StatusPickingUp)
}
assertJSONNumber(t, "导出行数", detail["row_count"], 1)
if !strings.Contains(string(audit.Detail), "req-pickup-export") {
t.Fatalf("审计日志缺少 request_id: %s", audit.Detail)
}
}
func TestHandlerExportWritesCSVDownloadResponse(t *testing.T) {
gin.SetMode(gin.TestMode)
repo, db := newPickupTestRepo(t)
seedPickup(t, db, StatusPickingUp)
handler := NewHandler(NewService(repo))
router := gin.New()
router.GET("/export", func(c *gin.Context) {
c.Set(middleware.ContextAdminID, uint64(99))
handler.Export(c)
})
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/export", nil)
router.ServeHTTP(recorder, request)
if recorder.Code != http.StatusOK {
t.Fatalf("状态码 = %d, want %d, body=%s", recorder.Code, http.StatusOK, recorder.Body.String())
}
if contentType := recorder.Header().Get("Content-Type"); !strings.HasPrefix(contentType, "text/csv") {
t.Fatalf("Content-Type = %q", contentType)
}
if disposition := recorder.Header().Get("Content-Disposition"); !strings.Contains(disposition, ".csv") {
t.Fatalf("Content-Disposition = %q", disposition)
}
if rowCount := recorder.Header().Get("X-Export-Row-Count"); rowCount != "1" {
t.Fatalf("X-Export-Row-Count = %q, want 1", rowCount)
}
if !bytes.HasPrefix(recorder.Body.Bytes(), []byte{0xEF, 0xBB, 0xBF}) {
t.Fatal("响应体缺少 UTF-8 BOM")
}
}