package order import ( "crypto/rand" "encoding/hex" "encoding/json" "errors" "strconv" "time" "hfb_sys/backend/internal/model" "hfb_sys/backend/internal/modules/notification" "hfb_sys/backend/internal/modules/wallet" "gorm.io/datatypes" "gorm.io/gorm" "gorm.io/gorm/clause" ) type Repository struct { db *gorm.DB } const defaultPendingPaymentTimeoutMinutes = 15 func NewRepository(db *gorm.DB) *Repository { return &Repository{db: db} } func (r *Repository) Create(renterID uint64, req CreateRequest) (*OrderDTO, error) { var createdID uint64 err := r.db.Transaction(func(tx *gorm.DB) error { var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, req.ListingID).Error; err != nil { return err } if listing.Status != "published" || listing.ReviewStatus != "approved" || listing.InTransaction { return ErrListingUnavailable } if listing.OwnerID == renterID { return ErrCannotRentOwnListing } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, listing.AccountID).Error; err != nil { return err } snapshot, err := makeAccountSnapshot(account) if err != nil { return err } orderNo, err := newOrderNo() if err != nil { return err } rentHours := internalOrderHours order := model.RentalOrder{ OrderNo: orderNo, ListingID: listing.ID, AccountID: listing.AccountID, OwnerID: listing.OwnerID, RenterID: renterID, RentHours: rentHours, RentAmount: listing.PriceHourly * float64(rentHours), DepositAmount: listing.DepositAmount, PlatformFee: 0, AccountSnapshot: snapshot, Status: "pending_payment", HandoffStatus: "none", SettlementStatus: "unsettled", } if err := tx.Create(&order).Error; err != nil { return err } orderID := order.ID if err := notification.Append(tx, notification.Entry{ UserID: order.RenterID, Type: "order", Title: "订单已创建", Content: "订单已创建,请在有效时间内完成支付。", BizType: "order", BizID: &orderID, }, ); err != nil { return err } listing.InTransaction = true if err := tx.Save(&listing).Error; err != nil { return err } createdID = order.ID return nil }) if err != nil { return nil, err } return r.FindForUser(renterID, createdID) } func (r *Repository) Pay(userID uint64, orderID uint64) error { return r.db.Transaction(func(tx *gorm.DB) error { var order model.RentalOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("id = ? AND renter_id = ?", orderID, userID). First(&order).Error; err != nil { return err } if order.Status != "pending_payment" { return ErrOrderCannotPay } timeoutMinutes := pendingPaymentTimeoutMinutes(tx) if order.CreatedAt.Before(time.Now().Add(-time.Duration(timeoutMinutes) * time.Minute)) { return ErrOrderCannotPay } var listing model.RentalListing if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&listing, order.ListingID).Error; err != nil { return err } if listing.Status != "published" || listing.ReviewStatus != "approved" || !listing.InTransaction { return ErrListingUnavailable } var account model.GameAccount if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&account, order.AccountID).Error; err != nil { return err } total := order.RentAmount + order.DepositAmount if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: order.RenterID, OrderID: &order.ID, Direction: "out", Amount: total, BalanceType: "available", BizType: "order_pay", BizNo: order.OrderNo, Remark: "订单支付扣减可用余额", }, wallet.Entry{ UserID: order.RenterID, OrderID: &order.ID, Direction: "in", Amount: total, BalanceType: "frozen", BizType: "order_lock", BizNo: order.OrderNo, Remark: "订单支付冻结租金和押金", }, ); err != nil { if errors.Is(err, wallet.ErrInsufficientBalance) { return ErrInsufficientBalance } return err } order.Status = "pending_handoff" order.HandoffStatus = "pending_owner" listing.Status = "rented" account.Status = "rented" orderID := order.ID if err := notification.Append(tx, notification.Entry{ UserID: order.OwnerID, Type: "order", Title: "收到新的租号订单", Content: "租客已完成支付,请尽快提交交接说明。", BizType: "order", BizID: &orderID, }, notification.Entry{ UserID: order.RenterID, Type: "order", Title: "订单支付成功", Content: "支付金额已冻结,等待号主提交交接说明。", BizType: "order", BizID: &orderID, }, ); 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) Cancel(userID uint64, orderID uint64) error { return r.db.Transaction(func(tx *gorm.DB) error { var order model.RentalOrder if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}). Where("id = ? AND renter_id = ?", orderID, userID). First(&order).Error; err != nil { return err } if order.Status != "pending_payment" && order.Status != "pending_handoff" { return ErrOrderCannotCancel } 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 } beforeStatus := order.Status order.Status = "cancelled" order.HandoffStatus = "cancelled" orderID := order.ID if beforeStatus == "pending_handoff" { total := order.RentAmount + order.DepositAmount if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: order.RenterID, OrderID: &orderID, Direction: "out", Amount: total, BalanceType: "frozen", BizType: "order_cancel", BizNo: order.OrderNo, Remark: "取消订单释放冻结金额", }, wallet.Entry{ UserID: order.RenterID, OrderID: &orderID, Direction: "in", Amount: total, BalanceType: "available", BizType: "order_cancel_refund", BizNo: order.OrderNo, Remark: "取消订单退回可用余额", }, ); err != nil { return err } } if err := notification.Append(tx, notification.Entry{ UserID: order.OwnerID, Type: "order", Title: "订单已取消", Content: "租客已取消订单,账号已重新释放。", BizType: "order", BizID: &orderID, }, notification.Entry{ UserID: order.RenterID, Type: "order", Title: "订单取消成功", Content: "订单已取消,相关金额已释放。", BizType: "order", BizID: &orderID, }, ); err != nil { return err } listing.Status = "published" listing.InTransaction = false account.Status = "published" 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) SubmitHandoff(userID uint64, orderID uint64, req SubmitHandoffRequest) (*HandoffRecordDTO, error) { var recordID uint64 err := r.db.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.OwnerID != userID { return ErrPermissionDenied } if order.Status != "pending_handoff" || order.HandoffStatus != "pending_owner" { return ErrOrderCannotHandoff } record := model.HandoffRecord{ OrderID: order.ID, FromUserID: order.OwnerID, ToUserID: order.RenterID, Type: "owner_handoff", Content: req.Content, } if err := tx.Create(&record).Error; err != nil { return err } order.HandoffStatus = "pending_renter_confirm" orderID := order.ID if err := notification.Append(tx, notification.Entry{ UserID: order.RenterID, Type: "handoff", Title: "号主已提交交接说明", Content: "请查看交接记录,确认账号可正常登录后点击确认收号。", BizType: "order", BizID: &orderID, }); err != nil { return err } if err := tx.Save(&order).Error; err != nil { return err } recordID = record.ID return nil }) if err != nil { return nil, err } return r.findHandoffRecord(recordID) } func (r *Repository) ConfirmReceive(userID uint64, orderID uint64) error { return r.db.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 { return ErrPermissionDenied } if order.Status != "pending_handoff" || order.HandoffStatus != "pending_renter_confirm" { return ErrOrderCannotReceive } now := time.Now() if err := tx.Model(&model.HandoffRecord{}). Where("order_id = ? AND type = ?", order.ID, "owner_handoff"). Update("confirmed_by_renter_at", now).Error; err != nil { return err } order.Status = "renting" order.HandoffStatus = "received" order.RentStartAt = &now rentEnd := now.Add(time.Duration(order.RentHours) * time.Hour) order.RentEndAt = &rentEnd orderID := order.ID if err := notification.Append(tx, notification.Entry{ UserID: order.OwnerID, Type: "handoff", Title: "租客已确认收号", Content: "订单已进入使用中。", BizType: "order", BizID: &orderID, }); err != nil { return err } return tx.Save(&order).Error }) } func (r *Repository) HandoffRecords(userID uint64, orderID uint64) ([]HandoffRecordDTO, error) { var order model.RentalOrder if err := r.db.Where("id = ? AND (renter_id = ? OR owner_id = ?)", orderID, userID, userID).First(&order).Error; err != nil { return nil, err } var records []model.HandoffRecord if err := r.db.Where("order_id = ?", orderID).Order("id ASC").Find(&records).Error; err != nil { return nil, err } items := make([]HandoffRecordDTO, 0, len(records)) for _, record := range records { items = append(items, toHandoffDTO(record)) } return items, nil } func (r *Repository) SubmitReturn(userID uint64, orderID uint64, req SubmitReturnRequest) (*HandoffRecordDTO, error) { var recordID uint64 err := r.db.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 { return ErrPermissionDenied } if (order.Status != "renting" && order.Status != "overdue") || (order.HandoffStatus != "received" && order.HandoffStatus != "return_overdue") { return ErrOrderCannotReturn } now := time.Now() record := model.HandoffRecord{ OrderID: order.ID, FromUserID: order.RenterID, ToUserID: order.OwnerID, Type: "renter_return", Content: req.Content, } if err := tx.Create(&record).Error; err != nil { return err } order.Status = "pending_return_confirm" order.HandoffStatus = "pending_owner_return_confirm" order.RentEndAt = &now orderID := order.ID if err := notification.Append(tx, notification.Entry{ UserID: order.OwnerID, Type: "return", Title: "租客已提交归还", Content: "请检查账号状态,确认无误后完成订单。", BizType: "order", BizID: &orderID, }); err != nil { return err } if err := tx.Save(&order).Error; err != nil { return err } recordID = record.ID return nil }) if err != nil { return nil, err } return r.findHandoffRecord(recordID) } func (r *Repository) ConfirmReturn(userID uint64, orderID uint64) error { return r.db.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.OwnerID != userID { return ErrPermissionDenied } if order.Status != "pending_return_confirm" || order.HandoffStatus != "pending_owner_return_confirm" { return ErrOrderCannotComplete } now := time.Now() if err := tx.Model(&model.HandoffRecord{}). Where("order_id = ? AND type = ?", order.ID, "renter_return"). Update("confirmed_by_owner_at", now).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 } order.Status = "completed" order.HandoffStatus = "returned" order.SettlementStatus = "settled" order.SettledAt = &now order.OwnerSettledAt = &now orderID := order.ID if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: order.RenterID, OrderID: &orderID, Direction: "out", Amount: order.RentAmount + order.DepositAmount, BalanceType: "frozen", BizType: "order_settle", BizNo: order.OrderNo, Remark: "订单完成释放模拟冻结金额", }, wallet.Entry{ UserID: order.OwnerID, OrderID: &orderID, Direction: "in", Amount: order.RentAmount, BalanceType: "available", BizType: "owner_income", BizNo: order.OrderNo, Remark: "订单完成模拟结算订单金额", }, wallet.Entry{ UserID: order.RenterID, OrderID: &orderID, Direction: "in", Amount: order.DepositAmount, BalanceType: "available", BizType: "deposit_release", BizNo: order.OrderNo, Remark: "订单完成模拟退回押金", }, ); err != nil { return err } if err := notification.Append(tx, notification.Entry{ UserID: order.RenterID, Type: "settlement", Title: "订单已完成", Content: "号主已确认归还,模拟押金已退回。", BizType: "order", BizID: &orderID, }, notification.Entry{ UserID: order.OwnerID, Type: "settlement", Title: "订单已完成", Content: "订单已完成,模拟订单金额已入账。", BizType: "order", BizID: &orderID, }, ); err != nil { return err } listing.Status = "published" listing.InTransaction = false account.Status = "published" 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) ListForUser(userID uint64) ([]OrderDTO, error) { var rows []orderRow err := r.baseQuery(). Where("o.renter_id = ?", userID). Order("o.id DESC"). Scan(&rows).Error if err != nil { return nil, err } items := make([]OrderDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return items, nil } func (r *Repository) ListAdmin() ([]OrderDTO, error) { var rows []orderRow err := r.adminQuery(). Order("o.id DESC"). Limit(200). Scan(&rows).Error if err != nil { return nil, err } items := make([]OrderDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO()) } return items, nil } func (r *Repository) FindAdmin(orderID uint64) (*OrderDTO, error) { var row orderRow if err := r.adminQuery().Where("o.id = ?", orderID).First(&row).Error; err != nil { return nil, err } dto := row.toDTO() return &dto, nil } func (r *Repository) HandoffRecordsAdmin(orderID uint64) ([]HandoffRecordDTO, error) { var order model.RentalOrder if err := r.db.First(&order, orderID).Error; err != nil { return nil, err } var records []model.HandoffRecord if err := r.db.Where("order_id = ?", orderID).Order("id ASC").Find(&records).Error; err != nil { return nil, err } items := make([]HandoffRecordDTO, 0, len(records)) for _, record := range records { items = append(items, toHandoffDTO(record)) } 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" listing.InTransaction = false account.Status = "offline" if beforeOrderStatus != "pending_payment" { total := order.RentAmount + order.DepositAmount if err := wallet.AppendEntries(tx, wallet.Entry{ UserID: order.RenterID, OrderID: &order.ID, Direction: "out", Amount: total, BalanceType: "frozen", BizType: "admin_order_close", BizNo: order.OrderNo, Remark: "后台关闭订单释放冻结金额", }, wallet.Entry{ UserID: order.RenterID, OrderID: &order.ID, Direction: "in", Amount: total, BalanceType: "available", BizType: "admin_order_close_refund", 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" listing.InTransaction = false 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(). Where("o.id = ? AND (o.renter_id = ? OR o.owner_id = ?)", orderID, userID, userID). First(&row).Error; err != nil { return nil, err } dto := row.toDTO() 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 { return nil, err } dto := toHandoffDTO(record) return &dto, nil } func pendingPaymentTimeoutMinutes(tx *gorm.DB) int { var row model.SystemConfig err := tx.Where("`key` = ?", "order.pending_payment_timeout_minutes").First(&row).Error if err != nil { return defaultPendingPaymentTimeoutMinutes } value, err := strconv.Atoi(row.Value) if err != nil || value <= 0 { return defaultPendingPaymentTimeoutMinutes } return value } func (r *Repository) baseQuery() *gorm.DB { return r.db.Table("rental_orders AS o"). Select("o.*, a.title, a.server_region, a.login_platform"). Joins("JOIN game_accounts AS a ON a.id = o.account_id") } func (r *Repository) adminQuery() *gorm.DB { return r.db.Table("rental_orders AS o"). Select("o.*, a.title, a.server_region, a.login_platform, owner.phone AS owner_phone, renter.phone AS renter_phone"). Joins("JOIN game_accounts AS a ON a.id = o.account_id"). Joins("JOIN users AS owner ON owner.id = o.owner_id"). Joins("JOIN users AS renter ON renter.id = o.renter_id") } type orderRow struct { model.RentalOrder Title string ServerRegion string LoginPlatform string OwnerPhone string RenterPhone string } func (row orderRow) toDTO() OrderDTO { return OrderDTO{ ID: row.ID, OrderNo: row.OrderNo, ListingID: row.ListingID, AccountID: row.AccountID, OwnerID: row.OwnerID, RenterID: row.RenterID, OwnerPhone: row.OwnerPhone, RenterPhone: row.RenterPhone, Title: row.Title, ServerRegion: row.ServerRegion, LoginPlatform: row.LoginPlatform, RentStartAt: row.RentStartAt, RentEndAt: row.RentEndAt, RentAmount: row.RentAmount, DepositAmount: row.DepositAmount, PlatformFee: row.PlatformFee, AccountSnapshot: row.AccountSnapshot, Status: row.Status, HandoffStatus: row.HandoffStatus, SettlementStatus: row.SettlementStatus, CreatedAt: row.CreatedAt, UpdatedAt: row.UpdatedAt, } } func toHandoffDTO(record model.HandoffRecord) HandoffRecordDTO { return HandoffRecordDTO{ ID: record.ID, OrderID: record.OrderID, FromUserID: record.FromUserID, ToUserID: record.ToUserID, Type: record.Type, Content: record.Content, ConfirmedByRenterAt: record.ConfirmedByRenterAt, ConfirmedByOwnerAt: record.ConfirmedByOwnerAt, CreatedAt: record.CreatedAt, } } func makeAccountSnapshot(account model.GameAccount) (datatypes.JSON, error) { payload := map[string]any{ "account_id": account.ID, "title": account.Title, "game_name": account.GameName, "server_region": account.ServerRegion, "login_platform": account.LoginPlatform, "rank_level": account.RankLevel, "haf_coin_amount": account.HafCoinAmount, "asset_summary": account.AssetSummary, "season_tags": account.SeasonTags, "screenshot_urls": account.ScreenshotURLS, "snapshot_version": 1, } raw, err := json.Marshal(payload) return datatypes.JSON(raw), err } func newOrderNo() (string, error) { buf := make([]byte, 4) if _, err := rand.Read(buf); err != nil { return "", err } 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) }