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