feat: add admin risk actions and arbitration settlement

This commit is contained in:
yml
2026-05-22 18:11:01 +08:00
parent dfe686ce0e
commit 55477d0ac6
26 changed files with 888 additions and 23 deletions
+8 -2
View File
@@ -32,6 +32,12 @@ type CreateRequest struct {
}
type ArbitrateRequest struct {
Result string `json:"result" binding:"required"`
Remark string `json:"remark" binding:"required"`
Result string `json:"result" binding:"required"`
Remark string `json:"remark" binding:"required"`
Amount float64 `json:"amount"`
}
type AuditMeta struct {
IP string
UserAgent string
}
+4 -1
View File
@@ -98,7 +98,10 @@ func (h *Handler) AdminArbitrate(c *gin.Context) {
response.BadRequest(c, "仲裁结果和备注不能为空")
return
}
item, err := h.service.Arbitrate(adminID, id, req)
item, err := h.service.Arbitrate(adminID, id, req, AuditMeta{
IP: c.ClientIP(),
UserAgent: c.GetHeader("User-Agent"),
})
if err != nil {
writeDisputeError(c, err)
return
+150 -3
View File
@@ -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)
}
+2 -2
View File
@@ -49,12 +49,12 @@ func (s *Service) ListAdmin() ([]DisputeDTO, error) {
return s.repo.ListAdmin()
}
func (s *Service) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest) (*DisputeDTO, error) {
func (s *Service) Arbitrate(adminID uint64, id uint64, req ArbitrateRequest, meta AuditMeta) (*DisputeDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if id == 0 || req.Result == "" || req.Remark == "" {
return nil, ErrInvalidDispute
}
return s.repo.Arbitrate(adminID, id, req)
return s.repo.Arbitrate(adminID, id, req, meta)
}
+9
View File
@@ -56,3 +56,12 @@ type AdminListQuery struct {
ReviewStatus string
Limit int
}
type AdminActionRequest struct {
Reason string `json:"reason" binding:"required"`
}
type AuditMeta struct {
IP string
UserAgent string
}
@@ -101,6 +101,50 @@ func (h *Handler) ListAdmin(c *gin.Context) {
response.OK(c, gin.H{"items": items})
}
func (h *Handler) FindAdmin(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
item, err := h.service.FindAdmin(id)
if err != nil {
writeListingError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) AdminOffline(c *gin.Context) {
h.adminAction(c, h.service.AdminOffline)
}
func (h *Handler) AdminMarkAbnormal(c *gin.Context) {
h.adminAction(c, h.service.AdminMarkAbnormal)
}
func (h *Handler) adminAction(c *gin.Context, fn func(uint64, uint64, AdminActionRequest, AuditMeta) (*ListingDTO, error)) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
id, ok := parseID(c)
if !ok {
return
}
var req AdminActionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "操作原因不能为空")
return
}
item, err := fn(adminID, id, req, AuditMeta{IP: c.ClientIP(), UserAgent: c.GetHeader("User-Agent")})
if err != nil {
writeListingError(c, err)
return
}
response.OK(c, item)
}
func (h *Handler) Approve(c *gin.Context) {
id, ok := parseID(c)
if !ok {
@@ -212,6 +256,15 @@ func currentUserID(c *gin.Context) (uint64, bool) {
return userID, ok
}
func currentAdminID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextAdminID)
if !ok {
return 0, false
}
adminID, ok := value.(uint64)
return adminID, ok
}
func parseID(c *gin.Context) (uint64, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
@@ -1,12 +1,14 @@
package listing
import (
"encoding/json"
"errors"
"time"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/notification"
"gorm.io/datatypes"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
@@ -162,6 +164,73 @@ func (r *Repository) ListAdmin(query AdminListQuery) ([]ListingDTO, error) {
return rowsToDTO(rows), nil
}
func (r *Repository) FindAdmin(listingID uint64) (*ListingDTO, error) {
return r.findDTO("l.id = ?", listingID)
}
func (r *Repository) AdminOffline(adminID uint64, listingID uint64, req AdminActionRequest, meta AuditMeta) (*ListingDTO, error) {
return r.adminUpdateStatus(adminID, listingID, req, meta, "offline", "offline", "listing.admin_offline", "商品已被后台下架", "你的租号商品已被后台下架,请查看原因后处理。")
}
func (r *Repository) AdminMarkAbnormal(adminID uint64, listingID uint64, req AdminActionRequest, meta AuditMeta) (*ListingDTO, error) {
return r.adminUpdateStatus(adminID, listingID, req, meta, "abnormal", "abnormal", "listing.mark_abnormal", "商品已被标记异常", "你的租号商品已被后台标记异常,请联系客服处理。")
}
func (r *Repository) adminUpdateStatus(adminID uint64, listingID uint64, req AdminActionRequest, meta AuditMeta, listingStatus string, accountStatus string, action string, title string, content string) (*ListingDTO, error) {
var dto *ListingDTO
err := r.db.Transaction(func(tx *gorm.DB) error {
listing, account, err := r.findForReviewUpdate(tx, listingID)
if err != nil {
return err
}
if listing.Status == "rented" {
return ErrListingLocked
}
beforeListingStatus := listing.Status
beforeAccountStatus := account.Status
beforeReviewReason := listing.ReviewReason
listing.Status = listingStatus
listing.ReviewReason = req.Reason
if listingStatus != "published" {
listing.PublishedAt = nil
}
account.Status = accountStatus
if err := tx.Save(account).Error; err != nil {
return err
}
if err := tx.Save(listing).Error; err != nil {
return err
}
if err := notification.Append(tx, notification.Entry{
UserID: listing.OwnerID,
Type: "listing_admin",
Title: title,
Content: content,
BizType: "listing",
BizID: &listingID,
}); err != nil {
return err
}
if err := appendAuditLog(tx, adminID, action, "listing", listing.ID, meta, map[string]any{
"listing_id": listing.ID,
"account_id": account.ID,
"owner_id": listing.OwnerID,
"reason": req.Reason,
"before_listing_status": beforeListingStatus,
"after_listing_status": listing.Status,
"before_account_status": beforeAccountStatus,
"after_account_status": account.Status,
"before_review_reason": beforeReviewReason,
"after_review_reason": listing.ReviewReason,
}); err != nil {
return err
}
dto = toDTO(*account, *listing)
return nil
})
return dto, err
}
func (r *Repository) Approve(listingID uint64) (*ListingDTO, error) {
var dto *ListingDTO
err := r.db.Transaction(func(tx *gorm.DB) error {
@@ -411,6 +480,24 @@ func toDTO(account model.GameAccount, listing model.RentalListing) *ListingDTO {
}
}
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)
}
@@ -59,6 +59,33 @@ func (s *Service) ListAdmin(query AdminListQuery) ([]ListingDTO, error) {
return s.repo.ListAdmin(query)
}
func (s *Service) FindAdmin(id uint64) (*ListingDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
return s.repo.FindAdmin(id)
}
func (s *Service) AdminOffline(adminID uint64, id uint64, req AdminActionRequest, meta AuditMeta) (*ListingDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if req.Reason == "" {
return nil, ErrInvalidInput
}
return s.repo.AdminOffline(adminID, id, req, meta)
}
func (s *Service) AdminMarkAbnormal(adminID uint64, id uint64, req AdminActionRequest, meta AuditMeta) (*ListingDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
}
if req.Reason == "" {
return nil, ErrInvalidInput
}
return s.repo.AdminMarkAbnormal(adminID, id, req, meta)
}
func (s *Service) Approve(id uint64) (*ListingDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable
+9
View File
@@ -45,6 +45,15 @@ type SubmitReturnRequest struct {
Content string `json:"content" binding:"required"`
}
type AdminActionRequest struct {
Reason string `json:"reason" binding:"required"`
}
type AuditMeta struct {
IP string
UserAgent string
}
type HandoffRecordDTO struct {
ID uint64 `json:"id"`
OrderID uint64 `json:"order_id"`
+39
View File
@@ -87,6 +87,36 @@ func (h *Handler) AdminHandoffRecords(c *gin.Context) {
response.OK(c, gin.H{"items": items})
}
func (h *Handler) AdminClose(c *gin.Context) {
h.adminAction(c, h.service.AdminClose, gin.H{"closed": true})
}
func (h *Handler) AdminMarkAbnormal(c *gin.Context) {
h.adminAction(c, h.service.AdminMarkAbnormal, gin.H{"abnormal": true})
}
func (h *Handler) adminAction(c *gin.Context, fn func(uint64, uint64, AdminActionRequest, AuditMeta) error, okData gin.H) {
adminID, ok := currentAdminID(c)
if !ok {
response.Unauthorized(c, "缺少管理员上下文")
return
}
id, ok := parseID(c)
if !ok {
return
}
var req AdminActionRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, "操作原因不能为空")
return
}
if err := fn(adminID, id, req, AuditMeta{IP: c.ClientIP(), UserAgent: c.GetHeader("User-Agent")}); err != nil {
writeOrderError(c, err)
return
}
response.OK(c, okData)
}
func (h *Handler) Detail(c *gin.Context) {
userID, ok := currentUserID(c)
if !ok {
@@ -229,6 +259,15 @@ func currentUserID(c *gin.Context) (uint64, bool) {
return userID, ok
}
func currentAdminID(c *gin.Context) (uint64, bool) {
value, ok := c.Get(middleware.ContextAdminID)
if !ok {
return 0, false
}
adminID, ok := value.(uint64)
return adminID, ok
}
func parseID(c *gin.Context) (uint64, bool) {
id, err := strconv.ParseUint(c.Param("id"), 10, 64)
if err != nil || id == 0 {
@@ -503,6 +503,152 @@ func (r *Repository) HandoffRecordsAdmin(orderID uint64) ([]HandoffRecordDTO, er
return items, nil
}
func (r *Repository) AdminClose(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
return r.db.Transaction(func(tx *gorm.DB) error {
order, listing, account, err := r.findOrderAssetsForAdminUpdate(tx, orderID)
if err != nil {
return err
}
if isTerminalStatus(order.Status) {
return ErrOrderCannotComplete
}
beforeOrderStatus := order.Status
beforeHandoffStatus := order.HandoffStatus
beforeSettlementStatus := order.SettlementStatus
beforeListingStatus := listing.Status
beforeAccountStatus := account.Status
now := time.Now()
order.Status = "closed"
order.HandoffStatus = "admin_closed"
order.SettlementStatus = "closed"
order.SettledAt = &now
listing.Status = "offline"
account.Status = "offline"
if err := wallet.AppendEntries(tx, wallet.Entry{
UserID: order.RenterID,
OrderID: &order.ID,
Direction: "out",
Amount: order.RentAmount + order.DepositAmount,
BalanceType: "frozen",
BizType: "admin_order_close",
BizNo: order.OrderNo,
Remark: "后台关闭订单释放模拟冻结金额",
}); err != nil {
return err
}
if err := notification.Append(tx,
notification.Entry{
UserID: order.RenterID,
Type: "order_admin",
Title: "订单已由客服关闭",
Content: "客服已关闭订单,模拟冻结金额已释放。原因:" + req.Reason,
BizType: "order",
BizID: &order.ID,
},
notification.Entry{
UserID: order.OwnerID,
Type: "order_admin",
Title: "订单已由客服关闭",
Content: "客服已关闭订单,关联商品已下架。原因:" + req.Reason,
BizType: "order",
BizID: &order.ID,
},
); err != nil {
return err
}
if err := appendAuditLog(tx, adminID, "order.admin_close", "order", order.ID, meta, map[string]any{
"order_id": order.ID,
"order_no": order.OrderNo,
"listing_id": order.ListingID,
"account_id": order.AccountID,
"reason": req.Reason,
"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 := tx.Save(order).Error; err != nil {
return err
}
if err := tx.Save(listing).Error; err != nil {
return err
}
return tx.Save(account).Error
})
}
func (r *Repository) AdminMarkAbnormal(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
return r.db.Transaction(func(tx *gorm.DB) error {
order, listing, account, err := r.findOrderAssetsForAdminUpdate(tx, orderID)
if err != nil {
return err
}
if isTerminalStatus(order.Status) {
return ErrOrderCannotComplete
}
beforeOrderStatus := order.Status
beforeHandoffStatus := order.HandoffStatus
beforeListingStatus := listing.Status
beforeAccountStatus := account.Status
order.Status = "abnormal"
order.HandoffStatus = "admin_abnormal"
listing.Status = "abnormal"
account.Status = "abnormal"
if err := notification.Append(tx,
notification.Entry{
UserID: order.RenterID,
Type: "order_admin",
Title: "订单已被标记异常",
Content: "客服已将订单标记为异常,请等待进一步处理。原因:" + req.Reason,
BizType: "order",
BizID: &order.ID,
},
notification.Entry{
UserID: order.OwnerID,
Type: "order_admin",
Title: "订单已被标记异常",
Content: "客服已将订单标记为异常,关联商品暂不可出租。原因:" + req.Reason,
BizType: "order",
BizID: &order.ID,
},
); err != nil {
return err
}
if err := appendAuditLog(tx, adminID, "order.mark_abnormal", "order", order.ID, meta, map[string]any{
"order_id": order.ID,
"order_no": order.OrderNo,
"listing_id": order.ListingID,
"account_id": order.AccountID,
"reason": req.Reason,
"before_order_status": beforeOrderStatus,
"after_order_status": order.Status,
"before_handoff_status": beforeHandoffStatus,
"after_handoff_status": order.HandoffStatus,
"before_listing_status": beforeListingStatus,
"after_listing_status": listing.Status,
"before_account_status": beforeAccountStatus,
"after_account_status": account.Status,
}); err != nil {
return err
}
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
})
}
func (r *Repository) FindForUser(userID uint64, orderID uint64) (*OrderDTO, error) {
var row orderRow
if err := r.baseQuery().
@@ -514,6 +660,26 @@ func (r *Repository) FindForUser(userID uint64, orderID uint64) (*OrderDTO, erro
return &dto, nil
}
func (r *Repository) findOrderAssetsForAdminUpdate(tx *gorm.DB, orderID uint64) (*model.RentalOrder, *model.RentalListing, *model.GameAccount, error) {
var order model.RentalOrder
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&order, orderID).Error; err != nil {
return nil, nil, nil, err
}
var listing model.RentalListing
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil {
return nil, nil, nil, err
}
var account model.GameAccount
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil {
return nil, nil, nil, err
}
return &order, &listing, &account, nil
}
func isTerminalStatus(status string) bool {
return status == "completed" || status == "cancelled" || status == "closed"
}
func (r *Repository) findHandoffRecord(id uint64) (*HandoffRecordDTO, error) {
var record model.HandoffRecord
if err := r.db.First(&record, id).Error; err != nil {
@@ -614,6 +780,24 @@ func newOrderNo() (string, error) {
return "RO" + strconv.FormatInt(time.Now().UnixNano(), 10) + hex.EncodeToString(buf), nil
}
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)
}
+20
View File
@@ -109,6 +109,26 @@ func (s *Service) HandoffRecordsAdmin(orderID uint64) ([]HandoffRecordDTO, error
return s.repo.HandoffRecordsAdmin(orderID)
}
func (s *Service) AdminClose(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
if s.repo == nil {
return ErrDependencyUnavailable
}
if orderID == 0 || req.Reason == "" {
return ErrOrderCannotComplete
}
return s.repo.AdminClose(adminID, orderID, req, meta)
}
func (s *Service) AdminMarkAbnormal(adminID uint64, orderID uint64, req AdminActionRequest, meta AuditMeta) error {
if s.repo == nil {
return ErrDependencyUnavailable
}
if orderID == 0 || req.Reason == "" {
return ErrOrderCannotComplete
}
return s.repo.AdminMarkAbnormal(adminID, orderID, req, meta)
}
func (s *Service) FindForUser(userID uint64, orderID uint64) (*OrderDTO, error) {
if s.repo == nil {
return nil, ErrDependencyUnavailable