账号上下架增加统计

This commit is contained in:
yml2213
2026-07-11 23:37:24 +08:00
parent 7614c0980e
commit 42b250cd2c
17 changed files with 792 additions and 27 deletions
+128
View File
@@ -0,0 +1,128 @@
package listingstatus
import (
"strings"
"time"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/timeutil"
"gorm.io/gorm"
)
// 统计用的事件类型(与 to_status 对齐时优先用这些标准值)。
const (
EventPublished = "published"
EventOffline = "offline"
EventRented = "rented"
EventCompleted = "completed"
EventSealed = "sealed"
EventAbnormal = "abnormal"
)
// 来源
const (
SourceSeller = "seller"
SourceAdmin = "admin"
SourceReview = "review"
SourceOrder = "order"
SourcePickup = "pickup"
SourceDispute = "dispute"
SourceSystem = "system"
)
// 操作者类型
const (
ActorUser = "user"
ActorAdmin = "admin"
ActorSystem = "system"
)
// Entry 单次状态变更事件。
type Entry struct {
ListingID uint64
OwnerID uint64
EventType string
FromStatus string
ToStatus string
Source string
ActorType string
ActorID uint64
Remark string
// CreatedAt 为空时使用上海时间当前时刻。
CreatedAt time.Time
}
// Append 写入一条状态事件。from==to 或 event 无法识别时跳过。
func Append(tx *gorm.DB, entry Entry) error {
if tx == nil {
return nil
}
from := strings.TrimSpace(entry.FromStatus)
to := strings.TrimSpace(entry.ToStatus)
if to == "" {
to = strings.TrimSpace(entry.EventType)
}
if to == "" || from == to {
return nil
}
eventType := strings.TrimSpace(entry.EventType)
if eventType == "" {
eventType = normalizeEventType(to)
}
if eventType == "" {
return nil
}
createdAt := entry.CreatedAt
if createdAt.IsZero() {
createdAt = timeutil.ShanghaiNow()
}
row := model.ListingStatusEvent{
ListingID: entry.ListingID,
OwnerID: entry.OwnerID,
EventType: eventType,
FromStatus: from,
ToStatus: to,
Source: strings.TrimSpace(entry.Source),
ActorType: strings.TrimSpace(entry.ActorType),
ActorID: entry.ActorID,
Remark: strings.TrimSpace(entry.Remark),
CreatedAt: createdAt,
}
return tx.Create(&row).Error
}
// AppendTransition 根据 listing 变更前后状态写事件。
func AppendTransition(
tx *gorm.DB,
listing *model.RentalListing,
fromStatus string,
source string,
actorType string,
actorID uint64,
remark string,
) error {
if listing == nil {
return nil
}
return Append(tx, Entry{
ListingID: listing.ID,
OwnerID: listing.OwnerID,
EventType: normalizeEventType(listing.Status),
FromStatus: fromStatus,
ToStatus: listing.Status,
Source: source,
ActorType: actorType,
ActorID: actorID,
Remark: remark,
})
}
func normalizeEventType(status string) string {
switch strings.TrimSpace(status) {
case EventPublished, EventOffline, EventRented, EventCompleted, EventSealed, EventAbnormal:
return status
default:
return ""
}
}
@@ -0,0 +1,73 @@
package listingstatus
import (
"testing"
"time"
"hfb_sys/backend/internal/model"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
func setupTestDB(t *testing.T) *gorm.DB {
t.Helper()
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),
})
if err != nil {
t.Fatalf("open sqlite: %v", err)
}
if err := db.AutoMigrate(&model.ListingStatusEvent{}); err != nil {
t.Fatalf("migrate: %v", err)
}
return db
}
func TestAppendSkipsUnchangedOrUnknown(t *testing.T) {
db := setupTestDB(t)
if err := Append(db, Entry{ListingID: 1, FromStatus: "published", ToStatus: "published", EventType: EventPublished}); err != nil {
t.Fatalf("same status should skip: %v", err)
}
if err := Append(db, Entry{ListingID: 1, FromStatus: "draft", ToStatus: "draft", EventType: ""}); err != nil {
t.Fatalf("unknown should skip: %v", err)
}
var count int64
if err := db.Model(&model.ListingStatusEvent{}).Count(&count).Error; err != nil {
t.Fatalf("count: %v", err)
}
if count != 0 {
t.Fatalf("expected 0 events, got %d", count)
}
}
func TestAppendTransitionWritesPublishedAndOffline(t *testing.T) {
db := setupTestDB(t)
listing := &model.RentalListing{ID: 9, OwnerID: 3, Status: EventPublished}
if err := AppendTransition(db, listing, "draft", SourceSeller, ActorUser, 3, "发布"); err != nil {
t.Fatalf("append published: %v", err)
}
listing.Status = EventOffline
if err := AppendTransition(db, listing, EventPublished, SourceAdmin, ActorAdmin, 1, "后台下架"); err != nil {
t.Fatalf("append offline: %v", err)
}
var rows []model.ListingStatusEvent
if err := db.Order("id asc").Find(&rows).Error; err != nil {
t.Fatalf("find: %v", err)
}
if len(rows) != 2 {
t.Fatalf("expected 2 events, got %d", len(rows))
}
if rows[0].EventType != EventPublished || rows[0].Source != SourceSeller {
t.Fatalf("unexpected first event: %#v", rows[0])
}
if rows[1].EventType != EventOffline || rows[1].Source != SourceAdmin {
t.Fatalf("unexpected second event: %#v", rows[1])
}
if rows[0].CreatedAt.IsZero() || rows[1].CreatedAt.After(time.Now().Add(time.Minute)) {
t.Fatalf("unexpected timestamps: %v %v", rows[0].CreatedAt, rows[1].CreatedAt)
}
}
@@ -0,0 +1,22 @@
package model
import "time"
// ListingStatusEvent 记录商品上架/主动下架/交易离架等状态变更,供后台日统计使用。
type ListingStatusEvent struct {
ID uint64 `gorm:"primaryKey" json:"id"`
ListingID uint64 `gorm:"not null;index" json:"listing_id"`
OwnerID uint64 `gorm:"not null;default:0" json:"owner_id"`
EventType string `gorm:"size:32;not null" json:"event_type"`
FromStatus string `gorm:"size:32;not null;default:''" json:"from_status"`
ToStatus string `gorm:"size:32;not null;default:''" json:"to_status"`
Source string `gorm:"size:32;not null;default:''" json:"source"`
ActorType string `gorm:"size:16;not null;default:''" json:"actor_type"`
ActorID uint64 `gorm:"not null;default:0" json:"actor_id"`
Remark string `gorm:"size:255;not null;default:''" json:"remark"`
CreatedAt time.Time `json:"created_at"`
}
func (ListingStatusEvent) TableName() string {
return "listing_status_events"
}
+21 -5
View File
@@ -3,11 +3,12 @@ package admindashboard
import "time"
type DashboardDTO struct {
Metrics MetricsDTO `json:"metrics"`
Pending PendingDTO `json:"pending"`
RecentOrders []RecentOrderDTO `json:"recent_orders"`
RecentDisputes []RecentDisputeDTO `json:"recent_disputes"`
GeneratedAt time.Time `json:"generated_at"`
Metrics MetricsDTO `json:"metrics"`
Pending PendingDTO `json:"pending"`
ListingDaily ListingDailyOverviewDTO `json:"listing_daily"`
RecentOrders []RecentOrderDTO `json:"recent_orders"`
RecentDisputes []RecentDisputeDTO `json:"recent_disputes"`
GeneratedAt time.Time `json:"generated_at"`
}
type MetricsDTO struct {
@@ -21,6 +22,21 @@ type MetricsDTO struct {
TodayLedgerAmountCent int64 `json:"today_ledger_amount_cent"`
}
// ListingDailyOverviewDTO 商品上下架统计:今日三组 + 近 7 日趋势。
type ListingDailyOverviewDTO struct {
Today ListingDayStatsDTO `json:"today"`
Trend []ListingDayStatsDTO `json:"trend"`
Days int `json:"days"`
Timezone string `json:"timezone"`
}
type ListingDayStatsDTO struct {
Date string `json:"date"` // YYYY-MM-DD(上海时区)
PublishedCount int64 `json:"published_count"`
ActiveOfflineCount int64 `json:"active_offline_count"` // 号主 + 后台主动下架
TradeLeaveCount int64 `json:"trade_leave_count"` // rented / completed / sealed / 交易侧归档下架
}
type PendingDTO struct {
ListingReviews int64 `json:"listing_reviews"`
Disputes int64 `json:"disputes"`
@@ -4,11 +4,15 @@ import (
"context"
"time"
"hfb_sys/backend/internal/listingstatus"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/timeutil"
"gorm.io/gorm"
)
const listingDailyTrendDays = 7
type Repository struct {
db *gorm.DB
}
@@ -19,8 +23,9 @@ func NewRepository(db *gorm.DB) *Repository {
func (r *Repository) Summary(ctx context.Context) (*DashboardDTO, error) {
db := r.db.WithContext(ctx)
now := time.Now()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
loc := timeutil.ShanghaiLocation()
now := timeutil.ShanghaiNow()
today := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc)
metrics := MetricsDTO{}
pending := PendingDTO{}
@@ -63,6 +68,11 @@ func (r *Repository) Summary(ctx context.Context) (*DashboardDTO, error) {
if err := db.Model(&model.RentalOrder{}).Where("status IN ?", []string{"pending_checkout_confirm", "pending_checkout_accept"}).Count(&pending.PendingReturnConfirms).Error; err != nil {
return nil, err
}
listingDaily, err := r.listingDailyOverview(ctx, now, listingDailyTrendDays)
if err != nil {
return nil, err
}
recentOrders, err := r.recentOrders(ctx)
if err != nil {
return nil, err
@@ -74,12 +84,103 @@ func (r *Repository) Summary(ctx context.Context) (*DashboardDTO, error) {
return &DashboardDTO{
Metrics: metrics,
Pending: pending,
ListingDaily: listingDaily,
RecentOrders: recentOrders,
RecentDisputes: recentDisputes,
GeneratedAt: now,
}, nil
}
func (r *Repository) listingDailyOverview(ctx context.Context, now time.Time, days int) (ListingDailyOverviewDTO, error) {
if days <= 0 {
days = listingDailyTrendDays
}
loc := timeutil.ShanghaiLocation()
now = now.In(loc)
// 右开区间 [start, end)
end := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, 1)
start := end.AddDate(0, 0, -days)
type eventRow struct {
EventType string
Source string
CreatedAt time.Time
}
events := make([]eventRow, 0)
if err := r.db.WithContext(ctx).Model(&model.ListingStatusEvent{}).
Select("event_type, source, created_at").
Where("created_at >= ? AND created_at < ?", start, end).
Scan(&events).Error; err != nil {
return ListingDailyOverviewDTO{}, err
}
type bucket struct {
published int64
activeOffline int64
tradeLeave int64
}
byDay := make(map[string]*bucket, days)
for _, ev := range events {
key := ev.CreatedAt.In(loc).Format("2006-01-02")
b := byDay[key]
if b == nil {
b = &bucket{}
byDay[key] = b
}
switch {
case ev.EventType == listingstatus.EventPublished:
b.published++
case isActiveOfflineEvent(ev.EventType, ev.Source):
b.activeOffline++
case isTradeLeaveEvent(ev.EventType, ev.Source):
b.tradeLeave++
}
}
trend := make([]ListingDayStatsDTO, 0, days)
for i := days - 1; i >= 0; i-- {
day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, loc).AddDate(0, 0, -i)
key := day.Format("2006-01-02")
item := ListingDayStatsDTO{Date: key}
if b := byDay[key]; b != nil {
item.PublishedCount = b.published
item.ActiveOfflineCount = b.activeOffline
item.TradeLeaveCount = b.tradeLeave
}
trend = append(trend, item)
}
todayStats := ListingDayStatsDTO{Date: now.Format("2006-01-02")}
if len(trend) > 0 {
todayStats = trend[len(trend)-1]
}
return ListingDailyOverviewDTO{
Today: todayStats,
Trend: trend,
Days: days,
Timezone: "Asia/Shanghai",
}, nil
}
func isActiveOfflineEvent(eventType, source string) bool {
return eventType == listingstatus.EventOffline &&
(source == listingstatus.SourceSeller || source == listingstatus.SourceAdmin)
}
func isTradeLeaveEvent(eventType, source string) bool {
switch eventType {
case listingstatus.EventRented, listingstatus.EventCompleted, listingstatus.EventSealed:
return true
case listingstatus.EventOffline:
return source == listingstatus.SourceOrder ||
source == listingstatus.SourceDispute ||
source == listingstatus.SourcePickup ||
source == listingstatus.SourceSystem
default:
return false
}
}
func (r *Repository) recentOrders(ctx context.Context) ([]RecentOrderDTO, error) {
rows := make([]RecentOrderDTO, 0)
err := r.db.WithContext(ctx).Table("rental_orders AS o").
@@ -7,6 +7,7 @@ import (
"log"
"time"
"hfb_sys/backend/internal/listingstatus"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/notification"
"hfb_sys/backend/internal/modules/wallet"
@@ -76,6 +77,17 @@ func (r *Repository) Arbitrate(ctx context.Context, adminID uint64, id uint64, r
listing.PublishedAt = nil
account.Status = "offline"
}
if err := listingstatus.AppendTransition(
tx,
&listing,
beforeListingStatus,
listingstatus.SourceDispute,
listingstatus.ActorAdmin,
adminID,
"仲裁结案",
); err != nil {
return err
}
if err := wallet.AppendEntries(tx, settlement.Entries...); err != nil {
return err
}
@@ -7,6 +7,7 @@ import (
"strings"
"time"
"hfb_sys/backend/internal/listingstatus"
"hfb_sys/backend/internal/model"
"gorm.io/datatypes"
@@ -64,6 +65,9 @@ func (r *Repository) Create(ctx context.Context, ownerID uint64, req CreateReque
if err := tx.Create(&listing).Error; err != nil {
return err
}
if err := listingstatus.AppendTransition(tx, &listing, "", listingstatus.SourceSeller, listingstatus.ActorUser, ownerID, "发布上架"); err != nil {
return err
}
// 发布提交时建发布群
var conversationID uint64
@@ -235,6 +239,7 @@ func (r *Repository) Update(ctx context.Context, ownerID uint64, listingID uint6
listing.PriceCent = normalizedListingPriceCent(req)
listing.DepositAmountCent = req.DepositAmountCent
fromStatus := listing.Status
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
listing.Status = listingStatus
listing.ReviewStatus = reviewStatus
@@ -247,6 +252,9 @@ func (r *Repository) Update(ctx context.Context, ownerID uint64, listingID uint6
if err := tx.Save(listing).Error; err != nil {
return err
}
if err := listingstatus.AppendTransition(tx, listing, fromStatus, listingstatus.SourceSeller, listingstatus.ActorUser, ownerID, "编辑后上架"); err != nil {
return err
}
dto = toDTO(*account, *listing)
return nil
})
@@ -263,6 +271,7 @@ func (r *Repository) SubmitReview(ctx context.Context, ownerID uint64, listingID
if listingLockedForOwnerMutation(listing) {
return ErrListingLocked
}
fromStatus := listing.Status
listingStatus, reviewStatus, publishedAt := initialPublishState(reviewRequired)
listing.Status = listingStatus
listing.ReviewStatus = reviewStatus
@@ -275,6 +284,9 @@ func (r *Repository) SubmitReview(ctx context.Context, ownerID uint64, listingID
if err := tx.Save(listing).Error; err != nil {
return err
}
if err := listingstatus.AppendTransition(tx, listing, fromStatus, listingstatus.SourceSeller, listingstatus.ActorUser, ownerID, "提交发布"); err != nil {
return err
}
dto = toDTO(*account, *listing)
return nil
})
@@ -353,6 +365,7 @@ func (r *Repository) Offline(ctx context.Context, ownerID uint64, listingID uint
if listingLockedForOwnerMutation(listing) {
return ErrListingLocked
}
fromStatus := listing.Status
listing.Status = "offline"
listing.ReviewStatus = "none"
listing.ReviewReason = "号主已手动下架"
@@ -364,6 +377,9 @@ func (r *Repository) Offline(ctx context.Context, ownerID uint64, listingID uint
if err := tx.Save(listing).Error; err != nil {
return err
}
if err := listingstatus.AppendTransition(tx, listing, fromStatus, listingstatus.SourceSeller, listingstatus.ActorUser, ownerID, "号主手动下架"); err != nil {
return err
}
dto = toDTO(*account, *listing)
return nil
})
@@ -6,6 +6,7 @@ import (
"strings"
"time"
"hfb_sys/backend/internal/listingstatus"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/notification"
@@ -174,6 +175,13 @@ func (r *Repository) adminUpdateStatus(ctx context.Context, adminID uint64, list
if err := tx.Save(listing).Error; err != nil {
return err
}
source := listingstatus.SourceAdmin
if listingStatus == "published" {
source = listingstatus.SourceReview
}
if err := listingstatus.AppendTransition(tx, listing, beforeListingStatus, source, listingstatus.ActorAdmin, adminID, req.Reason); err != nil {
return err
}
if err := notification.Append(tx, notification.Entry{
UserID: listing.OwnerID,
Type: "listing_admin",
@@ -214,6 +222,7 @@ func (r *Repository) Approve(ctx context.Context, listingID uint64) (*ListingDTO
if listing.Status == "rented" || listing.InTransaction {
return ErrListingLocked
}
fromStatus := listing.Status
now := time.Now()
listing.Status = "published"
listing.ReviewStatus = "approved"
@@ -226,6 +235,9 @@ func (r *Repository) Approve(ctx context.Context, listingID uint64) (*ListingDTO
if err := tx.Save(listing).Error; err != nil {
return err
}
if err := listingstatus.AppendTransition(tx, listing, fromStatus, listingstatus.SourceReview, listingstatus.ActorAdmin, 0, "审核通过上架"); err != nil {
return err
}
listingID := listing.ID
if err := notification.Append(tx, notification.Entry{
UserID: listing.OwnerID,
@@ -35,7 +35,9 @@ func (r *Repository) AdminClose(ctx context.Context, adminID uint64, orderID uin
order.HandoffStatus = handoffStatusAdminClosed
order.SettlementStatus = settlementStatusClosed
order.SettledAt = &now
archiveAssets(listing, account)
if err := archiveAssets(tx, listing, account); err != nil {
return err
}
if beforeOrderStatus != orderStatusPendingPayment {
totalCent := order.RentAmountCent + order.DepositAmountCent
// 押金暂扣:若订单已暂扣,拦截退款中的押金部分挂起不退,其余照常原路退。
@@ -118,7 +120,9 @@ func (r *Repository) AdminMarkAbnormal(ctx context.Context, adminID uint64, orde
beforeAccountStatus := account.Status
order.Status = orderStatusAbnormal
order.HandoffStatus = handoffStatusAdminAbnormal
markAssetsAbnormal(listing, account)
if err := markAssetsAbnormal(tx, listing, account); err != nil {
return err
}
if err := notification.Append(tx,
notification.Entry{
UserID: order.RenterID,
@@ -450,7 +454,9 @@ func (r *Repository) AdminRejectRefund(ctx context.Context, orderID uint64, acti
order.SettlementStatus = settlementStatusUnsettled
order.SettledAt = nil
reserveListingForOrder(listing)
markAssetsRented(listing, account)
if err := markAssetsRented(tx, listing, account); err != nil {
return err
}
case "close":
now := time.Now()
@@ -458,7 +464,9 @@ func (r *Repository) AdminRejectRefund(ctx context.Context, orderID uint64, acti
order.HandoffStatus = handoffStatusAdminClosed
order.SettlementStatus = settlementStatusClosed
order.SettledAt = &now
archiveAssets(listing, account)
if err := archiveAssets(tx, listing, account); err != nil {
return err
}
default:
return errors.New("无效的驳回操作,可选 restore 或 close")
@@ -500,7 +508,9 @@ func (r *Repository) AdminSeal(ctx context.Context, adminID uint64, orderID uint
order.HandoffStatus = handoffStatusAdminClosed
order.SettlementStatus = settlementStatusClosed
order.SettledAt = &now
sealAssets(listing, account)
if err := sealAssets(tx, listing, account); err != nil {
return err
}
if beforeOrderStatus != orderStatusPendingPayment {
totalCent := order.RentAmountCent + order.DepositAmountCent
// 押金暂扣:若订单已暂扣,拦截退款中的押金部分挂起不退,其余照常原路退。
+19 -6
View File
@@ -1,6 +1,7 @@
package order
import (
"hfb_sys/backend/internal/listingstatus"
"hfb_sys/backend/internal/model"
"gorm.io/gorm"
@@ -45,41 +46,53 @@ func reserveListingForOrder(listing *model.RentalListing) {
listing.InTransaction = true
}
func markAssetsRented(listing *model.RentalListing, account *model.GameAccount) {
func markAssetsRented(tx *gorm.DB, listing *model.RentalListing, account *model.GameAccount) error {
from := listing.Status
listing.Status = listingStatusRented
account.Status = accountStatusRented
return listingstatus.AppendTransition(tx, listing, from, listingstatus.SourceOrder, listingstatus.ActorSystem, 0, "支付成功租出")
}
func releaseAssetsForRental(listing *model.RentalListing, account *model.GameAccount) {
func releaseAssetsForRental(tx *gorm.DB, listing *model.RentalListing, account *model.GameAccount) error {
from := listing.Status
listing.Status = listingStatusPublished
listing.InTransaction = false
account.Status = accountStatusPublished
return listingstatus.AppendTransition(tx, listing, from, listingstatus.SourceOrder, listingstatus.ActorSystem, 0, "订单取消恢复上架")
}
func archiveAssets(listing *model.RentalListing, account *model.GameAccount) {
func archiveAssets(tx *gorm.DB, listing *model.RentalListing, account *model.GameAccount) error {
from := listing.Status
listing.Status = listingStatusOffline
listing.InTransaction = false
listing.PublishedAt = nil
account.Status = accountStatusOffline
return listingstatus.AppendTransition(tx, listing, from, listingstatus.SourceOrder, listingstatus.ActorSystem, 0, "订单归档下架")
}
func sealAssets(listing *model.RentalListing, account *model.GameAccount) {
func sealAssets(tx *gorm.DB, listing *model.RentalListing, account *model.GameAccount) error {
from := listing.Status
listing.Status = listingStatusSealed
listing.InTransaction = false
listing.PublishedAt = nil
account.Status = accountStatusSealed
return listingstatus.AppendTransition(tx, listing, from, listingstatus.SourceOrder, listingstatus.ActorSystem, 0, "订单封存")
}
func completeAssets(listing *model.RentalListing, account *model.GameAccount) {
func completeAssets(tx *gorm.DB, listing *model.RentalListing, account *model.GameAccount) error {
from := listing.Status
listing.Status = listingStatusCompleted
listing.InTransaction = false
listing.PublishedAt = nil
account.Status = accountStatusOffline
return listingstatus.AppendTransition(tx, listing, from, listingstatus.SourceOrder, listingstatus.ActorSystem, 0, "订单完成售出")
}
func markAssetsAbnormal(listing *model.RentalListing, account *model.GameAccount) {
func markAssetsAbnormal(tx *gorm.DB, listing *model.RentalListing, account *model.GameAccount) error {
from := listing.Status
listing.Status = listingStatusAbnormal
listing.InTransaction = false
listing.PublishedAt = nil
account.Status = accountStatusAbnormal
return listingstatus.AppendTransition(tx, listing, from, listingstatus.SourceOrder, listingstatus.ActorSystem, 0, "标记异常")
}
@@ -21,7 +21,9 @@ func (r *Repository) finalizeCheckout(tx *gorm.DB, order *model.RentalOrder, che
order.SettlementStatus = settlementStatusSettled
order.SettledAt = &now
order.OwnerSettledAt = &now
completeAssets(listing, account)
if err := completeAssets(tx, listing, account); err != nil {
return nil, err
}
settlement := buildCheckoutSettlement(*order, checkout)
if err := appendCheckoutOwnerIncome(tx, order, settlement); err != nil {
+6 -2
View File
@@ -181,7 +181,9 @@ func (r *Repository) ConfirmPaidFromChannelTx(tx *gorm.DB, orderID uint64) (uint
order.Status = orderStatusPendingHandoff
order.HandoffStatus = handoffStatusPendingOwner
order.HandoffStartedAt = &now
markAssetsRented(listing, account)
if err := markAssetsRented(tx, listing, account); err != nil {
return 0, err
}
// 拉租客进发布群(替代原来的建订单群)
conversationID := uint64(0)
@@ -275,7 +277,9 @@ func (r *Repository) Cancel(ctx context.Context, userID uint64, orderID uint64)
); err != nil {
return err
}
releaseAssetsForRental(listing, account)
if err := releaseAssetsForRental(tx, listing, account); err != nil {
return err
}
if err := closePendingOrderPayments(tx, order.ID, "order_cancel"); err != nil {
return err
}
@@ -149,7 +149,9 @@ func TestArchiveAssetsMovesListingOffline(t *testing.T) {
}
account := model.GameAccount{Status: accountStatusRented}
archiveAssets(&listing, &account)
if err := archiveAssets(nil, &listing, &account); err != nil {
t.Fatalf("archiveAssets: %v", err)
}
if listing.Status != listingStatusOffline {
t.Fatalf("listing.Status = %q, want %q", listing.Status, listingStatusOffline)
@@ -174,7 +176,9 @@ func TestCompleteAssetsMovesListingCompleted(t *testing.T) {
}
account := model.GameAccount{Status: accountStatusRented}
completeAssets(&listing, &account)
if err := completeAssets(nil, &listing, &account); err != nil {
t.Fatalf("completeAssets: %v", err)
}
if listing.Status != listingStatusCompleted {
t.Fatalf("listing.Status = %q, want %q", listing.Status, listingStatusCompleted)
@@ -12,6 +12,7 @@ import (
"time"
"hfb_sys/backend/internal/auditlog"
"hfb_sys/backend/internal/listingstatus"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/notification"
"hfb_sys/backend/internal/modules/wallet"
@@ -178,11 +179,23 @@ func (r *Repository) Complete(ctx context.Context, pickupID uint64, req Complete
}
// listing 置 completedlistingLockedForOwnerMutation 已拦截此状态再上架。
fromStatus := listing.Status
listing.InTransaction = true
listing.Status = "completed"
if err := tx.Save(&listing).Error; err != nil {
return err
}
if err := listingstatus.AppendTransition(
tx,
&listing,
fromStatus,
listingstatus.SourcePickup,
listingstatus.ActorAdmin,
0,
"管理员提号完成",
); err != nil {
return err
}
if err := wallet.AppendEntries(tx, wallet.Entry{
UserID: pickup.OwnerID,
@@ -0,0 +1,22 @@
-- +goose Up
CREATE TABLE IF NOT EXISTS listing_status_events (
id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT,
listing_id BIGINT UNSIGNED NOT NULL COMMENT '商品ID',
owner_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '号主ID',
event_type VARCHAR(32) NOT NULL COMMENT '事件类型: published/offline/rented/completed/sealed/abnormal',
from_status VARCHAR(32) NOT NULL DEFAULT '' COMMENT '变更前状态',
to_status VARCHAR(32) NOT NULL DEFAULT '' COMMENT '变更后状态',
source VARCHAR(32) NOT NULL DEFAULT '' COMMENT '来源: seller/admin/review/order/pickup/dispute/system',
actor_type VARCHAR(16) NOT NULL DEFAULT '' COMMENT '操作者类型: user/admin/system',
actor_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '操作者ID',
remark VARCHAR(255) NOT NULL DEFAULT '' COMMENT '备注',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '事件时间',
KEY idx_listing_status_events_day_type (created_at, event_type),
KEY idx_listing_status_events_type_source_day (event_type, source, created_at),
KEY idx_listing_status_events_listing (listing_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='商品上架/下架/交易离架状态事件';
-- +goose Down
DROP TABLE IF EXISTS listing_status_events;