257 lines
7.1 KiB
Go
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()
|
|
}
|