diff --git a/backend/internal/repository/authorization_code_repository.go b/backend/internal/repository/authorization_code_repository.go index 530d14b..47fe787 100644 --- a/backend/internal/repository/authorization_code_repository.go +++ b/backend/internal/repository/authorization_code_repository.go @@ -58,10 +58,11 @@ func (r *authorizationCodeRepository) GetAuthorizationCodeList(ctx context.Conte order = "desc" } - // 构建查询,包含状态计算 + // 构建查询,包含状态计算与当前激活数量 query := r.db.WithContext(ctx).Table("authorization_codes ac"). Select(`ac.id, ac.code, ac.customer_id, c.customer_name, ac.start_date, ac.end_date, ac.max_activations, + COALESCE(l.active_count, 0) AS current_activations, ac.deployment_type, ac.is_locked, ac.description, ac.created_at, CASE WHEN ac.is_locked = true THEN 'locked' @@ -69,7 +70,13 @@ func (r *authorizationCodeRepository) GetAuthorizationCodeList(ctx context.Conte WHEN ac.start_date <= NOW() AND ac.end_date >= NOW() THEN 'normal' ELSE 'expired' END AS status`). - Joins("LEFT JOIN customers c ON ac.customer_id = c.id AND c.deleted_at IS NULL") + Joins("LEFT JOIN customers c ON ac.customer_id = c.id AND c.deleted_at IS NULL"). + Joins(`LEFT JOIN ( + SELECT authorization_code_id, COUNT(*) AS active_count + FROM licenses + WHERE status = 'active' AND deleted_at IS NULL + GROUP BY authorization_code_id + ) l ON ac.id = l.authorization_code_id`) // 添加筛选条件 if req.CustomerID != "" { @@ -103,18 +110,19 @@ func (r *authorizationCodeRepository) GetAuthorizationCodeList(ctx context.Conte // 分页查询 var results []struct { - ID string `json:"id"` - Code string `json:"code"` - CustomerID string `json:"customer_id"` - CustomerName *string `json:"customer_name"` - StartDate string `json:"start_date"` - EndDate string `json:"end_date"` - MaxActivations int `json:"max_activations"` - DeploymentType string `json:"deployment_type"` - IsLocked bool `json:"is_locked"` - Description *string `json:"description"` - CreatedAt string `json:"created_at"` - Status string `json:"status"` // 添加状态字段 + ID string `json:"id"` + Code string `json:"code"` + CustomerID string `json:"customer_id"` + CustomerName *string `json:"customer_name"` + StartDate string `json:"start_date"` + EndDate string `json:"end_date"` + MaxActivations int `json:"max_activations"` + CurrentActivations int `json:"current_activations" gorm:"column:current_activations"` + DeploymentType string `json:"deployment_type"` + IsLocked bool `json:"is_locked"` + Description *string `json:"description"` + CreatedAt string `json:"created_at"` + Status string `json:"status"` // 添加状态字段 } offset := (page - 1) * pageSize @@ -140,7 +148,7 @@ func (r *authorizationCodeRepository) GetAuthorizationCodeList(ctx context.Conte StartDate: result.StartDate, EndDate: result.EndDate, MaxActivations: result.MaxActivations, - CurrentActivations: 0, // TODO: 从licenses表统计 + CurrentActivations: result.CurrentActivations, DeploymentType: result.DeploymentType, IsLocked: result.IsLocked, Description: result.Description, diff --git a/backend/internal/service/license_service.go b/backend/internal/service/license_service.go index 4e8c37a..6119cdf 100644 --- a/backend/internal/service/license_service.go +++ b/backend/internal/service/license_service.go @@ -125,14 +125,17 @@ func (s *licenseService) CreateLicense(ctx context.Context, req *models.LicenseC return nil, i18n.NewI18nError("900004", lang, err.Error()) } - // 构建许可证实体 + now := time.Now() + + // 构建许可证实体,手动添加的离线许可证直接视为已激活 license := &models.License{ LicenseKey: licenseKey, AuthorizationCodeID: req.AuthorizationCodeID, CustomerID: authCode.CustomerID, // 从授权码获取客户ID HardwareFingerprint: req.HardwareFingerprint, ActivationIP: req.ActivationIP, - Status: "inactive", // 手动创建的许可证默认为未激活状态 + Status: "active", + ActivatedAt: &now, } // 设置设备信息 diff --git a/frontend/src/api/license.ts b/frontend/src/api/license.ts index 28d1dc1..6033f60 100644 --- a/frontend/src/api/license.ts +++ b/frontend/src/api/license.ts @@ -20,6 +20,7 @@ export interface AuthorizationCode { encryption_type_display?: string; software_version?: string; max_activations: number; + current_activations?: number; activated_licenses_count?: number; is_locked?: boolean; feature_config?: any; @@ -301,6 +302,23 @@ export function getLicenseDeviceDetail(id: string): Promise + activation_ip?: string +} + +/** + * 手动创建许可证 + */ +export function createLicenseDevice( + data: LicenseDeviceCreateRequest +): Promise> { + return Axios.post('/api/v1/licenses', data) +} + // 授权变更历史相关类型定义 export interface AuthorizationChangeItem { id: string diff --git a/frontend/src/components/common/layout/Layout.vue b/frontend/src/components/common/layout/Layout.vue index 27169b5..4788f70 100644 --- a/frontend/src/components/common/layout/Layout.vue +++ b/frontend/src/components/common/layout/Layout.vue @@ -64,9 +64,10 @@ const { t } = useI18n() const defaultNavItems = computed(() => [ { id: "dashboard", label: t('navigation.menu.dashboard'), href: "/dashboard", icon: "dashboard" }, { id: "customers", label: t('navigation.menu.customers'), href: "/customers", icon: "customers" }, - { id: "licenses", label: t('navigation.menu.licenses'), href: "/licenses", icon: "licenses" }, - { id: "roles", label: t('navigation.menu.roles'), href: "/roles", icon: "roles" }, - { id: "users", label: t('navigation.menu.users'), href: "/users", icon: "users" } + { id: "licenses", label: t('navigation.menu.licenses'), href: "/licenses", icon: "licenses" } + // 暂时隐藏角色和系统用户菜单,后续版本再开启 + // { id: "roles", label: t('navigation.menu.roles'), href: "/roles", icon: "roles" }, + // { id: "users", label: t('navigation.menu.users'), href: "/users", icon: "users" } ]) // 使用 store 和路由 diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 2ba2418..608016f 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -316,7 +316,7 @@ "authorizationInfo": { "basicInfo": "Basic Information", "customerName": "Customer Name", - "customerId": "Customer ID", + "customerCode": "Customer Code", "createdBy": "Created By", "description": "Description", "title": "Authorization Information", @@ -334,6 +334,54 @@ "code": "Authorization Code", "tipText": "Provide this authorization code to the customer, who can automatically obtain the software license after configuring it in the software product" }, + "licenseInfo": { + "addButton": "Add License", + "emptyDescription": "No device licenses yet", + "deviceTitle": "Device {index}", + "deviceAddress": "Device address", + "lastActiveTime": "Last active time", + "hardwareSectionTitle": "Hardware Information", + "hardwareSectionSubtitle": "(Binding info)", + "hardwareFingerprint": "Hardware fingerprint", + "softwareSectionTitle": "Software License", + "softwareSectionSubtitle": "(Used for validation)", + "licenseKey": "License key", + "licenseStatus": "License status", + "statusDescription": "This license is generated and bound to the specified hardware for runtime validation.", + "messages": { + "loadError": "Failed to load license devices" + } + }, + "licenseModal": { + "title": "Add License", + "description": "Bind this authorization code to a hardware fingerprint.", + "fields": { + "authorizationCode": "Authorization Code", + "hardwareFingerprint": "Hardware Fingerprint", + "remark": "Remark" + }, + "placeholders": { + "hardwareFingerprint": "Enter hardware fingerprint", + "remark": "Add a remark (optional)" + }, + "validation": { + "hardwareRequired": "Hardware fingerprint is required", + "hardwareMax": "Hardware fingerprint cannot exceed 200 characters", + "remarkMax": "Remark cannot exceed 3000 characters" + }, + "messages": { + "success": "License created successfully", + "error": "Failed to create license", + "missingAuthorization": "Missing authorization code information", + "copySuccess": "Authorization code copied", + "copyError": "Failed to copy authorization code" + }, + "buttons": { + "cancel": "Cancel", + "submit": "Create", + "copy": "Copy" + } + }, "actions": { "copyCode": "Copy License Code", "updateLicense": "Update License", @@ -574,7 +622,7 @@ "customerNotExist": "Customer data does not exist" }, "basicInfo": { - "customerId": "Customer ID", + "customerCode": "Customer Code", "customerName": "Customer Name", "activatedDevices": "Activated Devices Count", "creationTime": "Creation Time", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index 6aa86d9..71e0c3f 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -316,7 +316,7 @@ "authorizationInfo": { "basicInfo": "基本情報", "customerName": "顧客名", - "customerId": "顧客ID", + "customerCode": "顧客コード", "createdBy": "作成者", "description": "備考", "title": "認証情報", @@ -334,6 +334,54 @@ "code": "認証コード", "tipText": "この認証コードを顧客に提供してください。顧客がソフトウェア製品で設定すると、ソフトウェアライセンスを自動的に取得できます" }, + "licenseInfo": { + "addButton": "ライセンスを追加", + "emptyDescription": "デバイスに紐づくライセンスはありません", + "deviceTitle": "デバイス{index}", + "deviceAddress": "デバイスアドレス", + "lastActiveTime": "最終アクティブ時間", + "hardwareSectionTitle": "ハードウェア情報", + "hardwareSectionSubtitle": "(バインド情報)", + "hardwareFingerprint": "ハードウェア指紋", + "softwareSectionTitle": "ソフトウェアライセンス", + "softwareSectionSubtitle": "(システム検証用)", + "licenseKey": "ライセンスキー", + "licenseStatus": "ライセンス状態", + "statusDescription": "このライセンスは指定ハードウェアにバインドされ、システム検証に利用されます", + "messages": { + "loadError": "デバイスライセンスの取得に失敗しました" + } + }, + "licenseModal": { + "title": "ライセンスを追加", + "description": "この認証コードをハードウェア指紋に紐づけてください。", + "fields": { + "authorizationCode": "認証コード", + "hardwareFingerprint": "ハードウェア指紋", + "remark": "備考" + }, + "placeholders": { + "hardwareFingerprint": "ハードウェア指紋を入力してください", + "remark": "備考(任意)を入力してください" + }, + "validation": { + "hardwareRequired": "ハードウェア指紋を入力してください", + "hardwareMax": "ハードウェア指紋は200文字以内で入力してください", + "remarkMax": "備考は3000文字以内で入力してください" + }, + "messages": { + "success": "ライセンスを作成しました", + "error": "ライセンスの作成に失敗しました", + "missingAuthorization": "認証コード情報が見つかりません", + "copySuccess": "認証コードをコピーしました", + "copyError": "認証コードのコピーに失敗しました" + }, + "buttons": { + "cancel": "キャンセル", + "submit": "作成", + "copy": "コピー" + } + }, "actions": { "copyCode": "ライセンスコードをコピー", "updateLicense": "ライセンス更新", @@ -574,7 +622,7 @@ "customerNotExist": "顧客データが存在しません" }, "basicInfo": { - "customerId": "顧客ID", + "customerCode": "顧客コード", "customerName": "顧客名", "activatedDevices": "アクティベート済みデバイス数", "creationTime": "作成日時", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index dd06708..f45d0da 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -316,7 +316,7 @@ "authorizationInfo": { "basicInfo": "基本信息", "customerName": "客户名称", - "customerId": "客户ID", + "customerCode": "客户编码", "createdBy": "创建人", "description": "备注", "title": "授权信息", @@ -334,6 +334,54 @@ "code": "授权码", "tipText": "将此授权码提供给客户,客户在软件产品中配置后可自动获取软件许可证" }, + "licenseInfo": { + "addButton": "新增许可证", + "emptyDescription": "暂无设备许可证信息", + "deviceTitle": "设备{index}", + "deviceAddress": "设备地址", + "lastActiveTime": "最后活跃时间", + "hardwareSectionTitle": "硬件信息", + "hardwareSectionSubtitle": "(绑定信息)", + "hardwareFingerprint": "硬件指纹", + "softwareSectionTitle": "软件许可证", + "softwareSectionSubtitle": "(用于系统验证)", + "licenseKey": "许可证密钥", + "licenseStatus": "许可证状态", + "statusDescription": "此许可证已生成并绑定到指定硬件,可用于系统运行验证", + "messages": { + "loadError": "获取设备列表失败" + } + }, + "licenseModal": { + "title": "新增许可证", + "description": "请填写硬件指纹信息,将授权码绑定到指定设备。", + "fields": { + "authorizationCode": "授权码", + "hardwareFingerprint": "硬件指纹", + "remark": "备注" + }, + "placeholders": { + "hardwareFingerprint": "请输入硬件指纹信息", + "remark": "请输入备注信息(可选)" + }, + "validation": { + "hardwareRequired": "请输入硬件指纹", + "hardwareMax": "硬件指纹长度不能超过200个字符", + "remarkMax": "备注长度不能超过3000个字符" + }, + "messages": { + "success": "许可证创建成功", + "error": "许可证创建失败", + "missingAuthorization": "缺少授权码信息", + "copySuccess": "授权码已复制", + "copyError": "复制授权码失败" + }, + "buttons": { + "cancel": "取消", + "submit": "创建", + "copy": "复制" + } + }, "actions": { "copyCode": "复制授权码", "updateLicense": "变更授权", @@ -574,7 +622,7 @@ "customerNotExist": "客户数据不存在" }, "basicInfo": { - "customerId": "客户ID", + "customerCode": "客户编码", "customerName": "客户名称", "activatedDevices": "已激活设备数量", "creationTime": "创建时间", diff --git a/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue b/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue index 96c4760..02fa782 100644 --- a/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue +++ b/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue @@ -9,8 +9,8 @@
{{ licenseData?.customer_info.customer_name|| '-' }}
-
{{ t('pages.licenses.detail.authorizationInfo.customerId') }}:
-
{{ licenseData?.customer_id || '-' }}
+
{{ t('pages.licenses.detail.authorizationInfo.customerCode') }}:
+
{{ licenseData?.customer_info?.customer_code || '-' }}
{{ t('pages.licenses.detail.authorizationInfo.createdBy') }}:
diff --git a/frontend/src/views/Licenses/LicensesView/components/BasicInfo.vue b/frontend/src/views/Licenses/LicensesView/components/BasicInfo.vue index 26af18f..2426fa1 100644 --- a/frontend/src/views/Licenses/LicensesView/components/BasicInfo.vue +++ b/frontend/src/views/Licenses/LicensesView/components/BasicInfo.vue @@ -3,8 +3,8 @@
-
{{ $t('customers.basicInfo.customerId') }}
-
{{ licenseData?.customer_id || '-' }}
+
{{ $t('customers.basicInfo.customerCode') }}
+
{{ licenseData?.customer_info?.customer_code || '-' }}
{{ $t('customers.basicInfo.customerName') }}
@@ -17,7 +17,7 @@
-
{{ licenseData?.activated_licenses_count || 0 }}
+
{{ getActivatedLicenses(licenseData) }}
{{ $t('customers.basicInfo.activatedDevices') }}
@@ -59,6 +59,17 @@ interface Props { defineProps() +const getActivatedLicenses = (license?: AuthorizationCode | null) => { + if (!license) return 0 + if (typeof license.current_activations === 'number') { + return license.current_activations + } + if (typeof license.activated_licenses_count === 'number') { + return license.activated_licenses_count + } + return 0 +} + const formatDateShort = (date?: string) => { if (!date) return '-' return formatDateUtil(date) diff --git a/frontend/src/views/Licenses/LicensesView/components/LicenseInfo.vue b/frontend/src/views/Licenses/LicensesView/components/LicenseInfo.vue index 10a616b..f130737 100644 --- a/frontend/src/views/Licenses/LicensesView/components/LicenseInfo.vue +++ b/frontend/src/views/Licenses/LicensesView/components/LicenseInfo.vue @@ -1,48 +1,116 @@ @@ -168,6 +366,68 @@ const handleAddLicense = () => { width: 100%; } +.license-add-dialog { + :deep(.el-dialog) { + border-radius: 12px; + padding-bottom: 12px; + } + + :deep(.el-dialog__header) { + margin-right: 0; + border-bottom: 1px solid #f0f2f5; + padding: 16px 24px; + } + + :deep(.el-dialog__title) { + font-family: 'Source Han Sans CN', sans-serif; + font-size: 18px; + font-weight: 600; + color: #1d1d1d; + } + + :deep(.el-dialog__body) { + padding: 16px 24px 0; + } + + :deep(.el-dialog__footer) { + padding: 0 24px 24px; + } +} + +.dialog-description { + font-size: 14px; + color: #666; + line-height: 22px; + margin-bottom: 16px; +} + +.license-form-dialog { + :deep(.el-form-item__label) { + font-weight: 500; + color: #1d1d1d; + } +} + +.auth-code-row { + display: flex; + align-items: center; + gap: 12px; + + :deep(.el-input__wrapper) { + background: rgba(247, 248, 250, 0.8); + } +} + +.copy-btn { + padding: 0 8px; +} + +.dialog-footer { + display: flex; + justify-content: flex-end; + gap: 12px; +} + .action-bar { margin-bottom: 16px; diff --git a/frontend/src/views/Licenses/LinenseList.vue b/frontend/src/views/Licenses/LinenseList.vue index 289cd9c..05b77cc 100644 --- a/frontend/src/views/Licenses/LinenseList.vue +++ b/frontend/src/views/Licenses/LinenseList.vue @@ -67,16 +67,16 @@ align="center" > @@ -185,11 +185,21 @@ const getStatusClass = (status: string) => { } } +const getActivatedCount = (row: AuthorizationCode) => { + if (typeof row.current_activations === 'number') { + return row.current_activations + } + if (typeof row.activated_licenses_count === 'number') { + return row.activated_licenses_count + } + return 0 +} + const getActivationProgress = (row: AuthorizationCode) => { if (!row.max_activations || row.max_activations === 0) { return 0 } - const activated = row.activated_licenses_count || 0 + const activated = getActivatedCount(row) return Math.round((activated / row.max_activations) * 100) } @@ -613,7 +623,7 @@ onMounted(async () => { z-index: 10; } - // 当 activated_licenses_count 为 0 时,文字显示为红色 + // 当激活数量为 0 时,文字显示为红色 &.zero-activated { :deep(.el-progress-bar__innerText), :deep(.el-progress__text) {