From f26e787cfcc5bc57f28475be4f22fdd0d8951acb Mon Sep 17 00:00:00 2001 From: "Logan.Li" Date: Wed, 12 Aug 2026 21:49:55 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A6=BB=E7=BA=BF=E8=AF=81=E5=88=B0?= =?UTF-8?q?=E6=9C=9F=E9=87=8D=E7=AD=BE=E7=A9=BA=E9=82=AE=E7=AE=B1=E6=92=9E?= =?UTF-8?q?=E5=94=AF=E4=B8=80=E7=B4=A2=E5=BC=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 复用 customer_id/同名客户,禁止重复插入空 email;重签前端带回原客户。 Co-authored-by: Cursor --- backend/internal/dto/license.go | 1 + .../service/license/license_service.go | 5 ++ .../service/license/offline_service.go | 65 +++++++++++++------ frontend/src/views/OfflineLicenses.vue | 15 +++-- 4 files changed, 62 insertions(+), 24 deletions(-) diff --git a/backend/internal/dto/license.go b/backend/internal/dto/license.go index a1f0188..c12d248 100644 --- a/backend/internal/dto/license.go +++ b/backend/internal/dto/license.go @@ -32,6 +32,7 @@ type LicenseResponse struct { DeviceID string `json:"device_id,omitempty"` // 离线证绑定设备 CustomerID uint `json:"customer_id"` CustomerName string `json:"customer_name"` + CustomerEmail string `json:"customer_email,omitempty"` ProductID uint `json:"product_id"` ProductName string `json:"product_name"` Status string `json:"status"` diff --git a/backend/internal/service/license/license_service.go b/backend/internal/service/license/license_service.go index 53d8ee7..4bde05d 100644 --- a/backend/internal/service/license/license_service.go +++ b/backend/internal/service/license/license_service.go @@ -304,8 +304,12 @@ func (s *LicenseService) ExportCSV(q ListQuery) (string, error) { // toLicenseResponse 模型转响应 DTO // Logan.Li func toLicenseResponse(l model.License) dto.LicenseResponse { customerName := "" + customerEmail := "" if l.Customer.ID > 0 { customerName = l.Customer.Name + if !strings.HasSuffix(l.Customer.Email, "@local.invalid") { + customerEmail = l.Customer.Email + } } productName := "" if l.Product.ID > 0 { @@ -332,6 +336,7 @@ func toLicenseResponse(l model.License) dto.LicenseResponse { DeviceID: deviceID, CustomerID: l.CustomerID, CustomerName: customerName, + CustomerEmail: customerEmail, ProductID: l.ProductID, ProductName: productName, Status: l.Status, diff --git a/backend/internal/service/license/offline_service.go b/backend/internal/service/license/offline_service.go index 4bb408e..2fd7fbc 100644 --- a/backend/internal/service/license/offline_service.go +++ b/backend/internal/service/license/offline_service.go @@ -6,6 +6,7 @@ import ( "backend/internal/model" "backend/utils" "errors" + "fmt" "strings" "time" @@ -85,34 +86,24 @@ func (s *OfflineIssuer) Issue(req dto.OfflineLicenseCreate) (*dto.OfflineLicense return nil, errors.New("客户不存在") } } else { - email := strings.TrimSpace(req.Email) - name := strings.TrimSpace(req.CustomerName) - if name == "" { - name = "离线客户" - } - if email != "" { - err := s.db.Where("email = ?", email).First(&customer).Error - if err != nil { - customer = model.Customer{Name: name, Email: email, Status: 1} - if err := s.db.Create(&customer).Error; err != nil { - return nil, errors.New("创建客户失败") - } - } - } else { - customer = model.Customer{Name: name, Email: "", Status: 1} - if err := s.db.Create(&customer).Error; err != nil { - return nil, errors.New("创建客户失败") - } + var err error + customer, err = s.resolveOrCreateCustomer(req) + if err != nil { + return nil, err } } now := time.Now().UTC() exp := now.AddDate(0, 0, duration) + claimEmail := strings.TrimSpace(req.Email) + if claimEmail == "" && customer.Email != "" && !strings.HasSuffix(customer.Email, "@local.invalid") { + claimEmail = customer.Email + } claims := utils.OfflineLicenseClaims{ V: 1, ProductCode: product.Code, DeviceID: deviceID, - Email: strings.TrimSpace(req.Email), + Email: claimEmail, PlanType: planType, IssuedAt: now.Unix(), ExpiresAt: exp.Unix(), @@ -175,3 +166,39 @@ func (s *OfflineIssuer) Issue(req dto.OfflineLicenseCreate) (*dto.OfflineLicense PublicKey: s.kp.OfflinePublicKeyHex(), }, nil } + +// resolveOrCreateCustomer 解析或创建离线签发客户;禁止重复插入空邮箱(uniqueIndex) // Logan.Li +func (s *OfflineIssuer) resolveOrCreateCustomer(req dto.OfflineLicenseCreate) (model.Customer, error) { + email := strings.TrimSpace(req.Email) + name := strings.TrimSpace(req.CustomerName) + if name == "" { + name = "离线客户" + } + + var customer model.Customer + if email != "" { + if err := s.db.Where("email = ?", email).First(&customer).Error; err == nil { + return customer, nil + } + customer = model.Customer{Name: name, Email: email, Status: 1} + if err := s.db.Create(&customer).Error; err != nil { + // 并发下可能已被创建,再查一次 // Logan.Li + if err2 := s.db.Where("email = ?", email).First(&customer).Error; err2 == nil { + return customer, nil + } + return customer, errors.New("创建客户失败") + } + return customer, nil + } + + // 无邮箱:优先复用同名客户,避免空邮箱触发 UNIQUE 冲突 // Logan.Li + if err := s.db.Where("name = ?", name).Order("id ASC").First(&customer).Error; err == nil { + return customer, nil + } + placeholder := fmt.Sprintf("offline+%d@local.invalid", time.Now().UnixNano()) + customer = model.Customer{Name: name, Email: placeholder, Status: 1} + if err := s.db.Create(&customer).Error; err != nil { + return customer, errors.New("创建客户失败") + } + return customer, nil +} diff --git a/frontend/src/views/OfflineLicenses.vue b/frontend/src/views/OfflineLicenses.vue index ff575f6..e560c3c 100644 --- a/frontend/src/views/OfflineLicenses.vue +++ b/frontend/src/views/OfflineLicenses.vue @@ -135,7 +135,7 @@ const issueVisible = ref(false) const issuing = ref(false) const formRef = ref(null) const form = reactive({ - device_id: '', email: '', customer_name: '', product_code: 'opsxshell', + device_id: '', email: '', customer_name: '', customer_id: 0, product_code: 'opsxshell', plan_type: 'yearly', duration_days: 365, notes: '', }) const result = ref(null) @@ -167,7 +167,7 @@ async function loadData() { function openIssue() { Object.assign(form, { - device_id: '', email: '', customer_name: '', product_code: 'opsxshell', + device_id: '', email: '', customer_name: '', customer_id: 0, product_code: 'opsxshell', plan_type: 'yearly', duration_days: 365, notes: '', }) result.value = null @@ -177,8 +177,9 @@ function openIssue() { function reissue(row) { Object.assign(form, { device_id: row.device_id || '', - email: '', + email: row.customer_email || '', customer_name: row.customer_name || '', + customer_id: row.customer_id || 0, product_code: 'opsxshell', plan_type: row.is_trial ? 'trial' : 'yearly', duration_days: row.is_trial ? 180 : 365, @@ -193,7 +194,7 @@ async function submitIssue() { if (!valid) return issuing.value = true try { - const res = await issueOfflineLicense({ + const payload = { device_id: form.device_id.trim(), email: form.email.trim() || undefined, customer_name: form.customer_name.trim() || undefined, @@ -201,7 +202,11 @@ async function submitIssue() { duration_days: form.duration_days, notes: form.notes, product_code: form.product_code.trim() || 'opsxshell', - }) + } + if (form.customer_id > 0) { + payload.customer_id = form.customer_id + } + const res = await issueOfflineLicense(payload) result.value = res.data loadData() ElMessage.success('已签发(请复制序列号;列表仅显示短码)')