feat(admin-order): 订单管理列表恢复号主实际收入列

在金额左侧展示已结账/已仲裁订单的到手金额;整页一条批量 SQL 取最新结账行,避免 N+1。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-17 18:13:36 +08:00
co-authored by Cursor
parent 54985b7c36
commit ff8c35ebad
5 changed files with 318 additions and 30 deletions
+59 -29
View File
@@ -173,9 +173,29 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminOrderQuery) (*Adm
}
}
// 号主实际收入:按需收集本页行号,一次性批量查询(单条 SQL),避免逐行查询的 N+1。
ownerIncome := make(map[uint64]int64)
incomeOrderIDs := make([]uint64, 0, len(rows))
for _, row := range rows {
if adminListItemNeedsOwnerIncome(row) {
incomeOrderIDs = append(incomeOrderIDs, row.ID)
}
}
if len(incomeOrderIDs) > 0 {
ownerIncome, err = r.latestOwnerIncomeByOrder(ctx, incomeOrderIDs)
if err != nil {
return nil, err
}
}
items := make([]AdminOrderListItemDTO, 0, len(rows))
for _, row := range rows {
items = append(items, row.toAdminListItem())
item := row.toAdminListItem()
if amount, ok := ownerIncome[row.ID]; ok {
value := amount
item.OwnerIncomeAmountCent = &value
}
items = append(items, item)
}
return &AdminOrderListPaginatedResult{
@@ -218,38 +238,57 @@ func (r *Repository) adminOrderCountQuery(ctx context.Context, query AdminOrderQ
return r.adminOrderJoinQuery(ctx)
}
// latestAdminCheckouts loads one latest checkout per order so the admin order
// list can display finalized income without issuing one query per row.
func (r *Repository) latestAdminCheckouts(ctx context.Context, rows []orderRow) (map[uint64]model.OrderCheckout, error) {
result := make(map[uint64]model.OrderCheckout, len(rows))
if len(rows) == 0 {
// latestOwnerIncomeByOrder 批量读取每单「最新一次结账」的号主实际到手金额(分)。
//
// 后台订单列表专用:一次查询覆盖整页,不会退化为逐行查询的 N+1。
// 只取结账表两列,不加载 content / evidence_urls 等大字段。
//
// 语义:多轮还价是原地更新同一行,但订单可能经历多次结账流程,
// 因此按 order_id 取 MAX(id) 作为最终结账行。
//
// 执行计划(MySQL 8.4 实测,EXPLAIN FORMAT=TREE):
//
// -> Nested loop inner join
// -> Materialize
// -> Group aggregate: max(order_checkouts.id)
// -> Covering index scan on order_checkouts using idx_order_checkouts_order_id
// -> Single-row index lookup on oc using PRIMARY (id=latest.latest_id)
//
// 用 STRAIGHT_JOIN 固定连接顺序:先物化「每单最新结账 id」小集合(走
// idx_order_checkouts_order_id 覆盖索引),再按主键逐行取金额。避免优化器
// 在数据量增长后改选全表扫描;开销只与当前页订单数相关,与结账表总量无关。
func (r *Repository) latestOwnerIncomeByOrder(ctx context.Context, orderIDs []uint64) (map[uint64]int64, error) {
result := make(map[uint64]int64, len(orderIDs))
if len(orderIDs) == 0 {
return result, nil
}
orderIDs := make([]uint64, 0, len(rows))
for _, row := range rows {
orderIDs = append(orderIDs, row.ID)
}
latest := r.db.WithContext(ctx).
Table("order_checkouts").
Select("MAX(id) AS id").
Select("order_id, MAX(id) AS latest_id").
Where("order_id IN ?", orderIDs).
Group("order_id")
var checkouts []model.OrderCheckout
var rows []struct {
OrderID uint64
OwnerIncomeAmountCent int64
}
if err := r.db.WithContext(ctx).
Table("order_checkouts AS oc").
Joins("JOIN (?) AS latest ON latest.id = oc.id", latest).
Find(&checkouts).Error; err != nil {
Table("(?) AS latest", latest).
Select("oc.order_id, oc.owner_income_amount_cent").
Joins("STRAIGHT_JOIN order_checkouts AS oc ON oc.id = latest.latest_id").
Scan(&rows).Error; err != nil {
return nil, err
}
for _, checkout := range checkouts {
result[checkout.OrderID] = checkout
for _, row := range rows {
result[row.OrderID] = row.OwnerIncomeAmountCent
}
return result, nil
}
func shouldAttachAdminCheckout(row orderRow) bool {
return shouldAttachCheckout(row.Status) ||
row.SettlementStatus == settlementStatusSettled ||
// adminListItemNeedsOwnerIncome 判断列表行是否需要附带号主实际收入。
// 仅已结账/已仲裁时库中金额为最终值;协商中的金额会随后续还价变化,
// 不作为列表结论展示(协商中的实时金额走详情接口)。
func adminListItemNeedsOwnerIncome(row orderRow) bool {
return row.SettlementStatus == settlementStatusSettled ||
row.SettlementStatus == settlementStatusArbitrated
}
@@ -415,15 +454,6 @@ func effectiveDisputeInitiatorType(row model.Dispute) string {
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 {