账号上下架增加统计

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)
}
}