拆分申诉 Repository 文件职责
This commit is contained in:
@@ -0,0 +1,341 @@
|
||||
package dispute
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
"hfb_sys/backend/pkg/money"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, req ArbitrateRequest, meta AuditMeta) (*DisputeDTO, error) {
|
||||
var refund *refundAction
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var row model.Dispute
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if row.Status != "open" && row.Status != "processing" {
|
||||
return ErrDisputeCannotHandle
|
||||
}
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, row.OrderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var account model.GameAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
beforeOrderStatus := order.Status
|
||||
beforeHandoffStatus := order.HandoffStatus
|
||||
beforeSettlementStatus := order.SettlementStatus
|
||||
beforeListingStatus := listing.Status
|
||||
beforeAccountStatus := account.Status
|
||||
frozenBalance, err := renterFrozenBalance(tx, order.RenterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settlement, err := buildArbitrationSettlement(order, req, frozenBalance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
row.Status = "resolved"
|
||||
row.ArbitrationResult = req.Result
|
||||
row.ArbitrationRemark = req.Remark
|
||||
row.HandledBy = &adminID
|
||||
row.HandledAt = &now
|
||||
order.Status = arbitrateOrderStatus(req.Result)
|
||||
order.SettlementStatus = "arbitrated"
|
||||
order.SettledAt = &now
|
||||
order.OwnerSettledAt = &now
|
||||
if order.HandoffStatus != "cancelled" && order.HandoffStatus != "returned" {
|
||||
order.HandoffStatus = "arbitrated"
|
||||
}
|
||||
if req.Result == "mark_abnormal" {
|
||||
listing.Status = "abnormal"
|
||||
listing.InTransaction = false
|
||||
listing.PublishedAt = nil
|
||||
account.Status = "abnormal"
|
||||
} else {
|
||||
// 仲裁会终结当前订单,账号统一归档下架,避免已结案账号重新出现在公开列表。
|
||||
listing.Status = "offline"
|
||||
listing.InTransaction = false
|
||||
listing.PublishedAt = nil
|
||||
account.Status = "offline"
|
||||
}
|
||||
if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil {
|
||||
return err
|
||||
}
|
||||
if settlement.RenterRefundAmountCent > 0 {
|
||||
action, err := r.prepareRefund(&order, settlement.RenterRefundAmountCent, "arbitration_refund", "仲裁退款原路退还")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
}
|
||||
if err := tx.Save(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
handoffRecord := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: adminID,
|
||||
ToUserID: order.RenterID,
|
||||
Type: "admin_arbitration",
|
||||
Content: buildArbitrationHandoffContent(req, settlement),
|
||||
}
|
||||
if err := tx.Create(&handoffRecord).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
disputeID := row.ID
|
||||
if err := appendAuditLog(tx, adminID, "dispute.arbitrate", "dispute", row.ID, meta, map[string]any{
|
||||
"dispute_id": row.ID,
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"result": req.Result,
|
||||
"remark": req.Remark,
|
||||
"input_amount_cent": req.AmountCent,
|
||||
"renter_refund_amount_cent": settlement.RenterRefundAmountCent,
|
||||
"owner_income_amount_cent": settlement.OwnerIncomeAmountCent,
|
||||
"deposit_deduct_amount_cent": settlement.DepositDeductAmountCent,
|
||||
"before_order_status": beforeOrderStatus,
|
||||
"after_order_status": order.Status,
|
||||
"before_handoff_status": beforeHandoffStatus,
|
||||
"after_handoff_status": order.HandoffStatus,
|
||||
"before_settlement_status": beforeSettlementStatus,
|
||||
"after_settlement_status": order.SettlementStatus,
|
||||
"before_listing_status": beforeListingStatus,
|
||||
"after_listing_status": listing.Status,
|
||||
"before_account_status": beforeAccountStatus,
|
||||
"after_account_status": account.Status,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "arbitration",
|
||||
Title: "申诉仲裁已完成",
|
||||
Content: "客服已给出仲裁结果,请在订单和申诉记录中查看处理说明。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "arbitration",
|
||||
Title: "申诉仲裁已完成",
|
||||
Content: "客服已给出仲裁结果,请在订单和申诉记录中查看处理说明。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.startRefundBestEffort(ctx, refund)
|
||||
var row disputeRow
|
||||
if err := r.baseQuery(ctx).Where("d.id = ?", id).First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := row.toDTO()
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
type arbitrationSettlement struct {
|
||||
Entries []wallet.Entry
|
||||
RenterRefundAmountCent int64
|
||||
OwnerIncomeAmountCent int64
|
||||
DepositDeductAmountCent int64
|
||||
}
|
||||
|
||||
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, error) {
|
||||
rentAmountCent := order.RentAmountCent
|
||||
depositAmountCent := order.DepositAmountCent
|
||||
ownerRentAmountCent := order.OwnerRentAmountCent
|
||||
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
|
||||
ownerRentAmountCent = rentAmountCent
|
||||
}
|
||||
totalCent := rentAmountCent + depositAmountCent
|
||||
settlement := arbitrationSettlement{}
|
||||
orderID := order.ID
|
||||
releaseFrozenAmountCent := money.MinCent(totalCent, renterFrozenBalanceCent)
|
||||
if releaseFrozenAmountCent > 0 {
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
Direction: "out",
|
||||
AmountCent: releaseFrozenAmountCent,
|
||||
BalanceType: "frozen",
|
||||
BizType: "arbitration_release_frozen",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "仲裁释放冻结金额",
|
||||
})
|
||||
}
|
||||
|
||||
addRenterRefund := func(amountCent int64, remark string) {
|
||||
if amountCent <= 0 {
|
||||
return
|
||||
}
|
||||
settlement.RenterRefundAmountCent += amountCent
|
||||
}
|
||||
addOwnerIncome := func(amountCent int64, remark string) {
|
||||
if amountCent <= 0 {
|
||||
return
|
||||
}
|
||||
settlement.OwnerIncomeAmountCent += amountCent
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
AmountCent: amountCent,
|
||||
BalanceType: "available",
|
||||
BizType: "arbitration_owner_income",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: remark,
|
||||
})
|
||||
}
|
||||
|
||||
switch req.Result {
|
||||
case "full_refund":
|
||||
addRenterRefund(totalCent, "仲裁全额退款")
|
||||
case "partial_refund":
|
||||
if req.AmountCent <= 0 || req.AmountCent > totalCent {
|
||||
return settlement, ErrInvalidDispute
|
||||
}
|
||||
addRenterRefund(req.AmountCent, "仲裁部分退款")
|
||||
addOwnerIncome(money.MinCent(totalCent-req.AmountCent, ownerRentAmountCent+depositAmountCent), "仲裁剩余金额结算给号主")
|
||||
case "release_deposit":
|
||||
addOwnerIncome(ownerRentAmountCent, "仲裁确认订单金额结算给号主")
|
||||
addRenterRefund(depositAmountCent, "仲裁释放押金给租客")
|
||||
case "deduct_deposit", "compensate_owner":
|
||||
deductAmountCent := req.AmountCent
|
||||
if deductAmountCent <= 0 {
|
||||
deductAmountCent = depositAmountCent
|
||||
}
|
||||
if deductAmountCent > depositAmountCent {
|
||||
return settlement, ErrInvalidDispute
|
||||
}
|
||||
settlement.DepositDeductAmountCent = deductAmountCent
|
||||
addOwnerIncome(ownerRentAmountCent+deductAmountCent, "仲裁订单金额及押金赔付结算给号主")
|
||||
addRenterRefund(depositAmountCent-deductAmountCent, "仲裁退回剩余押金给租客")
|
||||
case "order_close":
|
||||
// Only release frozen funds. No available-balance settlement happens in development mode.
|
||||
case "mark_abnormal":
|
||||
// 标记异常只释放冻结账务,后续由客服继续线下复核。
|
||||
default:
|
||||
return settlement, ErrInvalidDispute
|
||||
}
|
||||
return settlement, nil
|
||||
}
|
||||
|
||||
func (r *Repository) prepareRefund(order *model.RentalOrder, amountCent int64, bizType string, remark string) (*refundAction, error) {
|
||||
if amountCent <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if r.refundFunc == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
order.RefundStatus = "pending"
|
||||
order.RefundAmountCent = amountCent
|
||||
order.RefundedAt = nil
|
||||
return &refundAction{
|
||||
OrderID: order.ID,
|
||||
RefundAmountCent: amountCent,
|
||||
BizType: bizType,
|
||||
Remark: remark,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) startRefundBestEffort(ctx context.Context, action *refundAction) {
|
||||
if action == nil || r.refundFunc == nil {
|
||||
return
|
||||
}
|
||||
_, _ = r.refundFunc(ctx, action.OrderID, action.RefundAmountCent, action.BizType, action.Remark)
|
||||
}
|
||||
|
||||
func renterFrozenBalance(tx *gorm.DB, renterID uint64) (int64, error) {
|
||||
var account model.WalletAccount
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("user_id = ?", renterID).
|
||||
First(&account).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return account.FrozenBalanceCent, nil
|
||||
}
|
||||
|
||||
func arbitrateOrderStatus(result string) string {
|
||||
switch result {
|
||||
case "full_refund", "partial_refund", "order_close":
|
||||
return "closed"
|
||||
case "mark_abnormal":
|
||||
return "abnormal"
|
||||
default:
|
||||
return "completed"
|
||||
}
|
||||
}
|
||||
|
||||
func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitrationSettlement) string {
|
||||
content := fmt.Sprintf("客服仲裁结果:%s", arbitrationResultLabel(req.Result))
|
||||
if req.Remark != "" {
|
||||
content += "\n处理说明:" + req.Remark
|
||||
}
|
||||
if settlement.RenterRefundAmountCent > 0 {
|
||||
content += "\n退款给租客:" + money.FormatWithSymbol(settlement.RenterRefundAmountCent)
|
||||
}
|
||||
if settlement.OwnerIncomeAmountCent > 0 {
|
||||
content += "\n结算给号主:" + money.FormatWithSymbol(settlement.OwnerIncomeAmountCent)
|
||||
}
|
||||
if settlement.DepositDeductAmountCent > 0 {
|
||||
content += "\n押金扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func arbitrationResultLabel(result string) string {
|
||||
switch result {
|
||||
case "full_refund":
|
||||
return "全额退款"
|
||||
case "partial_refund":
|
||||
return "部分退款"
|
||||
case "deduct_deposit":
|
||||
return "扣押金"
|
||||
case "release_deposit":
|
||||
return "释放押金"
|
||||
case "compensate_owner":
|
||||
return "赔付号主"
|
||||
case "order_close":
|
||||
return "关闭订单"
|
||||
case "mark_abnormal":
|
||||
return "标记异常"
|
||||
default:
|
||||
return result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package dispute
|
||||
|
||||
import (
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizType string, bizID uint64, meta AuditMeta, detail map[string]any) error {
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
BizType: bizType,
|
||||
BizID: &bizID,
|
||||
Meta: meta,
|
||||
Detail: detail,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
package dispute
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
func (r *Repository) Create(ctx context.Context, userID uint64, orderID uint64, req CreateRequest) (*DisputeDTO, error) {
|
||||
var createdID uint64
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if order.RenterID != userID && order.OwnerID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status == "completed" || order.Status == "cancelled" || order.Status == "closed" {
|
||||
return ErrInvalidDispute
|
||||
}
|
||||
isCheckoutDispute := order.Status == "pending_checkout_confirm" || order.Status == "pending_checkout_accept"
|
||||
var count int64
|
||||
if err := tx.Model(&model.Dispute{}).
|
||||
Where("order_id = ? AND status IN ?", order.ID, []string{"open", "processing"}).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return ErrDisputeExists
|
||||
}
|
||||
|
||||
targetID := order.OwnerID
|
||||
if userID == order.OwnerID {
|
||||
targetID = order.RenterID
|
||||
}
|
||||
evidence, err := marshalEvidence(req.EvidenceURLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row := model.Dispute{
|
||||
OrderID: order.ID,
|
||||
InitiatorID: userID,
|
||||
TargetUserID: targetID,
|
||||
Type: disputeType(req.Type, isCheckoutDispute),
|
||||
Status: "open",
|
||||
Description: req.Description,
|
||||
EvidenceURLS: evidence,
|
||||
}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if isCheckoutDispute {
|
||||
now := time.Now()
|
||||
order.Status = "checkout_disputing"
|
||||
order.HandoffStatus = "checkout_disputed"
|
||||
order.SettlementStatus = "disputed"
|
||||
updates := map[string]any{
|
||||
"status": "disputed",
|
||||
"updated_at": now,
|
||||
}
|
||||
if userID == order.RenterID {
|
||||
updates["renter_rejected_at"] = now
|
||||
}
|
||||
if err := tx.Model(&model.OrderCheckout{}).
|
||||
Where("order_id = ? AND status IN ?", order.ID, []string{"submitted", "countered"}).
|
||||
Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
order.Status = "disputing"
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
disputeID := row.ID
|
||||
title := "订单进入申诉"
|
||||
content := "对方已发起申诉,请等待客服仲裁或补充沟通记录。"
|
||||
if isCheckoutDispute {
|
||||
title = "订单进入结账争议"
|
||||
content = "对方已发起结账争议,请等待客服仲裁或补充结账证据。"
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: targetID,
|
||||
Type: "dispute",
|
||||
Title: title,
|
||||
Content: content,
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: userID,
|
||||
Type: "dispute",
|
||||
Title: "申诉已提交",
|
||||
Content: "申诉已进入待处理状态,客服仲裁后会通知双方。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
createdID = row.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.FindForUser(ctx, userID, createdID)
|
||||
}
|
||||
|
||||
func disputeType(input string, isCheckoutDispute bool) string {
|
||||
if isCheckoutDispute {
|
||||
return "checkout_dispute"
|
||||
}
|
||||
return input
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package dispute
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
)
|
||||
|
||||
type disputeRow struct {
|
||||
model.Dispute
|
||||
OrderNo string
|
||||
ListingNo string
|
||||
Title string
|
||||
}
|
||||
|
||||
func (row disputeRow) toDTO() DisputeDTO {
|
||||
return DisputeDTO{
|
||||
ID: row.ID,
|
||||
OrderID: row.OrderID,
|
||||
OrderNo: row.OrderNo,
|
||||
ListingNo: row.ListingNo,
|
||||
Title: row.Title,
|
||||
InitiatorID: row.InitiatorID,
|
||||
TargetUserID: row.TargetUserID,
|
||||
Type: row.Type,
|
||||
Status: row.Status,
|
||||
Description: row.Description,
|
||||
EvidenceURLS: row.EvidenceURLS,
|
||||
ArbitrationResult: row.ArbitrationResult,
|
||||
ArbitrationRemark: row.ArbitrationRemark,
|
||||
HandledBy: row.HandledBy,
|
||||
HandledAt: row.HandledAt,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toDTOs(rows []disputeRow) []DisputeDTO {
|
||||
items := make([]DisputeDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, row.toDTO())
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func marshalEvidence(urls []string) (datatypes.JSON, error) {
|
||||
if len(urls) == 0 {
|
||||
return datatypes.JSON([]byte("[]")), nil
|
||||
}
|
||||
raw, err := json.Marshal(urls)
|
||||
return datatypes.JSON(raw), err
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package dispute
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"hfb_sys/backend/internal/model"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
db := r.db.WithContext(ctx)
|
||||
conditions := db.Model(&model.Dispute{}).Where("initiator_id = ? OR target_user_id = ?", userID, userID)
|
||||
var total int64
|
||||
if err := conditions.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
var rows []disputeRow
|
||||
err := r.baseQuery(ctx).
|
||||
Where("d.initiator_id = ? OR d.target_user_id = ?", userID, userID).
|
||||
Order("d.id DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindForUser(ctx context.Context, userID uint64, id uint64) (*DisputeDTO, error) {
|
||||
var row disputeRow
|
||||
if err := r.baseQuery(ctx).
|
||||
Where("d.id = ? AND (d.initiator_id = ? OR d.target_user_id = ?)", id, userID, userID).
|
||||
First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := row.toDTO()
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListAdmin(ctx context.Context, page, pageSize int) (*PaginatedResult, error) {
|
||||
var total int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.Dispute{}).Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
var rows []disputeRow
|
||||
err := r.baseQuery(ctx).Order("d.id DESC").Offset(offset).Limit(pageSize).Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
||||
return r.db.WithContext(ctx).Table("disputes AS d").
|
||||
Select("d.*, o.order_no, l.listing_no, a.title").
|
||||
Joins("JOIN rental_orders AS o ON o.id = d.order_id").
|
||||
Joins("JOIN rental_listings AS l ON l.id = o.listing_id").
|
||||
Joins("JOIN game_accounts AS a ON a.id = o.account_id")
|
||||
}
|
||||
@@ -2,20 +2,9 @@ package dispute
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"hfb_sys/backend/internal/auditlog"
|
||||
"hfb_sys/backend/internal/model"
|
||||
"hfb_sys/backend/internal/modules/notification"
|
||||
"hfb_sys/backend/internal/modules/wallet"
|
||||
"hfb_sys/backend/pkg/money"
|
||||
|
||||
"gorm.io/datatypes"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Repository struct {
|
||||
@@ -23,6 +12,8 @@ type Repository struct {
|
||||
refundFunc RefundFunc
|
||||
}
|
||||
|
||||
// RefundFunc 由 payment 模块注入,避免 dispute 与 payment 形成循环依赖。
|
||||
|
||||
// RefundFunc 由 payment 模块注入,避免 dispute 与 payment 形成循环依赖。
|
||||
type RefundFunc func(ctx context.Context, orderID uint64, refundAmountCent int64, bizType string, remark string) (status string, err error)
|
||||
|
||||
@@ -41,551 +32,6 @@ func (r *Repository) SetRefundFunc(fn RefundFunc) {
|
||||
r.refundFunc = fn
|
||||
}
|
||||
|
||||
func (r *Repository) Create(ctx context.Context, userID uint64, orderID uint64, req CreateRequest) (*DisputeDTO, error) {
|
||||
var createdID uint64
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if order.RenterID != userID && order.OwnerID != userID {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
if order.Status == "completed" || order.Status == "cancelled" || order.Status == "closed" {
|
||||
return ErrInvalidDispute
|
||||
}
|
||||
isCheckoutDispute := order.Status == "pending_checkout_confirm" || order.Status == "pending_checkout_accept"
|
||||
var count int64
|
||||
if err := tx.Model(&model.Dispute{}).
|
||||
Where("order_id = ? AND status IN ?", order.ID, []string{"open", "processing"}).
|
||||
Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if count > 0 {
|
||||
return ErrDisputeExists
|
||||
}
|
||||
|
||||
targetID := order.OwnerID
|
||||
if userID == order.OwnerID {
|
||||
targetID = order.RenterID
|
||||
}
|
||||
evidence, err := marshalEvidence(req.EvidenceURLS)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
row := model.Dispute{
|
||||
OrderID: order.ID,
|
||||
InitiatorID: userID,
|
||||
TargetUserID: targetID,
|
||||
Type: disputeType(req.Type, isCheckoutDispute),
|
||||
Status: "open",
|
||||
Description: req.Description,
|
||||
EvidenceURLS: evidence,
|
||||
}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if isCheckoutDispute {
|
||||
now := time.Now()
|
||||
order.Status = "checkout_disputing"
|
||||
order.HandoffStatus = "checkout_disputed"
|
||||
order.SettlementStatus = "disputed"
|
||||
updates := map[string]any{
|
||||
"status": "disputed",
|
||||
"updated_at": now,
|
||||
}
|
||||
if userID == order.RenterID {
|
||||
updates["renter_rejected_at"] = now
|
||||
}
|
||||
if err := tx.Model(&model.OrderCheckout{}).
|
||||
Where("order_id = ? AND status IN ?", order.ID, []string{"submitted", "countered"}).
|
||||
Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
order.Status = "disputing"
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
disputeID := row.ID
|
||||
title := "订单进入申诉"
|
||||
content := "对方已发起申诉,请等待客服仲裁或补充沟通记录。"
|
||||
if isCheckoutDispute {
|
||||
title = "订单进入结账争议"
|
||||
content = "对方已发起结账争议,请等待客服仲裁或补充结账证据。"
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: targetID,
|
||||
Type: "dispute",
|
||||
Title: title,
|
||||
Content: content,
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: userID,
|
||||
Type: "dispute",
|
||||
Title: "申诉已提交",
|
||||
Content: "申诉已进入待处理状态,客服仲裁后会通知双方。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
createdID = row.ID
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r.FindForUser(ctx, userID, createdID)
|
||||
}
|
||||
|
||||
func (r *Repository) ListForUser(ctx context.Context, userID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
db := r.db.WithContext(ctx)
|
||||
conditions := db.Model(&model.Dispute{}).Where("initiator_id = ? OR target_user_id = ?", userID, userID)
|
||||
var total int64
|
||||
if err := conditions.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
var rows []disputeRow
|
||||
err := r.baseQuery(ctx).
|
||||
Where("d.initiator_id = ? OR d.target_user_id = ?", userID, userID).
|
||||
Order("d.id DESC").
|
||||
Offset(offset).Limit(pageSize).
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) FindForUser(ctx context.Context, userID uint64, id uint64) (*DisputeDTO, error) {
|
||||
var row disputeRow
|
||||
if err := r.baseQuery(ctx).
|
||||
Where("d.id = ? AND (d.initiator_id = ? OR d.target_user_id = ?)", id, userID, userID).
|
||||
First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := row.toDTO()
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
func (r *Repository) ListAdmin(ctx context.Context, page, pageSize int) (*PaginatedResult, error) {
|
||||
var total int64
|
||||
if err := r.db.WithContext(ctx).Model(&model.Dispute{}).Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
offset := (page - 1) * pageSize
|
||||
var rows []disputeRow
|
||||
err := r.baseQuery(ctx).Order("d.id DESC").Offset(offset).Limit(pageSize).Scan(&rows).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PaginatedResult{Items: toDTOs(rows), Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, req ArbitrateRequest, meta AuditMeta) (*DisputeDTO, error) {
|
||||
var refund *refundAction
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var row model.Dispute
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, id).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if row.Status != "open" && row.Status != "processing" {
|
||||
return ErrDisputeCannotHandle
|
||||
}
|
||||
var order model.RentalOrder
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, row.OrderID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var listing model.RentalListing
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
var account model.GameAccount
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
beforeOrderStatus := order.Status
|
||||
beforeHandoffStatus := order.HandoffStatus
|
||||
beforeSettlementStatus := order.SettlementStatus
|
||||
beforeListingStatus := listing.Status
|
||||
beforeAccountStatus := account.Status
|
||||
frozenBalance, err := renterFrozenBalance(tx, order.RenterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
settlement, err := buildArbitrationSettlement(order, req, frozenBalance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
row.Status = "resolved"
|
||||
row.ArbitrationResult = req.Result
|
||||
row.ArbitrationRemark = req.Remark
|
||||
row.HandledBy = &adminID
|
||||
row.HandledAt = &now
|
||||
order.Status = arbitrateOrderStatus(req.Result)
|
||||
order.SettlementStatus = "arbitrated"
|
||||
order.SettledAt = &now
|
||||
order.OwnerSettledAt = &now
|
||||
if order.HandoffStatus != "cancelled" && order.HandoffStatus != "returned" {
|
||||
order.HandoffStatus = "arbitrated"
|
||||
}
|
||||
if req.Result == "mark_abnormal" {
|
||||
listing.Status = "abnormal"
|
||||
listing.InTransaction = false
|
||||
listing.PublishedAt = nil
|
||||
account.Status = "abnormal"
|
||||
} else {
|
||||
// 仲裁会终结当前订单,账号统一归档下架,避免已结案账号重新出现在公开列表。
|
||||
listing.Status = "offline"
|
||||
listing.InTransaction = false
|
||||
listing.PublishedAt = nil
|
||||
account.Status = "offline"
|
||||
}
|
||||
if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil {
|
||||
return err
|
||||
}
|
||||
if settlement.RenterRefundAmountCent > 0 {
|
||||
action, err := r.prepareRefund(&order, settlement.RenterRefundAmountCent, "arbitration_refund", "仲裁退款原路退还")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
refund = action
|
||||
}
|
||||
if err := tx.Save(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&order).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&listing).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Save(&account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
handoffRecord := model.HandoffRecord{
|
||||
OrderID: order.ID,
|
||||
FromUserID: adminID,
|
||||
ToUserID: order.RenterID,
|
||||
Type: "admin_arbitration",
|
||||
Content: buildArbitrationHandoffContent(req, settlement),
|
||||
}
|
||||
if err := tx.Create(&handoffRecord).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
disputeID := row.ID
|
||||
if err := appendAuditLog(tx, adminID, "dispute.arbitrate", "dispute", row.ID, meta, map[string]any{
|
||||
"dispute_id": row.ID,
|
||||
"order_id": order.ID,
|
||||
"order_no": order.OrderNo,
|
||||
"result": req.Result,
|
||||
"remark": req.Remark,
|
||||
"input_amount_cent": req.AmountCent,
|
||||
"renter_refund_amount_cent": settlement.RenterRefundAmountCent,
|
||||
"owner_income_amount_cent": settlement.OwnerIncomeAmountCent,
|
||||
"deposit_deduct_amount_cent": settlement.DepositDeductAmountCent,
|
||||
"before_order_status": beforeOrderStatus,
|
||||
"after_order_status": order.Status,
|
||||
"before_handoff_status": beforeHandoffStatus,
|
||||
"after_handoff_status": order.HandoffStatus,
|
||||
"before_settlement_status": beforeSettlementStatus,
|
||||
"after_settlement_status": order.SettlementStatus,
|
||||
"before_listing_status": beforeListingStatus,
|
||||
"after_listing_status": listing.Status,
|
||||
"before_account_status": beforeAccountStatus,
|
||||
"after_account_status": account.Status,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := notification.Append(tx,
|
||||
notification.Entry{
|
||||
UserID: order.RenterID,
|
||||
Type: "arbitration",
|
||||
Title: "申诉仲裁已完成",
|
||||
Content: "客服已给出仲裁结果,请在订单和申诉记录中查看处理说明。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
notification.Entry{
|
||||
UserID: order.OwnerID,
|
||||
Type: "arbitration",
|
||||
Title: "申诉仲裁已完成",
|
||||
Content: "客服已给出仲裁结果,请在订单和申诉记录中查看处理说明。",
|
||||
BizType: "dispute",
|
||||
BizID: &disputeID,
|
||||
},
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.startRefundBestEffort(ctx, refund)
|
||||
var row disputeRow
|
||||
if err := r.baseQuery(ctx).Where("d.id = ?", id).First(&row).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := row.toDTO()
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
type arbitrationSettlement struct {
|
||||
Entries []wallet.Entry
|
||||
RenterRefundAmountCent int64
|
||||
OwnerIncomeAmountCent int64
|
||||
DepositDeductAmountCent int64
|
||||
}
|
||||
|
||||
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest, renterFrozenBalanceCent int64) (arbitrationSettlement, error) {
|
||||
rentAmountCent := order.RentAmountCent
|
||||
depositAmountCent := order.DepositAmountCent
|
||||
ownerRentAmountCent := order.OwnerRentAmountCent
|
||||
if ownerRentAmountCent <= 0 || ownerRentAmountCent > rentAmountCent {
|
||||
ownerRentAmountCent = rentAmountCent
|
||||
}
|
||||
totalCent := rentAmountCent + depositAmountCent
|
||||
settlement := arbitrationSettlement{}
|
||||
orderID := order.ID
|
||||
releaseFrozenAmountCent := money.MinCent(totalCent, renterFrozenBalanceCent)
|
||||
if releaseFrozenAmountCent > 0 {
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.RenterID,
|
||||
OrderID: &orderID,
|
||||
Direction: "out",
|
||||
AmountCent: releaseFrozenAmountCent,
|
||||
BalanceType: "frozen",
|
||||
BizType: "arbitration_release_frozen",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: "仲裁释放冻结金额",
|
||||
})
|
||||
}
|
||||
|
||||
addRenterRefund := func(amountCent int64, remark string) {
|
||||
if amountCent <= 0 {
|
||||
return
|
||||
}
|
||||
settlement.RenterRefundAmountCent += amountCent
|
||||
}
|
||||
addOwnerIncome := func(amountCent int64, remark string) {
|
||||
if amountCent <= 0 {
|
||||
return
|
||||
}
|
||||
settlement.OwnerIncomeAmountCent += amountCent
|
||||
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
||||
UserID: order.OwnerID,
|
||||
OrderID: &orderID,
|
||||
Direction: "in",
|
||||
AmountCent: amountCent,
|
||||
BalanceType: "available",
|
||||
BizType: "arbitration_owner_income",
|
||||
BizNo: order.OrderNo,
|
||||
Remark: remark,
|
||||
})
|
||||
}
|
||||
|
||||
switch req.Result {
|
||||
case "full_refund":
|
||||
addRenterRefund(totalCent, "仲裁全额退款")
|
||||
case "partial_refund":
|
||||
if req.AmountCent <= 0 || req.AmountCent > totalCent {
|
||||
return settlement, ErrInvalidDispute
|
||||
}
|
||||
addRenterRefund(req.AmountCent, "仲裁部分退款")
|
||||
addOwnerIncome(money.MinCent(totalCent-req.AmountCent, ownerRentAmountCent+depositAmountCent), "仲裁剩余金额结算给号主")
|
||||
case "release_deposit":
|
||||
addOwnerIncome(ownerRentAmountCent, "仲裁确认订单金额结算给号主")
|
||||
addRenterRefund(depositAmountCent, "仲裁释放押金给租客")
|
||||
case "deduct_deposit", "compensate_owner":
|
||||
deductAmountCent := req.AmountCent
|
||||
if deductAmountCent <= 0 {
|
||||
deductAmountCent = depositAmountCent
|
||||
}
|
||||
if deductAmountCent > depositAmountCent {
|
||||
return settlement, ErrInvalidDispute
|
||||
}
|
||||
settlement.DepositDeductAmountCent = deductAmountCent
|
||||
addOwnerIncome(ownerRentAmountCent+deductAmountCent, "仲裁订单金额及押金赔付结算给号主")
|
||||
addRenterRefund(depositAmountCent-deductAmountCent, "仲裁退回剩余押金给租客")
|
||||
case "order_close":
|
||||
// Only release frozen funds. No available-balance settlement happens in development mode.
|
||||
case "mark_abnormal":
|
||||
// 标记异常只释放冻结账务,后续由客服继续线下复核。
|
||||
default:
|
||||
return settlement, ErrInvalidDispute
|
||||
}
|
||||
return settlement, nil
|
||||
}
|
||||
|
||||
func (r *Repository) prepareRefund(order *model.RentalOrder, amountCent int64, bizType string, remark string) (*refundAction, error) {
|
||||
if amountCent <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if r.refundFunc == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
order.RefundStatus = "pending"
|
||||
order.RefundAmountCent = amountCent
|
||||
order.RefundedAt = nil
|
||||
return &refundAction{
|
||||
OrderID: order.ID,
|
||||
RefundAmountCent: amountCent,
|
||||
BizType: bizType,
|
||||
Remark: remark,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Repository) startRefundBestEffort(ctx context.Context, action *refundAction) {
|
||||
if action == nil || r.refundFunc == nil {
|
||||
return
|
||||
}
|
||||
_, _ = r.refundFunc(ctx, action.OrderID, action.RefundAmountCent, action.BizType, action.Remark)
|
||||
}
|
||||
|
||||
func renterFrozenBalance(tx *gorm.DB, renterID uint64) (int64, error) {
|
||||
var account model.WalletAccount
|
||||
err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("user_id = ?", renterID).
|
||||
First(&account).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, nil
|
||||
}
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return account.FrozenBalanceCent, nil
|
||||
}
|
||||
|
||||
func (r *Repository) baseQuery(ctx context.Context) *gorm.DB {
|
||||
return r.db.WithContext(ctx).Table("disputes AS d").
|
||||
Select("d.*, o.order_no, l.listing_no, a.title").
|
||||
Joins("JOIN rental_orders AS o ON o.id = d.order_id").
|
||||
Joins("JOIN rental_listings AS l ON l.id = o.listing_id").
|
||||
Joins("JOIN game_accounts AS a ON a.id = o.account_id")
|
||||
}
|
||||
|
||||
type disputeRow struct {
|
||||
model.Dispute
|
||||
OrderNo string
|
||||
ListingNo string
|
||||
Title string
|
||||
}
|
||||
|
||||
func (row disputeRow) toDTO() DisputeDTO {
|
||||
return DisputeDTO{
|
||||
ID: row.ID,
|
||||
OrderID: row.OrderID,
|
||||
OrderNo: row.OrderNo,
|
||||
ListingNo: row.ListingNo,
|
||||
Title: row.Title,
|
||||
InitiatorID: row.InitiatorID,
|
||||
TargetUserID: row.TargetUserID,
|
||||
Type: row.Type,
|
||||
Status: row.Status,
|
||||
Description: row.Description,
|
||||
EvidenceURLS: row.EvidenceURLS,
|
||||
ArbitrationResult: row.ArbitrationResult,
|
||||
ArbitrationRemark: row.ArbitrationRemark,
|
||||
HandledBy: row.HandledBy,
|
||||
HandledAt: row.HandledAt,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func toDTOs(rows []disputeRow) []DisputeDTO {
|
||||
items := make([]DisputeDTO, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, row.toDTO())
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func marshalEvidence(urls []string) (datatypes.JSON, error) {
|
||||
if len(urls) == 0 {
|
||||
return datatypes.JSON([]byte("[]")), nil
|
||||
}
|
||||
raw, err := json.Marshal(urls)
|
||||
return datatypes.JSON(raw), err
|
||||
}
|
||||
|
||||
func arbitrateOrderStatus(result string) string {
|
||||
switch result {
|
||||
case "full_refund", "partial_refund", "order_close":
|
||||
return "closed"
|
||||
case "mark_abnormal":
|
||||
return "abnormal"
|
||||
default:
|
||||
return "completed"
|
||||
}
|
||||
}
|
||||
|
||||
func buildArbitrationHandoffContent(req ArbitrateRequest, settlement arbitrationSettlement) string {
|
||||
content := fmt.Sprintf("客服仲裁结果:%s", arbitrationResultLabel(req.Result))
|
||||
if req.Remark != "" {
|
||||
content += "\n处理说明:" + req.Remark
|
||||
}
|
||||
if settlement.RenterRefundAmountCent > 0 {
|
||||
content += "\n退款给租客:" + money.FormatWithSymbol(settlement.RenterRefundAmountCent)
|
||||
}
|
||||
if settlement.OwnerIncomeAmountCent > 0 {
|
||||
content += "\n结算给号主:" + money.FormatWithSymbol(settlement.OwnerIncomeAmountCent)
|
||||
}
|
||||
if settlement.DepositDeductAmountCent > 0 {
|
||||
content += "\n押金扣除:" + money.FormatWithSymbol(settlement.DepositDeductAmountCent)
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func arbitrationResultLabel(result string) string {
|
||||
switch result {
|
||||
case "full_refund":
|
||||
return "全额退款"
|
||||
case "partial_refund":
|
||||
return "部分退款"
|
||||
case "deduct_deposit":
|
||||
return "扣押金"
|
||||
case "release_deposit":
|
||||
return "释放押金"
|
||||
case "compensate_owner":
|
||||
return "赔付号主"
|
||||
case "order_close":
|
||||
return "关闭订单"
|
||||
case "mark_abnormal":
|
||||
return "标记异常"
|
||||
default:
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
func disputeType(input string, isCheckoutDispute bool) string {
|
||||
if isCheckoutDispute {
|
||||
return "checkout_dispute"
|
||||
}
|
||||
return input
|
||||
}
|
||||
|
||||
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizType string, bizID uint64, meta AuditMeta, detail map[string]any) error {
|
||||
return auditlog.Append(tx, auditlog.Entry{
|
||||
ActorType: "admin",
|
||||
ActorID: actorID,
|
||||
Action: action,
|
||||
BizType: bizType,
|
||||
BizID: &bizID,
|
||||
Meta: meta,
|
||||
Detail: detail,
|
||||
})
|
||||
}
|
||||
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user