|
|
|
@@ -7,6 +7,7 @@ import (
|
|
|
|
|
|
|
|
|
|
"hfb_sys/backend/internal/model"
|
|
|
|
|
"hfb_sys/backend/internal/modules/notification"
|
|
|
|
|
"hfb_sys/backend/internal/modules/wallet"
|
|
|
|
|
|
|
|
|
|
"gorm.io/datatypes"
|
|
|
|
|
"gorm.io/gorm"
|
|
|
|
@@ -130,7 +131,7 @@ func (r *Repository) ListAdmin() ([]DisputeDTO, error) {
|
|
|
|
|
return toDTOs(rows), nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest) (*DisputeDTO, error) {
|
|
|
|
|
func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest, meta AuditMeta) (*DisputeDTO, error) {
|
|
|
|
|
err := r.db.Transaction(func(tx *gorm.DB) error {
|
|
|
|
|
var row model.Dispute
|
|
|
|
|
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&row, id).Error; err != nil {
|
|
|
|
@@ -151,6 +152,15 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest)
|
|
|
|
|
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
|
|
|
|
|
settlement, err := buildArbitrationSettlement(order, req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
now := time.Now()
|
|
|
|
|
row.Status = "resolved"
|
|
|
|
|
row.ArbitrationResult = req.Result
|
|
|
|
@@ -164,8 +174,16 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest)
|
|
|
|
|
if order.HandoffStatus != "cancelled" && order.HandoffStatus != "returned" {
|
|
|
|
|
order.HandoffStatus = "arbitrated"
|
|
|
|
|
}
|
|
|
|
|
listing.Status = "published"
|
|
|
|
|
account.Status = "published"
|
|
|
|
|
if req.Result == "order_close" {
|
|
|
|
|
listing.Status = "offline"
|
|
|
|
|
account.Status = "offline"
|
|
|
|
|
} else {
|
|
|
|
|
listing.Status = "published"
|
|
|
|
|
account.Status = "published"
|
|
|
|
|
}
|
|
|
|
|
if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
if err := tx.Save(&row).Error; err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
@@ -179,6 +197,29 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest)
|
|
|
|
|
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": req.Amount,
|
|
|
|
|
"renter_refund_amount": settlement.RenterRefundAmount,
|
|
|
|
|
"owner_income_amount": settlement.OwnerIncomeAmount,
|
|
|
|
|
"deposit_deduct_amount": settlement.DepositDeductAmount,
|
|
|
|
|
"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,
|
|
|
|
@@ -212,6 +253,94 @@ func (r *Repository) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest)
|
|
|
|
|
return &dto, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type arbitrationSettlement struct {
|
|
|
|
|
Entries []wallet.Entry
|
|
|
|
|
RenterRefundAmount float64
|
|
|
|
|
OwnerIncomeAmount float64
|
|
|
|
|
DepositDeductAmount float64
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildArbitrationSettlement(order model.RentalOrder, req ArbitrateRequest) (arbitrationSettlement, error) {
|
|
|
|
|
total := order.RentAmount + order.DepositAmount
|
|
|
|
|
settlement := arbitrationSettlement{}
|
|
|
|
|
orderID := order.ID
|
|
|
|
|
if total > 0 {
|
|
|
|
|
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "out",
|
|
|
|
|
Amount: total,
|
|
|
|
|
BalanceType: "frozen",
|
|
|
|
|
BizType: "arbitration_release_frozen",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: "仲裁释放开发态模拟冻结金额",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
addRenterRefund := func(amount float64, remark string) {
|
|
|
|
|
if amount <= 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
settlement.RenterRefundAmount += amount
|
|
|
|
|
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
|
|
|
|
UserID: order.RenterID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "in",
|
|
|
|
|
Amount: amount,
|
|
|
|
|
BalanceType: "available",
|
|
|
|
|
BizType: "arbitration_renter_refund",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: remark,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
addOwnerIncome := func(amount float64, remark string) {
|
|
|
|
|
if amount <= 0 {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
settlement.OwnerIncomeAmount += amount
|
|
|
|
|
settlement.Entries = append(settlement.Entries, wallet.Entry{
|
|
|
|
|
UserID: order.OwnerID,
|
|
|
|
|
OrderID: &orderID,
|
|
|
|
|
Direction: "in",
|
|
|
|
|
Amount: amount,
|
|
|
|
|
BalanceType: "available",
|
|
|
|
|
BizType: "arbitration_owner_income",
|
|
|
|
|
BizNo: order.OrderNo,
|
|
|
|
|
Remark: remark,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch req.Result {
|
|
|
|
|
case "full_refund":
|
|
|
|
|
addRenterRefund(total, "仲裁全额退款")
|
|
|
|
|
case "partial_refund":
|
|
|
|
|
if req.Amount <= 0 || req.Amount > total {
|
|
|
|
|
return settlement, ErrInvalidDispute
|
|
|
|
|
}
|
|
|
|
|
addRenterRefund(req.Amount, "仲裁部分退款")
|
|
|
|
|
addOwnerIncome(total-req.Amount, "仲裁剩余金额结算给号主")
|
|
|
|
|
case "release_deposit":
|
|
|
|
|
addOwnerIncome(order.RentAmount, "仲裁确认租金结算给号主")
|
|
|
|
|
addRenterRefund(order.DepositAmount, "仲裁释放押金给租客")
|
|
|
|
|
case "deduct_deposit", "compensate_owner":
|
|
|
|
|
deductAmount := req.Amount
|
|
|
|
|
if deductAmount <= 0 {
|
|
|
|
|
deductAmount = order.DepositAmount
|
|
|
|
|
}
|
|
|
|
|
if deductAmount > order.DepositAmount {
|
|
|
|
|
return settlement, ErrInvalidDispute
|
|
|
|
|
}
|
|
|
|
|
settlement.DepositDeductAmount = deductAmount
|
|
|
|
|
addOwnerIncome(order.RentAmount+deductAmount, "仲裁租金及押金赔付结算给号主")
|
|
|
|
|
addRenterRefund(order.DepositAmount-deductAmount, "仲裁退回剩余押金给租客")
|
|
|
|
|
case "order_close":
|
|
|
|
|
// Only release frozen funds. No available-balance settlement happens in development mode.
|
|
|
|
|
default:
|
|
|
|
|
return settlement, ErrInvalidDispute
|
|
|
|
|
}
|
|
|
|
|
return settlement, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (r *Repository) baseQuery() *gorm.DB {
|
|
|
|
|
return r.db.Table("disputes AS d").
|
|
|
|
|
Select("d.*, o.order_no, a.title").
|
|
|
|
@@ -271,6 +400,24 @@ func arbitrateOrderStatus(result string) string {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func appendAuditLog(tx *gorm.DB, actorID uint64, action string, bizType string, bizID uint64, meta AuditMeta, detail map[string]any) error {
|
|
|
|
|
raw, err := json.Marshal(detail)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
row := model.AuditLog{
|
|
|
|
|
ActorType: "admin",
|
|
|
|
|
ActorID: actorID,
|
|
|
|
|
Action: action,
|
|
|
|
|
BizType: bizType,
|
|
|
|
|
BizID: &bizID,
|
|
|
|
|
IP: meta.IP,
|
|
|
|
|
UserAgent: meta.UserAgent,
|
|
|
|
|
Detail: datatypes.JSON(raw),
|
|
|
|
|
}
|
|
|
|
|
return tx.Create(&row).Error
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func IsNotFound(err error) bool {
|
|
|
|
|
return errors.Is(err, gorm.ErrRecordNotFound)
|
|
|
|
|
}
|
|
|
|
|