账号上下架增加统计
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user