163 lines
5.1 KiB
Go
163 lines
5.1 KiB
Go
package task
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"time"
|
|
|
|
"github.com/gogf/gf/v2/frame/g"
|
|
|
|
"github.com/qianfree/team-api/internal/dao"
|
|
do "github.com/qianfree/team-api/internal/model/do"
|
|
)
|
|
|
|
type ExportPayload struct {
|
|
TenantID int64 `json:"tenant_id"`
|
|
Scopes []string `json:"scopes"`
|
|
RequestedBy int64 `json:"requested_by"`
|
|
}
|
|
|
|
type DeletionPayload struct {
|
|
TenantID int64 `json:"tenant_id"`
|
|
Reason string `json:"reason"`
|
|
RequestedBy int64 `json:"requested_by"`
|
|
}
|
|
|
|
func init() {
|
|
RegisterHandler("data_export", handleDataExport)
|
|
RegisterHandler("data_deletion_request", handleDeletionRequest)
|
|
RegisterHandler("data_export_cleanup", handleExportCleanup)
|
|
}
|
|
|
|
func handleDataExport(ctx context.Context, payload json.RawMessage) (any, error) {
|
|
var p ExportPayload
|
|
if err := json.Unmarshal(payload, &p); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
exportData := make(map[string]any)
|
|
for _, scope := range p.Scopes {
|
|
switch scope {
|
|
case "members":
|
|
var members []map[string]any
|
|
_ = dao.TntUsers.Ctx(ctx).Where("tenant_id", p.TenantID).
|
|
Fields("id, username, display_name, role, status, created_at").Scan(&members)
|
|
for _, m := range members {
|
|
delete(m, "email")
|
|
}
|
|
exportData["members"] = members
|
|
case "usage":
|
|
var logs []map[string]any
|
|
_ = dao.BilUsageLogs.Ctx(ctx).Where("tenant_id", p.TenantID).
|
|
OrderDesc("created_at").Limit(10000).
|
|
Fields("id, model_name, prompt_tokens, completion_tokens, total_tokens, cost, created_at").Scan(&logs)
|
|
exportData["usage"] = logs
|
|
case "billing":
|
|
var records []map[string]any
|
|
_ = dao.BilRecords.Ctx(ctx).Where("tenant_id", p.TenantID).
|
|
OrderDesc("created_at").Limit(10000).
|
|
Fields("id, type, amount, balance_before, balance_after, description, created_at").Scan(&records)
|
|
exportData["billing_records"] = records
|
|
case "logs":
|
|
var logs []map[string]any
|
|
_ = dao.AudOperationLogs.Ctx(ctx).Where("tenant_id", p.TenantID).
|
|
OrderDesc("created_at").Limit(10000).
|
|
Fields("id, action, resource_type, resource_id, ip_address, user_agent, created_at").Scan(&logs)
|
|
exportData["operation_logs"] = logs
|
|
}
|
|
}
|
|
|
|
jsonData, err := json.MarshalIndent(exportData, "", " ")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("marshal export data: %w", err)
|
|
}
|
|
|
|
storagePath := fmt.Sprintf("exports/tenant_%d/%s.json", p.TenantID, time.Now().Format("20060102_150405"))
|
|
password := generateRandomPassword(16)
|
|
|
|
_, err = dao.FilFiles.Ctx(ctx).Data(do.FilFiles{
|
|
TenantId: p.TenantID,
|
|
UserId: p.RequestedBy,
|
|
OriginalName: fmt.Sprintf("export_tenant_%d_%s.json", p.TenantID, time.Now().Format("20060102")),
|
|
StoragePath: storagePath,
|
|
MimeType: "application/json",
|
|
Size: int64(len(jsonData)),
|
|
}).Insert()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("save file metadata: %w", err)
|
|
}
|
|
|
|
g.Log().Infof(ctx, "data export completed for tenant %d, scopes=%v, size=%d bytes", p.TenantID, p.Scopes, len(jsonData))
|
|
return map[string]any{
|
|
"storage_path": storagePath,
|
|
"scopes": p.Scopes,
|
|
"size": len(jsonData),
|
|
"password": password,
|
|
}, nil
|
|
}
|
|
|
|
func handleDeletionRequest(ctx context.Context, payload json.RawMessage) (any, error) {
|
|
var p DeletionPayload
|
|
if err := json.Unmarshal(payload, &p); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
LogInfo(ctx, 0, fmt.Sprintf("开始处理租户 %d 的数据删除请求", p.TenantID))
|
|
|
|
_, err := dao.TntUsers.Ctx(ctx).
|
|
Where("tenant_id", p.TenantID).
|
|
Data(do.TntUsers{
|
|
DisplayName: "[deleted]",
|
|
Email: fmt.Sprintf("deleted_%d@deleted.local", p.TenantID),
|
|
}).Update()
|
|
if err != nil {
|
|
LogError(ctx, 0, fmt.Sprintf("匿名化用户数据失败: %v", err))
|
|
return nil, fmt.Errorf("anonymize users: %w", err)
|
|
}
|
|
|
|
_, _ = dao.ApiKeys.Ctx(ctx).
|
|
Where("tenant_id", p.TenantID).
|
|
Data(do.ApiKeys{Status: "disabled"}).Update()
|
|
|
|
_, _ = dao.AudSensitiveAccessLogs.Ctx(ctx).Where("tenant_id", p.TenantID).Delete()
|
|
|
|
_, _ = dao.TntTenants.Ctx(ctx).
|
|
Where("id", p.TenantID).
|
|
Data(do.TntTenants{Status: "terminated"}).Update()
|
|
|
|
proof := map[string]any{
|
|
"tenant_id": p.TenantID,
|
|
"reason": p.Reason,
|
|
"requested_by": p.RequestedBy,
|
|
"completed_at": time.Now().Format(time.RFC3339),
|
|
"actions": []string{"用户数据已匿名化", "API Key 已禁用", "敏感日志已删除", "租户已标记 terminated"},
|
|
}
|
|
LogInfo(ctx, 0, fmt.Sprintf("数据删除完成: 租户%d", p.TenantID))
|
|
return proof, nil
|
|
}
|
|
|
|
func handleExportCleanup(ctx context.Context, payload json.RawMessage) (any, error) {
|
|
var p struct {
|
|
FileID int64 `json:"file_id"`
|
|
TenantID int64 `json:"tenant_id"`
|
|
}
|
|
if err := json.Unmarshal(payload, &p); err != nil {
|
|
return nil, err
|
|
}
|
|
_, _ = dao.FilFiles.Ctx(ctx).Where("id", p.FileID).Delete()
|
|
g.Log().Infof(ctx, "cleaned up export file %d for tenant %d", p.FileID, p.TenantID)
|
|
return map[string]any{"file_id": p.FileID, "deleted": true}, nil
|
|
}
|
|
|
|
func generateRandomPassword(length int) string {
|
|
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
|
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
|
b := make([]byte, length)
|
|
for i := range b {
|
|
b[i] = charset[r.Intn(len(charset))]
|
|
}
|
|
return string(b)
|
|
}
|