197 lines
6.1 KiB
Go
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")
|
|
}
|
|
}
|