From ce789093bd21de602366f69d758f4f975819c3da Mon Sep 17 00:00:00 2001 From: wood911010 Date: Fri, 28 Nov 2025 00:23:31 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E8=AE=B8=E5=8F=AF?= =?UTF-8?q?=E8=AF=81=E8=A1=A8=E5=8D=95=E5=92=8C=E6=8E=88=E6=9D=83=E4=BF=A1?= =?UTF-8?q?=E6=81=AF=E5=B1=95=E7=A4=BA=EF=BC=8C=E5=A2=9E=E5=8A=A0=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E9=85=8D=E7=BD=AE=E5=92=8C=E8=87=AA=E5=AE=9A=E4=B9=89?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E6=94=AF=E6=8C=81=EF=BC=8C=E4=BC=98=E5=8C=96?= =?UTF-8?q?=E5=9B=BD=E9=99=85=E5=8C=96=E6=96=87=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 42 +++- README_EN.md | 36 +++ backend/internal/service/license_service.go | 72 +++--- frontend/src/i18n/locales/en.json | 11 +- frontend/src/i18n/locales/ja.json | 15 +- frontend/src/i18n/locales/zh.json | 15 +- frontend/src/views/Licenses/LicenseForm.vue | 216 ++++++++++++++---- .../components/AuthorizationInfo.vue | 33 ++- 8 files changed, 342 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 8629f3f..c3eab26 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,23 @@ # 软件授权管理平台 +

+ + Latest Release + + + Downloads + + + License: GPL-3.0 + + + Open Issues + + + Open PRs + +

+ [中文](README.md) | [English](README_EN.md) --- @@ -9,13 +27,35 @@ 软件授权管理平台是一个独立的软件授权管理系统,为软件系统提供授权码生成、分发、验证和管理服务。支持在线和离线两种授权模式,基于硬件绑定的授权机制确保安全性。 ## Demo环境 + + Live Demo + 访问地址:[lm.cedar-v.com:18080](http://lm.cedar-v.com:18080) **登录凭证:** - 用户名:`admin` - 密码:`admin@123` -> 💡 这是一个当前功能的演示环境,您可以体验已开发的核心特性。我们正在持续开发中,更多功能将陆续上线。 +> 💡 这是一个当前功能的演示环境,您可以体验已开发的核心特性。 + +## 官方文档 + +官方文档请访问:[https://docs.cedar-v.com](https://docs.lm.cedar-v.com) + +## Star收藏与关注 + + + GitHub Stars + + + GitHub Watchers + + +如果这个项目对你有帮助,请给个star收藏,你的支持是我们最大的动力! + +## 客户端SDK + +客户端SDK请访问:[https://github.com/cedar-v/license-manager-sdk](https://github.com/cedar-v/license-manage-sdk-go) ## 系统界面 diff --git a/README_EN.md b/README_EN.md index 4e8bbae..2905006 100644 --- a/README_EN.md +++ b/README_EN.md @@ -1,5 +1,23 @@ # License Manager +

+ + Latest Release + + + Downloads + + + License: GPL-3.0 + + + Open Issues + + + Open PRs + +

+ [中文](README.md) | [English](README_EN.md) --- @@ -9,6 +27,9 @@ License Manager is an independent software licensing platform that provides license code generation, distribution, validation, and management services for IoT platforms and other software systems. It offers both online and offline licensing modes with hardware-based binding for enhanced security. ## Demo Environment + + Live Demo + Access URL: [lm.cedar-v.com:18080](http://lm.cedar-v.com:18080) **Login Credentials:** @@ -17,6 +38,21 @@ Access URL: [lm.cedar-v.com:18080](http://lm.cedar-v.com:18080) > 💡 This is a demo environment showcasing our current features. You can experience the implemented core functionalities while we continue active development with more features coming soon. +## Stars & Followers + + + GitHub Stars + + + GitHub Watchers + + +If this project helps you, please give it a star—your support keeps us moving! + +## Client SDK + +Client SDK repository: [https://github.com/cedar-v/license-manager-sdk](https://github.com/cedar-v/license-manage-sdk-go) + ## System Interface ### Login Page diff --git a/backend/internal/service/license_service.go b/backend/internal/service/license_service.go index 3f7cc9d..f1b2509 100644 --- a/backend/internal/service/license_service.go +++ b/backend/internal/service/license_service.go @@ -11,14 +11,15 @@ import ( "strings" "time" - "github.com/sirupsen/logrus" - "gorm.io/gorm" "license-manager/internal/config" "license-manager/internal/models" "license-manager/internal/repository" pkgcontext "license-manager/pkg/context" "license-manager/pkg/i18n" "license-manager/pkg/utils" + + "github.com/sirupsen/logrus" + "gorm.io/gorm" ) type licenseService struct { @@ -333,27 +334,16 @@ func (s *licenseService) GenerateLicenseFile(ctx context.Context, id string) ([] licenseFileData["max_activations"] = license.AuthorizationCode.MaxActivations // 包含功能配置 - if len(license.AuthorizationCode.FeatureConfig) > 0 { - var featureConfig map[string]interface{} - if err := json.Unmarshal(license.AuthorizationCode.FeatureConfig, &featureConfig); err == nil { - licenseFileData["feature_config"] = featureConfig - } + if featureConfig := parseJSONField(license.AuthorizationCode.FeatureConfig); len(featureConfig) > 0 { + licenseFileData["feature_config"] = featureConfig } - // 包含使用限制 - if len(license.AuthorizationCode.UsageLimits) > 0 { - var usageLimits map[string]interface{} - if err := json.Unmarshal(license.AuthorizationCode.UsageLimits, &usageLimits); err == nil { - licenseFileData["usage_limits"] = usageLimits - } + if usageLimits := parseJSONField(license.AuthorizationCode.UsageLimits); len(usageLimits) > 0 { + licenseFileData["usage_limits"] = usageLimits } - // 包含自定义参数 - if len(license.AuthorizationCode.CustomParameters) > 0 { - var customParameters map[string]interface{} - if err := json.Unmarshal(license.AuthorizationCode.CustomParameters, &customParameters); err == nil { - licenseFileData["custom_parameters"] = customParameters - } + if customParameters := parseJSONField(license.AuthorizationCode.CustomParameters); len(customParameters) > 0 { + licenseFileData["custom_parameters"] = customParameters } } @@ -669,25 +659,16 @@ func (s *licenseService) generateLicenseFileContent(license *models.License, aut licenseFileData["max_activations"] = authCode.MaxActivations // 包含功能配置等 - if len(authCode.FeatureConfig) > 0 { - var featureConfig map[string]interface{} - if err := json.Unmarshal(authCode.FeatureConfig, &featureConfig); err == nil { - licenseFileData["feature_config"] = featureConfig - } + if featureConfig := parseJSONField(authCode.FeatureConfig); len(featureConfig) > 0 { + licenseFileData["feature_config"] = featureConfig } - if len(authCode.UsageLimits) > 0 { - var usageLimits map[string]interface{} - if err := json.Unmarshal(authCode.UsageLimits, &usageLimits); err == nil { - licenseFileData["usage_limits"] = usageLimits - } + if usageLimits := parseJSONField(authCode.UsageLimits); len(usageLimits) > 0 { + licenseFileData["usage_limits"] = usageLimits } - if len(authCode.CustomParameters) > 0 { - var customParameters map[string]interface{} - if err := json.Unmarshal(authCode.CustomParameters, &customParameters); err == nil { - licenseFileData["custom_parameters"] = customParameters - } + if customParameters := parseJSONField(authCode.CustomParameters); len(customParameters) > 0 { + licenseFileData["custom_parameters"] = customParameters } } @@ -764,6 +745,29 @@ func (s *licenseService) signLicenseFile(data []byte) ([]byte, error) { return []byte(encoded), nil } +// parseJSONField 将数据库中的 JSON 字段解析为 map,兼容被字符串包裹的情况 +func parseJSONField(raw models.JSON) map[string]interface{} { + if len(raw) == 0 { + return nil + } + + var result map[string]interface{} + if err := json.Unmarshal(raw, &result); err == nil { + return result + } + + // 兼容字段以字符串形式保存 JSON 的情况,例如 "\"{\\\"key\\\":123}\"" + var rawString string + if err := json.Unmarshal(raw, &rawString); err != nil { + return nil + } + + if err := json.Unmarshal([]byte(rawString), &result); err != nil { + return nil + } + return result +} + // GetStatsOverview 获取授权概览统计 func (s *licenseService) GetStatsOverview(ctx context.Context) (*models.StatsOverviewResponse, error) { lang := pkgcontext.GetLanguageFromContext(ctx) diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json index 532dbef..373d92d 100644 --- a/frontend/src/i18n/locales/en.json +++ b/frontend/src/i18n/locales/en.json @@ -339,8 +339,9 @@ "forActivation": "(For Client Activation)", "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", - "customParameters": "Feature Settings", + "featureConfig": "Feature Configuration", "usageLimits": "Usage Limits", + "customParameters": "Custom Parameters", "typeColumn": "Type", "keyColumn": "Key", "valueColumn": "Value", @@ -449,8 +450,9 @@ "sections": { "basicInfo": "Basic Information", "licenseConfig": "License Configuration", - "customParameters": "Feature Settings", - "usageLimits": "Usage Limits" + "featureConfig": "Feature Configuration", + "usageLimits": "Usage Limits", + "customParameters": "Custom Parameters" }, "fields": { "customerName": "Associated Customer Name", @@ -550,8 +552,9 @@ "booleanFalse": "false", "validationFailed": "Fix the errors in feature or limit settings before submitting", "importJson": "Import JSON", - "importTitleFeature": "Import Feature Settings JSON", + "importTitleFeature": "Import Feature Configuration JSON", "importTitleLimit": "Import Usage Limits JSON", + "importTitleCustom": "Import Custom Parameters JSON", "importDescription": "Paste a JSON object whose values can be string, number, or boolean. Confirming will overwrite current entries.", "importPlaceholder": "{ \"feature_dashboard\": \"enabled\" }", "importCancel": "Cancel", diff --git a/frontend/src/i18n/locales/ja.json b/frontend/src/i18n/locales/ja.json index ff4bf4a..04902c9 100644 --- a/frontend/src/i18n/locales/ja.json +++ b/frontend/src/i18n/locales/ja.json @@ -339,8 +339,9 @@ "forActivation": "(クライアントアクティベーション用)", "code": "認証コード", "tipText": "この認証コードを顧客に提供してください。顧客がソフトウェア製品で設定すると、ソフトウェアライセンスを自動的に取得できます", - "customParameters": "機能設定", - "usageLimits": "制限設定", + "featureConfig": "機能構成", + "usageLimits": "利用制限", + "customParameters": "カスタムパラメータ", "typeColumn": "タイプ", "keyColumn": "キー", "valueColumn": "値", @@ -449,8 +450,9 @@ "sections": { "basicInfo": "基本情報", "licenseConfig": "ライセンス設定", - "customParameters": "機能設定", - "usageLimits": "制限設定" + "featureConfig": "機能構成", + "usageLimits": "利用制限", + "customParameters": "カスタムパラメータ" }, "fields": { "customerName": "関連顧客名", @@ -550,8 +552,9 @@ "booleanFalse": "false", "validationFailed": "送信前に機能設定または制限設定のエラーを修正してください", "importJson": "JSONをインポート", - "importTitleFeature": "機能設定 JSON をインポート", - "importTitleLimit": "制限設定 JSON をインポート", + "importTitleFeature": "機能構成 JSON をインポート", + "importTitleLimit": "利用制限 JSON をインポート", + "importTitleCustom": "カスタムパラメータ JSON をインポート", "importDescription": "キーを設定名とするオブジェクト形式の JSON を貼り付けてください。値は文字列・数値・ブールをサポートします。確定すると現在のリストを上書きします。", "importPlaceholder": "{ \"feature_dashboard\": \"enabled\" }", "importCancel": "キャンセル", diff --git a/frontend/src/i18n/locales/zh.json b/frontend/src/i18n/locales/zh.json index 838a6dc..5e74f35 100644 --- a/frontend/src/i18n/locales/zh.json +++ b/frontend/src/i18n/locales/zh.json @@ -339,8 +339,9 @@ "forActivation": "(用于客户端激活)", "code": "授权码", "tipText": "将此授权码提供给客户,客户在软件产品中配置后可自动获取软件许可证", - "customParameters": "功能设置", - "usageLimits": "限制设置", + "featureConfig": "功能配置", + "usageLimits": "使用限制", + "customParameters": "自定义参数", "typeColumn": "类型", "keyColumn": "键", "valueColumn": "值", @@ -449,8 +450,9 @@ "sections": { "basicInfo": "基本信息", "licenseConfig": "授权配置", - "customParameters": "功能设置", - "usageLimits": "限制设置" + "featureConfig": "功能配置", + "usageLimits": "使用限制", + "customParameters": "自定义参数" }, "fields": { "customerName": "关联客户名称", @@ -550,8 +552,9 @@ "booleanFalse": "false", "validationFailed": "请先修复功能或限制设置中的错误再提交", "importJson": "导入JSON", - "importTitleFeature": "导入功能设置 JSON", - "importTitleLimit": "导入限制设置 JSON", + "importTitleFeature": "导入功能配置 JSON", + "importTitleLimit": "导入使用限制 JSON", + "importTitleCustom": "导入自定义参数 JSON", "importDescription": "粘贴一个对象格式的 JSON,键为配置项,值支持字符串、数字或布尔类型。提交后将覆盖当前列表。", "importPlaceholder": "{ \"feature_dashboard\": \"enabled\" }", "importCancel": "取消", diff --git a/frontend/src/views/Licenses/LicenseForm.vue b/frontend/src/views/Licenses/LicenseForm.vue index a56d72a..945ec72 100644 --- a/frontend/src/views/Licenses/LicenseForm.vue +++ b/frontend/src/views/Licenses/LicenseForm.vue @@ -211,22 +211,27 @@ - +
-

{{ t('pages.licenses.form.sections.customParameters') }}

+

{{ t('pages.licenses.form.sections.featureConfig') }}

- + {{ t('pages.licenses.form.keyValue.addItem') }} - + {{ t('pages.licenses.form.keyValue.clearAll') }} {{ t('pages.licenses.form.keyValue.copyJson') }} @@ -237,7 +242,7 @@
-
+

{{ item.valueError }}

- + {{ t('pages.licenses.form.keyValue.remove') }}
-
+
{{ t('pages.licenses.form.keyValue.emptyState') }}
@@ -293,7 +298,7 @@
{{ t('pages.licenses.form.keyValue.preview') }}
- +
@@ -382,17 +387,99 @@
+ + +
+
+

{{ t('pages.licenses.form.sections.customParameters') }}

+
+ + {{ t('pages.licenses.form.keyValue.addItem') }} + + + {{ t('pages.licenses.form.keyValue.clearAll') }} + + + {{ t('pages.licenses.form.keyValue.copyJson') }} + + + {{ t('pages.licenses.form.keyValue.importJson') }} + +
+
+ +
+
+
+ + +

{{ item.keyError }}

+
+
+ + + + +
+
+ + + +

{{ item.valueError }}

+
+ + {{ t('pages.licenses.form.keyValue.remove') }} + +
+ +
+ {{ t('pages.licenses.form.keyValue.emptyState') }} +
+
+ +
+
+ {{ t('pages.licenses.form.keyValue.preview') }} +
+ +
+
@@ -462,10 +549,20 @@ const submitting = ref(false) const customerLoading = ref(false) const customerOptions = ref([]) -const featureEntries = ref([]) +const featureConfigEntries = ref([]) const limitEntries = ref([]) +const customEntries = ref([]) const importDialogVisible = ref(false) -const importDialogTarget = ref<'feature' | 'limit'>('feature') +const importDialogTarget = ref<'feature' | 'limit' | 'custom'>('feature') +const importDialogTitle = computed(() => { + if (importDialogTarget.value === 'limit') { + return t('pages.licenses.form.keyValue.importTitleLimit') + } + if (importDialogTarget.value === 'custom') { + return t('pages.licenses.form.keyValue.importTitleCustom') + } + return t('pages.licenses.form.keyValue.importTitleFeature') +}) const importDialogContent = ref('') const importDialogError = ref('') @@ -497,8 +594,9 @@ const formData = reactive< // 独立的响应式变量 const dateRange = ref<[string, string] | null>(null) -const featureJsonPreview = computed(() => buildJsonString(featureEntries.value)) +const featureConfigPreview = computed(() => buildJsonString(featureConfigEntries.value)) const usageJsonPreview = computed(() => buildJsonString(limitEntries.value)) +const customJsonPreview = computed(() => buildJsonString(customEntries.value)) // 计算属性 const isEdit = computed(() => { @@ -671,9 +769,9 @@ const convertValueByType = (value: string, type: KeyValueType) => { return value } -const buildJsonString = (entries: KeyValueItem[]) => { +const buildJsonRecord = (entries: KeyValueItem[]) => { if (!entries.length) { - return '' + return null } const result: Record = {} entries.forEach(item => { @@ -686,33 +784,50 @@ const buildJsonString = (entries: KeyValueItem[]) => { } } }) - return Object.keys(result).length ? JSON.stringify(result) : '' + return Object.keys(result).length ? result : null } -const addFeatureEntry = () => { - featureEntries.value.push(createEmptyEntry()) +const buildJsonString = (entries: KeyValueItem[]) => { + const record = buildJsonRecord(entries) + return record ? JSON.stringify(record) : '' +} + +const addFeatureConfigEntry = () => { + featureConfigEntries.value.push(createEmptyEntry()) } const addLimitEntry = () => { limitEntries.value.push(createEmptyEntry()) } -const removeFeatureEntry = (index: number) => { - featureEntries.value.splice(index, 1) +const addCustomEntry = () => { + customEntries.value.push(createEmptyEntry()) +} + +const removeFeatureConfigEntry = (index: number) => { + featureConfigEntries.value.splice(index, 1) } const removeLimitEntry = (index: number) => { limitEntries.value.splice(index, 1) } -const clearFeatureEntries = () => { - featureEntries.value = [] +const removeCustomEntry = (index: number) => { + customEntries.value.splice(index, 1) +} + +const clearFeatureConfigEntries = () => { + featureConfigEntries.value = [] } const clearLimitEntries = () => { limitEntries.value = [] } +const clearCustomEntries = () => { + customEntries.value = [] +} + const handleEntryTypeChange = (entry: KeyValueItem) => { if (entry.type === 'bool') { entry.value = entry.value === 'false' ? 'false' : 'true' @@ -799,10 +914,14 @@ const validateKeyValueEntries = (entries: KeyValueItem[]) => { return isValid } -const openImportDialog = (target: 'feature' | 'limit') => { +const openImportDialog = (target: 'feature' | 'limit' | 'custom') => { importDialogTarget.value = target importDialogContent.value = - target === 'feature' ? featureJsonPreview.value : usageJsonPreview.value + target === 'feature' + ? featureConfigPreview.value + : target === 'limit' + ? usageJsonPreview.value + : customJsonPreview.value importDialogError.value = '' importDialogVisible.value = true } @@ -813,9 +932,11 @@ const handleImportConfirm = () => { const content = importDialogContent.value.trim() if (!content) { if (importDialogTarget.value === 'feature') { - featureEntries.value = [] - } else { + featureConfigEntries.value = [] + } else if (importDialogTarget.value === 'limit') { limitEntries.value = [] + } else { + customEntries.value = [] } } else { const entries = parseJsonToEntries(content, { strict: true }) @@ -823,9 +944,11 @@ const handleImportConfirm = () => { throw new Error(t('pages.licenses.form.keyValue.importEmpty')) } if (importDialogTarget.value === 'feature') { - featureEntries.value = entries - } else { + featureConfigEntries.value = entries + } else if (importDialogTarget.value === 'limit') { limitEntries.value = entries + } else { + customEntries.value = entries } } importDialogVisible.value = false @@ -1078,8 +1201,9 @@ const loadLicenseDetail = async () => { ] } - hydrateEntriesFromField(data.custom_parameters, featureEntries) + hydrateEntriesFromField(data.feature_config, featureConfigEntries) hydrateEntriesFromField(data.usage_limits, limitEntries) + hydrateEntriesFromField(data.custom_parameters, customEntries) } else { throw new Error(response.message || t('pages.licenses.form.messages.loadDetailError')) } @@ -1102,15 +1226,17 @@ const handleSubmit = async () => { // 执行表单验证 await formRef.value.validate() - const customValid = validateKeyValueEntries(featureEntries.value) + const featureConfigValid = validateKeyValueEntries(featureConfigEntries.value) const limitValid = validateKeyValueEntries(limitEntries.value) - if (!customValid || !limitValid) { + const customValid = validateKeyValueEntries(customEntries.value) + if (!featureConfigValid || !limitValid || !customValid) { ElMessage.error(t('pages.licenses.form.keyValue.validationFailed')) return } - formData.custom_parameters = featureJsonPreview.value + formData.feature_config = buildJsonRecord(featureConfigEntries.value) || {} formData.usage_limits = usageJsonPreview.value + formData.custom_parameters = customJsonPreview.value submitting.value = true @@ -1200,17 +1326,25 @@ watch( ) watch( - featureEntries, + limitEntries, () => { - formData.custom_parameters = featureJsonPreview.value + formData.usage_limits = usageJsonPreview.value }, { deep: true } ) watch( - limitEntries, + featureConfigEntries, () => { - formData.usage_limits = usageJsonPreview.value + formData.feature_config = buildJsonRecord(featureConfigEntries.value) || {} + }, + { deep: true } +) + +watch( + customEntries, + () => { + formData.custom_parameters = customJsonPreview.value }, { deep: true } ) diff --git a/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue b/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue index 57722f4..c704a84 100644 --- a/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue +++ b/frontend/src/views/Licenses/LicensesView/components/AuthorizationInfo.vue @@ -66,16 +66,16 @@ - +
-
{{ t('pages.licenses.detail.authorizationInfo.customParameters') }}
-
+
{{ t('pages.licenses.detail.authorizationInfo.featureConfig') }}
+
{{ t('pages.licenses.detail.authorizationInfo.keyColumn') }} {{ t('pages.licenses.detail.authorizationInfo.typeColumn') }} {{ t('pages.licenses.detail.authorizationInfo.valueColumn') }}
-
+
{{ item.key }} {{ t(`pages.licenses.detail.authorizationInfo.typeOptions.${item.type}`) }} {{ item.value || '-' }} @@ -86,7 +86,7 @@
- +
{{ t('pages.licenses.detail.authorizationInfo.usageLimits') }}
@@ -105,6 +105,26 @@ {{ t('pages.licenses.detail.authorizationInfo.keyValueEmpty') }}
+ + +
+
{{ t('pages.licenses.detail.authorizationInfo.customParameters') }}
+
+
+ {{ t('pages.licenses.detail.authorizationInfo.keyColumn') }} + {{ t('pages.licenses.detail.authorizationInfo.typeColumn') }} + {{ t('pages.licenses.detail.authorizationInfo.valueColumn') }} +
+
+ {{ item.key }} + {{ t(`pages.licenses.detail.authorizationInfo.typeOptions.${item.type}`) }} + {{ item.value || '-' }} +
+
+
+ {{ t('pages.licenses.detail.authorizationInfo.keyValueEmpty') }} +
+
@@ -168,8 +188,9 @@ const parseKeyValueData = (source: unknown): KeyValueItem[] => { }) } -const customParameters = computed(() => parseKeyValueData(props.licenseData?.custom_parameters)) +const featureConfig = computed(() => parseKeyValueData(props.licenseData?.feature_config)) const usageLimits = computed(() => parseKeyValueData(props.licenseData?.usage_limits)) +const customParameters = computed(() => parseKeyValueData(props.licenseData?.custom_parameters)) // 获取授权期限类型 const getLicensePeriodType = () => {