Files

257 lines
7.1 KiB
Go

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()
}