KyrosDev commited on
Commit
b37d8d7
·
1 Parent(s): 949365d

重新實施機器名稱綁定功能

Browse files

- 從commit 31441f0恢復完整機器名稱功能
- 資料庫新增machine_name欄位到licenses和usage_logs表
- C# plugin自動擷取並傳送Environment.MachineName
- 後端API完整支援機器名稱儲存和更新邏輯
- 前端UI顯示機器名稱於硬體ID前方(5列布局)
- 授權記錄頁面移除敏感授權碼,新增機器名稱列
- 儀表板最近活動顯示機器名稱資訊
- 開發模式包含完整機器名稱模擬資料
- 保持強制sessionStorage認證機制

app/api/license.py CHANGED
@@ -33,7 +33,8 @@ async def activate_license(
33
  ```json
34
  {
35
  "license_code": "KS12345678",
36
- "hardware_id": "HW_ABC123456789"
 
37
  }
38
  ```
39
 
@@ -63,7 +64,8 @@ async def validate_license(
63
  ```json
64
  {
65
  "license_code": "KS12345678",
66
- "hardware_id": "HW_ABC123456789"
 
67
  }
68
  ```
69
 
 
33
  ```json
34
  {
35
  "license_code": "KS12345678",
36
+ "hardware_id": "HW_ABC123456789",
37
+ "machine_name": "DESKTOP-DEV01"
38
  }
39
  ```
40
 
 
64
  ```json
65
  {
66
  "license_code": "KS12345678",
67
+ "hardware_id": "HW_ABC123456789",
68
+ "machine_name": "DESKTOP-DEV01"
69
  }
70
  ```
71
 
app/models/license.py CHANGED
@@ -13,11 +13,13 @@ class LicenseActivation(BaseModel):
13
  """授權啟用請求"""
14
  license_code: str
15
  hardware_id: str
 
16
 
17
  class LicenseValidation(BaseModel):
18
  """授權驗證請求"""
19
  license_code: str
20
  hardware_id: str
 
21
 
22
  class LicenseCreate(BaseModel):
23
  """建立授權請求"""
@@ -62,6 +64,7 @@ class License(BaseModel):
62
  id: UUID
63
  license_code: str
64
  hardware_id: Optional[str] = None
 
65
  user_name: str
66
  user_email: Optional[str] = None
67
  expires_at: datetime
@@ -79,6 +82,7 @@ class UsageLog(BaseModel):
79
  ip_address: Optional[str] = None
80
  user_agent: Optional[str] = None
81
  hardware_info: Optional[str] = None
 
82
  error_message: Optional[str] = None
83
  created_at: datetime
84
 
 
13
  """授權啟用請求"""
14
  license_code: str
15
  hardware_id: str
16
+ machine_name: Optional[str] = None
17
 
18
  class LicenseValidation(BaseModel):
19
  """授權驗證請求"""
20
  license_code: str
21
  hardware_id: str
22
+ machine_name: Optional[str] = None
23
 
24
  class LicenseCreate(BaseModel):
25
  """建立授權請求"""
 
64
  id: UUID
65
  license_code: str
66
  hardware_id: Optional[str] = None
67
+ machine_name: Optional[str] = None
68
  user_name: str
69
  user_email: Optional[str] = None
70
  expires_at: datetime
 
82
  ip_address: Optional[str] = None
83
  user_agent: Optional[str] = None
84
  hardware_info: Optional[str] = None
85
+ machine_name: Optional[str] = None
86
  error_message: Optional[str] = None
87
  created_at: datetime
88
 
app/services/license_service.py CHANGED
@@ -131,9 +131,10 @@ class LicenseService:
131
  message="此授權已綁定其他設備"
132
  )
133
 
134
- # 更新授權 (綁定硬體ID和啟用時間)
135
  update_data = {
136
  "hardware_id": activation.hardware_id,
 
137
  "activated_at": datetime.now(timezone.utc).isoformat(),
138
  "last_used_at": datetime.now(timezone.utc).isoformat()
139
  }
@@ -141,7 +142,7 @@ class LicenseService:
141
  self.supabase.table("licenses").update(update_data).eq("id", license_id).execute()
142
 
143
  # 記錄使用日誌
144
- await self._log_usage(license_id, "activate", client_ip, activation.hardware_id)
145
 
146
  return ActivationResponse(
147
  success=True,
@@ -194,13 +195,18 @@ class LicenseService:
194
  message="授權已過期"
195
  )
196
 
197
- # 更新最後使用時間
198
- self.supabase.table("licenses").update({
199
  "last_used_at": datetime.now(timezone.utc).isoformat()
200
- }).eq("id", license_id).execute()
 
 
 
 
 
201
 
202
  # 記錄使用日誌
203
- await self._log_usage(license_id, "validate", client_ip, validation.hardware_id)
204
 
205
  return ValidationResponse(
206
  valid=True,
@@ -465,7 +471,8 @@ class LicenseService:
465
  return {"success": False, "message": f"延長失敗: {str(e)}"}
466
 
467
  async def _log_usage(self, license_id: Optional[str], action: str, ip_address: Optional[str] = None,
468
- hardware_info: Optional[str] = None, error_message: Optional[str] = None):
 
469
  """記錄使用日誌"""
470
  try:
471
  log_data = {
@@ -473,6 +480,7 @@ class LicenseService:
473
  "action": action,
474
  "ip_address": ip_address,
475
  "hardware_info": hardware_info,
 
476
  "error_message": error_message
477
  }
478
 
 
131
  message="此授權已綁定其他設備"
132
  )
133
 
134
+ # 更新授權 (綁定硬體ID、機器名稱和啟用時間)
135
  update_data = {
136
  "hardware_id": activation.hardware_id,
137
+ "machine_name": activation.machine_name,
138
  "activated_at": datetime.now(timezone.utc).isoformat(),
139
  "last_used_at": datetime.now(timezone.utc).isoformat()
140
  }
 
142
  self.supabase.table("licenses").update(update_data).eq("id", license_id).execute()
143
 
144
  # 記錄使用日誌
145
+ await self._log_usage(license_id, "activate", client_ip, activation.hardware_id, activation.machine_name)
146
 
147
  return ActivationResponse(
148
  success=True,
 
195
  message="授權已過期"
196
  )
197
 
198
+ # 更新最後使用時間和機器名稱
199
+ update_data = {
200
  "last_used_at": datetime.now(timezone.utc).isoformat()
201
+ }
202
+ # 如果提供了機器名稱,也一併更新
203
+ if validation.machine_name:
204
+ update_data["machine_name"] = validation.machine_name
205
+
206
+ self.supabase.table("licenses").update(update_data).eq("id", license_id).execute()
207
 
208
  # 記錄使用日誌
209
+ await self._log_usage(license_id, "validate", client_ip, validation.hardware_id, validation.machine_name)
210
 
211
  return ValidationResponse(
212
  valid=True,
 
471
  return {"success": False, "message": f"延長失敗: {str(e)}"}
472
 
473
  async def _log_usage(self, license_id: Optional[str], action: str, ip_address: Optional[str] = None,
474
+ hardware_info: Optional[str] = None, machine_name: Optional[str] = None,
475
+ error_message: Optional[str] = None):
476
  """記錄使用日誌"""
477
  try:
478
  log_data = {
 
480
  "action": action,
481
  "ip_address": ip_address,
482
  "hardware_info": hardware_info,
483
+ "machine_name": machine_name,
484
  "error_message": error_message
485
  }
486
 
frontend/css/style.css CHANGED
@@ -1880,11 +1880,23 @@ body {
1880
  }
1881
 
1882
  .info-column:nth-child(1) {
1883
- flex: 1.8;
1884
  }
1885
 
1886
  .info-column:nth-child(2) {
1887
- flex: 1.2;
 
 
 
 
 
 
 
 
 
 
 
 
1888
  }
1889
 
1890
  .info-column:last-child {
@@ -1998,6 +2010,32 @@ body {
1998
  text-overflow: ellipsis;
1999
  }
2000
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2001
  /* XS Buttons for Horizontal Cards */
2002
  .btn-xs {
2003
  padding: 3px 6px;
 
1880
  }
1881
 
1882
  .info-column:nth-child(1) {
1883
+ flex: 1.5; /* 授權碼 */
1884
  }
1885
 
1886
  .info-column:nth-child(2) {
1887
+ flex: 1.5; /* 機器名稱 */
1888
+ }
1889
+
1890
+ .info-column:nth-child(3) {
1891
+ flex: 1.5; /* 硬體ID - 較寬 */
1892
+ }
1893
+
1894
+ .info-column:nth-child(4) {
1895
+ flex: 1.3; /* 到期時間 */
1896
+ }
1897
+
1898
+ .info-column:nth-child(5) {
1899
+ flex: 1.0; /* 狀態 */
1900
  }
1901
 
1902
  .info-column:last-child {
 
2010
  text-overflow: ellipsis;
2011
  }
2012
 
2013
+ /* Machine Name Styles */
2014
+ .machine-name {
2015
+ color: var(--text-secondary);
2016
+ font-size: 0.875rem;
2017
+ margin-top: 0.25rem;
2018
+ }
2019
+
2020
+ .machine-name i {
2021
+ margin-right: 0.5rem;
2022
+ color: var(--accent-blue);
2023
+ }
2024
+
2025
+ .machine-name-compact {
2026
+ display: inline-flex;
2027
+ align-items: center;
2028
+ font-size: 0.85rem;
2029
+ color: var(--text-secondary);
2030
+ gap: 4px;
2031
+ }
2032
+
2033
+ .machine-name-compact i {
2034
+ font-size: 0.75rem;
2035
+ color: var(--accent-blue);
2036
+ flex-shrink: 0;
2037
+ }
2038
+
2039
  /* XS Buttons for Horizontal Cards */
2040
  .btn-xs {
2041
  padding: 3px 6px;
frontend/js/api.js CHANGED
@@ -21,6 +21,7 @@ class ApiClient {
21
  id: 'lic_001',
22
  user_name: '張三工程師',
23
  user_email: 'zhang.san@company.com',
 
24
  hardware_id: '4A1F2E3D5B6C7A8E9F0A1B2C3D4E5F6G',
25
  license_key: 'KSTOOLS-2024-ABCD-EFGH-IJKL-MNOP',
26
  valid_days: 365,
@@ -33,6 +34,7 @@ class ApiClient {
33
  id: 'lic_002',
34
  user_name: '李四設計師',
35
  user_email: 'li.si@company.com',
 
36
  hardware_id: '7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V',
37
  license_key: 'KSTOOLS-2024-WXYZ-1234-5678-9ABC',
38
  valid_days: 180,
@@ -45,6 +47,7 @@ class ApiClient {
45
  id: 'lic_003',
46
  user_name: '王五主管',
47
  user_email: 'wang.wu@company.com',
 
48
  hardware_id: '3V4W5X6Y7Z8A9B0C1D2E3F4G5H6I7J8K',
49
  license_key: 'KSTOOLS-2024-DEFG-HIJK-LMNO-PQRS',
50
  valid_days: 90,
@@ -57,6 +60,7 @@ class ApiClient {
57
  id: 'lic_004',
58
  user_name: '趙六實習生',
59
  user_email: 'zhao.liu@company.com',
 
60
  hardware_id: '9K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z',
61
  license_key: 'KSTOOLS-2024-TUVW-XYZ1-2345-6789',
62
  valid_days: 30,
@@ -69,6 +73,7 @@ class ApiClient {
69
  id: 'lic_005',
70
  user_name: '陳七顧問',
71
  user_email: 'chen.qi@consultant.com',
 
72
  hardware_id: '5Z6A7B8C9D0E1F2G3H4I5J6K7L8M9N0O',
73
  license_key: 'KSTOOLS-2024-ABCD-1234-WXYZ-5678',
74
  valid_days: 365,
@@ -83,7 +88,12 @@ class ApiClient {
83
  total_licenses: 5,
84
  active_licenses: 4,
85
  inactive_licenses: 1,
 
86
  expiring_soon: 1,
 
 
 
 
87
  total_users: 5
88
  };
89
 
@@ -91,32 +101,90 @@ class ApiClient {
91
  {
92
  id: 'log_001',
93
  license_id: 'lic_001',
94
- action: 'license_activated',
95
  user_name: '張三工程師',
 
96
  hardware_id: '4A1F2E3D5B6C7A8E9F0A1B2C3D4E5F6G',
97
- timestamp: '2024-09-10T08:30:00Z',
98
  ip_address: '192.168.1.100',
99
- details: '授權啟動成功'
100
  },
101
  {
102
  id: 'log_002',
103
  license_id: 'lic_002',
104
- action: 'license_checked',
105
  user_name: '李四設計師',
 
106
  hardware_id: '7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V',
107
- timestamp: '2024-09-10T10:15:00Z',
108
  ip_address: '192.168.1.101',
109
- details: '授權驗證'
110
  },
111
  {
112
  id: 'log_003',
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
  license_id: 'lic_003',
114
- action: 'license_deactivated',
115
  user_name: '王五主管',
 
116
  hardware_id: '3V4W5X6Y7Z8A9B0C1D2E3F4G5H6I7J8K',
117
- timestamp: '2024-09-09T17:20:00Z',
118
  ip_address: '192.168.1.102',
119
- details: '授權已停用'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  }
121
  ];
122
  }
 
21
  id: 'lic_001',
22
  user_name: '張三工程師',
23
  user_email: 'zhang.san@company.com',
24
+ machine_name: 'DESKTOP-DEV01',
25
  hardware_id: '4A1F2E3D5B6C7A8E9F0A1B2C3D4E5F6G',
26
  license_key: 'KSTOOLS-2024-ABCD-EFGH-IJKL-MNOP',
27
  valid_days: 365,
 
34
  id: 'lic_002',
35
  user_name: '李四設計師',
36
  user_email: 'li.si@company.com',
37
+ machine_name: 'BIM-WORKSTATION-02',
38
  hardware_id: '7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V',
39
  license_key: 'KSTOOLS-2024-WXYZ-1234-5678-9ABC',
40
  valid_days: 180,
 
47
  id: 'lic_003',
48
  user_name: '王五主管',
49
  user_email: 'wang.wu@company.com',
50
+ machine_name: 'MANAGER-LAPTOP',
51
  hardware_id: '3V4W5X6Y7Z8A9B0C1D2E3F4G5H6I7J8K',
52
  license_key: 'KSTOOLS-2024-DEFG-HIJK-LMNO-PQRS',
53
  valid_days: 90,
 
60
  id: 'lic_004',
61
  user_name: '趙六實習生',
62
  user_email: 'zhao.liu@company.com',
63
+ machine_name: 'INTERN-PC-01',
64
  hardware_id: '9K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z',
65
  license_key: 'KSTOOLS-2024-TUVW-XYZ1-2345-6789',
66
  valid_days: 30,
 
73
  id: 'lic_005',
74
  user_name: '陳七顧問',
75
  user_email: 'chen.qi@consultant.com',
76
+ machine_name: 'CONSULTANT-SURFACE',
77
  hardware_id: '5Z6A7B8C9D0E1F2G3H4I5J6K7L8M9N0O',
78
  license_key: 'KSTOOLS-2024-ABCD-1234-WXYZ-5678',
79
  valid_days: 365,
 
88
  total_licenses: 5,
89
  active_licenses: 4,
90
  inactive_licenses: 1,
91
+ expired_licenses: 0,
92
  expiring_soon: 1,
93
+ today_activations: 2,
94
+ yesterday_activations: 1,
95
+ this_week_activations: 8,
96
+ this_month_activations: 12,
97
  total_users: 5
98
  };
99
 
 
101
  {
102
  id: 'log_001',
103
  license_id: 'lic_001',
104
+ action: 'activate',
105
  user_name: '張三工程師',
106
+ machine_name: 'DESKTOP-DEV01',
107
  hardware_id: '4A1F2E3D5B6C7A8E9F0A1B2C3D4E5F6G',
108
+ created_at: new Date(Date.now() - 5 * 60000).toISOString(), // 5分鐘前
109
  ip_address: '192.168.1.100',
110
+ error_message: null
111
  },
112
  {
113
  id: 'log_002',
114
  license_id: 'lic_002',
115
+ action: 'validate',
116
  user_name: '李四設計師',
117
+ machine_name: 'BIM-WORKSTATION-02',
118
  hardware_id: '7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V',
119
+ created_at: new Date(Date.now() - 15 * 60000).toISOString(), // 15分鐘前
120
  ip_address: '192.168.1.101',
121
+ error_message: null
122
  },
123
  {
124
  id: 'log_003',
125
+ license_id: 'lic_005',
126
+ action: 'activate',
127
+ user_name: '陳七顧問',
128
+ machine_name: 'CONSULTANT-SURFACE',
129
+ hardware_id: '5Z6A7B8C9D0E1F2G3H4I5J6K7L8M9N0O',
130
+ created_at: new Date(Date.now() - 30 * 60000).toISOString(), // 30分鐘前
131
+ ip_address: '192.168.1.105',
132
+ error_message: null
133
+ },
134
+ {
135
+ id: 'log_004',
136
+ license_id: 'lic_004',
137
+ action: 'validate',
138
+ user_name: '趙六實習生',
139
+ machine_name: 'INTERN-PC-01',
140
+ hardware_id: '9K0L1M2N3O4P5Q6R7S8T9U0V1W2X3Y4Z',
141
+ created_at: new Date(Date.now() - 45 * 60000).toISOString(), // 45分鐘前
142
+ ip_address: '192.168.1.103',
143
+ error_message: '授權即將到期,請聯繫管理員續期'
144
+ },
145
+ {
146
+ id: 'log_005',
147
  license_id: 'lic_003',
148
+ action: 'deactivate_license',
149
  user_name: '王五主管',
150
+ machine_name: 'MANAGER-LAPTOP',
151
  hardware_id: '3V4W5X6Y7Z8A9B0C1D2E3F4G5H6I7J8K',
152
+ created_at: new Date(Date.now() - 2 * 60 * 60000).toISOString(), // 2小時前
153
  ip_address: '192.168.1.102',
154
+ error_message: null
155
+ },
156
+ {
157
+ id: 'log_006',
158
+ license_id: 'lic_001',
159
+ action: 'activate',
160
+ user_name: '張三工程師',
161
+ machine_name: 'DESKTOP-DEV01',
162
+ hardware_id: '4A1F2E3D5B6C7A8E9F0A1B2C3D4E5F6G',
163
+ created_at: new Date(Date.now() - 24 * 60 * 60000).toISOString(), // 1天前
164
+ ip_address: '192.168.1.100',
165
+ error_message: null
166
+ },
167
+ {
168
+ id: 'log_007',
169
+ license_id: 'lic_006',
170
+ action: 'create',
171
+ user_name: '新用戶測試',
172
+ machine_name: null,
173
+ hardware_id: null,
174
+ created_at: new Date(Date.now() - 3 * 24 * 60 * 60000).toISOString(), // 3天前
175
+ ip_address: '192.168.1.10',
176
+ error_message: null
177
+ },
178
+ {
179
+ id: 'log_008',
180
+ license_id: 'lic_002',
181
+ action: 'extend',
182
+ user_name: '李四設計師',
183
+ machine_name: 'BIM-WORKSTATION-02',
184
+ hardware_id: '7G8H9I0J1K2L3M4N5O6P7Q8R9S0T1U2V',
185
+ created_at: new Date(Date.now() - 5 * 24 * 60 * 60000).toISOString(), // 5天前
186
+ ip_address: '192.168.1.10',
187
+ error_message: null
188
  }
189
  ];
190
  }
frontend/js/pages/dashboard.js CHANGED
@@ -340,7 +340,7 @@ class DashboardPage {
340
  activityArray = activities.data;
341
  } else if (activities && typeof activities === 'object') {
342
  // 如果是對象,嘗試提取可能的陣列屬性
343
- const possibleArrays = ['activities', 'data', 'items', 'records'];
344
  for (const key of possibleArrays) {
345
  if (Array.isArray(activities[key])) {
346
  activityArray = activities[key];
@@ -364,6 +364,7 @@ class DashboardPage {
364
  const statusIcon = this.getActivityStatusIcon(activity);
365
  const actionText = this.getActionText(activity.action);
366
  const userName = activity.user_name || activity.licenses?.user_name || '未知用戶';
 
367
  const time = Utils.getRelativeTime(activity.created_at);
368
  const ipAddress = activity.ip_address || '未知';
369
 
@@ -377,6 +378,11 @@ class DashboardPage {
377
  displayText += ` - ${userName}`;
378
  }
379
 
 
 
 
 
 
380
  displayText += ` (IP: ${ipAddress}`;
381
 
382
  // 如果有錯誤訊息,放在IP後面
 
340
  activityArray = activities.data;
341
  } else if (activities && typeof activities === 'object') {
342
  // 如果是對象,嘗試提取可能的陣列屬性
343
+ const possibleArrays = ['logs', 'activities', 'data', 'items', 'records'];
344
  for (const key of possibleArrays) {
345
  if (Array.isArray(activities[key])) {
346
  activityArray = activities[key];
 
364
  const statusIcon = this.getActivityStatusIcon(activity);
365
  const actionText = this.getActionText(activity.action);
366
  const userName = activity.user_name || activity.licenses?.user_name || '未知用戶';
367
+ const machineName = activity.machine_name || activity.licenses?.machine_name || '';
368
  const time = Utils.getRelativeTime(activity.created_at);
369
  const ipAddress = activity.ip_address || '未知';
370
 
 
378
  displayText += ` - ${userName}`;
379
  }
380
 
381
+ // 加入機器名稱
382
+ if (machineName) {
383
+ displayText += ` (機器: ${machineName})`;
384
+ }
385
+
386
  displayText += ` (IP: ${ipAddress}`;
387
 
388
  // 如果有錯誤訊息,放在IP後面
frontend/js/pages/logs.js CHANGED
@@ -244,7 +244,7 @@ class LogsPage {
244
  <th>時間</th>
245
  <th>動作</th>
246
  <th>用戶</th>
247
- <th>授權碼</th>
248
  <th>IP地址</th>
249
  <th>狀態</th>
250
  <th>詳情</th>
@@ -268,7 +268,7 @@ class LogsPage {
268
 
269
  const actionText = this.getActionText(log.action);
270
  const userName = log.user_name || log.licenses?.user_name || '未知用戶';
271
- const licenseCode = log.license_code || log.licenses?.license_code || 'N/A';
272
  const time = Utils.formatDateTime(log.created_at);
273
  const ip = log.ip_address || '未知';
274
 
@@ -283,10 +283,10 @@ class LogsPage {
283
  </td>
284
  <td>${userName}</td>
285
  <td>
286
- ${licenseCode !== 'N/A'
287
- ? `<code class="license-code-sm">${licenseCode.substring(0, 8)}...</code>`
288
- : licenseCode
289
- }
290
  </td>
291
  <td>${ip}</td>
292
  <td>${statusBadge}</td>
@@ -436,15 +436,6 @@ class LogsPage {
436
  <label>IP地址</label>
437
  <div>${log.ip_address || '未知'}</div>
438
  </div>
439
- <div class="detail-item">
440
- <label>授權碼</label>
441
- <div>
442
- ${log.license_code || log.licenses?.license_code
443
- ? `<code>${Utils.formatLicenseCode(log.license_code || log.licenses?.license_code)}</code>`
444
- : 'N/A'
445
- }
446
- </div>
447
- </div>
448
  <div class="detail-item">
449
  <label>動作類型</label>
450
  <div>
 
244
  <th>時間</th>
245
  <th>動作</th>
246
  <th>用戶</th>
247
+ <th>機器名稱</th>
248
  <th>IP地址</th>
249
  <th>狀態</th>
250
  <th>詳情</th>
 
268
 
269
  const actionText = this.getActionText(log.action);
270
  const userName = log.user_name || log.licenses?.user_name || '未知用戶';
271
+ const machineName = log.machine_name || '未知';
272
  const time = Utils.formatDateTime(log.created_at);
273
  const ip = log.ip_address || '未知';
274
 
 
283
  </td>
284
  <td>${userName}</td>
285
  <td>
286
+ <span class="machine-name-compact">
287
+ <i class="fas fa-desktop"></i>
288
+ ${machineName}
289
+ </span>
290
  </td>
291
  <td>${ip}</td>
292
  <td>${statusBadge}</td>
 
436
  <label>IP地址</label>
437
  <div>${log.ip_address || '未知'}</div>
438
  </div>
 
 
 
 
 
 
 
 
 
439
  <div class="detail-item">
440
  <label>動作類型</label>
441
  <div>
frontend/js/pages/users.js CHANGED
@@ -319,7 +319,7 @@ class UsersPage {
319
  </div>
320
  </div>
321
 
322
- <!-- Main Content - 4 Equal Columns -->
323
  <div class="card-body-horizontal">
324
  <div class="info-column">
325
  <div class="column-header">授權碼</div>
@@ -331,6 +331,16 @@ class UsersPage {
331
  </div>
332
  </div>
333
 
 
 
 
 
 
 
 
 
 
 
334
  <div class="info-column">
335
  <div class="column-header">硬體ID</div>
336
  <div class="column-content">
 
319
  </div>
320
  </div>
321
 
322
+ <!-- Main Content - 5 Equal Columns -->
323
  <div class="card-body-horizontal">
324
  <div class="info-column">
325
  <div class="column-header">授權碼</div>
 
331
  </div>
332
  </div>
333
 
334
+ <div class="info-column">
335
+ <div class="column-header">機器名稱</div>
336
+ <div class="column-content">
337
+ <span class="machine-name-compact">
338
+ <i class="fas fa-desktop"></i>
339
+ ${license.machine_name || '未綁定'}
340
+ </span>
341
+ </div>
342
+ </div>
343
+
344
  <div class="info-column">
345
  <div class="column-header">硬體ID</div>
346
  <div class="column-content">
revit-plugin/ApiClient.cs CHANGED
@@ -58,7 +58,8 @@ namespace KSTools.Licensing
58
  var request = new
59
  {
60
  license_code = licenseCode,
61
- hardware_id = hardwareId
 
62
  };
63
 
64
  var json = JsonSerializer.Serialize(request, new JsonSerializerOptions
@@ -149,7 +150,8 @@ namespace KSTools.Licensing
149
  var request = new
150
  {
151
  license_code = licenseCode,
152
- hardware_id = hardwareId
 
153
  };
154
 
155
  var json = JsonSerializer.Serialize(request, new JsonSerializerOptions
 
58
  var request = new
59
  {
60
  license_code = licenseCode,
61
+ hardware_id = hardwareId,
62
+ machine_name = Environment.MachineName
63
  };
64
 
65
  var json = JsonSerializer.Serialize(request, new JsonSerializerOptions
 
150
  var request = new
151
  {
152
  license_code = licenseCode,
153
+ hardware_id = hardwareId,
154
+ machine_name = Environment.MachineName
155
  };
156
 
157
  var json = JsonSerializer.Serialize(request, new JsonSerializerOptions
supabase-schema.sql CHANGED
@@ -13,6 +13,7 @@ CREATE TABLE licenses (
13
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
14
  license_code VARCHAR(50) UNIQUE NOT NULL,
15
  hardware_id VARCHAR(100),
 
16
  user_name VARCHAR(255) NOT NULL,
17
  user_email VARCHAR(255),
18
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
@@ -28,6 +29,7 @@ COMMENT ON TABLE licenses IS 'KSTools 授權管理表';
28
  COMMENT ON COLUMN licenses.id IS '唯一識別碼';
29
  COMMENT ON COLUMN licenses.license_code IS '授權碼 (格式: 32位隨機字符,支援XXXX-XXXX-XXXX-XXXX分段顯示)';
30
  COMMENT ON COLUMN licenses.hardware_id IS '硬體指紋ID';
 
31
  COMMENT ON COLUMN licenses.user_name IS '授權用戶名稱';
32
  COMMENT ON COLUMN licenses.user_email IS '用戶電子郵件';
33
  COMMENT ON COLUMN licenses.expires_at IS '授權到期時間';
@@ -45,6 +47,7 @@ CREATE TABLE usage_logs (
45
  ip_address INET,
46
  user_agent TEXT,
47
  hardware_info TEXT,
 
48
  error_message TEXT, -- 錯誤訊息 (如果有)
49
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
50
  );
@@ -56,6 +59,7 @@ COMMENT ON COLUMN usage_logs.action IS '動作類型: activate/validate/error';
56
  COMMENT ON COLUMN usage_logs.ip_address IS '客戶端IP地址';
57
  COMMENT ON COLUMN usage_logs.user_agent IS '用戶代理字符串';
58
  COMMENT ON COLUMN usage_logs.hardware_info IS '硬體資訊';
 
59
  COMMENT ON COLUMN usage_logs.error_message IS '錯誤訊息 (發生錯誤時)';
60
 
61
  -- =====================================================
@@ -65,6 +69,7 @@ COMMENT ON COLUMN usage_logs.error_message IS '錯誤訊息 (發生錯誤時)';
65
  -- 授權表索引
66
  CREATE INDEX idx_licenses_code ON licenses(license_code);
67
  CREATE INDEX idx_licenses_hardware ON licenses(hardware_id);
 
68
  CREATE INDEX idx_licenses_active ON licenses(is_active, expires_at);
69
  CREATE INDEX idx_licenses_user_email ON licenses(user_email);
70
  CREATE INDEX idx_licenses_created_at ON licenses(created_at);
@@ -137,7 +142,8 @@ SELECT
137
  WHEN l.is_active = false THEN 'inactive'
138
  WHEN l.activated_at IS NULL THEN 'not_activated'
139
  ELSE 'active'
140
- END as status
 
141
  FROM licenses l;
142
 
143
  COMMENT ON VIEW license_details IS '授權詳細資訊視圖 (含統計)';
@@ -156,6 +162,7 @@ SELECT
156
  ul.ip_address,
157
  ul.user_agent,
158
  ul.hardware_info,
 
159
  ul.error_message,
160
  ul.created_at,
161
  l.license_code,
@@ -261,64 +268,64 @@ COMMENT ON FUNCTION generate_license_report IS '生成指定日期範圍的授
261
  -- =====================================================
262
 
263
  -- 插入測試用授權 (啟用來測試系統) - 統一使用台灣時間00:00計算(-8hr)
264
- INSERT INTO licenses (license_code, user_name, user_email, expires_at, hardware_id, is_active, activated_at) VALUES
265
  -- 已啟用用戶 (正常使用中)
266
- ('A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6', '王小明', 'ming@company.com.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '90 days' - INTERVAL '1 second', 'CPU123ABC456DEF789GHI012JKL345MN', true, NOW() - INTERVAL '5 days'),
267
- ('X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2', '李美華', 'li.meihua@design.studio', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '365 days' - INTERVAL '1 second', 'CPU789XYZ012ABC345DEF678GHI901JK', true, NOW() - INTERVAL '30 days'),
268
 
269
  -- 試用用戶 (7天試用)
270
- ('B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4', '張工程師', 'engineer.zhang@tech.com', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '7 days' - INTERVAL '1 second', 'CPUABC123XYZ456QWE789RTY012UIU34', true, NOW() - INTERVAL '2 days'),
271
 
272
  -- 更多正常用戶
273
- ('M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0', '陳建築師', 'chen@architect.firm', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '30 days' - INTERVAL '1 second', 'CPUARCH456BUILD789DESIGN012PROJ34', true, NOW() - INTERVAL '1 day'),
274
- ('F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8', '劉設計師', 'liu.designer@studio.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '60 days' - INTERVAL '1 second', 'CPUDESIGN123CREATIVE456ART789STUD', true, NOW() - INTERVAL '7 days'),
275
 
276
  -- 過期授權 (已過期15天)
277
- ('Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5', '周老闆', 'boss.zhou@construction.co', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' - INTERVAL '15 days' - INTERVAL '1 second', 'CPU456DEF789ABC012GHI345JKL678MN', true, NOW() - INTERVAL '45 days'),
278
 
279
  -- 已停用授權 (管理員手動停用)
280
- ('Q2W3E4R5T6Y7U8I9O0P1A2S3D4F5G6H7', '黃違規用戶', 'huang@suspended.user', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '20 days' - INTERVAL '1 second', 'CPUXYZ789ABC012DEF345GHI678JKL90', false, NOW() - INTERVAL '10 days'),
281
 
282
  -- 永久授權 (企業用戶) - 50年
283
- ('K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6', 'KSTools管理員', 'admin@kstools.com.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '18250 days' - INTERVAL '1 second', 'CPUADMIN123MASTER456CONTROL789AB', true, NOW() - INTERVAL '1 day'),
284
 
285
  -- 即將過期 (今天結束就過期)
286
- ('V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3', '趙用戶', 'zhao@expires.soon', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '1 day' - INTERVAL '1 second', 'CPU999END888SOON777EXPIRE666ABC', true, NOW() - INTERVAL '28 days')
287
  ON CONFLICT (license_code) DO NOTHING;
288
 
289
  -- 插入測試使用記錄
290
- INSERT INTO usage_logs (license_id, action, ip_address, hardware_info) VALUES
291
  -- 正常用戶的使用記錄
292
- ((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'activate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN'),
293
- ((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN'),
294
- ((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN'),
295
 
296
  -- 企業用戶的使用記錄
297
- ((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'activate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK'),
298
- ((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'validate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK'),
299
 
300
  -- 其他正常用戶記錄
301
- ((SELECT id FROM licenses WHERE license_code = 'M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0'), 'activate', '192.168.50.20', 'CPUARCH456BUILD789DESIGN012PROJ34'),
302
- ((SELECT id FROM licenses WHERE license_code = 'M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0'), 'validate', '192.168.50.20', 'CPUARCH456BUILD789DESIGN012PROJ34'),
303
 
304
- ((SELECT id FROM licenses WHERE license_code = 'F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8'), 'activate', '172.20.10.15', 'CPUDESIGN123CREATIVE456ART789STUD'),
305
- ((SELECT id FROM licenses WHERE license_code = 'F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8'), 'validate', '172.20.10.15', 'CPUDESIGN123CREATIVE456ART789STUD'),
306
 
307
  -- 試用用戶的記錄
308
- ((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'activate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34'),
309
- ((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'validate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34'),
310
 
311
  -- 過期用戶的歷史記錄
312
- ((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'activate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN'),
313
- ((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'validate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN'),
314
 
315
  -- 管理員測試記錄
316
- ((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'activate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB'),
317
- ((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'validate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB'),
318
 
319
  -- 即將過期用戶的記錄
320
- ((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'activate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC'),
321
- ((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'validate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC');
322
 
323
  -- =====================================================
324
  -- 權限設定 (建議)
 
13
  id UUID DEFAULT gen_random_uuid() PRIMARY KEY,
14
  license_code VARCHAR(50) UNIQUE NOT NULL,
15
  hardware_id VARCHAR(100),
16
+ machine_name VARCHAR(255),
17
  user_name VARCHAR(255) NOT NULL,
18
  user_email VARCHAR(255),
19
  expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
 
29
  COMMENT ON COLUMN licenses.id IS '唯一識別碼';
30
  COMMENT ON COLUMN licenses.license_code IS '授權碼 (格式: 32位隨機字符,支援XXXX-XXXX-XXXX-XXXX分段顯示)';
31
  COMMENT ON COLUMN licenses.hardware_id IS '硬體指紋ID';
32
+ COMMENT ON COLUMN licenses.machine_name IS '機器名稱';
33
  COMMENT ON COLUMN licenses.user_name IS '授權用戶名稱';
34
  COMMENT ON COLUMN licenses.user_email IS '用戶電子郵件';
35
  COMMENT ON COLUMN licenses.expires_at IS '授權到期時間';
 
47
  ip_address INET,
48
  user_agent TEXT,
49
  hardware_info TEXT,
50
+ machine_name VARCHAR(255),
51
  error_message TEXT, -- 錯誤訊息 (如果有)
52
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
53
  );
 
59
  COMMENT ON COLUMN usage_logs.ip_address IS '客戶端IP地址';
60
  COMMENT ON COLUMN usage_logs.user_agent IS '用戶代理字符串';
61
  COMMENT ON COLUMN usage_logs.hardware_info IS '硬體資訊';
62
+ COMMENT ON COLUMN usage_logs.machine_name IS '機器名稱';
63
  COMMENT ON COLUMN usage_logs.error_message IS '錯誤訊息 (發生錯誤時)';
64
 
65
  -- =====================================================
 
69
  -- 授權表索引
70
  CREATE INDEX idx_licenses_code ON licenses(license_code);
71
  CREATE INDEX idx_licenses_hardware ON licenses(hardware_id);
72
+ CREATE INDEX idx_licenses_machine_name ON licenses(machine_name);
73
  CREATE INDEX idx_licenses_active ON licenses(is_active, expires_at);
74
  CREATE INDEX idx_licenses_user_email ON licenses(user_email);
75
  CREATE INDEX idx_licenses_created_at ON licenses(created_at);
 
142
  WHEN l.is_active = false THEN 'inactive'
143
  WHEN l.activated_at IS NULL THEN 'not_activated'
144
  ELSE 'active'
145
+ END as status,
146
+ l.machine_name as device_name
147
  FROM licenses l;
148
 
149
  COMMENT ON VIEW license_details IS '授權詳細資訊視圖 (含統計)';
 
162
  ul.ip_address,
163
  ul.user_agent,
164
  ul.hardware_info,
165
+ ul.machine_name,
166
  ul.error_message,
167
  ul.created_at,
168
  l.license_code,
 
268
  -- =====================================================
269
 
270
  -- 插入測試用授權 (啟用來測試系統) - 統一使用台灣時間00:00計算(-8hr)
271
+ INSERT INTO licenses (license_code, user_name, user_email, expires_at, hardware_id, machine_name, is_active, activated_at) VALUES
272
  -- 已啟用用戶 (正常使用中)
273
+ ('A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6', '王小明', 'ming@company.com.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '90 days' - INTERVAL '1 second', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC', true, NOW() - INTERVAL '5 days'),
274
+ ('X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2', '李美華', 'li.meihua@design.studio', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '365 days' - INTERVAL '1 second', 'CPU789XYZ012ABC345DEF678GHI901JK', 'DESIGN-WORKSTATION-02', true, NOW() - INTERVAL '30 days'),
275
 
276
  -- 試用用戶 (7天試用)
277
+ ('B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4', '張工程師', 'engineer.zhang@tech.com', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '7 days' - INTERVAL '1 second', 'CPUABC123XYZ456QWE789RTY012UIU34', 'ENGINEER-LAPTOP-01', true, NOW() - INTERVAL '2 days'),
278
 
279
  -- 更多正常用戶
280
+ ('M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0', '陳建築師', 'chen@architect.firm', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '30 days' - INTERVAL '1 second', 'CPUARCH456BUILD789DESIGN012PROJ34', 'ARCHITECT-STUDIO-PC', true, NOW() - INTERVAL '1 day'),
281
+ ('F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8', '劉設計師', 'liu.designer@studio.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '60 days' - INTERVAL '1 second', 'CPUDESIGN123CREATIVE456ART789STUD', 'CREATIVE-WORKSTATION', true, NOW() - INTERVAL '7 days'),
282
 
283
  -- 過期授權 (已過期15天)
284
+ ('Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5', '周老闆', 'boss.zhou@construction.co', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' - INTERVAL '15 days' - INTERVAL '1 second', 'CPU456DEF789ABC012GHI345JKL678MN', 'BOSS-OFFICE-PC', true, NOW() - INTERVAL '45 days'),
285
 
286
  -- 已停用授權 (管理員手動停用)
287
+ ('Q2W3E4R5T6Y7U8I9O0P1A2S3D4F5G6H7', '黃違規用戶', 'huang@suspended.user', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '20 days' - INTERVAL '1 second', 'CPUXYZ789ABC012DEF345GHI678JKL90', 'SUSPENDED-PC', false, NOW() - INTERVAL '10 days'),
288
 
289
  -- 永久授權 (企業用戶) - 50年
290
+ ('K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6', 'KSTools管理員', 'admin@kstools.com.tw', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '18250 days' - INTERVAL '1 second', 'CPUADMIN123MASTER456CONTROL789AB', 'ADMIN-MASTER-SERVER', true, NOW() - INTERVAL '1 day'),
291
 
292
  -- 即將過期 (今天結束就過期)
293
+ ('V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3', '趙用戶', 'zhao@expires.soon', DATE_TRUNC('day', NOW()) - INTERVAL '8 hours' + INTERVAL '1 day' - INTERVAL '1 second', 'CPU999END888SOON777EXPIRE666ABC', 'EXPIRING-PC-TODAY', true, NOW() - INTERVAL '28 days')
294
  ON CONFLICT (license_code) DO NOTHING;
295
 
296
  -- 插入測試使用記錄
297
+ INSERT INTO usage_logs (license_id, action, ip_address, hardware_info, machine_name) VALUES
298
  -- 正常用戶的使用記錄
299
+ ((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'activate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC'),
300
+ ((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC'),
301
+ ((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC'),
302
 
303
  -- 企業用戶的使用記錄
304
+ ((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'activate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK', 'DESIGN-WORKSTATION-02'),
305
+ ((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'validate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK', 'DESIGN-WORKSTATION-02'),
306
 
307
  -- 其他正常用戶記錄
308
+ ((SELECT id FROM licenses WHERE license_code = 'M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0'), 'activate', '192.168.50.20', 'CPUARCH456BUILD789DESIGN012PROJ34', 'ARCHITECT-STUDIO-PC'),
309
+ ((SELECT id FROM licenses WHERE license_code = 'M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0'), 'validate', '192.168.50.20', 'CPUARCH456BUILD789DESIGN012PROJ34', 'ARCHITECT-STUDIO-PC'),
310
 
311
+ ((SELECT id FROM licenses WHERE license_code = 'F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8'), 'activate', '172.20.10.15', 'CPUDESIGN123CREATIVE456ART789STUD', 'CREATIVE-WORKSTATION'),
312
+ ((SELECT id FROM licenses WHERE license_code = 'F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8'), 'validate', '172.20.10.15', 'CPUDESIGN123CREATIVE456ART789STUD', 'CREATIVE-WORKSTATION'),
313
 
314
  -- 試用用戶的記錄
315
+ ((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'activate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34', 'ENGINEER-LAPTOP-01'),
316
+ ((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'validate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34', 'ENGINEER-LAPTOP-01'),
317
 
318
  -- 過期用戶的歷史記錄
319
+ ((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'activate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN', 'BOSS-OFFICE-PC'),
320
+ ((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'validate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN', 'BOSS-OFFICE-PC'),
321
 
322
  -- 管理員測試記錄
323
+ ((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'activate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB', 'ADMIN-MASTER-SERVER'),
324
+ ((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'validate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB', 'ADMIN-MASTER-SERVER'),
325
 
326
  -- 即將過期用戶的記錄
327
+ ((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'activate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC', 'EXPIRING-PC-TODAY'),
328
+ ((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'validate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC', 'EXPIRING-PC-TODAY');
329
 
330
  -- =====================================================
331
  -- 權限設定 (建議)