feat(admin-order): 订单管理列表恢复号主实际收入列
在金额左侧展示已结账/已仲裁订单的到手金额;整页一条批量 SQL 取最新结账行,避免 N+1。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user