package pickup import ( "bytes" "context" "encoding/csv" "fmt" "strings" "time" "hfb_jysh/backend/internal/auditlog" "hfb_jysh/backend/internal/timeutil" "gorm.io/gorm" ) const maxAdminPickupExportRows = 50000 var adminPickupExportHeader = []string{ "提号编号", "账号标题", "区服", "登录平台", "上架编号", "卖家手机号", "操作人", "账号来源", "来源渠道", "交易渠道", "第三方单号", "店铺名称", "提号备注", "下单金额(元)", "押金金额(元)", "线下利润(元)", "结算金额(元)", "提号状态", "结算状态", "创建时间", "完成时间", "取消时间", "完成备注", } // ListAdminForExport 按后台列表相同条件查询全部匹配提号,不返回账号快照。 func (r *Repository) ListAdminForExport(ctx context.Context, query AdminPickupQuery) ([]PickupDTO, error) { if r == nil || r.db == nil { return nil, ErrDependencyUnavailable } var rows []pickupRow db := applyAdminPickupFilters(r.adminPickupExportQuery(ctx), query) if err := db.Order("p.id DESC").Limit(maxAdminPickupExportRows + 1).Scan(&rows).Error; err != nil { return nil, err } if len(rows) > maxAdminPickupExportRows { return nil, ErrPickupExportTooLarge } items := make([]PickupDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return items, nil } // RecordAdminPickupExport 记录敏感数据导出行为。 func (r *Repository) RecordAdminPickupExport( ctx context.Context, adminID uint64, meta auditlog.Meta, query AdminPickupQuery, rowCount int, ) error { if r == nil || r.db == nil { return ErrDependencyUnavailable } return auditlog.Append(r.db.WithContext(ctx), auditlog.Entry{ ActorType: "admin", ActorID: adminID, Action: "pickup_export", BizType: "admin_pickup", Meta: meta, Detail: map[string]any{ "row_count": rowCount, "keyword": strings.TrimSpace(query.Keyword), "status": strings.TrimSpace(query.Status), "shop_name": strings.TrimSpace(query.ShopName), "account_source": strings.TrimSpace(query.AccountSource), "offline_settlement_status": strings.TrimSpace(query.OfflineSettlementStatus), "operator": strings.TrimSpace(query.Operator), "third_party_order_no": strings.TrimSpace(query.ThirdPartyOrderNo), "start_time": formatExportTimePointer(query.StartTime), "end_time": formatExportTimePointer(query.EndTime), }, }) } func (r *Repository) adminPickupExportQuery(ctx context.Context) *gorm.DB { return r.db.WithContext(ctx).Table("admin_pickups AS p"). Select(`p.id, p.pickup_no, p.listing_id, p.account_id, p.owner_id, p.admin_id, p.operator, p.platform, p.third_party_order_no, p.shop_name, p.raw_shop_name, p.account_source, p.source_channel, p.settlement_mode, p.order_amount_cent, p.deposit_amount_cent, p.profit_amount_cent, p.settle_amount_cent, p.status, p.offline_settlement_status, p.remark, p.complete_remark, p.created_at, p.completed_at, p.cancelled_at, l.listing_no, a.title AS account_title, a.server_region, a.login_platform, owner.phone AS owner_phone, COALESCE((SELECT SUM(fa.profit_delta_cent) FROM admin_pickup_financial_adjustments AS fa WHERE fa.pickup_id = p.id), 0) AS profit_adjustment_cent, COALESCE((SELECT SUM(fa.settle_delta_cent) FROM admin_pickup_financial_adjustments AS fa WHERE fa.pickup_id = p.id), 0) AS settle_adjustment_cent`). Joins("JOIN rental_listings AS l ON l.id = p.listing_id"). Joins("JOIN game_accounts AS a ON a.id = p.account_id"). Joins("JOIN users AS owner ON owner.id = p.owner_id") } func buildAdminPickupCSV(items []PickupDTO) ([]byte, error) { var buf bytes.Buffer buf.WriteString("\xEF\xBB\xBF") writer := csv.NewWriter(&buf) writer.UseCRLF = true if err := writer.Write(adminPickupExportHeader); err != nil { return nil, err } for _, item := range items { settleAmount := "" if item.Status == StatusCompleted { settleAmount = formatExportAmount(item.EffectiveSettleAmountCent) } record := []string{ exportCSVText(item.PickupNo), exportCSVText(item.AccountTitle), exportCSVText(item.ServerRegion), exportCSVText(item.LoginPlatform), exportCSVText(item.ListingNo), exportCSVText(item.OwnerPhone), exportCSVText(item.Operator), exportAccountSourceLabel(item.AccountSource), exportCSVText(item.SourceChannel), exportCSVText(item.Platform), exportCSVText(item.ThirdPartyOrderNo), exportCSVText(item.ShopName), exportCSVText(item.Remark), formatExportAmount(item.OrderAmountCent), formatExportAmount(item.DepositAmountCent), formatExportAmount(item.EffectiveProfitAmountCent), settleAmount, exportPickupStatusLabel(item.Status), exportSettlementStatusLabel(item), formatExportTime(item.CreatedAt), formatExportTimePointer(item.CompletedAt), formatExportTimePointer(item.CancelledAt), exportCSVText(item.CompleteRemark), } if err := writer.Write(record); err != nil { return nil, err } } writer.Flush() if err := writer.Error(); err != nil { return nil, err } return buf.Bytes(), nil } func exportCSVText(value string) string { if value == "" { return "" } trimmed := strings.TrimLeft(value, " \t\r") if trimmed == "" { return value } switch trimmed[0] { case '=', '+', '-', '@', '\t', '\r': return "'" + value default: return value } } func exportPickupStatusLabel(status string) string { switch status { case StatusPickingUp: return "提号中" case StatusCompleted: return "已完成" case StatusCancelled: return "已取消" default: return status } } func exportAccountSourceLabel(source string) string { switch source { case AccountSourceExternalPlatformManaged: return "外部上传 · 平台代管" case AccountSourceInternal: return "站内上传" default: return source } } func exportSettlementStatusLabel(item PickupDTO) string { if item.SettlementMode != SettlementModePlatformManaged { return "钱包已结算" } switch item.OfflineSettlementStatus { case OfflineSettlementStatusPending: return "待线下结算" case OfflineSettlementStatusSettled: return "已线下结算" default: return "待完成提号" } } func formatExportAmount(cent int64) string { if cent < 0 { return "-" + formatExportAmount(-cent) } return fmt.Sprintf("%d.%02d", cent/100, cent%100) } func formatExportTime(value time.Time) string { if value.IsZero() { return "" } return value.In(timeutil.ShanghaiLocation()).Format("2006-01-02 15:04:05") } func formatExportTimePointer(value *time.Time) string { if value == nil { return "" } return formatExportTime(*value) } func encodeExportFilename(filename string) string { var builder strings.Builder for _, value := range []byte(filename) { if (value >= 'a' && value <= 'z') || (value >= 'A' && value <= 'Z') || (value >= '0' && value <= '9') || value == '-' || value == '_' || value == '.' || value == '~' { builder.WriteByte(value) continue } builder.WriteByte('%') builder.WriteString(strings.ToUpper(fmt.Sprintf("%02X", value))) } return builder.String() }