486 lines
14 KiB
Go
486 lines
14 KiB
Go
package chat
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"hfb_sys/backend/internal/model"
|
|
|
|
"gorm.io/driver/sqlite"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/logger"
|
|
)
|
|
|
|
func setupQrCodeTestDB(t *testing.T) *gorm.DB {
|
|
t.Helper()
|
|
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{
|
|
Logger: logger.Default.LogMode(logger.Silent),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("无法创建测试数据库: %v", err)
|
|
}
|
|
if err := db.AutoMigrate(
|
|
&model.ChatQrCode{},
|
|
&model.ChatConversation{},
|
|
&model.ChatParticipant{},
|
|
&model.ChatMessage{},
|
|
&model.ChatQrCodeDeliveryTask{},
|
|
&model.ChatSupportGroup{},
|
|
&model.ChatSupportGroupMember{},
|
|
&model.AdminUser{},
|
|
&model.AdminUserRole{},
|
|
&model.Role{},
|
|
&model.SystemConfig{},
|
|
); err != nil {
|
|
t.Fatalf("数据库迁移失败: %v", err)
|
|
}
|
|
return db
|
|
}
|
|
|
|
func TestGetQrCodeOCRConfigReadsSystemConfig(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
repo := NewRepository(db, nil, nil)
|
|
rows := []model.SystemConfig{
|
|
{Key: qrCodeOCRTokenConfigKey, Value: "test-token"},
|
|
{Key: qrCodeOCRJobURLConfigKey, Value: "https://example.test/ocr/jobs"},
|
|
{Key: qrCodeOCRModelConfigKey, Value: "PaddleOCR-Test"},
|
|
}
|
|
if err := db.Create(&rows).Error; err != nil {
|
|
t.Fatalf("创建系统配置失败: %v", err)
|
|
}
|
|
|
|
config, err := repo.GetQrCodeOCRConfig(t.Context())
|
|
if err != nil {
|
|
t.Fatalf("读取 OCR 配置失败: %v", err)
|
|
}
|
|
if config.Token != "test-token" {
|
|
t.Fatalf("token = %q", config.Token)
|
|
}
|
|
if config.JobURL != "https://example.test/ocr/jobs" {
|
|
t.Fatalf("job_url = %q", config.JobURL)
|
|
}
|
|
if config.Model != "PaddleOCR-Test" {
|
|
t.Fatalf("model = %q", config.Model)
|
|
}
|
|
}
|
|
|
|
func TestListQrCodesIncludesBoundConversationTitle(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
repo := NewRepository(db, nil, nil)
|
|
conversation := model.ChatConversation{
|
|
Title: "账号群 L202606180001",
|
|
Type: ConversationTypeListingGroup,
|
|
Status: "active",
|
|
}
|
|
if err := db.Create(&conversation).Error; err != nil {
|
|
t.Fatalf("创建群聊失败: %v", err)
|
|
}
|
|
|
|
qrcode := model.ChatQrCode{
|
|
ImageURL: "/api/files/object?key=qrcode/group.png",
|
|
GroupName: "王大锤-鼠鼠跑刀一群",
|
|
Status: QrCodeStatusUsed,
|
|
ConversationID: &conversation.ID,
|
|
CreatedBy: 1,
|
|
}
|
|
if err := db.Create(&qrcode).Error; err != nil {
|
|
t.Fatalf("创建二维码失败: %v", err)
|
|
}
|
|
|
|
items, total, err := repo.ListQrCodes(t.Context(), QrCodeListRequest{Page: 1, Limit: 20})
|
|
if err != nil {
|
|
t.Fatalf("查询二维码失败: %v", err)
|
|
}
|
|
if total != 1 || len(items) != 1 {
|
|
t.Fatalf("total = %d, len = %d, want 1", total, len(items))
|
|
}
|
|
if items[0].GroupName != "王大锤-鼠鼠跑刀一群" {
|
|
t.Fatalf("群名 = %q", items[0].GroupName)
|
|
}
|
|
if items[0].BoundConversationTitle != "账号群 L202606180001" {
|
|
t.Fatalf("绑定群名 = %q", items[0].BoundConversationTitle)
|
|
}
|
|
}
|
|
|
|
func TestListQrCodesFilters(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
repo := NewRepository(db, nil, nil)
|
|
now := time.Now()
|
|
expiredAt := now.Add(-time.Hour)
|
|
validAt := now.Add(7 * 24 * time.Hour)
|
|
expiringAt := now.Add(2 * time.Hour)
|
|
conversation := model.ChatConversation{
|
|
Title: "账号群 绑定测试",
|
|
Type: ConversationTypeListingGroup,
|
|
Status: "active",
|
|
}
|
|
if err := db.Create(&conversation).Error; err != nil {
|
|
t.Fatalf("创建群聊失败: %v", err)
|
|
}
|
|
|
|
qrcodes := []model.ChatQrCode{
|
|
{
|
|
ImageURL: "/api/files/object?key=qrcode/unused-a.png",
|
|
GroupName: "待用群A",
|
|
Status: QrCodeStatusUnused,
|
|
CreatedBy: 1,
|
|
Note: "六月首批",
|
|
ExpiresAt: &validAt,
|
|
CreatedAt: now.Add(-48 * time.Hour),
|
|
},
|
|
{
|
|
ImageURL: "/api/files/object?key=qrcode/used-b.png",
|
|
GroupName: "已发群B",
|
|
Status: QrCodeStatusUsed,
|
|
ConversationID: &conversation.ID,
|
|
CreatedBy: 1,
|
|
Note: "特殊备注",
|
|
WecomRenamed: true,
|
|
ExpiresAt: &expiredAt,
|
|
CreatedAt: now.Add(-2 * time.Hour),
|
|
},
|
|
{
|
|
ImageURL: "/api/files/object?key=qrcode/permanent-c.png",
|
|
GroupName: "永久群C",
|
|
Status: QrCodeStatusDisabled,
|
|
CreatedBy: 1,
|
|
Note: "长期备用",
|
|
CreatedAt: now.Add(-24 * time.Hour),
|
|
},
|
|
{
|
|
ImageURL: "/api/files/object?key=qrcode/expiring-d.png",
|
|
GroupName: "临期群D",
|
|
Status: QrCodeStatusUnused,
|
|
CreatedBy: 1,
|
|
Note: "三天内到期",
|
|
ExpiresAt: &expiringAt,
|
|
CreatedAt: now.Add(-30 * time.Minute),
|
|
},
|
|
}
|
|
if err := db.Create(&qrcodes).Error; err != nil {
|
|
t.Fatalf("创建二维码失败: %v", err)
|
|
}
|
|
|
|
assertListIDs := func(name string, req QrCodeListRequest, want ...uint64) {
|
|
t.Helper()
|
|
req.Page = 1
|
|
req.Limit = 20
|
|
items, total, err := repo.ListQrCodes(t.Context(), req)
|
|
if err != nil {
|
|
t.Fatalf("%s 查询二维码失败: %v", name, err)
|
|
}
|
|
if total != int64(len(want)) || len(items) != len(want) {
|
|
t.Fatalf("%s total = %d, len = %d, want %d", name, total, len(items), len(want))
|
|
}
|
|
for i, id := range want {
|
|
if items[i].ID != id {
|
|
t.Fatalf("%s 第 %d 条 ID = %d, want %d", name, i, items[i].ID, id)
|
|
}
|
|
}
|
|
}
|
|
|
|
assertListIDs("状态", QrCodeListRequest{Status: QrCodeStatusUnused}, qrcodes[3].ID, qrcodes[0].ID)
|
|
assertListIDs("关键词群名", QrCodeListRequest{Keyword: "待用群"}, qrcodes[0].ID)
|
|
assertListIDs("关键词备注", QrCodeListRequest{Keyword: "特殊备注"}, qrcodes[1].ID)
|
|
assertListIDs("关键词绑定群", QrCodeListRequest{Keyword: "绑定测试"}, qrcodes[1].ID)
|
|
assertListIDs("企微改名", QrCodeListRequest{WecomRenamed: "true"}, qrcodes[1].ID)
|
|
assertListIDs("未绑定", QrCodeListRequest{Bound: "false"}, qrcodes[3].ID, qrcodes[2].ID, qrcodes[0].ID)
|
|
assertListIDs("已过期", QrCodeListRequest{ExpireStatus: "expired"}, qrcodes[1].ID)
|
|
assertListIDs("永久有效", QrCodeListRequest{ExpireStatus: "permanent"}, qrcodes[2].ID)
|
|
assertListIDs("即将过期", QrCodeListRequest{ExpireStatus: "expiring_soon"}, qrcodes[3].ID)
|
|
assertListIDs(
|
|
"上传时间",
|
|
QrCodeListRequest{
|
|
CreatedStart: now.Add(-3 * time.Hour).Format(time.RFC3339),
|
|
CreatedEnd: now.Add(-90 * time.Minute).Format(time.RFC3339),
|
|
},
|
|
qrcodes[1].ID,
|
|
)
|
|
}
|
|
|
|
func TestParseQrCodeGroupNameCandidates(t *testing.T) {
|
|
rawText := strings.Join([]string{
|
|
"使用微信或企业微信扫码加入",
|
|
"王大锤-鼠鼠跑刀一群",
|
|
"这二维码7天内有效",
|
|
}, "\n")
|
|
|
|
candidates := parseQrCodeGroupNameCandidates(rawText)
|
|
if len(candidates) == 0 {
|
|
t.Fatal("未解析出群名候选")
|
|
}
|
|
if candidates[0] != "王大锤-鼠鼠跑刀一群" {
|
|
t.Fatalf("首个候选 = %q", candidates[0])
|
|
}
|
|
}
|
|
|
|
func TestUpdateQrCodeRejectsUsedToUnused(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
repo := NewRepository(db, nil, nil)
|
|
now := time.Date(2026, 6, 18, 12, 0, 0, 0, time.UTC)
|
|
conversationID := uint64(1001)
|
|
qrcode := model.ChatQrCode{
|
|
ImageURL: "/api/files/object?key=qrcode/used.png",
|
|
Status: QrCodeStatusUsed,
|
|
ConversationID: &conversationID,
|
|
UsedAt: &now,
|
|
CreatedBy: 1,
|
|
}
|
|
if err := db.Create(&qrcode).Error; err != nil {
|
|
t.Fatalf("创建二维码失败: %v", err)
|
|
}
|
|
|
|
status := QrCodeStatusUnused
|
|
err := repo.UpdateQrCode(t.Context(), qrcode.ID, UpdateQrCodeRequest{Status: &status})
|
|
if !errors.Is(err, ErrQrCodeCannotReenable) {
|
|
t.Fatalf("error = %v, want ErrQrCodeCannotReenable", err)
|
|
}
|
|
|
|
var saved model.ChatQrCode
|
|
if err := db.First(&saved, qrcode.ID).Error; err != nil {
|
|
t.Fatalf("查询二维码失败: %v", err)
|
|
}
|
|
if saved.Status != QrCodeStatusUsed {
|
|
t.Fatalf("二维码状态 = %q, want %q", saved.Status, QrCodeStatusUsed)
|
|
}
|
|
}
|
|
|
|
func TestUpdateQrCodeRejectsIssuedDisabledToUnused(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
repo := NewRepository(db, nil, nil)
|
|
conversationID := uint64(1002)
|
|
qrcode := model.ChatQrCode{
|
|
ImageURL: "/api/files/object?key=qrcode/disabled.png",
|
|
Status: QrCodeStatusDisabled,
|
|
ConversationID: &conversationID,
|
|
CreatedBy: 1,
|
|
}
|
|
if err := db.Create(&qrcode).Error; err != nil {
|
|
t.Fatalf("创建二维码失败: %v", err)
|
|
}
|
|
|
|
status := QrCodeStatusUnused
|
|
err := repo.UpdateQrCode(t.Context(), qrcode.ID, UpdateQrCodeRequest{Status: &status})
|
|
if !errors.Is(err, ErrQrCodeCannotReenable) {
|
|
t.Fatalf("error = %v, want ErrQrCodeCannotReenable", err)
|
|
}
|
|
}
|
|
|
|
func TestUpdateQrCodeAllowsUnusedToDisabled(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
repo := NewRepository(db, nil, nil)
|
|
qrcode := model.ChatQrCode{
|
|
ImageURL: "/api/files/object?key=qrcode/unused.png",
|
|
Status: QrCodeStatusUnused,
|
|
CreatedBy: 1,
|
|
}
|
|
if err := db.Create(&qrcode).Error; err != nil {
|
|
t.Fatalf("创建二维码失败: %v", err)
|
|
}
|
|
|
|
status := QrCodeStatusDisabled
|
|
if err := repo.UpdateQrCode(t.Context(), qrcode.ID, UpdateQrCodeRequest{Status: &status}); err != nil {
|
|
t.Fatalf("更新二维码失败: %v", err)
|
|
}
|
|
|
|
var saved model.ChatQrCode
|
|
if err := db.First(&saved, qrcode.ID).Error; err != nil {
|
|
t.Fatalf("查询二维码失败: %v", err)
|
|
}
|
|
if saved.Status != QrCodeStatusDisabled {
|
|
t.Fatalf("二维码状态 = %q, want %q", saved.Status, QrCodeStatusDisabled)
|
|
}
|
|
}
|
|
|
|
func TestUpdateQrCodeGroupNameAndRenameFlag(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
repo := NewRepository(db, nil, nil)
|
|
qrcode := model.ChatQrCode{
|
|
ImageURL: "/api/files/object?key=qrcode/unused.png",
|
|
Status: QrCodeStatusUnused,
|
|
CreatedBy: 1,
|
|
}
|
|
if err := db.Create(&qrcode).Error; err != nil {
|
|
t.Fatalf("创建二维码失败: %v", err)
|
|
}
|
|
|
|
groupName := "王大锤-鼠鼠跑刀一群"
|
|
renamed := true
|
|
if err := repo.UpdateQrCode(t.Context(), qrcode.ID, UpdateQrCodeRequest{
|
|
GroupName: &groupName,
|
|
WecomRenamed: &renamed,
|
|
}); err != nil {
|
|
t.Fatalf("更新二维码失败: %v", err)
|
|
}
|
|
|
|
var saved model.ChatQrCode
|
|
if err := db.First(&saved, qrcode.ID).Error; err != nil {
|
|
t.Fatalf("查询二维码失败: %v", err)
|
|
}
|
|
if saved.GroupName != groupName {
|
|
t.Fatalf("群名 = %q, want %q", saved.GroupName, groupName)
|
|
}
|
|
if !saved.WecomRenamed {
|
|
t.Fatal("企微改名标记未保存")
|
|
}
|
|
}
|
|
|
|
func TestDeleteQrCodeAllowsIssuedQrCode(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
repo := NewRepository(db, nil, nil)
|
|
now := time.Date(2026, 6, 19, 17, 8, 0, 0, time.UTC)
|
|
conversationID := uint64(1003)
|
|
qrcode := model.ChatQrCode{
|
|
ImageURL: "/api/files/object?key=qrcode/issued.png",
|
|
Status: QrCodeStatusUsed,
|
|
ConversationID: &conversationID,
|
|
UsedAt: &now,
|
|
CreatedBy: 1,
|
|
}
|
|
if err := db.Create(&qrcode).Error; err != nil {
|
|
t.Fatalf("创建二维码失败: %v", err)
|
|
}
|
|
|
|
if err := repo.DeleteQrCode(t.Context(), qrcode.ID); err != nil {
|
|
t.Fatalf("删除已发放二维码失败: %v", err)
|
|
}
|
|
|
|
var count int64
|
|
if err := db.Model(&model.ChatQrCode{}).Where("id = ?", qrcode.ID).Count(&count).Error; err != nil {
|
|
t.Fatalf("统计二维码失败: %v", err)
|
|
}
|
|
if count != 0 {
|
|
t.Fatalf("二维码数量 = %d, want 0", count)
|
|
}
|
|
}
|
|
|
|
func TestBatchDeleteQrCodes(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
repo := NewRepository(db, nil, nil)
|
|
now := time.Date(2026, 6, 19, 17, 8, 0, 0, time.UTC)
|
|
conversationID := uint64(1004)
|
|
qrcodes := []model.ChatQrCode{
|
|
{
|
|
ImageURL: "/api/files/object?key=qrcode/unused.png",
|
|
Status: QrCodeStatusUnused,
|
|
CreatedBy: 1,
|
|
},
|
|
{
|
|
ImageURL: "/api/files/object?key=qrcode/used.png",
|
|
Status: QrCodeStatusUsed,
|
|
ConversationID: &conversationID,
|
|
UsedAt: &now,
|
|
CreatedBy: 1,
|
|
},
|
|
{
|
|
ImageURL: "/api/files/object?key=qrcode/keep.png",
|
|
Status: QrCodeStatusDisabled,
|
|
CreatedBy: 1,
|
|
},
|
|
}
|
|
if err := db.Create(&qrcodes).Error; err != nil {
|
|
t.Fatalf("创建二维码失败: %v", err)
|
|
}
|
|
|
|
deleted, err := repo.BatchDeleteQrCodes(t.Context(), []uint64{qrcodes[0].ID, qrcodes[1].ID})
|
|
if err != nil {
|
|
t.Fatalf("批量删除二维码失败: %v", err)
|
|
}
|
|
if deleted != 2 {
|
|
t.Fatalf("删除数量 = %d, want 2", deleted)
|
|
}
|
|
|
|
var remaining []model.ChatQrCode
|
|
if err := db.Order("id ASC").Find(&remaining).Error; err != nil {
|
|
t.Fatalf("查询剩余二维码失败: %v", err)
|
|
}
|
|
if len(remaining) != 1 || remaining[0].ID != qrcodes[2].ID {
|
|
t.Fatalf("剩余二维码 = %+v, want id %d", remaining, qrcodes[2].ID)
|
|
}
|
|
}
|
|
|
|
func TestEnsureListingConversationCreatesQrCodeDeliveryTaskWhenStockEmpty(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
listing := model.RentalListing{
|
|
ID: 101,
|
|
ListingNo: "L202606260001",
|
|
OwnerID: 10,
|
|
}
|
|
|
|
if err := db.Transaction(func(tx *gorm.DB) error {
|
|
_, err := EnsureListingConversation(tx, listing, 0)
|
|
return err
|
|
}); err != nil {
|
|
t.Fatalf("建发布群失败: %v", err)
|
|
}
|
|
|
|
var task model.ChatQrCodeDeliveryTask
|
|
if err := db.First(&task).Error; err != nil {
|
|
t.Fatalf("未创建补发任务: %v", err)
|
|
}
|
|
if task.Status != "pending" {
|
|
t.Fatalf("补发任务状态 = %q, want pending", task.Status)
|
|
}
|
|
}
|
|
|
|
func TestBatchCreateQrCodeDeliversPendingTask(t *testing.T) {
|
|
db := setupQrCodeTestDB(t)
|
|
repo := NewRepository(db, nil, nil)
|
|
conversation := model.ChatConversation{
|
|
Title: "账号群 L202606260002",
|
|
Type: ConversationTypeListingGroup,
|
|
Status: "active",
|
|
}
|
|
if err := db.Create(&conversation).Error; err != nil {
|
|
t.Fatalf("创建群聊失败: %v", err)
|
|
}
|
|
task := model.ChatQrCodeDeliveryTask{
|
|
ConversationID: conversation.ID,
|
|
Status: "pending",
|
|
}
|
|
if err := db.Create(&task).Error; err != nil {
|
|
t.Fatalf("创建补发任务失败: %v", err)
|
|
}
|
|
|
|
_, err := repo.BatchCreateQrCode(t.Context(), 1, BatchCreateQrCodeRequest{
|
|
Items: []CreateQrCodeRequest{
|
|
{ImageURL: "/api/files/object?key=qrcode/replenish.png", GroupName: "补发群"},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("批量创建二维码失败: %v", err)
|
|
}
|
|
|
|
var savedTask model.ChatQrCodeDeliveryTask
|
|
if err := db.First(&savedTask, task.ID).Error; err != nil {
|
|
t.Fatalf("查询补发任务失败: %v", err)
|
|
}
|
|
if savedTask.Status != "sent" {
|
|
t.Fatalf("补发任务状态 = %q, want sent", savedTask.Status)
|
|
}
|
|
if savedTask.QrCodeID == nil {
|
|
t.Fatal("补发任务未记录二维码 ID")
|
|
}
|
|
if savedTask.SentAt == nil {
|
|
t.Fatal("补发任务未记录 sent_at")
|
|
}
|
|
|
|
var qrcode model.ChatQrCode
|
|
if err := db.First(&qrcode, *savedTask.QrCodeID).Error; err != nil {
|
|
t.Fatalf("查询二维码失败: %v", err)
|
|
}
|
|
if qrcode.Status != QrCodeStatusUsed {
|
|
t.Fatalf("二维码状态 = %q, want used", qrcode.Status)
|
|
}
|
|
if qrcode.ConversationID == nil || *qrcode.ConversationID != conversation.ID {
|
|
t.Fatalf("二维码绑定会话 = %v, want %d", qrcode.ConversationID, conversation.ID)
|
|
}
|
|
|
|
var message model.ChatMessage
|
|
if err := db.Where("conversation_id = ? AND content_type = ?", conversation.ID, "image").First(&message).Error; err != nil {
|
|
t.Fatalf("未生成二维码图片消息: %v", err)
|
|
}
|
|
}
|