feat: add admin risk actions and arbitration settlement
This commit is contained in:
@@ -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"`
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user