合并 main 分支并融合订单列表最小化与 SQL 治理
- Auth/权限中间件取 main 演进版(错误细分、token 来源),保留 dev 的用户 cookie 与登出撤销语义 - 后台订单列表:融合 main 的首页免 COUNT 优化与关键词索引路由,保留 dev 的最小化 AdminOrderListItemDTO(结账明细仍由详情接口返回,同步调整对应集成测试) - 保留 dev 的 token_version 冻结/解冻递增与 main 的状态变化判断及通知 - 前端:保留 dev 登出接口与 main 的 fetchMe silent 参数、SSE 登录跳转;后台订单表按 dev 最小化 DTO 移除号主到手列
This commit is contained in:
@@ -391,10 +391,13 @@ func (r *Repository) buildRefundStatusDTO(order *model.RentalOrder) *RefundStatu
|
||||
func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) error {
|
||||
var refund *refundAction
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
|
||||
assets, err := r.lockOrderAssets(tx, orderID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
order := assets.Order
|
||||
listing := assets.Listing
|
||||
account := assets.Account
|
||||
if order.RefundStatus != refundStatusPendingReview {
|
||||
return errors.New("订单不处于待审核退款状态")
|
||||
}
|
||||
@@ -407,14 +410,23 @@ func (r *Repository) AdminApproveRefund(ctx context.Context, orderID uint64) err
|
||||
// 全部押金被暂扣且无其他可退金额,直接标记为已处理,等待后续手动退押金。
|
||||
order.RefundStatus = "none"
|
||||
order.RefundAmountCent = 0
|
||||
return tx.Save(&order).Error
|
||||
} else {
|
||||
action, err := r.prepareRefund(order, refundAmountCent, refundBizCancel, "取消订单原路退款(客服审核通过)")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
}
|
||||
action, err := r.prepareRefund(&order, refundAmountCent, refundBizCancel, "取消订单原路退款(客服审核通过)")
|
||||
if err != nil {
|
||||
if err := releaseAssetsForRentalIfIdle(tx, order, listing, account); err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
return tx.Save(&order).Error
|
||||
if err := tx.Save(order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Save(account).Error
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -31,9 +32,12 @@ func (r *Repository) SubmitCheckout(ctx context.Context, userID uint64, orderID
|
||||
if hasOpen {
|
||||
return ErrCheckoutCannotSubmit
|
||||
}
|
||||
before := orderState(order)
|
||||
checkoutToUserID := order.OwnerID
|
||||
checkoutTargetType := processlog.ActorUser
|
||||
if isPlatformSettlementOrder(order) && platformManagedAdminID(order) > 0 {
|
||||
checkoutToUserID = platformManagedAdminID(order)
|
||||
checkoutTargetType = processlog.ActorAdmin
|
||||
}
|
||||
record := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
@@ -61,6 +65,9 @@ func (r *Repository) SubmitCheckout(ctx context.Context, userID uint64, orderID
|
||||
order.SettlementStatus = settlementStatusPending
|
||||
now := time.Now()
|
||||
order.HandoffStartedAt = &now
|
||||
if err := appendOrderEvent(tx, order, "checkout", "checkout_submitted", processlog.ActorUser, userID, checkoutTargetType, &checkoutToUserID, req.Content, "", checkoutEventPayload(checkout), before, req.EvidenceURLS); err != nil {
|
||||
return err
|
||||
}
|
||||
orderID := order.ID
|
||||
if isPlatformSettlementOrder(order) {
|
||||
if err := appendManagedAdminNotification(tx, order, "checkout", "代管订单待确认结账", "租客已发起结账,请检查账号状态和消耗明细后确认。"); err != nil {
|
||||
@@ -108,6 +115,7 @@ func (r *Repository) ConfirmCheckout(ctx context.Context, userID uint64, orderID
|
||||
if order.Status != orderStatusPendingCheckoutConfirm || order.HandoffStatus != handoffStatusPendingOwnerCheckout {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
before := orderState(order)
|
||||
checkout, err := lockOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -128,7 +136,10 @@ func (r *Repository) ConfirmCheckout(ctx context.Context, userID uint64, orderID
|
||||
checkout.OwnerAdjustedAt = &now
|
||||
action, err := r.finalizeCheckout(tx, &order, checkout, "号主已确认结账,订单完成。")
|
||||
refund = action
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return appendOrderEvent(tx, order, "checkout", "checkout_confirmed", processlog.ActorUser, userID, processlog.ActorUser, &order.RenterID, "号主确认当前结账方案,订单完成。", "", checkoutEventPayload(*checkout), before, decodeStringList(checkout.EvidenceURLS))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -154,6 +165,7 @@ func (r *Repository) CounterCheckout(ctx context.Context, userID uint64, orderID
|
||||
if order.Status != orderStatusPendingCheckoutConfirm && order.Status != orderStatusPendingCheckoutAccept {
|
||||
return ErrCheckoutCannotCounter
|
||||
}
|
||||
before := orderState(order)
|
||||
checkout, err := lockOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -221,6 +233,13 @@ func (r *Repository) CounterCheckout(ctx context.Context, userID uint64, orderID
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
targetID := order.OwnerID
|
||||
if isOwner {
|
||||
targetID = order.RenterID
|
||||
}
|
||||
if err := appendOrderEvent(tx, order, "checkout", "checkout_countered", processlog.ActorUser, userID, processlog.ActorUser, &targetID, "", req.Reason, checkoutEventPayload(*checkout), before, req.EvidenceURLS); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(checkout).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -285,6 +304,7 @@ func (r *Repository) AcceptCheckout(ctx context.Context, userID uint64, orderID
|
||||
if order.Status != orderStatusPendingCheckoutAccept || order.HandoffStatus != handoffStatusPendingRenterCheckout {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
before := orderState(order)
|
||||
checkout, err := lockOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -300,7 +320,10 @@ func (r *Repository) AcceptCheckout(ctx context.Context, userID uint64, orderID
|
||||
checkout.RenterConfirmedAt = &now
|
||||
action, err := r.finalizeCheckout(tx, &order, checkout, "租客已确认结账协商,订单完成。")
|
||||
refund = action
|
||||
return err
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return appendOrderEvent(tx, order, "checkout", "checkout_accepted", processlog.ActorUser, userID, processlog.ActorUser, &order.OwnerID, "租客接受当前结账方案,订单完成。", "", checkoutEventPayload(*checkout), before, decodeStringList(checkout.EvidenceURLS))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -32,11 +32,12 @@ const (
|
||||
handoffStatusAdminClosed = "admin_closed"
|
||||
handoffStatusAdminAbnormal = "admin_abnormal"
|
||||
|
||||
settlementStatusUnsettled = "unsettled"
|
||||
settlementStatusPending = "pending"
|
||||
settlementStatusSettled = "settled"
|
||||
settlementStatusClosed = "closed"
|
||||
settlementStatusDisputed = "disputed"
|
||||
settlementStatusUnsettled = "unsettled"
|
||||
settlementStatusPending = "pending"
|
||||
settlementStatusSettled = "settled"
|
||||
settlementStatusClosed = "closed"
|
||||
settlementStatusDisputed = "disputed"
|
||||
settlementStatusArbitrated = "arbitrated"
|
||||
|
||||
handoffModeOwner = "owner"
|
||||
handoffModePlatform = "platform"
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -23,6 +24,7 @@ func (r *Repository) AdminHoldDeposit(ctx context.Context, adminID uint64, order
|
||||
if !canHoldDeposit(order) {
|
||||
return ErrDepositCannotHold
|
||||
}
|
||||
before := orderState(order)
|
||||
beforeStatus := order.DepositHoldStatus
|
||||
now := time.Now()
|
||||
order.DepositHoldStatus = depositHoldStatusHeld
|
||||
@@ -30,6 +32,9 @@ func (r *Repository) AdminHoldDeposit(ctx context.Context, adminID uint64, order
|
||||
order.DepositHeldBy = &adminID
|
||||
order.DepositHeldAt = &now
|
||||
order.DepositHoldReleasedAt = nil
|
||||
if err := appendOrderEvent(tx, order, "settlement", "deposit_held", processlog.ActorAdmin, adminID, processlog.ActorUser, &order.RenterID, "客服暂扣订单押金。", req.Reason, map[string]any{"deposit_amount_cent": order.DepositAmountCent}, before, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, adminID, "order.deposit_hold", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
@@ -61,9 +66,13 @@ func (r *Repository) AdminReleaseDeposit(ctx context.Context, adminID uint64, or
|
||||
if holdAmountCent <= 0 {
|
||||
return ErrDepositHoldAmountEmpty
|
||||
}
|
||||
before := orderState(order)
|
||||
now := time.Now()
|
||||
order.DepositHoldStatus = depositHoldStatusReleased
|
||||
order.DepositHoldReleasedAt = &now
|
||||
if err := appendOrderEvent(tx, order, "settlement", "deposit_released", processlog.ActorAdmin, adminID, processlog.ActorUser, &order.RenterID, "客服归还已暂扣的押金。", req.Reason, map[string]any{"deposit_hold_amount_cent": holdAmountCent}, before, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
action, err := r.prepareRefund(&order, holdAmountCent, refundBizDeposit, "暂扣押金归还原路退回")
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -51,6 +51,8 @@ type OrderDTO struct {
|
||||
GrowthPointsAwarded int64 `json:"growth_points_awarded,omitempty"`
|
||||
GrowthPointsAwardedAt *time.Time `json:"growth_points_awarded_at,omitempty"`
|
||||
AccountSnapshot datatypes.JSON `json:"account_snapshot"`
|
||||
AccountSource string `json:"account_source"`
|
||||
SourceChannel string `json:"source_channel,omitempty"`
|
||||
Status string `json:"status"`
|
||||
HandoffStatus string `json:"handoff_status"`
|
||||
HandoffMode string `json:"handoff_mode"`
|
||||
@@ -172,7 +174,8 @@ type CounterCheckoutRequest struct {
|
||||
}
|
||||
|
||||
type AdminActionRequest struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
ConfirmZeroOwnerIncome bool `json:"confirm_zero_owner_income"`
|
||||
}
|
||||
|
||||
type PlatformHandoffRequest struct {
|
||||
@@ -180,7 +183,7 @@ type PlatformHandoffRequest struct {
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
}
|
||||
|
||||
// ForceHandoffRequest 客服确认普通号主订单已完成线下交接。
|
||||
// ForceHandoffRequest 客服确认订单已完成交接。
|
||||
type ForceHandoffRequest struct {
|
||||
Content string `json:"content"`
|
||||
Reason string `json:"reason" binding:"required"`
|
||||
@@ -270,6 +273,28 @@ type HandoffRecordDTO struct {
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
// ProcessEventDTO 是后台交易时间线中的一条不可变过程记录。
|
||||
type ProcessEventDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
BusinessType string `json:"business_type"`
|
||||
BusinessID uint64 `json:"business_id"`
|
||||
Stage string `json:"stage"`
|
||||
Action string `json:"action"`
|
||||
ActorType string `json:"actor_type"`
|
||||
ActorID uint64 `json:"actor_id"`
|
||||
ActorName string `json:"actor_name"`
|
||||
TargetType string `json:"target_type"`
|
||||
TargetID *uint64 `json:"target_id,omitempty"`
|
||||
TargetName string `json:"target_name"`
|
||||
Content string `json:"content"`
|
||||
Reason string `json:"reason"`
|
||||
Payload datatypes.JSON `json:"payload"`
|
||||
AttachmentURLs []string `json:"attachment_urls"`
|
||||
StateBefore datatypes.JSON `json:"state_before"`
|
||||
StateAfter datatypes.JSON `json:"state_after"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
|
||||
type CheckoutDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
OrderID uint64 `json:"order_id"`
|
||||
|
||||
@@ -7,13 +7,20 @@ import (
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func canAdminForceHandoff(order model.RentalOrder) bool {
|
||||
if isPlatformHandoffOrder(order) || isPlatformSettlementOrder(order) {
|
||||
if isPlatformHandoffOrder(order) {
|
||||
if order.Status == orderStatusPendingHandoff {
|
||||
return order.HandoffStatus == handoffStatusPendingRenterConfirm
|
||||
}
|
||||
return order.Status == orderStatusAbnormal && order.HandoffStatus == handoffStatusRenterConfirmTimeout
|
||||
}
|
||||
if isPlatformSettlementOrder(order) {
|
||||
return false
|
||||
}
|
||||
if order.Status == orderStatusPendingHandoff {
|
||||
@@ -24,11 +31,18 @@ func canAdminForceHandoff(order model.RentalOrder) bool {
|
||||
return order.Status == orderStatusAbnormal && order.HandoffStatus == handoffStatusRenterConfirmTimeout
|
||||
}
|
||||
|
||||
func forceHandoffRecordType(order model.RentalOrder) string {
|
||||
if isPlatformHandoffOrder(order) {
|
||||
return "platform_handoff"
|
||||
}
|
||||
return "owner_handoff"
|
||||
}
|
||||
|
||||
func forceHandoffNeedsContent(order model.RentalOrder) bool {
|
||||
return order.HandoffStatus == handoffStatusPendingOwner || order.HandoffStatus == handoffStatusOwnerTimeout
|
||||
}
|
||||
|
||||
// AdminForceHandoff 由客服确认普通号主订单已完成线下交接,并立即开始租期。
|
||||
// AdminForceHandoff 由客服确认订单已完成交接,并立即开始租期。
|
||||
func (r *Repository) AdminForceHandoff(ctx context.Context, adminID uint64, orderID uint64, req ForceHandoffRequest, meta AuditMeta) error {
|
||||
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var order model.RentalOrder
|
||||
@@ -38,6 +52,7 @@ func (r *Repository) AdminForceHandoff(ctx context.Context, adminID uint64, orde
|
||||
if !canAdminForceHandoff(order) || strings.TrimSpace(req.Reason) == "" {
|
||||
return ErrOrderCannotForceHandoff
|
||||
}
|
||||
before := orderState(order)
|
||||
if order.RefundStatus == refundStatusPending || order.RefundStatus == refundStatusPendingReview {
|
||||
return ErrOrderCannotForceHandoff
|
||||
}
|
||||
@@ -60,7 +75,7 @@ func (r *Repository) AdminForceHandoff(ctx context.Context, adminID uint64, orde
|
||||
if !needsContent {
|
||||
var handoffCount int64
|
||||
if err := tx.Model(&model.HandoffRecord{}).
|
||||
Where("order_id = ? AND type = ?", order.ID, "owner_handoff").
|
||||
Where("order_id = ? AND type = ?", order.ID, forceHandoffRecordType(order)).
|
||||
Count(&handoffCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -87,6 +102,9 @@ func (r *Repository) AdminForceHandoff(ctx context.Context, adminID uint64, orde
|
||||
order.HandoffStatus = handoffStatusReceived
|
||||
order.EstimatedDurationHours = estimateOrderDurationHours(order.AccountSnapshot)
|
||||
order.RentedAt = &now
|
||||
if err := appendOrderEvent(tx, order, "handoff", "admin_force_handoff", processlog.ActorAdmin, adminID, processlog.ActorUser, &order.RenterID, content, req.Reason, nil, before, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
|
||||
@@ -60,6 +60,19 @@ func (h *Handler) AdminHandoffRecords(c *gin.Context) {
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *Handler) AdminProcessEvents(c *gin.Context) {
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
items, err := h.service.ProcessEventsAdmin(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *Handler) AdminClose(c *gin.Context) {
|
||||
h.adminAction(c, h.service.AdminClose, gin.H{"closed": true})
|
||||
}
|
||||
@@ -248,6 +261,15 @@ func (h *Handler) ListPendingRefund(c *gin.Context) {
|
||||
response.OK(c, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *Handler) PendingRefundCount(c *gin.Context) {
|
||||
count, err := h.service.PendingRefundCount(c.Request.Context())
|
||||
if err != nil {
|
||||
writeOrderError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"pending_refund_count": count})
|
||||
}
|
||||
|
||||
func (h *Handler) adminAction(c *gin.Context, fn func(context.Context, uint64, uint64, AdminActionRequest, AuditMeta) error, okData gin.H) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
|
||||
@@ -46,6 +46,8 @@ func writeOrderError(c *gin.Context, err error) {
|
||||
response.Error(c, http.StatusConflict, "checkout_cannot_submit", "当前订单不能发起结账")
|
||||
case errors.Is(err, ErrCheckoutCannotConfirm):
|
||||
response.Error(c, http.StatusConflict, "checkout_cannot_confirm", "当前结账不能确认")
|
||||
case errors.Is(err, ErrZeroOwnerIncomeConfirmation):
|
||||
response.Error(c, http.StatusConflict, "zero_owner_income_confirmation_required", "号主收入为 0,请确认后再结算")
|
||||
case errors.Is(err, ErrCheckoutCannotCounter):
|
||||
response.Error(c, http.StatusConflict, "checkout_cannot_counter", "当前结账不能修改")
|
||||
case errors.Is(err, ErrCheckoutMaxRounds):
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -20,6 +21,7 @@ func (r *Repository) SubmitHandoff(ctx context.Context, userID uint64, orderID u
|
||||
if order.OwnerID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
before := orderState(order)
|
||||
// 号主交接超时(owner_timeout)后仍允许补提交,避免临时延误导致订单卡死。
|
||||
if order.Status != orderStatusPendingHandoff ||
|
||||
(order.HandoffStatus != handoffStatusPendingOwner && order.HandoffStatus != handoffStatusOwnerTimeout) {
|
||||
@@ -37,6 +39,9 @@ func (r *Repository) SubmitHandoff(ctx context.Context, userID uint64, orderID u
|
||||
return err
|
||||
}
|
||||
order.HandoffStatus = handoffStatusPendingRenterConfirm
|
||||
if err := appendOrderEvent(tx, order, "handoff", "owner_handoff_submitted", processlog.ActorUser, userID, processlog.ActorUser, &order.RenterID, req.Content, "", nil, before, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
orderID := order.ID
|
||||
renterContent := "请查看交接记录,确认账号可正常登录后点击确认收号。"
|
||||
if lateSubmit {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"hfb_sys/backend/internal/modules/distribution"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/modules/rentergrowth"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
@@ -45,6 +46,10 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
accountSource, sourceChannel, err := orderAccountSourceSnapshot(tx, listing.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
orderNo, err := newOrderNo()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -94,6 +99,8 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
||||
RenterGrowthLevelName: growthSnapshot.LevelName,
|
||||
RenterDiscountBps: growthSnapshot.DiscountBps,
|
||||
AccountSnapshot: snapshot,
|
||||
AccountSource: accountSource,
|
||||
SourceChannel: sourceChannel,
|
||||
Status: orderStatusPendingPayment,
|
||||
HandoffStatus: handoffStatusNone,
|
||||
HandoffMode: listingHandoffMode(listing),
|
||||
@@ -136,6 +143,18 @@ func (r *Repository) Create(ctx context.Context, renterID uint64, req CreateRequ
|
||||
return r.FindForUser(ctx, renterID, createdID)
|
||||
}
|
||||
|
||||
func orderAccountSourceSnapshot(tx *gorm.DB, listingID uint64) (string, string, error) {
|
||||
var upload model.ListingUpload
|
||||
err := tx.Where("listing_id = ?", listingID).Order("id DESC").First(&upload).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return "internal", "", nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return "external", upload.SourceChannel, nil
|
||||
}
|
||||
|
||||
func listingHandoffMode(listing model.RentalListing) string {
|
||||
if listing.HandoffMode != "" {
|
||||
return listing.HandoffMode
|
||||
@@ -378,8 +397,11 @@ func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64)
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := releaseAssetsForRentalIfIdle(tx, &order, listing, account); err != nil {
|
||||
return err
|
||||
// 已付款订单取消后需等待客服审核退款,审核通过前继续占用账号。
|
||||
if beforeStatus == orderStatusPendingPayment {
|
||||
if err := releaseAssetsForRentalIfIdle(tx, &order, listing, account); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := closePendingOrderPayments(tx, order.ID, "order_cancel"); err != nil {
|
||||
return err
|
||||
@@ -428,6 +450,7 @@ func (r *Repository) ConfirmReceive(ctx context.Context, userID uint64, orderID
|
||||
if order.Status != orderStatusPendingHandoff || order.HandoffStatus != handoffStatusPendingRenterConfirm {
|
||||
return ErrOrderCannotReceive
|
||||
}
|
||||
before := orderState(order)
|
||||
now := time.Now()
|
||||
if err := tx.Model(&model.HandoffRecord{}).
|
||||
Where("order_id = ? AND type IN ?", order.ID, []string{"owner_handoff", "platform_handoff"}).
|
||||
@@ -438,6 +461,9 @@ func (r *Repository) ConfirmReceive(ctx context.Context, userID uint64, orderID
|
||||
order.HandoffStatus = handoffStatusReceived
|
||||
order.EstimatedDurationHours = estimateOrderDurationHours(order.AccountSnapshot)
|
||||
order.RentedAt = &now
|
||||
if err := appendOrderEvent(tx, order, "handoff", "renter_received_confirmed", processlog.ActorUser, userID, processlog.ActorUser, &order.OwnerID, "租客确认已收号,订单开始租用。", "", nil, before, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
orderID := order.ID
|
||||
if isPlatformHandoffOrder(order) {
|
||||
if err := appendManagedAdminNotification(tx, order, "handoff", "租客已确认收号", "代管订单已进入使用中。"); err != nil {
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/adminnotification"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
@@ -66,7 +67,7 @@ func canAdminPlatformCheckoutConfirm(order model.RentalOrder) bool {
|
||||
func canAdminMarkOfflineSettlement(order model.RentalOrder) bool {
|
||||
return isPlatformSettlementOrder(order) &&
|
||||
order.Status == orderStatusCompleted &&
|
||||
order.SettlementStatus == settlementStatusSettled &&
|
||||
(order.SettlementStatus == settlementStatusSettled || order.SettlementStatus == settlementStatusArbitrated) &&
|
||||
effectiveOfflineSettlementStatus(order) == offlineSettlementStatusPending &&
|
||||
order.OfflineSettlementAmountCent > 0
|
||||
}
|
||||
@@ -81,6 +82,7 @@ func (r *Repository) AdminPlatformHandoff(ctx context.Context, adminID uint64, o
|
||||
if !canAdminPlatformHandoff(order) {
|
||||
return ErrOrderCannotHandoff
|
||||
}
|
||||
before := orderState(order)
|
||||
beforeHandoffStatus := order.HandoffStatus
|
||||
if order.ManagedAdminID == nil {
|
||||
order.ManagedAdminID = &adminID
|
||||
@@ -98,6 +100,9 @@ func (r *Repository) AdminPlatformHandoff(ctx context.Context, adminID uint64, o
|
||||
now := time.Now()
|
||||
order.HandoffStatus = handoffStatusPendingRenterConfirm
|
||||
order.HandoffStartedAt = &now
|
||||
if err := appendOrderEvent(tx, order, "handoff", "platform_handoff_submitted", processlog.ActorAdmin, adminID, processlog.ActorUser, &order.RenterID, req.Content, req.Reason, nil, before, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
orderID := order.ID
|
||||
content := "客服已提交交接说明,请查看交接记录,确认账号可正常登录后点击确认收号。"
|
||||
if beforeHandoffStatus == handoffStatusOwnerTimeout {
|
||||
@@ -146,6 +151,7 @@ func (r *Repository) AdminPlatformCheckoutConfirm(ctx context.Context, adminID u
|
||||
if !canAdminPlatformCheckoutConfirm(order) {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
before := orderState(order)
|
||||
checkout, err := lockOpenCheckout(tx, order.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -153,6 +159,9 @@ func (r *Repository) AdminPlatformCheckoutConfirm(ctx context.Context, adminID u
|
||||
if normalizeCheckoutTurn(checkout) != checkoutTurnOwner {
|
||||
return ErrCheckoutCannotConfirm
|
||||
}
|
||||
if checkout.OwnerIncomeAmountCent == 0 && !req.ConfirmZeroOwnerIncome {
|
||||
return ErrZeroOwnerIncomeConfirmation
|
||||
}
|
||||
if err := ensureCheckoutCompletable(order, checkout); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -175,6 +184,9 @@ func (r *Repository) AdminPlatformCheckoutConfirm(ctx context.Context, adminID u
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendOrderEvent(tx, order, "checkout", "platform_checkout_confirmed", processlog.ActorAdmin, adminID, processlog.ActorUser, &order.RenterID, "客服确认当前结账方案,订单完成。", req.Reason, checkoutEventPayload(*checkout), before, decodeStringList(checkout.EvidenceURLS)); err != nil {
|
||||
return err
|
||||
}
|
||||
return appendAuditLog(tx, adminID, "order.platform_checkout_confirm", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
@@ -187,6 +199,7 @@ func (r *Repository) AdminPlatformCheckoutConfirm(ctx context.Context, adminID u
|
||||
"after_settlement_status": order.SettlementStatus,
|
||||
"offline_settlement_status": order.OfflineSettlementStatus,
|
||||
"offline_settlement_amount_cent": order.OfflineSettlementAmountCent,
|
||||
"confirm_zero_owner_income": req.ConfirmZeroOwnerIncome,
|
||||
})
|
||||
})
|
||||
if err != nil {
|
||||
@@ -207,6 +220,7 @@ func (r *Repository) AdminPlatformCheckoutCounter(ctx context.Context, adminID u
|
||||
if !canAdminPlatformCheckoutConfirm(order) {
|
||||
return ErrCheckoutCannotCounter
|
||||
}
|
||||
before := orderState(order)
|
||||
if order.ManagedAdminID == nil {
|
||||
order.ManagedAdminID = &adminID
|
||||
}
|
||||
@@ -259,6 +273,9 @@ func (r *Repository) AdminPlatformCheckoutCounter(ctx context.Context, adminID u
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendOrderEvent(tx, order, "checkout", "platform_checkout_countered", processlog.ActorAdmin, adminID, processlog.ActorUser, &order.RenterID, "", req.Reason, checkoutEventPayload(*checkout), before, req.EvidenceURLS); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, adminID, "order.platform_checkout_counter", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
@@ -308,6 +325,7 @@ func (r *Repository) AdminPlatformCheckoutDispute(ctx context.Context, adminID u
|
||||
if !canAdminPlatformCheckoutConfirm(order) {
|
||||
return ErrCheckoutCannotCounter
|
||||
}
|
||||
before := orderState(order)
|
||||
if order.ManagedAdminID == nil {
|
||||
order.ManagedAdminID = &adminID
|
||||
}
|
||||
@@ -369,6 +387,9 @@ func (r *Repository) AdminPlatformCheckoutDispute(ctx context.Context, adminID u
|
||||
if err := tx.Create(&record).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendOrderEvent(tx, order, "dispute", "platform_checkout_dispute_opened", processlog.ActorAdmin, adminID, processlog.ActorUser, &order.RenterID, "客服发起结账争议。", req.Reason, map[string]any{"checkout_id": checkout.ID, "dispute_id": row.ID}, before, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
disputeID := row.ID
|
||||
orderID := order.ID
|
||||
if err := notification.Append(tx, notification.Entry{
|
||||
@@ -415,6 +436,7 @@ func (r *Repository) AdminMarkOfflineSettlement(ctx context.Context, adminID uin
|
||||
if !canAdminMarkOfflineSettlement(order) {
|
||||
return ErrOfflineSettlementCannotMark
|
||||
}
|
||||
before := orderState(order)
|
||||
beforeStatus := order.OfflineSettlementStatus
|
||||
now := time.Now()
|
||||
order.OfflineSettlementStatus = offlineSettlementStatusSettled
|
||||
@@ -422,6 +444,9 @@ func (r *Repository) AdminMarkOfflineSettlement(ctx context.Context, adminID uin
|
||||
order.OfflineSettledBy = &adminID
|
||||
order.OfflineSettledAt = &now
|
||||
order.OwnerSettledAt = &now
|
||||
if err := appendOrderEvent(tx, order, "settlement", "offline_settlement_confirmed", processlog.ActorAdmin, adminID, processlog.ActorUser, &order.OwnerID, "确认已完成线下结算。", req.Remark, map[string]any{"offline_settlement_amount_cent": order.OfflineSettlementAmountCent}, before, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := appendAuditLog(tx, adminID, "order.offline_settlement", "order", order.ID, meta, map[string]any{
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
|
||||
@@ -108,6 +108,8 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
||||
GrowthPointsAwarded: row.GrowthPointsAwarded,
|
||||
GrowthPointsAwardedAt: row.GrowthPointsAwardedAt,
|
||||
AccountSnapshot: row.AccountSnapshot,
|
||||
AccountSource: effectiveAccountSource(row.RentalOrder),
|
||||
SourceChannel: row.SourceChannel,
|
||||
Status: row.Status,
|
||||
HandoffStatus: row.HandoffStatus,
|
||||
HandoffMode: effectiveHandoffMode(row.RentalOrder),
|
||||
@@ -132,6 +134,13 @@ func (row orderRow) toAdminDTO() OrderDTO {
|
||||
}
|
||||
}
|
||||
|
||||
func effectiveAccountSource(order model.RentalOrder) string {
|
||||
if order.AccountSource != "" {
|
||||
return order.AccountSource
|
||||
}
|
||||
return "internal"
|
||||
}
|
||||
|
||||
func (row orderRow) toDTOForUser(userID uint64) OrderDTO {
|
||||
dto := row.toAdminDTO()
|
||||
dto.AdminActions = nil
|
||||
@@ -147,6 +156,9 @@ func (row orderRow) toDTOForUser(userID uint64) OrderDTO {
|
||||
dto.OfflineSettlementRemark = ""
|
||||
dto.OfflineSettledBy = nil
|
||||
dto.OfflineSettledAt = nil
|
||||
// 外部来源渠道属于后台经营信息,仅在管理员订单详情展示。
|
||||
dto.AccountSource = ""
|
||||
dto.SourceChannel = ""
|
||||
applyOrderPriceView(&dto, row.RentalOrder, userID)
|
||||
return dto
|
||||
}
|
||||
@@ -335,7 +347,7 @@ func refreshCheckoutSettlementDTO(dto *CheckoutDTO, order model.RentalOrder, che
|
||||
if checkout.Status != checkoutStatusSubmitted && checkout.Status != checkoutStatusCountered {
|
||||
return
|
||||
}
|
||||
settlement := calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, checkout.DepositDeductAmountCent)
|
||||
settlement := calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, effectiveCheckoutDepositDeductCent(checkout))
|
||||
rent := settlement.ActualRentAmountCent
|
||||
ownerRent := settlement.OwnerRentIncomeCent
|
||||
platform := settlement.PlatformFeeCent
|
||||
|
||||
@@ -278,7 +278,16 @@ func buildCheckout(order model.RentalOrder, initiatedBy uint64, status string, c
|
||||
}
|
||||
|
||||
func buildCheckoutSettlement(order model.RentalOrder, checkout *model.OrderCheckout) checkoutSettlement {
|
||||
return calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, checkout.DepositDeductAmountCent)
|
||||
return calculateCheckoutSettlement(order, checkout.ConsumableAmountCent, checkout.CoinConsumedM, effectiveCheckoutDepositDeductCent(*checkout))
|
||||
}
|
||||
|
||||
// effectiveCheckoutDepositDeductCent keeps legacy checkouts that only populated
|
||||
// other_amount_cent from losing their deposit compensation at finalization.
|
||||
func effectiveCheckoutDepositDeductCent(checkout model.OrderCheckout) int64 {
|
||||
if checkout.DepositDeductAmountCent > 0 {
|
||||
return checkout.DepositDeductAmountCent
|
||||
}
|
||||
return maxCent(checkout.OtherAmountCent, 0)
|
||||
}
|
||||
|
||||
func calculateCheckoutSettlement(order model.RentalOrder, consumableAmountCent int64, coinConsumedM float64, depositDeductAmountCent int64) checkoutSettlement {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/processlog"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
const processBusinessOrder = "order"
|
||||
|
||||
func orderState(order model.RentalOrder) map[string]any {
|
||||
return map[string]any{
|
||||
"order_status": order.Status, "handoff_status": order.HandoffStatus,
|
||||
"settlement_status": order.SettlementStatus,
|
||||
"offline_settlement_status": effectiveOfflineSettlementStatus(order),
|
||||
}
|
||||
}
|
||||
|
||||
func checkoutEventPayload(checkout model.OrderCheckout) map[string]any {
|
||||
return map[string]any{
|
||||
"checkout_id": checkout.ID, "round": checkout.RoundCount, "turn": normalizeCheckoutTurn(&checkout),
|
||||
"proposed_by": checkout.ProposedBy, "rent_amount_cent": checkout.RentAmountCent,
|
||||
"owner_rent_amount_cent": checkout.OwnerRentAmountCent, "platform_fee_cent": checkout.PlatformFeeCent,
|
||||
"deposit_amount_cent": checkout.DepositAmountCent, "consumable_amount_cent": checkout.ConsumableAmountCent,
|
||||
"coin_consumed_m": checkout.CoinConsumedM, "deposit_deduct_amount_cent": checkout.DepositDeductAmountCent,
|
||||
"renter_refund_amount_cent": checkout.RenterRefundAmountCent, "owner_income_amount_cent": checkout.OwnerIncomeAmountCent,
|
||||
"shortfall_cent": checkout.ShortfallCent, "overshoot_amount_cent": checkout.OvershootAmountCent,
|
||||
}
|
||||
}
|
||||
|
||||
func appendOrderEvent(tx *gorm.DB, order model.RentalOrder, stage, action, actorType string, actorID uint64, targetType string, targetID *uint64, content, reason string, payload, before map[string]any, attachments []string) error {
|
||||
return processlog.Append(tx, processlog.Entry{
|
||||
BusinessType: processBusinessOrder, BusinessID: order.ID, Stage: stage, Action: action,
|
||||
ActorType: actorType, ActorID: actorID, TargetType: targetType, TargetID: targetID,
|
||||
Content: content, Reason: reason, Payload: payload, Attachments: attachments,
|
||||
StateBefore: before, StateAfter: orderState(order),
|
||||
})
|
||||
}
|
||||
|
||||
func (r *Repository) ProcessEventsAdmin(ctx context.Context, orderID uint64) ([]ProcessEventDTO, error) {
|
||||
var order model.RentalOrder
|
||||
if err := r.db.WithContext(ctx).First(&order, orderID).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rows, err := processlog.List(r.db.WithContext(ctx), processBusinessOrder, orderID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]ProcessEventDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, ProcessEventDTO{
|
||||
ID: row.ID, BusinessType: row.BusinessType, BusinessID: row.BusinessID, Stage: row.Stage,
|
||||
Action: row.Action, ActorType: row.ActorType, ActorID: row.ActorID, ActorName: row.ActorName,
|
||||
TargetType: row.TargetType, TargetID: row.TargetID, TargetName: row.TargetName,
|
||||
Content: row.Content, Reason: row.Reason, Payload: row.Payload,
|
||||
AttachmentURLs: decodeStringList(row.AttachmentURLs), StateBefore: row.StateBefore,
|
||||
StateAfter: row.StateAfter, CreatedAt: row.CreatedAt,
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -120,12 +120,6 @@ func isSellerHandoffStatus(status string) bool {
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -135,6 +129,7 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminOrderQuery) (*Adm
|
||||
pageSize = 20
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
|
||||
var rows []orderRow
|
||||
err := applyAdminOrderFilters(r.adminQuery(ctx), query).
|
||||
Order("o.id DESC").
|
||||
@@ -145,6 +140,17 @@ func (r *Repository) ListAdmin(ctx context.Context, query AdminOrderQuery) (*Adm
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var total int64
|
||||
// 首页查询且返回条数小于 pageSize 时,总数等于当前返回条数,免除第二次 COUNT(*) 扫描
|
||||
if page == 1 && len(rows) < pageSize {
|
||||
total = int64(len(rows))
|
||||
} else {
|
||||
countDB := applyAdminOrderFilters(r.adminOrderCountQuery(ctx, query), query)
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
items := make([]AdminOrderListItemDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, row.toAdminListItem())
|
||||
@@ -171,6 +177,60 @@ func normalizePagination(page, pageSize int) (int, int) {
|
||||
return page, pageSize
|
||||
}
|
||||
|
||||
func (r *Repository) adminOrderCountQuery(ctx context.Context, query AdminOrderQuery) *gorm.DB {
|
||||
kw := strings.TrimSpace(query.Keyword)
|
||||
upper := strings.ToUpper(kw)
|
||||
needsJoin := false
|
||||
if kw != "" {
|
||||
if strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") {
|
||||
needsJoin = false
|
||||
} else if id, err := strconv.ParseUint(kw, 10, 64); err == nil && id > 0 && kw[0] != '0' && len(kw) < 9 {
|
||||
needsJoin = false
|
||||
} else {
|
||||
needsJoin = true
|
||||
}
|
||||
}
|
||||
if !needsJoin {
|
||||
return r.db.WithContext(ctx).Table("rental_orders AS o")
|
||||
}
|
||||
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 {
|
||||
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").
|
||||
Where("order_id IN ?", orderIDs).
|
||||
Group("order_id")
|
||||
var checkouts []model.OrderCheckout
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
for _, checkout := range checkouts {
|
||||
result[checkout.OrderID] = checkout
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func shouldAttachAdminCheckout(row orderRow) bool {
|
||||
return shouldAttachCheckout(row.Status) ||
|
||||
row.SettlementStatus == settlementStatusSettled ||
|
||||
row.SettlementStatus == settlementStatusArbitrated
|
||||
}
|
||||
|
||||
func applyAdminOrderFilters(db *gorm.DB, query AdminOrderQuery) *gorm.DB {
|
||||
if query.Status != "" {
|
||||
db = db.Where("o.status = ?", query.Status)
|
||||
@@ -191,21 +251,21 @@ func applyAdminOrderFilters(db *gorm.DB, query AdminOrderQuery) *gorm.DB {
|
||||
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,
|
||||
)
|
||||
upper := strings.ToUpper(keyword)
|
||||
if len(keyword) == 11 && keyword[0] == '1' && isAllDigits(keyword) {
|
||||
// 11 位手机号:命中 users(phone) 唯一索引
|
||||
db = db.Where("(owner.phone = ? OR renter.phone = ?)", keyword, keyword)
|
||||
} else if strings.HasPrefix(upper, "RO") || strings.HasPrefix(upper, "ORD") {
|
||||
// 订单号:命中 rental_orders(order_no) 唯一索引
|
||||
db = db.Where("o.order_no = ?", keyword)
|
||||
} else if strings.HasPrefix(upper, "L") || (len(keyword) >= 12 && isAllDigits(keyword)) {
|
||||
// 商品编号(如 L前缀或时间计算生成的 12位纯数字如 202609040184):命中 rental_listings(listing_no) 唯一索引
|
||||
db = db.Where("l.listing_no = ?", keyword)
|
||||
} else if id, err := strconv.ParseUint(keyword, 10, 64); err == nil && id > 0 && keyword[0] != '0' && id < 100000000 {
|
||||
// 短纯数字 ID(非零开头):命中 rental_orders(id) 主键及关联 ID
|
||||
db = db.Where("(o.id = ? OR o.listing_id = ? OR o.owner_id = ? OR o.renter_id = ?)", id, id, id, id)
|
||||
} else {
|
||||
like := "%" + keyword + "%"
|
||||
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,
|
||||
@@ -377,3 +437,12 @@ type orderRow struct {
|
||||
OwnerPhone string
|
||||
RenterPhone string
|
||||
}
|
||||
|
||||
func isAllDigits(s string) bool {
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] < '0' || s[i] > '9' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(s) > 0
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
)
|
||||
|
||||
// ListPendingRefund 查询所有待客服审核退款的订单。
|
||||
@@ -26,3 +28,13 @@ func (r *Repository) ListPendingRefund(ctx context.Context) ([]OrderDTO, error)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// PendingRefundCount 返回待客服审核退款数量,用于后台导航角标。
|
||||
func (r *Repository) PendingRefundCount(ctx context.Context) (int64, error) {
|
||||
var count int64
|
||||
err := r.db.WithContext(ctx).
|
||||
Model(&model.RentalOrder{}).
|
||||
Where("refund_status = ?", refundStatusPendingReview).
|
||||
Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -379,6 +381,20 @@ func TestBuildCheckoutSettlementWithDepositDeduct(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCheckoutSettlementFallsBackToLegacyOtherAmount(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
RentAmountCent: 24000,
|
||||
OwnerRentAmountCent: 21600,
|
||||
DepositAmountCent: 5000,
|
||||
}
|
||||
checkout := &model.OrderCheckout{OtherAmountCent: 2000}
|
||||
|
||||
settlement := buildCheckoutSettlement(order, checkout)
|
||||
if settlement.DepositCompensationCent != 2000 || settlement.OwnerIncomeCent != 23600 {
|
||||
t.Fatalf("legacy other amount settlement = compensation %d, owner income %d, want 2000/23600", settlement.DepositCompensationCent, settlement.OwnerIncomeCent)
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildCheckoutSettlementWithConsumable 测试额外消耗
|
||||
func TestBuildCheckoutSettlementWithConsumable(t *testing.T) {
|
||||
order := model.RentalOrder{
|
||||
@@ -721,6 +737,105 @@ func TestCancelClosesPendingPaymentOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelPendingHandoffKeepsAssetsUntilRefundApproved(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
repo := NewRepository(db, Dependencies{
|
||||
RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) {
|
||||
return "refunding", nil
|
||||
}),
|
||||
})
|
||||
owner := model.User{Phone: "13800001002"}
|
||||
renter := model.User{Phone: "13900001002"}
|
||||
if err := db.Create(&owner).Error; err != nil {
|
||||
t.Fatalf("create owner failed: %v", err)
|
||||
}
|
||||
if err := db.Create(&renter).Error; err != nil {
|
||||
t.Fatalf("create renter failed: %v", err)
|
||||
}
|
||||
account := model.GameAccount{
|
||||
OwnerID: owner.ID,
|
||||
Status: accountStatusRented,
|
||||
ServerRegion: "国服",
|
||||
LoginPlatform: "steam",
|
||||
Title: "待退款审核账号",
|
||||
}
|
||||
if err := db.Create(&account).Error; err != nil {
|
||||
t.Fatalf("create account failed: %v", err)
|
||||
}
|
||||
listing := model.RentalListing{
|
||||
ListingNo: "LST-REFUND-REVIEW-001",
|
||||
OwnerID: owner.ID,
|
||||
AccountID: account.ID,
|
||||
Status: listingStatusRented,
|
||||
ReviewStatus: listingReviewStatusApproved,
|
||||
InTransaction: true,
|
||||
PriceCent: 1000,
|
||||
}
|
||||
if err := db.Create(&listing).Error; err != nil {
|
||||
t.Fatalf("create listing failed: %v", err)
|
||||
}
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-REFUND-REVIEW-001",
|
||||
ListingID: listing.ID,
|
||||
AccountID: account.ID,
|
||||
OwnerID: owner.ID,
|
||||
RenterID: renter.ID,
|
||||
RentAmountCent: 1000,
|
||||
Status: orderStatusPendingHandoff,
|
||||
HandoffStatus: handoffStatusPendingOwner,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatalf("create order failed: %v", err)
|
||||
}
|
||||
|
||||
if err := repo.Cancel(t.Context(), renter.ID, order.ID); err != nil {
|
||||
t.Fatalf("Cancel() error = %v", err)
|
||||
}
|
||||
assertAssetsRented := func() {
|
||||
var savedListing model.RentalListing
|
||||
if err := db.First(&savedListing, listing.ID).Error; err != nil {
|
||||
t.Fatalf("find listing failed: %v", err)
|
||||
}
|
||||
if savedListing.Status != listingStatusRented || !savedListing.InTransaction {
|
||||
t.Fatalf("listing = %s/in_transaction:%t, want rented/true", savedListing.Status, savedListing.InTransaction)
|
||||
}
|
||||
var savedAccount model.GameAccount
|
||||
if err := db.First(&savedAccount, account.ID).Error; err != nil {
|
||||
t.Fatalf("find account failed: %v", err)
|
||||
}
|
||||
if savedAccount.Status != accountStatusRented {
|
||||
t.Fatalf("account status = %q, want %q", savedAccount.Status, accountStatusRented)
|
||||
}
|
||||
}
|
||||
assertAssetsRented()
|
||||
|
||||
var pendingReviewOrder model.RentalOrder
|
||||
if err := db.First(&pendingReviewOrder, order.ID).Error; err != nil {
|
||||
t.Fatalf("find cancelled order failed: %v", err)
|
||||
}
|
||||
if pendingReviewOrder.RefundStatus != refundStatusPendingReview {
|
||||
t.Fatalf("refund status = %q, want %q", pendingReviewOrder.RefundStatus, refundStatusPendingReview)
|
||||
}
|
||||
if err := repo.AdminApproveRefund(t.Context(), order.ID); err != nil {
|
||||
t.Fatalf("AdminApproveRefund() error = %v", err)
|
||||
}
|
||||
|
||||
var releasedListing model.RentalListing
|
||||
if err := db.First(&releasedListing, listing.ID).Error; err != nil {
|
||||
t.Fatalf("find released listing failed: %v", err)
|
||||
}
|
||||
if releasedListing.Status != listingStatusPublished || releasedListing.InTransaction {
|
||||
t.Fatalf("listing = %s/in_transaction:%t, want published/false", releasedListing.Status, releasedListing.InTransaction)
|
||||
}
|
||||
var releasedAccount model.GameAccount
|
||||
if err := db.First(&releasedAccount, account.ID).Error; err != nil {
|
||||
t.Fatalf("find released account failed: %v", err)
|
||||
}
|
||||
if releasedAccount.Status != accountStatusPublished {
|
||||
t.Fatalf("account status = %q, want %q", releasedAccount.Status, accountStatusPublished)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCancelDoesNotReleaseAssetsWhenSiblingOrderActive(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
@@ -1079,7 +1194,7 @@ func TestAdminForceHandoffStartsNormalOwnerOrder(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminForceHandoffUsesExistingOwnerRecordAndRejectsPlatformOrder(t *testing.T) {
|
||||
func TestAdminForceHandoffUsesExistingOwnerRecordAndRejectsPlatformOrderBeforeHandoff(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
if err := db.AutoMigrate(&model.Dispute{}); err != nil {
|
||||
t.Fatalf("migrate disputes failed: %v", err)
|
||||
@@ -1160,6 +1275,72 @@ func TestAdminForceHandoffUsesExistingOwnerRecordAndRejectsPlatformOrder(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminForceHandoffStartsPlatformManagedOrderAfterPlatformHandoff(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
if err := db.AutoMigrate(&model.Dispute{}); err != nil {
|
||||
t.Fatalf("migrate disputes failed: %v", err)
|
||||
}
|
||||
repo := NewRepository(db)
|
||||
|
||||
owner := model.User{Phone: "admin:force-handoff"}
|
||||
renter := model.User{Phone: "13900003008"}
|
||||
if err := db.Create(&owner).Error; err != nil {
|
||||
t.Fatalf("create owner failed: %v", err)
|
||||
}
|
||||
if err := db.Create(&renter).Error; err != nil {
|
||||
t.Fatalf("create renter failed: %v", err)
|
||||
}
|
||||
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-FORCE-HANDOFF-PLATFORM-001",
|
||||
ListingID: 4,
|
||||
AccountID: 4,
|
||||
OwnerID: owner.ID,
|
||||
RenterID: renter.ID,
|
||||
Status: orderStatusPendingHandoff,
|
||||
HandoffStatus: handoffStatusPendingRenterConfirm,
|
||||
HandoffMode: handoffModePlatform,
|
||||
SettlementMode: settlementModePlatformManaged,
|
||||
AccountSnapshot: datatypes.JSON([]byte(`{
|
||||
"haf_coin_amount": 100000000,
|
||||
"daily_loss_m": 50
|
||||
}`)),
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatalf("create order failed: %v", err)
|
||||
}
|
||||
if err := db.Create(&model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: 99,
|
||||
ToUserID: renter.ID,
|
||||
Type: "platform_handoff",
|
||||
Content: "客服已提交账号和登录说明",
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("create platform handoff record failed: %v", err)
|
||||
}
|
||||
|
||||
if err := repo.AdminForceHandoff(t.Context(), 99, order.ID, ForceHandoffRequest{
|
||||
Reason: "租客未确认,客服已核实账号可正常使用",
|
||||
}, AuditMeta{}); err != nil {
|
||||
t.Fatalf("AdminForceHandoff() error = %v", err)
|
||||
}
|
||||
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("load order failed: %v", err)
|
||||
}
|
||||
if saved.Status != orderStatusRenting || saved.HandoffStatus != handoffStatusReceived {
|
||||
t.Fatalf("status = %s/%s, want renting/received", saved.Status, saved.HandoffStatus)
|
||||
}
|
||||
var record model.HandoffRecord
|
||||
if err := db.Where("order_id = ? AND type = ?", order.ID, "admin_force_handoff").First(&record).Error; err != nil {
|
||||
t.Fatalf("find force handoff record failed: %v", err)
|
||||
}
|
||||
if record.Content != "客服已确认双方完成交接,订单已进入使用中。" {
|
||||
t.Fatalf("record content = %q", record.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSubmitCheckoutRefreshesStageTime(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
@@ -1414,6 +1595,46 @@ func TestAdminPlatformCheckoutCounterCanBeAcceptedByRenter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminMarkOfflineSettlementAllowsArbitratedPlatformOrder(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
adminID := uint64(79)
|
||||
order := model.RentalOrder{
|
||||
OrderNo: "ORD-OFFLINE-ARBITRATED-001",
|
||||
ListingID: 1,
|
||||
AccountID: 1,
|
||||
OwnerID: 1,
|
||||
RenterID: 2,
|
||||
Status: orderStatusCompleted,
|
||||
SettlementStatus: settlementStatusArbitrated,
|
||||
HandoffMode: handoffModePlatform,
|
||||
SettlementMode: settlementModePlatformManaged,
|
||||
OfflineSettlementStatus: offlineSettlementStatusPending,
|
||||
OfflineSettlementAmountCent: 8000,
|
||||
}
|
||||
if err := db.Create(&order).Error; err != nil {
|
||||
t.Fatalf("create order failed: %v", err)
|
||||
}
|
||||
|
||||
if !canAdminMarkOfflineSettlement(order) {
|
||||
t.Fatal("arbitrated platform order should allow offline settlement")
|
||||
}
|
||||
if err := repo.AdminMarkOfflineSettlement(t.Context(), adminID, order.ID, OfflineSettlementRequest{Remark: "仲裁后线下转账"}, AuditMeta{}); err != nil {
|
||||
t.Fatalf("AdminMarkOfflineSettlement() error = %v", err)
|
||||
}
|
||||
|
||||
var saved model.RentalOrder
|
||||
if err := db.First(&saved, order.ID).Error; err != nil {
|
||||
t.Fatalf("load order failed: %v", err)
|
||||
}
|
||||
if saved.OfflineSettlementStatus != offlineSettlementStatusSettled {
|
||||
t.Fatalf("offline settlement status = %q, want %q", saved.OfflineSettlementStatus, offlineSettlementStatusSettled)
|
||||
}
|
||||
if saved.OfflineSettledBy == nil || *saved.OfflineSettledBy != adminID {
|
||||
t.Fatalf("offline settled by = %#v, want %d", saved.OfflineSettledBy, adminID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformManagedCheckoutIncludesConsumableAndDepositCompensation(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
@@ -1588,3 +1809,78 @@ func createOpenPlatformCheckout(t *testing.T, db *gorm.DB, order model.RentalOrd
|
||||
}
|
||||
return checkout
|
||||
}
|
||||
|
||||
func TestListAdminUsesFinalizedCheckoutOwnerIncome(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
repo := NewRepository(db)
|
||||
adminID := uint64(7002)
|
||||
_, renter, order := createPlatformManagedCheckoutOrder(t, db, adminID)
|
||||
order.Status = orderStatusCompleted
|
||||
order.SettlementStatus = settlementStatusSettled
|
||||
if err := db.Save(&order).Error; err != nil {
|
||||
t.Fatalf("save completed order failed: %v", err)
|
||||
}
|
||||
checkout := createOpenPlatformCheckout(t, db, order, renter.ID)
|
||||
checkout.Status = checkoutStatusAccepted
|
||||
checkout.OwnerIncomeAmountCent = 6000
|
||||
if err := db.Save(&checkout).Error; err != nil {
|
||||
t.Fatalf("save finalized checkout failed: %v", err)
|
||||
}
|
||||
|
||||
result, err := repo.ListAdmin(t.Context(), AdminOrderQuery{Page: 1, PageSize: 20})
|
||||
if err != nil {
|
||||
t.Fatalf("ListAdmin() error = %v", err)
|
||||
}
|
||||
if len(result.Items) != 1 {
|
||||
t.Fatalf("ListAdmin() returned %d items, want 1", len(result.Items))
|
||||
}
|
||||
// 订单列表已最小化为 AdminOrderListItemDTO,结账明细(含号主到手金额)由订单详情接口返回。
|
||||
if result.Items[0].SettlementStatus != settlementStatusSettled {
|
||||
t.Fatalf("settlement status = %q, want %q", result.Items[0].SettlementStatus, settlementStatusSettled)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdminPlatformCheckoutConfirmRequiresExplicitConfirmationForZeroOwnerIncome(t *testing.T) {
|
||||
db := setupOrderTestDB(t)
|
||||
repo := NewRepository(db, Dependencies{
|
||||
RefundStarter: RefundStarterFunc(func(context.Context, uint64, int64, string, string) (string, error) {
|
||||
return "pending", nil
|
||||
}),
|
||||
})
|
||||
const adminID uint64 = 7001
|
||||
_, renter, order := createPlatformManagedCheckoutOrder(t, db, adminID)
|
||||
checkout := createOpenPlatformCheckout(t, db, order, renter.ID)
|
||||
checkout.OwnerIncomeAmountCent = 0
|
||||
if err := db.Save(&checkout).Error; err != nil {
|
||||
t.Fatalf("将号主收入调整为零失败: %v", err)
|
||||
}
|
||||
|
||||
err := repo.AdminPlatformCheckoutConfirm(t.Context(), *order.ManagedAdminID, order.ID, AdminActionRequest{
|
||||
Reason: "客服核对结算",
|
||||
}, AuditMeta{})
|
||||
if !errors.Is(err, ErrZeroOwnerIncomeConfirmation) {
|
||||
t.Fatalf("未确认零收入时 error = %v, want %v", err, ErrZeroOwnerIncomeConfirmation)
|
||||
}
|
||||
|
||||
var pending model.RentalOrder
|
||||
if err := db.First(&pending, order.ID).Error; err != nil {
|
||||
t.Fatalf("读取订单失败: %v", err)
|
||||
}
|
||||
if pending.Status != orderStatusPendingCheckoutConfirm {
|
||||
t.Fatalf("未确认零收入后订单状态 = %s, want %s", pending.Status, orderStatusPendingCheckoutConfirm)
|
||||
}
|
||||
|
||||
if err := repo.AdminPlatformCheckoutConfirm(t.Context(), *order.ManagedAdminID, order.ID, AdminActionRequest{
|
||||
Reason: "已确认号主收入为零",
|
||||
ConfirmZeroOwnerIncome: true,
|
||||
}, AuditMeta{}); err != nil {
|
||||
t.Fatalf("确认零收入后结算失败: %v", err)
|
||||
}
|
||||
var completed model.RentalOrder
|
||||
if err := db.First(&completed, order.ID).Error; err != nil {
|
||||
t.Fatalf("读取完成订单失败: %v", err)
|
||||
}
|
||||
if completed.Status != orderStatusCompleted {
|
||||
t.Fatalf("确认零收入后订单状态 = %s, want %s", completed.Status, orderStatusCompleted)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package order
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -9,6 +10,8 @@ import (
|
||||
"hfb_sys/backend/internal/timeutil"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/driver/sqlite"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func pureCoinDiscountOrder() model.RentalOrder {
|
||||
@@ -470,3 +473,58 @@ func TestNewOrderNoUsesShanghaiTimeWhenLocalIsUTC(t *testing.T) {
|
||||
t.Fatalf("order no time = %s, want between %s and %s", parsed, before, after)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyAdminOrderFiltersSmartRouting(t *testing.T) {
|
||||
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{DryRun: true})
|
||||
if err != nil {
|
||||
t.Fatalf("打开测试数据库失败: %v", err)
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
query AdminOrderQuery
|
||||
wantSQLFrag string
|
||||
}{
|
||||
{
|
||||
name: "11位手机号路由至users(phone)",
|
||||
query: AdminOrderQuery{Keyword: "13800138000"},
|
||||
wantSQLFrag: "owner.phone = ? OR renter.phone = ?",
|
||||
},
|
||||
{
|
||||
name: "RO前缀订单号路由至rental_orders(order_no)",
|
||||
query: AdminOrderQuery{Keyword: "RO202609051000"},
|
||||
wantSQLFrag: "o.order_no = ?",
|
||||
},
|
||||
{
|
||||
name: "12位时间计算的纯数字商品编号路由至rental_listings(listing_no)",
|
||||
query: AdminOrderQuery{Keyword: "202609040184"},
|
||||
wantSQLFrag: "l.listing_no = ?",
|
||||
},
|
||||
{
|
||||
name: "L前缀商品编号路由至rental_listings(listing_no)",
|
||||
query: AdminOrderQuery{Keyword: "L20260905100"},
|
||||
wantSQLFrag: "l.listing_no = ?",
|
||||
},
|
||||
{
|
||||
name: "短数字ID路由至主键及关联ID",
|
||||
query: AdminOrderQuery{Keyword: "12345"},
|
||||
wantSQLFrag: "o.id = ? OR o.listing_id = ? OR o.owner_id = ? OR o.renter_id = ?",
|
||||
},
|
||||
{
|
||||
name: "普通文字退回多列LIKE模糊匹配",
|
||||
query: AdminOrderQuery{Keyword: "测试账号"},
|
||||
wantSQLFrag: "o.order_no LIKE ? OR l.listing_no LIKE ? OR a.title LIKE ? OR owner.phone LIKE ? OR renter.phone LIKE ?",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
stmt := applyAdminOrderFilters(db.Table("rental_orders AS o"), tt.query).Find(&[]struct{}{}).Statement
|
||||
sql := stmt.SQL.String()
|
||||
if !strings.Contains(sql, tt.wantSQLFrag) {
|
||||
t.Fatalf("期望 SQL 包含 %q, 实际生成: %s", tt.wantSQLFrag, sql)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ var (
|
||||
ErrOrderCannotComplete = errors.New("order cannot complete")
|
||||
ErrCheckoutCannotSubmit = errors.New("checkout cannot submit")
|
||||
ErrCheckoutCannotConfirm = errors.New("checkout cannot confirm")
|
||||
ErrZeroOwnerIncomeConfirmation = errors.New("zero owner income confirmation required")
|
||||
ErrCheckoutCannotCounter = errors.New("checkout cannot counter")
|
||||
ErrCheckoutMaxRounds = errors.New("checkout max rounds reached")
|
||||
ErrCheckoutDepositShortfall = errors.New("checkout deposit shortfall")
|
||||
@@ -195,6 +196,13 @@ func (s *Service) HandoffRecordsAdmin(ctx context.Context, orderID uint64) ([]Ha
|
||||
return s.repo.HandoffRecordsAdmin(ctx, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) ProcessEventsAdmin(ctx context.Context, orderID uint64) ([]ProcessEventDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.ProcessEventsAdmin(ctx, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) AdminClose(ctx context.Context, adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
|
||||
if s.repo == nil {
|
||||
return ErrDependencyUnavailable
|
||||
@@ -362,6 +370,13 @@ func (s *Service) ListPendingRefund(ctx context.Context) ([]OrderDTO, error) {
|
||||
return s.repo.ListPendingRefund(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) PendingRefundCount(ctx context.Context) (int64, error) {
|
||||
if s.repo == nil {
|
||||
return 0, ErrDependencyUnavailable
|
||||
}
|
||||
return s.repo.PendingRefundCount(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) FindForUser(ctx context.Context, userID uint64, orderID uint64) (*OrderDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
|
||||
Reference in New Issue
Block a user