Files
yml2213 85332df2bd 订单接口最小化与私有文件访问加固
- 订单列表使用独立最小 DTO 并分页,号主待办提供独立接口与统计
- 用户 token 增加版本控制,冻结/改密/退出即时撤销会话
- 移除 URL token 传参,SSE 与接口统一使用 HttpOnly Cookie
- 私有文件按上传归属与业务关联授权,收款凭证转私有访问并校验归属
- 公开商品接口返回最小字段,隐藏号主身份与内部状态
- 每日清理超过 30 天未关联业务的上传归属,上传归属失败时补偿删除对象
2026-08-16 21:47:46 +08:00

380 lines
12 KiB
Go

package order
import (
"context"
"strconv"
"strings"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/gorm"
)
func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*UserPaginatedResult, error) {
page, pageSize = normalizePagination(page, pageSize)
base := r.baseQuery(ctx).Where("o.renter_id = ? OR o.owner_id = ?", userID, userID)
var total int64
if err := r.db.WithContext(ctx).Table("rental_orders AS o").
Where("o.renter_id = ? OR o.owner_id = ?", userID, userID).
Count(&total).Error; err != nil {
return nil, err
}
var rows []orderRow
db := r.db.WithContext(ctx)
err := base.
Order("o.id DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
Scan(&rows).Error
if err != nil {
return nil, err
}
items := make([]UserOrderListItemDTO, 0, len(rows))
paymentTimeoutMinutes := pendingPaymentTimeoutMinutes(db)
for _, row := range rows {
var deadline *time.Time
if row.Status == orderStatusPendingPayment && paymentTimeoutMinutes > 0 {
value := row.CreatedAt.Add(time.Duration(paymentTimeoutMinutes) * time.Minute)
deadline = &value
}
items = append(items, row.toUserListItem(userID, deadline))
}
return &UserPaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) ListSellerHandoffs(ctx context.Context, userID uint64, query SellerHandoffQuery) (*SellerHandoffPaginatedResult, error) {
page, pageSize := normalizePagination(query.Page, query.PageSize)
base := r.baseQuery(ctx).
Where("o.owner_id = ?", userID).
Where("o.status IN ?", sellerHandoffStatuses())
if query.Status != "" {
base = base.Where("o.status = ?", query.Status)
}
var total int64
if err := base.Session(&gorm.Session{}).Count(&total).Error; err != nil {
return nil, err
}
var rows []orderRow
if err := base.Order("o.id DESC").Offset((page - 1) * pageSize).Limit(pageSize).Scan(&rows).Error; err != nil {
return nil, err
}
items := make([]UserOrderListItemDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toUserListItem(userID, nil))
}
metrics, err := r.sellerHandoffMetrics(ctx, userID)
if err != nil {
return nil, err
}
return &SellerHandoffPaginatedResult{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
Metrics: metrics,
}, nil
}
func (r *Repository) sellerHandoffMetrics(ctx context.Context, userID uint64) (SellerHandoffMetrics, error) {
base := r.db.WithContext(ctx).Table("rental_orders AS o").Where("o.owner_id = ?", userID)
var metrics SellerHandoffMetrics
if err := base.Where("o.status = ? AND o.handoff_status = ?", orderStatusPendingHandoff, handoffStatusPendingOwner).Count(&metrics.PendingHandoff).Error; err != nil {
return SellerHandoffMetrics{}, err
}
if err := r.db.WithContext(ctx).Table("rental_orders AS o").Where("o.owner_id = ? AND o.status = ?", userID, orderStatusPendingCheckoutConfirm).Count(&metrics.PendingCheckout).Error; err != nil {
return SellerHandoffMetrics{}, err
}
if err := r.db.WithContext(ctx).Table("rental_orders AS o").Where("o.owner_id = ? AND o.status IN ?", userID, sellerHandoffAbnormalStatuses()).Count(&metrics.Abnormal).Error; err != nil {
return SellerHandoffMetrics{}, err
}
return metrics, nil
}
func sellerHandoffStatuses() []string {
return []string{
orderStatusPendingHandoff,
orderStatusRenting,
orderStatusOverdue,
orderStatusPendingCheckoutConfirm,
orderStatusPendingCheckoutAccept,
orderStatusCheckoutDisputing,
"disputing",
orderStatusAbnormal,
}
}
func sellerHandoffAbnormalStatuses() []string {
return []string{orderStatusOverdue, orderStatusCheckoutDisputing, "disputing", orderStatusAbnormal}
}
func isSellerHandoffStatus(status string) bool {
for _, candidate := range sellerHandoffStatuses() {
if status == candidate {
return true
}
}
return false
}
func (r *Repository) ListAdmin(ctx context.Context, query AdminOrderQuery) (*AdminOrderListPaginatedResult, error) {
var total int64
countDB := applyAdminOrderFilters(r.adminOrderJoinQuery(ctx), query)
if err := countDB.Count(&total).Error; err != nil {
return nil, err
}
page := query.Page
pageSize := query.PageSize
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
offset := (page - 1) * pageSize
var rows []orderRow
err := applyAdminOrderFilters(r.adminQuery(ctx), query).
Order("o.id DESC").
Limit(pageSize).
Offset(offset).
Scan(&rows).Error
if err != nil {
return nil, err
}
items := make([]AdminOrderListItemDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toAdminListItem())
}
return &AdminOrderListPaginatedResult{
Items: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
func normalizePagination(page, pageSize int) (int, int) {
if page < 1 {
page = 1
}
if pageSize < 1 {
pageSize = 20
}
if pageSize > 100 {
pageSize = 100
}
return page, pageSize
}
func applyAdminOrderFilters(db *gorm.DB, query AdminOrderQuery) *gorm.DB {
if query.Status != "" {
db = db.Where("o.status = ?", query.Status)
}
if query.HandoffStatus != "" {
db = db.Where("o.handoff_status = ?", query.HandoffStatus)
}
switch query.HandoffMode {
case handoffModePlatform:
db = db.Where("(o.handoff_mode = ? OR o.settlement_mode = ?)", handoffModePlatform, settlementModePlatformManaged)
case handoffModeOwner, "normal":
db = db.Where("COALESCE(NULLIF(o.handoff_mode, ''), ?) <> ? AND COALESCE(NULLIF(o.settlement_mode, ''), ?) <> ?", handoffModeOwner, handoffModePlatform, settlementModeOwnerWallet, settlementModePlatformManaged)
}
if query.SettlementStatus != "" {
db = db.Where("o.settlement_status = ?", query.SettlementStatus)
}
if query.OfflineSettlementStatus != "" {
db = db.Where("COALESCE(NULLIF(o.offline_settlement_status, ''), 'none') = ?", query.OfflineSettlementStatus)
}
if keyword := strings.TrimSpace(query.Keyword); keyword != "" {
like := "%" + keyword + "%"
if id, err := strconv.ParseUint(keyword, 10, 64); err == nil {
db = db.Where(
`(o.order_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ? OR renter.phone LIKE ? OR o.id = ? OR o.listing_id = ? OR o.owner_id = ? OR o.renter_id = ?)`,
like,
like,
like,
like,
like,
id,
id,
id,
id,
)
} else {
db = db.Where(
`(o.order_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ? OR renter.phone LIKE ?)`,
like,
like,
like,
like,
like,
)
}
}
return db
}
func (r *Repository) FindAdmin(ctx context.Context, orderID uint64) (*OrderDTO, error) {
var row orderRow
db := r.db.WithContext(ctx)
if err := r.adminQuery(ctx).Where("o.id = ?", orderID).First(&row).Error; err != nil {
return nil, err
}
dto := row.toAdminDTO()
applyPaymentDeadline(&dto, row.RentalOrder, pendingPaymentTimeoutMinutes(db))
dto.ActiveDispute = r.activeDisputeDTO(ctx, orderID)
dto.Checkout = r.latestCheckoutAdminDTO(ctx, orderID)
return &dto, nil
}
func (r *Repository) FindLatestAdminByListing(ctx context.Context, listingID uint64) (*OrderDTO, error) {
var row orderRow
db := r.db.WithContext(ctx)
if err := r.adminQuery(ctx).
Where("o.listing_id = ?", listingID).
Order("o.id DESC").
First(&row).Error; err != nil {
return nil, err
}
dto := row.toAdminDTO()
applyPaymentDeadline(&dto, row.RentalOrder, pendingPaymentTimeoutMinutes(db))
dto.ActiveDispute = r.activeDisputeDTO(ctx, row.ID)
dto.Checkout = r.latestCheckoutAdminDTO(ctx, row.ID)
return &dto, nil
}
func (r *Repository) FindForUser(ctx context.Context, userID uint64, orderID uint64) (*OrderDTO, error) {
var row orderRow
db := r.db.WithContext(ctx)
if err := r.baseQuery(ctx).
Where("o.id = ? AND (o.renter_id = ? OR o.owner_id = ?)", orderID, userID, userID).
First(&row).Error; err != nil {
return nil, err
}
dto := row.toDTOForUser(userID)
applyPaymentDeadline(&dto, row.RentalOrder, pendingPaymentTimeoutMinutes(db))
dto.ActiveDispute = r.activeDisputeDTO(ctx, orderID)
dto.Checkout = r.latestCheckoutDTOForUser(ctx, orderID, userID, row.RentalOrder)
return &dto, nil
}
func pendingPaymentTimeoutMinutes(tx *gorm.DB) int {
var row model.SystemConfig
err := tx.Where("`key` = ?", "order.pending_payment_timeout_minutes").First(&row).Error
if err != nil {
return defaultPendingPaymentTimeoutMinutes
}
value, err := strconv.Atoi(row.Value)
if err != nil || value < 0 {
return defaultPendingPaymentTimeoutMinutes
}
return value
}
func applyPaymentDeadline(dto *OrderDTO, order model.RentalOrder, timeoutMinutes int) {
if dto == nil || order.Status != orderStatusPendingPayment || timeoutMinutes <= 0 {
return
}
deadline := order.CreatedAt.Add(time.Duration(timeoutMinutes) * time.Minute)
dto.PaymentDeadlineAt = &deadline
}
func (r *Repository) latestCheckoutAdminDTO(ctx context.Context, orderID uint64) *CheckoutDTO {
var checkout model.OrderCheckout
if err := r.db.WithContext(ctx).Where("order_id = ?", orderID).Order("id DESC").First(&checkout).Error; err != nil {
return nil
}
dto := toCheckoutAdminDTO(checkout)
var order model.RentalOrder
if err := r.db.WithContext(ctx).First(&order, orderID).Error; err == nil {
refreshCheckoutSettlementDTO(&dto, order, checkout)
}
return &dto
}
func (r *Repository) activeDisputeDTO(ctx context.Context, orderID uint64) *ActiveDisputeDTO {
var row model.Dispute
if err := r.db.WithContext(ctx).
Where("order_id = ? AND status IN ?", orderID, []string{"open", "processing"}).
Order("id DESC").
First(&row).Error; err != nil {
return nil
}
return &ActiveDisputeDTO{
ID: row.ID,
InitiatorID: row.InitiatorID,
InitiatorType: effectiveDisputeInitiatorType(row),
InitiatorAdminID: row.InitiatorAdminID,
Type: row.Type,
Status: row.Status,
}
}
func effectiveDisputeInitiatorType(row model.Dispute) string {
if row.InitiatorType == "admin" {
return "admin"
}
return "user"
}
func shouldAttachCheckout(status string) bool {
switch status {
case orderStatusPendingCheckoutConfirm, orderStatusPendingCheckoutAccept, orderStatusCheckoutDisputing, orderStatusCompleted:
return true
default:
return false
}
}
func (r *Repository) latestCheckoutDTOForUser(ctx context.Context, orderID uint64, userID uint64, order model.RentalOrder) *CheckoutDTO {
var checkout model.OrderCheckout
if err := r.db.WithContext(ctx).Where("order_id = ?", orderID).Order("id DESC").First(&checkout).Error; err != nil {
return nil
}
dto := toCheckoutDTOForUser(checkout, userID, order)
return &dto
}
func (r *Repository) findCheckout(ctx context.Context, id uint64) (*model.OrderCheckout, error) {
var checkout model.OrderCheckout
if err := r.db.WithContext(ctx).First(&checkout, id).Error; err != nil {
return nil, err
}
return &checkout, nil
}
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
return r.db.WithContext(ctx).Table("rental_orders AS o").
Select("o.*, l.listing_no, a.title, a.server_region, a.login_platform").
Joins("JOIN rental_listings AS l ON l.id = o.listing_id").
Joins("JOIN game_accounts AS a ON a.id = o.account_id")
}
func (r *Repository) adminQuery(ctx context.Context) *gorm.DB {
return r.adminOrderJoinQuery(ctx).
Select("o.*, l.listing_no, a.title, a.server_region, a.login_platform, owner.phone AS owner_phone, renter.phone AS renter_phone")
}
func (r *Repository) adminOrderJoinQuery(ctx context.Context) *gorm.DB {
return r.db.WithContext(ctx).Table("rental_orders AS o").
Joins("JOIN rental_listings AS l ON l.id = o.listing_id").
Joins("JOIN game_accounts AS a ON a.id = o.account_id").
Joins("JOIN users AS owner ON owner.id = o.owner_id").
Joins("JOIN users AS renter ON renter.id = o.renter_id")
}
type orderRow struct {
model.RentalOrder
Title string
ListingNo string
ServerRegion string
LoginPlatform string
OwnerPhone string
RenterPhone string
}