fix: 离线证到期重签空邮箱撞唯一索引
复用 customer_id/同名客户,禁止重复插入空 email;重签前端带回原客户。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
bac72e6945
commit
f26e787cfc
4 files changed
+62
-24
No files matched your search
@@ -32,6 +32,7 @@ type LicenseResponse struct {
|
|||||||
DeviceID string `json:"device_id,omitempty"` // 离线证绑定设备
|
DeviceID string `json:"device_id,omitempty"` // 离线证绑定设备
|
||||||
CustomerID uint `json:"customer_id"`
|
CustomerID uint `json:"customer_id"`
|
||||||
CustomerName string `json:"customer_name"`
|
CustomerName string `json:"customer_name"`
|
||||||
|
CustomerEmail string `json:"customer_email,omitempty"`
|
||||||
ProductID uint `json:"product_id"`
|
ProductID uint `json:"product_id"`
|
||||||
ProductName string `json:"product_name"`
|
ProductName string `json:"product_name"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
|||||||
@@ -304,8 +304,12 @@ func (s *LicenseService) ExportCSV(q ListQuery) (string, error) {
|
|||||||
// toLicenseResponse 模型转响应 DTO // Logan.Li
|
// toLicenseResponse 模型转响应 DTO // Logan.Li
|
||||||
func toLicenseResponse(l model.License) dto.LicenseResponse {
|
func toLicenseResponse(l model.License) dto.LicenseResponse {
|
||||||
customerName := ""
|
customerName := ""
|
||||||
|
customerEmail := ""
|
||||||
if l.Customer.ID > 0 {
|
if l.Customer.ID > 0 {
|
||||||
customerName = l.Customer.Name
|
customerName = l.Customer.Name
|
||||||
|
if !strings.HasSuffix(l.Customer.Email, "@local.invalid") {
|
||||||
|
customerEmail = l.Customer.Email
|
||||||
|
}
|
||||||
}
|
}
|
||||||
productName := ""
|
productName := ""
|
||||||
if l.Product.ID > 0 {
|
if l.Product.ID > 0 {
|
||||||
@@ -332,6 +336,7 @@ func toLicenseResponse(l model.License) dto.LicenseResponse {
|
|||||||
DeviceID: deviceID,
|
DeviceID: deviceID,
|
||||||
CustomerID: l.CustomerID,
|
CustomerID: l.CustomerID,
|
||||||
CustomerName: customerName,
|
CustomerName: customerName,
|
||||||
|
CustomerEmail: customerEmail,
|
||||||
ProductID: l.ProductID,
|
ProductID: l.ProductID,
|
||||||
ProductName: productName,
|
ProductName: productName,
|
||||||
Status: l.Status,
|
Status: l.Status,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"backend/internal/model"
|
"backend/internal/model"
|
||||||
"backend/utils"
|
"backend/utils"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -85,34 +86,24 @@ func (s *OfflineIssuer) Issue(req dto.OfflineLicenseCreate) (*dto.OfflineLicense
|
|||||||
return nil, errors.New("客户不存在")
|
return nil, errors.New("客户不存在")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
email := strings.TrimSpace(req.Email)
|
var err error
|
||||||
name := strings.TrimSpace(req.CustomerName)
|
customer, err = s.resolveOrCreateCustomer(req)
|
||||||
if name == "" {
|
if err != nil {
|
||||||
name = "离线客户"
|
return nil, err
|
||||||
}
|
|
||||||
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("创建客户失败")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
exp := now.AddDate(0, 0, duration)
|
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{
|
claims := utils.OfflineLicenseClaims{
|
||||||
V: 1,
|
V: 1,
|
||||||
ProductCode: product.Code,
|
ProductCode: product.Code,
|
||||||
DeviceID: deviceID,
|
DeviceID: deviceID,
|
||||||
Email: strings.TrimSpace(req.Email),
|
Email: claimEmail,
|
||||||
PlanType: planType,
|
PlanType: planType,
|
||||||
IssuedAt: now.Unix(),
|
IssuedAt: now.Unix(),
|
||||||
ExpiresAt: exp.Unix(),
|
ExpiresAt: exp.Unix(),
|
||||||
@@ -175,3 +166,39 @@ func (s *OfflineIssuer) Issue(req dto.OfflineLicenseCreate) (*dto.OfflineLicense
|
|||||||
PublicKey: s.kp.OfflinePublicKeyHex(),
|
PublicKey: s.kp.OfflinePublicKeyHex(),
|
||||||
}, nil
|
}, 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
|
||||||
|
}
|
||||||
@@ -135,7 +135,7 @@ const issueVisible = ref(false)
|
|||||||
const issuing = ref(false)
|
const issuing = ref(false)
|
||||||
const formRef = ref(null)
|
const formRef = ref(null)
|
||||||
const form = reactive({
|
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: '',
|
plan_type: 'yearly', duration_days: 365, notes: '',
|
||||||
})
|
})
|
||||||
const result = ref(null)
|
const result = ref(null)
|
||||||
@@ -167,7 +167,7 @@ async function loadData() {
|
|||||||
|
|
||||||
function openIssue() {
|
function openIssue() {
|
||||||
Object.assign(form, {
|
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: '',
|
plan_type: 'yearly', duration_days: 365, notes: '',
|
||||||
})
|
})
|
||||||
result.value = null
|
result.value = null
|
||||||
@@ -177,8 +177,9 @@ function openIssue() {
|
|||||||
function reissue(row) {
|
function reissue(row) {
|
||||||
Object.assign(form, {
|
Object.assign(form, {
|
||||||
device_id: row.device_id || '',
|
device_id: row.device_id || '',
|
||||||
email: '',
|
email: row.customer_email || '',
|
||||||
customer_name: row.customer_name || '',
|
customer_name: row.customer_name || '',
|
||||||
|
customer_id: row.customer_id || 0,
|
||||||
product_code: 'opsxshell',
|
product_code: 'opsxshell',
|
||||||
plan_type: row.is_trial ? 'trial' : 'yearly',
|
plan_type: row.is_trial ? 'trial' : 'yearly',
|
||||||
duration_days: row.is_trial ? 180 : 365,
|
duration_days: row.is_trial ? 180 : 365,
|
||||||
@@ -193,7 +194,7 @@ async function submitIssue() {
|
|||||||
if (!valid) return
|
if (!valid) return
|
||||||
issuing.value = true
|
issuing.value = true
|
||||||
try {
|
try {
|
||||||
const res = await issueOfflineLicense({
|
const payload = {
|
||||||
device_id: form.device_id.trim(),
|
device_id: form.device_id.trim(),
|
||||||
email: form.email.trim() || undefined,
|
email: form.email.trim() || undefined,
|
||||||
customer_name: form.customer_name.trim() || undefined,
|
customer_name: form.customer_name.trim() || undefined,
|
||||||
@@ -201,7 +202,11 @@ async function submitIssue() {
|
|||||||
duration_days: form.duration_days,
|
duration_days: form.duration_days,
|
||||||
notes: form.notes,
|
notes: form.notes,
|
||||||
product_code: form.product_code.trim() || 'opsxshell',
|
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
|
result.value = res.data
|
||||||
loadData()
|
loadData()
|
||||||
ElMessage.success('已签发(请复制序列号;列表仅显示短码)')
|
ElMessage.success('已签发(请复制序列号;列表仅显示短码)')
|
||||||
|
|||||||
Reference in new issue
Block a user