|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DROP TABLE IF EXISTS usage_logs CASCADE; |
|
|
DROP TABLE IF EXISTS licenses CASCADE; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE licenses ( |
|
|
id UUID DEFAULT gen_random_uuid() PRIMARY KEY, |
|
|
license_code VARCHAR(50) UNIQUE NOT NULL, |
|
|
hardware_id VARCHAR(100), |
|
|
machine_name VARCHAR(255), |
|
|
user_name VARCHAR(255) NOT NULL, |
|
|
user_email VARCHAR(255), |
|
|
expires_at TIMESTAMP WITH TIME ZONE NOT NULL, |
|
|
is_active BOOLEAN DEFAULT true, |
|
|
activated_at TIMESTAMP WITH TIME ZONE, |
|
|
last_used_at TIMESTAMP WITH TIME ZONE, |
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(), |
|
|
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
|
|
); |
|
|
|
|
|
|
|
|
COMMENT ON TABLE licenses IS 'KSTools 授權管理表'; |
|
|
COMMENT ON COLUMN licenses.id IS '唯一識別碼'; |
|
|
COMMENT ON COLUMN licenses.license_code IS '授權碼 (格式: 32位隨機字符,支援XXXX-XXXX-XXXX-XXXX分段顯示)'; |
|
|
COMMENT ON COLUMN licenses.hardware_id IS '硬體指紋ID'; |
|
|
COMMENT ON COLUMN licenses.machine_name IS '機器名稱'; |
|
|
COMMENT ON COLUMN licenses.user_name IS '授權用戶名稱'; |
|
|
COMMENT ON COLUMN licenses.user_email IS '用戶電子郵件'; |
|
|
COMMENT ON COLUMN licenses.expires_at IS '授權到期時間'; |
|
|
COMMENT ON COLUMN licenses.is_active IS '是否啟用'; |
|
|
COMMENT ON COLUMN licenses.activated_at IS '首次啟用時間'; |
|
|
COMMENT ON COLUMN licenses.last_used_at IS '最後使用時間'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CREATE TABLE usage_logs ( |
|
|
id BIGSERIAL PRIMARY KEY, |
|
|
license_id UUID REFERENCES licenses(id) ON DELETE CASCADE, |
|
|
action VARCHAR(50) NOT NULL, |
|
|
ip_address INET, |
|
|
user_agent TEXT, |
|
|
hardware_info TEXT, |
|
|
machine_name VARCHAR(255), |
|
|
error_message TEXT, |
|
|
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW() |
|
|
); |
|
|
|
|
|
|
|
|
COMMENT ON TABLE usage_logs IS 'KSTools 授權使用記錄表'; |
|
|
COMMENT ON COLUMN usage_logs.license_id IS '關聯的授權ID'; |
|
|
COMMENT ON COLUMN usage_logs.action IS '動作類型: activate/validate/error'; |
|
|
COMMENT ON COLUMN usage_logs.ip_address IS '客戶端IP地址'; |
|
|
COMMENT ON COLUMN usage_logs.user_agent IS '用戶代理字符串'; |
|
|
COMMENT ON COLUMN usage_logs.hardware_info IS '硬體資訊'; |
|
|
COMMENT ON COLUMN usage_logs.machine_name IS '機器名稱'; |
|
|
COMMENT ON COLUMN usage_logs.error_message IS '錯誤訊息 (發生錯誤時)'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CREATE INDEX idx_licenses_code ON licenses(license_code); |
|
|
CREATE INDEX idx_licenses_hardware ON licenses(hardware_id); |
|
|
CREATE INDEX idx_licenses_machine_name ON licenses(machine_name); |
|
|
CREATE INDEX idx_licenses_active ON licenses(is_active, expires_at); |
|
|
CREATE INDEX idx_licenses_user_email ON licenses(user_email); |
|
|
CREATE INDEX idx_licenses_created_at ON licenses(created_at); |
|
|
|
|
|
|
|
|
CREATE INDEX idx_usage_logs_license ON usage_logs(license_id); |
|
|
CREATE INDEX idx_usage_logs_created ON usage_logs(created_at); |
|
|
CREATE INDEX idx_usage_logs_action ON usage_logs(action); |
|
|
CREATE INDEX idx_usage_logs_ip ON usage_logs(ip_address); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION update_updated_at_column() |
|
|
RETURNS TRIGGER AS $$ |
|
|
BEGIN |
|
|
NEW.updated_at = NOW(); |
|
|
RETURN NEW; |
|
|
END; |
|
|
$$ language 'plpgsql'; |
|
|
|
|
|
CREATE TRIGGER update_licenses_updated_at |
|
|
BEFORE UPDATE ON licenses |
|
|
FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
ALTER TABLE licenses ENABLE ROW LEVEL SECURITY; |
|
|
ALTER TABLE usage_logs ENABLE ROW LEVEL SECURITY; |
|
|
|
|
|
|
|
|
CREATE POLICY "Allow all operations on licenses" ON licenses |
|
|
FOR ALL USING (true); |
|
|
|
|
|
|
|
|
CREATE POLICY "Allow insert and select on usage_logs" ON usage_logs |
|
|
FOR ALL USING (true); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE VIEW license_stats AS |
|
|
SELECT |
|
|
COUNT(*) as total_licenses, |
|
|
COUNT(*) FILTER (WHERE is_active = true AND expires_at > NOW()) as active_licenses, |
|
|
COUNT(*) FILTER (WHERE is_active = false) as inactive_licenses, |
|
|
COUNT(*) FILTER (WHERE expires_at <= NOW()) as expired_licenses, |
|
|
COUNT(*) FILTER (WHERE is_active = true AND expires_at > NOW()) as valid_licenses, |
|
|
COUNT(*) FILTER (WHERE activated_at >= CURRENT_DATE) as today_activations, |
|
|
COUNT(*) FILTER (WHERE activated_at >= CURRENT_DATE - INTERVAL '1 day' AND activated_at < CURRENT_DATE) as yesterday_activations, |
|
|
COUNT(*) FILTER (WHERE activated_at >= CURRENT_DATE - INTERVAL '7 days') as this_week_activations, |
|
|
COUNT(*) FILTER (WHERE activated_at >= CURRENT_DATE - INTERVAL '30 days') as this_month_activations |
|
|
FROM licenses; |
|
|
|
|
|
COMMENT ON VIEW license_stats IS '授權統計視圖'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE VIEW license_details AS |
|
|
SELECT |
|
|
l.*, |
|
|
(SELECT COUNT(*) FROM usage_logs ul WHERE ul.license_id = l.id) as usage_count, |
|
|
(SELECT MAX(created_at) FROM usage_logs ul WHERE ul.license_id = l.id AND action = 'validate') as last_validation, |
|
|
CASE |
|
|
WHEN l.expires_at <= NOW() THEN 'expired' |
|
|
WHEN l.is_active = false THEN 'inactive' |
|
|
WHEN l.activated_at IS NULL THEN 'not_activated' |
|
|
ELSE 'active' |
|
|
END as status, |
|
|
l.machine_name as device_name |
|
|
FROM licenses l; |
|
|
|
|
|
COMMENT ON VIEW license_details IS '授權詳細資訊視圖 (含統計)'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
DROP VIEW IF EXISTS activity_logs; |
|
|
|
|
|
CREATE VIEW activity_logs AS |
|
|
SELECT |
|
|
ul.id, |
|
|
ul.license_id, |
|
|
ul.action, |
|
|
ul.ip_address, |
|
|
ul.user_agent, |
|
|
ul.hardware_info, |
|
|
ul.machine_name, |
|
|
ul.error_message, |
|
|
ul.created_at, |
|
|
l.license_code, |
|
|
l.user_name, |
|
|
l.user_email, |
|
|
CASE |
|
|
|
|
|
WHEN ul.action IN ('create', 'extend', 'activate_license', 'deactivate_license', 'delete') THEN ul.action |
|
|
|
|
|
WHEN ul.action IN ('activate', 'validate') THEN |
|
|
CASE |
|
|
WHEN ul.error_message IS NOT NULL THEN 'error' |
|
|
ELSE ul.action |
|
|
END |
|
|
|
|
|
ELSE |
|
|
CASE |
|
|
WHEN ul.error_message IS NOT NULL THEN 'error' |
|
|
ELSE ul.action |
|
|
END |
|
|
END as activity_type |
|
|
FROM usage_logs ul |
|
|
LEFT JOIN licenses l ON ul.license_id = l.id |
|
|
ORDER BY ul.created_at DESC; |
|
|
|
|
|
COMMENT ON VIEW activity_logs IS '活動記錄視圖 (前端儀表板使用)'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION cleanup_old_logs(days_to_keep INTEGER DEFAULT 90) |
|
|
RETURNS INTEGER AS $$ |
|
|
DECLARE |
|
|
deleted_count INTEGER; |
|
|
BEGIN |
|
|
DELETE FROM usage_logs |
|
|
WHERE created_at < NOW() - (days_to_keep || ' days')::INTERVAL; |
|
|
|
|
|
GET DIAGNOSTICS deleted_count = ROW_COUNT; |
|
|
RETURN deleted_count; |
|
|
END; |
|
|
$$ LANGUAGE plpgsql; |
|
|
|
|
|
COMMENT ON FUNCTION cleanup_old_logs IS '清理指定天數前的使用記錄'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
CREATE OR REPLACE FUNCTION generate_license_report( |
|
|
start_date DATE DEFAULT CURRENT_DATE - INTERVAL '30 days', |
|
|
end_date DATE DEFAULT CURRENT_DATE |
|
|
) |
|
|
RETURNS TABLE ( |
|
|
date DATE, |
|
|
new_licenses INTEGER, |
|
|
activations INTEGER, |
|
|
validations INTEGER, |
|
|
errors INTEGER |
|
|
) AS $$ |
|
|
BEGIN |
|
|
RETURN QUERY |
|
|
SELECT |
|
|
d.date, |
|
|
COALESCE(l.new_licenses, 0) as new_licenses, |
|
|
COALESCE(ul_activate.activations, 0) as activations, |
|
|
COALESCE(ul_validate.validations, 0) as validations, |
|
|
COALESCE(ul_error.errors, 0) as errors |
|
|
FROM ( |
|
|
SELECT generate_series(start_date, end_date, '1 day'::interval)::date as date |
|
|
) d |
|
|
LEFT JOIN ( |
|
|
SELECT created_at::date as date, COUNT(*) as new_licenses |
|
|
FROM licenses |
|
|
WHERE created_at::date BETWEEN start_date AND end_date |
|
|
GROUP BY created_at::date |
|
|
) l ON d.date = l.date |
|
|
LEFT JOIN ( |
|
|
SELECT created_at::date as date, COUNT(*) as activations |
|
|
FROM usage_logs |
|
|
WHERE action = 'activate' AND created_at::date BETWEEN start_date AND end_date |
|
|
GROUP BY created_at::date |
|
|
) ul_activate ON d.date = ul_activate.date |
|
|
LEFT JOIN ( |
|
|
SELECT created_at::date as date, COUNT(*) as validations |
|
|
FROM usage_logs |
|
|
WHERE action = 'validate' AND created_at::date BETWEEN start_date AND end_date |
|
|
GROUP BY created_at::date |
|
|
) ul_validate ON d.date = ul_validate.date |
|
|
LEFT JOIN ( |
|
|
SELECT created_at::date as date, COUNT(*) as errors |
|
|
FROM usage_logs |
|
|
WHERE error_message IS NOT NULL AND created_at::date BETWEEN start_date AND end_date |
|
|
GROUP BY created_at::date |
|
|
) ul_error ON d.date = ul_error.date |
|
|
ORDER BY d.date; |
|
|
END; |
|
|
$$ LANGUAGE plpgsql; |
|
|
|
|
|
COMMENT ON FUNCTION generate_license_report IS '生成指定日期範圍的授權統計報告'; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
INSERT INTO licenses (license_code, user_name, user_email, expires_at, hardware_id, machine_name, is_active, activated_at) VALUES |
|
|
|
|
|
('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'), |
|
|
('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'), |
|
|
|
|
|
|
|
|
('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'), |
|
|
|
|
|
|
|
|
('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'), |
|
|
('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'), |
|
|
|
|
|
|
|
|
('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'), |
|
|
|
|
|
|
|
|
('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'), |
|
|
|
|
|
|
|
|
('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'), |
|
|
|
|
|
|
|
|
('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') |
|
|
ON CONFLICT (license_code) DO NOTHING; |
|
|
|
|
|
|
|
|
INSERT INTO usage_logs (license_id, action, ip_address, hardware_info, machine_name) VALUES |
|
|
|
|
|
((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'activate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC'), |
|
|
((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC'), |
|
|
((SELECT id FROM licenses WHERE license_code = 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'), 'validate', '192.168.1.100', 'CPU123ABC456DEF789GHI012JKL345MN', 'DESKTOP-MING-PC'), |
|
|
|
|
|
|
|
|
((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'activate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK', 'DESIGN-WORKSTATION-02'), |
|
|
((SELECT id FROM licenses WHERE license_code = 'X7Y8Z9W0Q1R2S3T4U5V6A7B8C9D0E1F2'), 'validate', '10.0.1.50', 'CPU789XYZ012ABC345DEF678GHI901JK', 'DESIGN-WORKSTATION-02'), |
|
|
|
|
|
|
|
|
((SELECT id FROM licenses WHERE license_code = 'M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0'), 'activate', '192.168.50.20', 'CPUARCH456BUILD789DESIGN012PROJ34', 'ARCHITECT-STUDIO-PC'), |
|
|
((SELECT id FROM licenses WHERE license_code = 'M5N6O7P8Q9R0S1T2U3V4W5X6Y7Z8A9B0'), 'validate', '192.168.50.20', 'CPUARCH456BUILD789DESIGN012PROJ34', 'ARCHITECT-STUDIO-PC'), |
|
|
|
|
|
((SELECT id FROM licenses WHERE license_code = 'F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8'), 'activate', '172.20.10.15', 'CPUDESIGN123CREATIVE456ART789STUD', 'CREATIVE-WORKSTATION'), |
|
|
((SELECT id FROM licenses WHERE license_code = 'F3G4H5I6J7K8L9M0N1O2P3Q4R5S6T7U8'), 'validate', '172.20.10.15', 'CPUDESIGN123CREATIVE456ART789STUD', 'CREATIVE-WORKSTATION'), |
|
|
|
|
|
|
|
|
((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'activate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34', 'ENGINEER-LAPTOP-01'), |
|
|
((SELECT id FROM licenses WHERE license_code = 'B9C8D7E6F5G4H3I2J1K0L9M8N7O6P5Q4'), 'validate', '203.74.123.45', 'CPUABC123XYZ456QWE789RTY012UIU34', 'ENGINEER-LAPTOP-01'), |
|
|
|
|
|
|
|
|
((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'activate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN', 'BOSS-OFFICE-PC'), |
|
|
((SELECT id FROM licenses WHERE license_code = 'Z0Y9X8W7V6U5T4S3R2Q1P0O9N8M7L6K5'), 'validate', '172.16.0.10', 'CPU456DEF789ABC012GHI345JKL678MN', 'BOSS-OFFICE-PC'), |
|
|
|
|
|
|
|
|
((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'activate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB', 'ADMIN-MASTER-SERVER'), |
|
|
((SELECT id FROM licenses WHERE license_code = 'K1L2M3N4O5P6Q7R8S9T0U1V2W3X4Y5Z6'), 'validate', '127.0.0.1', 'CPUADMIN123MASTER456CONTROL789AB', 'ADMIN-MASTER-SERVER'), |
|
|
|
|
|
|
|
|
((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'activate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC', 'EXPIRING-PC-TODAY'), |
|
|
((SELECT id FROM licenses WHERE license_code = 'V8W9X0Y1Z2A3B4C5D6E7F8G9H0I1J2K3'), 'validate', '118.163.88.99', 'CPU999END888SOON777EXPIRE666ABC', 'EXPIRING-PC-TODAY'); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
SELECT |
|
|
schemaname, |
|
|
tablename, |
|
|
tableowner |
|
|
FROM pg_tables |
|
|
WHERE tablename IN ('licenses', 'usage_logs') |
|
|
ORDER BY tablename; |
|
|
|
|
|
|
|
|
SELECT |
|
|
indexname, |
|
|
tablename, |
|
|
indexdef |
|
|
FROM pg_indexes |
|
|
WHERE tablename IN ('licenses', 'usage_logs') |
|
|
ORDER BY tablename, indexname; |