禁止已发放二维码回退待用
This commit is contained in:
@@ -97,6 +97,10 @@ func (h *Handler) UpdateQrCodeHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "二维码不存在"})
|
||||
return
|
||||
}
|
||||
if err == ErrQrCodeCannotReenable {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "已发放的二维码不能改回待用"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -18,8 +18,9 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrQrCodeNotFound = errors.New("二维码不存在")
|
||||
ErrQrCodeCannotDelete = errors.New("已使用的二维码不能删除")
|
||||
ErrQrCodeNotFound = errors.New("二维码不存在")
|
||||
ErrQrCodeCannotDelete = errors.New("已使用的二维码不能删除")
|
||||
ErrQrCodeCannotReenable = errors.New("已发放的二维码不能改回待用")
|
||||
)
|
||||
|
||||
// CreateQrCodeRequest 创建二维码请求
|
||||
@@ -172,6 +173,14 @@ func (r *Repository) GetQrCodeStats(ctx context.Context) (*QrCodeStats, error) {
|
||||
|
||||
// UpdateQrCode 更新二维码
|
||||
func (r *Repository) UpdateQrCode(ctx context.Context, id uint64, req UpdateQrCodeRequest) error {
|
||||
var qrcode model.ChatQrCode
|
||||
if err := r.db.WithContext(ctx).First(&qrcode, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return ErrQrCodeNotFound
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
updates := make(map[string]interface{})
|
||||
|
||||
if req.ImageURL != nil {
|
||||
@@ -185,6 +194,9 @@ func (r *Repository) UpdateQrCode(ctx context.Context, id uint64, req UpdateQrCo
|
||||
if *req.Status != QrCodeStatusUnused && *req.Status != QrCodeStatusUsed && *req.Status != QrCodeStatusDisabled {
|
||||
return errors.New("无效的状态值")
|
||||
}
|
||||
if *req.Status == QrCodeStatusUnused && qrcodeWasIssued(qrcode) {
|
||||
return ErrQrCodeCannotReenable
|
||||
}
|
||||
updates["status"] = *req.Status
|
||||
}
|
||||
if req.ExpiresAt != nil {
|
||||
@@ -197,17 +209,17 @@ func (r *Repository) UpdateQrCode(ctx context.Context, id uint64, req UpdateQrCo
|
||||
return nil
|
||||
}
|
||||
|
||||
result := r.db.WithContext(ctx).Model(&model.ChatQrCode{}).Where("id = ?", id).Updates(updates)
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return ErrQrCodeNotFound
|
||||
if err := r.db.WithContext(ctx).Model(&qrcode).Updates(updates).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func qrcodeWasIssued(qrcode model.ChatQrCode) bool {
|
||||
return qrcode.Status == QrCodeStatusUsed || qrcode.ConversationID != nil || qrcode.UsedAt != nil
|
||||
}
|
||||
|
||||
// DeleteQrCode 删除二维码(仅未使用的可删除)
|
||||
func (r *Repository) DeleteQrCode(ctx context.Context, id uint64) error {
|
||||
var qrcode model.ChatQrCode
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"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{}); err != nil {
|
||||
t.Fatalf("数据库迁移失败: %v", err)
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func TestUpdateQrCodeRejectsUsedToUnused(t *testing.T) {
|
||||
db := setupQrCodeTestDB(t)
|
||||
repo := NewRepository(db, 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)
|
||||
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)
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -47,6 +47,7 @@ const editForm = reactive({
|
||||
note: '',
|
||||
status: 'unused' as QrCodeStatus,
|
||||
expires_at: '' as string,
|
||||
was_issued: false,
|
||||
})
|
||||
|
||||
const statusOptions: { label: string; value: QrCodeStatus | ''; type: string }[] = [
|
||||
@@ -64,6 +65,12 @@ const statusMap = computed(() => {
|
||||
return m
|
||||
})
|
||||
|
||||
const editStatusOptions = computed(() =>
|
||||
editForm.was_issued
|
||||
? statusOptions.filter(option => option.value === 'used' || option.value === 'disabled')
|
||||
: statusOptions.filter(option => option.value === 'unused' || option.value === 'disabled')
|
||||
)
|
||||
|
||||
const maxBatchUploadCount = 20
|
||||
const uploading = computed(() => uploadPendingCount.value > 0)
|
||||
const uploadBusy = computed(() => uploading.value || uploadSaving.value)
|
||||
@@ -172,6 +179,7 @@ function openEdit(row: ChatQrCode) {
|
||||
editForm.note = row.note
|
||||
editForm.status = row.status
|
||||
editForm.expires_at = row.expires_at ? row.expires_at.replace('T', ' ').slice(0, 16) : ''
|
||||
editForm.was_issued = row.status === 'used' || Boolean(row.conversation_id || row.used_at)
|
||||
editVisible.value = true
|
||||
}
|
||||
|
||||
@@ -451,8 +459,12 @@ onMounted(reloadAll)
|
||||
</el-form-item>
|
||||
<el-form-item label="状态">
|
||||
<el-select v-model="editForm.status" style="width: 100%">
|
||||
<el-option label="待用" value="unused" />
|
||||
<el-option label="已停用" value="disabled" />
|
||||
<el-option
|
||||
v-for="option in editStatusOptions"
|
||||
:key="option.value"
|
||||
:label="option.label"
|
||||
:value="option.value"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
|
||||
Reference in New Issue
Block a user