feat: qrcode com contador de leituras
This commit is contained in:
parent
2edb4e1196
commit
8541e68711
@ -307,8 +307,8 @@ namespace QRRapidoApp.Controllers
|
||||
var isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
|
||||
|
||||
// DEBUG: Log detalhado dos parâmetros recebidos
|
||||
_logger.LogInformation("🔍 [DEBUG] GenerateRapidWithLogo called - RequestId: {RequestId}, ApplyLogoColorization: {ApplyLogoColorization}, LogoSizePercent: {LogoSizePercent}, HasLogo: {HasLogo}",
|
||||
requestId, request.ApplyLogoColorization, request.LogoSizePercent, request.HasLogo);
|
||||
_logger.LogInformation("🔍 [DEBUG] GenerateRapidWithLogo called - RequestId: {RequestId}, EnableTracking: {EnableTracking}, Type: {Type}, ApplyLogoColorization: {ApplyLogoColorization}, LogoSizePercent: {LogoSizePercent}, HasLogo: {HasLogo}",
|
||||
requestId, request.EnableTracking, request.Type, request.ApplyLogoColorization, request.LogoSizePercent, request.HasLogo);
|
||||
|
||||
using (_logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
|
||||
138
Controllers/TrackingController.cs
Normal file
138
Controllers/TrackingController.cs
Normal file
@ -0,0 +1,138 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using QRRapidoApp.Services;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace QRRapidoApp.Controllers
|
||||
{
|
||||
/// <summary>
|
||||
/// Controller for tracking QR code scans (Premium feature)
|
||||
/// Handles redirect URLs and analytics
|
||||
/// </summary>
|
||||
[Route("t")]
|
||||
public class TrackingController : ControllerBase
|
||||
{
|
||||
private readonly IUserService _userService;
|
||||
private readonly ILogger<TrackingController> _logger;
|
||||
|
||||
public TrackingController(IUserService userService, ILogger<TrackingController> logger)
|
||||
{
|
||||
_userService = userService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Track QR code scan and redirect to original URL
|
||||
/// URL format: https://qrrapido.site/t/{trackingId}
|
||||
/// </summary>
|
||||
[HttpGet("{trackingId}")]
|
||||
public async Task<IActionResult> TrackAndRedirect(string trackingId)
|
||||
{
|
||||
var stopwatch = Stopwatch.StartNew();
|
||||
|
||||
using (_logger.BeginScope(new Dictionary<string, object>
|
||||
{
|
||||
["TrackingId"] = trackingId,
|
||||
["QRTracking"] = true
|
||||
}))
|
||||
{
|
||||
_logger.LogInformation("QR tracking request - TrackingId: {TrackingId}", trackingId);
|
||||
|
||||
try
|
||||
{
|
||||
// Get QR code from database
|
||||
var qr = await _userService.GetQRByTrackingIdAsync(trackingId);
|
||||
|
||||
if (qr == null)
|
||||
{
|
||||
_logger.LogWarning("QR code not found for tracking - TrackingId: {TrackingId}", trackingId);
|
||||
return NotFound("QR code not found");
|
||||
}
|
||||
|
||||
// Increment scan count and update last access (fire and forget for performance)
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await _userService.IncrementQRScanCountAsync(trackingId);
|
||||
_logger.LogDebug("QR scan count incremented - TrackingId: {TrackingId}, NewCount: {NewCount}",
|
||||
trackingId, qr.ScanCount + 1);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error incrementing scan count - TrackingId: {TrackingId}", trackingId);
|
||||
}
|
||||
});
|
||||
|
||||
stopwatch.Stop();
|
||||
_logger.LogInformation("QR redirect - TrackingId: {TrackingId}, Destination: {Destination}, ProcessingTime: {ProcessingTimeMs}ms",
|
||||
trackingId, qr.Content, stopwatch.ElapsedMilliseconds);
|
||||
|
||||
// Redirect to original URL (HTTP 302 temporary redirect)
|
||||
return Redirect(qr.Content);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
stopwatch.Stop();
|
||||
_logger.LogError(ex, "QR tracking failed - TrackingId: {TrackingId}, ProcessingTime: {ProcessingTimeMs}ms",
|
||||
trackingId, stopwatch.ElapsedMilliseconds);
|
||||
|
||||
// Return error page instead of exposing exception details
|
||||
return StatusCode(500, "Error processing QR code");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get analytics for a specific QR code (requires authentication)
|
||||
/// </summary>
|
||||
[HttpGet("analytics/{trackingId}")]
|
||||
public async Task<IActionResult> GetAnalytics(string trackingId)
|
||||
{
|
||||
var userId = User?.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
|
||||
|
||||
if (string.IsNullOrEmpty(userId))
|
||||
{
|
||||
_logger.LogWarning("Analytics request without authentication - TrackingId: {TrackingId}", trackingId);
|
||||
return Unauthorized();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var qr = await _userService.GetQRByTrackingIdAsync(trackingId);
|
||||
|
||||
if (qr == null)
|
||||
{
|
||||
_logger.LogWarning("Analytics request for non-existent QR - TrackingId: {TrackingId}", trackingId);
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
// Verify ownership
|
||||
if (qr.UserId != userId)
|
||||
{
|
||||
_logger.LogWarning("Analytics request for QR owned by different user - TrackingId: {TrackingId}, RequestingUser: {UserId}, Owner: {OwnerId}",
|
||||
trackingId, userId, qr.UserId);
|
||||
return Forbid();
|
||||
}
|
||||
|
||||
_logger.LogInformation("Analytics retrieved - TrackingId: {TrackingId}, UserId: {UserId}, ScanCount: {ScanCount}",
|
||||
trackingId, userId, qr.ScanCount);
|
||||
|
||||
return Ok(new
|
||||
{
|
||||
trackingId = qr.TrackingId,
|
||||
scanCount = qr.ScanCount,
|
||||
createdAt = qr.CreatedAt,
|
||||
lastAccessedAt = qr.LastAccessedAt,
|
||||
content = qr.Content,
|
||||
type = qr.Type
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error retrieving analytics - TrackingId: {TrackingId}, UserId: {UserId}",
|
||||
trackingId, userId);
|
||||
return StatusCode(500);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
345
Docs/QR-Analytics-Feature.md
Normal file
345
Docs/QR-Analytics-Feature.md
Normal file
@ -0,0 +1,345 @@
|
||||
# 📊 QR Code Analytics - Premium Feature
|
||||
|
||||
Sistema de rastreamento de leituras de QR codes para usuários premium.
|
||||
|
||||
## 🎯 Visão Geral
|
||||
|
||||
Permite que usuários premium acompanhem quantas vezes seus QR codes foram escaneados, com timestamps e histórico completo.
|
||||
|
||||
### Como Funciona
|
||||
|
||||
1. **Usuário premium** gera QR code com analytics habilitado
|
||||
2. QR code aponta para URL de redirect: `https://qrrapido.site/t/{trackingId}`
|
||||
3. Ao escanear, o sistema:
|
||||
- Incrementa contador de leituras (`scanCount`)
|
||||
- Atualiza timestamp da última leitura (`lastAccessedAt`)
|
||||
- Redireciona para URL original (HTTP 302)
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Implementação Técnica
|
||||
|
||||
### Novos Componentes
|
||||
|
||||
#### 1. **TrackingController** (`Controllers/TrackingController.cs`)
|
||||
|
||||
Endpoints:
|
||||
- `GET /t/{trackingId}` - Redireciona e contabiliza leitura
|
||||
- `GET /t/analytics/{trackingId}` - Retorna analytics (requer autenticação)
|
||||
|
||||
#### 2. **Campos no MongoDB**
|
||||
|
||||
**QRCodeHistory** (campos já existentes, agora em uso):
|
||||
```csharp
|
||||
public int ScanCount { get; set; } = 0; // Contador de leituras
|
||||
public DateTime LastAccessedAt { get; set; } // Última leitura
|
||||
public bool IsDynamic { get; set; } = false; // Se é trackable
|
||||
public string? TrackingId { get; set; } // ID público (novo)
|
||||
```
|
||||
|
||||
#### 3. **QRGenerationRequest** (novo campo)
|
||||
```csharp
|
||||
public bool EnableTracking { get; set; } = false; // Premium feature
|
||||
```
|
||||
|
||||
#### 4. **UserService** (novos métodos)
|
||||
```csharp
|
||||
Task<QRCodeHistory?> GetQRByTrackingIdAsync(string trackingId);
|
||||
Task IncrementQRScanCountAsync(string trackingId);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Como Usar
|
||||
|
||||
### Backend (já implementado)
|
||||
|
||||
#### Gerar QR com Analytics
|
||||
|
||||
```csharp
|
||||
var request = new QRGenerationRequest
|
||||
{
|
||||
Type = "url",
|
||||
Content = "https://google.com",
|
||||
IsPremium = true,
|
||||
EnableTracking = true // ✅ Habilita analytics
|
||||
};
|
||||
|
||||
var result = await _qrService.GenerateRapidAsync(request);
|
||||
|
||||
// result.TrackingId conterá o ID para analytics
|
||||
// QR code aponta para: https://qrrapido.site/t/{trackingId}
|
||||
```
|
||||
|
||||
#### Consultar Analytics
|
||||
|
||||
```csharp
|
||||
// Via UserService
|
||||
var qr = await _userService.GetQRByTrackingIdAsync("abc123def456");
|
||||
Console.WriteLine($"Leituras: {qr.ScanCount}");
|
||||
Console.WriteLine($"Última leitura: {qr.LastAccessedAt}");
|
||||
|
||||
// Via API (requer autenticação)
|
||||
GET /t/analytics/abc123def456
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"trackingId": "abc123def456",
|
||||
"scanCount": 42,
|
||||
"createdAt": "2025-10-19T10:00:00Z",
|
||||
"lastAccessedAt": "2025-10-20T15:30:00Z",
|
||||
"content": "https://google.com",
|
||||
"type": "url"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Frontend (ainda não implementado)
|
||||
|
||||
#### Opção 1: Checkbox no Gerador
|
||||
|
||||
```html
|
||||
<!-- Em Views/Home/Index.cshtml -->
|
||||
<div class="form-check premium-feature">
|
||||
<input type="checkbox" id="enableTracking" class="form-check-input">
|
||||
<label for="enableTracking">
|
||||
<i class="fas fa-chart-line"></i> Habilitar Analytics
|
||||
<span class="badge bg-warning">Premium</span>
|
||||
</label>
|
||||
</div>
|
||||
```
|
||||
|
||||
```javascript
|
||||
// Em wwwroot/js/qr-speed-generator.js
|
||||
const enableTracking = document.getElementById('enableTracking');
|
||||
|
||||
formData.append('EnableTracking', enableTracking?.checked && this.isPremium);
|
||||
```
|
||||
|
||||
#### Opção 2: Dashboard de Analytics
|
||||
|
||||
```html
|
||||
<!-- Nova view: Views/Premium/Analytics.cshtml -->
|
||||
<div class="analytics-dashboard">
|
||||
<h2>Meus QR Codes com Analytics</h2>
|
||||
|
||||
<div class="qr-list">
|
||||
@foreach (var qr in Model.TrackableQRs)
|
||||
{
|
||||
<div class="qr-card">
|
||||
<h3>@qr.Content</h3>
|
||||
<p><strong>Leituras:</strong> @qr.ScanCount</p>
|
||||
<p><strong>Última leitura:</strong> @qr.LastAccessedAt.ToString("dd/MM/yyyy HH:mm")</p>
|
||||
<a href="/t/analytics/@qr.TrackingId">Ver detalhes</a>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Limitações Conhecidas
|
||||
|
||||
### 1. **Apenas QR codes de URL**
|
||||
- Tracking funciona apenas para `Type = "url"`
|
||||
- WiFi, vCard, SMS, etc. **não suportam** redirect
|
||||
|
||||
**Razão**: Esses tipos não aceitam URLs customizadas
|
||||
|
||||
**Solução futura**: Implementar pixel de tracking invisível ou API de scan manual
|
||||
|
||||
### 2. **Redirect adiciona ~50-200ms**
|
||||
- Usuário experimenta pequeno delay ao escanear
|
||||
- Impacto: Mínimo para maioria dos casos
|
||||
|
||||
**Otimização**: Redirect é assíncrono, contador atualiza em background
|
||||
|
||||
### 3. **Bloqueadores de ads podem afetar**
|
||||
- Alguns bloqueadores podem marcar `/t/` como tracking
|
||||
- Probabilidade: Baixa (não usa cookies ou JS)
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Segurança
|
||||
|
||||
### Proteções Implementadas
|
||||
|
||||
1. **Tracking ID não-sequencial**: GUID truncado (12 chars)
|
||||
2. **Validação de ownership**: Endpoint `/t/analytics/` verifica se QR pertence ao usuário
|
||||
3. **Fire-and-forget counting**: Não bloqueia redirect se MongoDB estiver lento
|
||||
4. **Logging completo**: Todas as leituras são logadas
|
||||
|
||||
### Considerações de Privacidade
|
||||
|
||||
- **Não coleta**: IP, device, localização (por enquanto)
|
||||
- **Apenas conta**: Quantidade de leituras + timestamp
|
||||
- **LGPD compliant**: Usuário premium pode deletar histórico a qualquer momento
|
||||
|
||||
---
|
||||
|
||||
## 📊 Exemplos de Uso
|
||||
|
||||
### Caso 1: Rastreamento de Campanha
|
||||
|
||||
```csharp
|
||||
// Gerar QR para campanha de marketing
|
||||
var campaign = new QRGenerationRequest
|
||||
{
|
||||
Type = "url",
|
||||
Content = "https://minhaloja.com/promo-verao",
|
||||
IsPremium = true,
|
||||
EnableTracking = true
|
||||
};
|
||||
|
||||
var qr = await _qrService.GenerateRapidAsync(campaign);
|
||||
|
||||
// Distribuir QR em materiais impressos
|
||||
// Depois consultar: await _userService.GetQRByTrackingIdAsync(qr.TrackingId);
|
||||
```
|
||||
|
||||
### Caso 2: Validação de Engajamento
|
||||
|
||||
```csharp
|
||||
// Verificar se QR code em outdoor teve leituras
|
||||
var qr = await _userService.GetQRByTrackingIdAsync("abc123def456");
|
||||
|
||||
if (qr.ScanCount > 100)
|
||||
{
|
||||
Console.WriteLine("Outdoor teve boa visibilidade!");
|
||||
}
|
||||
else if (qr.ScanCount == 0)
|
||||
{
|
||||
Console.WriteLine("QR pode estar ilegível ou mal posicionado");
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Configuração
|
||||
|
||||
### appsettings.json
|
||||
|
||||
```json
|
||||
{
|
||||
"App": {
|
||||
"BaseUrl": "https://qrrapido.site" // ✅ Usado para gerar URLs de tracking
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### appsettings.Development.json
|
||||
|
||||
```json
|
||||
{
|
||||
"App": {
|
||||
"BaseUrl": "https://localhost:52428" // ✅ Para testes locais
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testando Localmente
|
||||
|
||||
### 1. Gerar QR com Tracking
|
||||
|
||||
```bash
|
||||
# POST /api/QR/GenerateRapid
|
||||
curl -X POST https://localhost:52428/api/QR/GenerateRapid \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"type": "url",
|
||||
"content": "https://google.com",
|
||||
"isPremium": true,
|
||||
"enableTracking": true
|
||||
}'
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"qrCodeBase64": "iVBORw0KG...",
|
||||
"trackingId": "a1b2c3d4e5f6",
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Simular Leitura do QR
|
||||
|
||||
```bash
|
||||
# Abrir no navegador (ou curl)
|
||||
curl -L https://localhost:52428/t/a1b2c3d4e5f6
|
||||
# Deve redirecionar para https://google.com
|
||||
```
|
||||
|
||||
### 3. Verificar Contador
|
||||
|
||||
```bash
|
||||
# Acessar MongoDB ou via API
|
||||
curl https://localhost:52428/t/analytics/a1b2c3d4e5f6 \
|
||||
-H "Authorization: Bearer {token}"
|
||||
```
|
||||
|
||||
**Response:**
|
||||
```json
|
||||
{
|
||||
"trackingId": "a1b2c3d4e5f6",
|
||||
"scanCount": 1,
|
||||
"lastAccessedAt": "2025-10-20T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📈 Próximos Passos (Features Futuras)
|
||||
|
||||
### Analytics Avançado
|
||||
- [ ] Gráfico de leituras por dia/semana/mês
|
||||
- [ ] Geo-localização (IP → País/Cidade)
|
||||
- [ ] Tipo de device (mobile/desktop)
|
||||
- [ ] Browser/OS usado para escanear
|
||||
|
||||
### UI/UX
|
||||
- [ ] Dashboard de analytics no painel premium
|
||||
- [ ] Exportar dados para CSV/Excel
|
||||
- [ ] Notificações quando QR atingir X leituras
|
||||
|
||||
### Performance
|
||||
- [ ] Cache Redis para evitar queries MongoDB em cada scan
|
||||
- [ ] Batch updates (atualizar contador a cada N leituras)
|
||||
|
||||
---
|
||||
|
||||
## 📚 Referências
|
||||
|
||||
- **Tracking URLs**: Mesma estratégia usada por bit.ly, tinyurl, etc.
|
||||
- **HTTP 302 Redirect**: Padrão para preservar SEO e funcionalidade
|
||||
- **Fire-and-forget**: Pattern do ASP.NET Core para operações assíncronas não-bloqueantes
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Problema: "QR code not found"
|
||||
- Verificar se `trackingId` existe no MongoDB
|
||||
- Verificar se QR foi salvo com `IsDynamic = true`
|
||||
|
||||
### Problema: Contador não incrementa
|
||||
- Verificar logs do `TrackingController`
|
||||
- Verificar conectividade MongoDB
|
||||
- Verificar se `IncrementQRScanCountAsync` está sendo chamado
|
||||
|
||||
### Problema: Redirect não funciona
|
||||
- Verificar se URL original está salva em `Content`
|
||||
- Verificar se `BaseUrl` está configurado corretamente
|
||||
- Verificar logs de erro no controller
|
||||
|
||||
---
|
||||
|
||||
**Implementado em**: 2025-10-20
|
||||
**Versão**: 1.0.0
|
||||
**Status**: ✅ Backend completo, Frontend pendente
|
||||
@ -36,6 +36,9 @@ namespace QRRapidoApp.Models
|
||||
[BsonElement("isDynamic")]
|
||||
public bool IsDynamic { get; set; } = false;
|
||||
|
||||
[BsonElement("trackingId")]
|
||||
public string? TrackingId { get; set; } // Public ID for tracking URL (premium feature)
|
||||
|
||||
[BsonElement("size")]
|
||||
public int Size { get; set; } = 300;
|
||||
|
||||
|
||||
@ -28,6 +28,12 @@ namespace QRRapidoApp.Models.ViewModels
|
||||
/// Se deve aplicar a cor do QR code no logo (Premium feature)
|
||||
/// </summary>
|
||||
public bool ApplyLogoColorization { get; set; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Se deve habilitar analytics de leitura (Premium feature)
|
||||
/// Quando habilitado, o QR code usa URL de redirect para contabilizar leituras
|
||||
/// </summary>
|
||||
public bool EnableTracking { get; set; } = false;
|
||||
}
|
||||
|
||||
public class QRGenerationResult
|
||||
@ -42,5 +48,6 @@ namespace QRRapidoApp.Models.ViewModels
|
||||
public bool Success { get; set; } = true;
|
||||
public string? ErrorMessage { get; set; }
|
||||
public LogoReadabilityInfo? ReadabilityInfo { get; set; } // Nova propriedade para análise de legibilidade
|
||||
public string? TrackingId { get; set; } // Tracking ID for analytics (Premium feature)
|
||||
}
|
||||
}
|
||||
@ -783,4 +783,23 @@
|
||||
<data name="AnonymousUserLimit" xml:space="preserve">
|
||||
<value>Anonymous users: 3 QR codes per day</value>
|
||||
</data>
|
||||
<!-- Premium Features -->
|
||||
<data name="AdvancedCustomization" xml:space="preserve">
|
||||
<value>Advanced customization (colors, corners)</value>
|
||||
</data>
|
||||
<data name="LogoSupport" xml:space="preserve">
|
||||
<value>Logo and image support</value>
|
||||
</data>
|
||||
<data name="HistoryAndDownloads" xml:space="preserve">
|
||||
<value>Unlimited history and downloads</value>
|
||||
</data>
|
||||
<data name="QRReadCounter" xml:space="preserve">
|
||||
<value>QR code scan counter</value>
|
||||
</data>
|
||||
<data name="EnableTracking" xml:space="preserve">
|
||||
<value>Enable scan counter</value>
|
||||
</data>
|
||||
<data name="TrackingInfo" xml:space="preserve">
|
||||
<value>QR code will use redirect URL to count scans</value>
|
||||
</data>
|
||||
</root>
|
||||
@ -2041,6 +2041,15 @@
|
||||
<data name="HistoryAndDownloads" xml:space="preserve">
|
||||
<value>Historial y descargas ilimitadas</value>
|
||||
</data>
|
||||
<data name="QRReadCounter" xml:space="preserve">
|
||||
<value>Contador de lecturas de códigos QR</value>
|
||||
</data>
|
||||
<data name="EnableTracking" xml:space="preserve">
|
||||
<value>Habilitar contador de lecturas</value>
|
||||
</data>
|
||||
<data name="TrackingInfo" xml:space="preserve">
|
||||
<value>El código QR usará URL de redireccionamiento para contabilizar lecturas</value>
|
||||
</data>
|
||||
<!-- FAQ Updates -->
|
||||
<data name="FAQ_StaticQRExplanation" xml:space="preserve">
|
||||
<value>Todos los códigos QR generados por QR Rápido son estáticos y permanentes. Esto significa que el contenido está codificado directamente en el código QR y no puede ser modificado después de la generación. Este enfoque garantiza máxima privacidad, ya que no rastreamos escaneos, y funciona sin conexión sin depender de servidores externos.</value>
|
||||
|
||||
@ -2131,6 +2131,15 @@
|
||||
<data name="HistoryAndDownloads" xml:space="preserve">
|
||||
<value>Histórico e downloads ilimitados</value>
|
||||
</data>
|
||||
<data name="QRReadCounter" xml:space="preserve">
|
||||
<value>Contador de leituras de QR codes</value>
|
||||
</data>
|
||||
<data name="EnableTracking" xml:space="preserve">
|
||||
<value>Habilitar contador de leituras</value>
|
||||
</data>
|
||||
<data name="TrackingInfo" xml:space="preserve">
|
||||
<value>QR code usará URL de redirect para contabilizar leituras</value>
|
||||
</data>
|
||||
<!-- FAQ Updates -->
|
||||
<data name="FAQ_StaticQRExplanation" xml:space="preserve">
|
||||
<value>Todos os QR codes gerados pelo QR Rápido são estáticos e permanentes. Isso significa que o conteúdo é codificado diretamente no QR code e não pode ser alterado após a geração. Essa abordagem garante máxima privacidade, pois não rastreamos scans, e funciona offline sem depender de servidores externos.</value>
|
||||
|
||||
@ -28,5 +28,9 @@ namespace QRRapidoApp.Services
|
||||
Task MarkPremiumCancelledAsync(string userId, DateTime cancelledAt);
|
||||
Task<List<User>> GetUsersForHistoryCleanupAsync(DateTime cutoffDate);
|
||||
Task DeleteUserHistoryAsync(string userId);
|
||||
|
||||
// QR Code Tracking (Analytics) - Premium feature
|
||||
Task<QRCodeHistory?> GetQRByTrackingIdAsync(string trackingId);
|
||||
Task IncrementQRScanCountAsync(string trackingId);
|
||||
}
|
||||
}
|
||||
@ -45,6 +45,30 @@ namespace QRRapidoApp.Services
|
||||
{
|
||||
await _semaphore.WaitAsync();
|
||||
|
||||
// Generate tracking ID for premium users with tracking enabled
|
||||
string? trackingId = null;
|
||||
string originalContent = request.Content;
|
||||
|
||||
// DEBUG: Log all conditions
|
||||
_logger.LogInformation("[TRACKING DEBUG] IsPremium: {IsPremium}, EnableTracking: {EnableTracking}, Type: {Type}",
|
||||
request.IsPremium, request.EnableTracking, request.Type);
|
||||
|
||||
if (request.IsPremium && request.EnableTracking && request.Type == "url")
|
||||
{
|
||||
// Only URL type QR codes can use tracking (redirect functionality)
|
||||
trackingId = Guid.NewGuid().ToString("N").Substring(0, 12); // 12-char tracking ID
|
||||
var baseUrl = _config.GetValue<string>("App:BaseUrl", "https://qrrapido.site");
|
||||
request.Content = $"{baseUrl}/t/{trackingId}";
|
||||
|
||||
_logger.LogInformation("✅ Tracking enabled for QR - TrackingId: {TrackingId}, OriginalURL: {OriginalURL}",
|
||||
trackingId, originalContent);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("❌ Tracking NOT enabled - IsPremium: {IsPremium}, EnableTracking: {EnableTracking}, Type: {Type}",
|
||||
request.IsPremium, request.EnableTracking, request.Type);
|
||||
}
|
||||
|
||||
// Cache key based on content and settings
|
||||
var cacheKey = GenerateCacheKey(request);
|
||||
var cached = await _cache.GetStringAsync(cacheKey);
|
||||
@ -86,9 +110,16 @@ namespace QRRapidoApp.Services
|
||||
Size = qrCode.Length,
|
||||
RequestSettings = request,
|
||||
Success = true,
|
||||
ReadabilityInfo = readabilityInfo
|
||||
ReadabilityInfo = readabilityInfo,
|
||||
TrackingId = trackingId // Include tracking ID if enabled
|
||||
};
|
||||
|
||||
// Restore original content if tracking was enabled
|
||||
if (trackingId != null)
|
||||
{
|
||||
request.Content = originalContent;
|
||||
}
|
||||
|
||||
// Cache for configurable time
|
||||
var cacheExpiration = TimeSpan.FromMinutes(_config.GetValue<int>("Performance:CacheExpirationMinutes", 60));
|
||||
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result), new DistributedCacheEntryOptions
|
||||
|
||||
@ -231,7 +231,9 @@ namespace QRRapidoApp.Services
|
||||
GenerationTimeMs = qrResult.GenerationTimeMs,
|
||||
FromCache = qrResult.FromCache,
|
||||
IsActive = true,
|
||||
LastAccessedAt = DateTime.UtcNow
|
||||
LastAccessedAt = DateTime.UtcNow,
|
||||
TrackingId = qrResult.TrackingId, // Save tracking ID for analytics
|
||||
IsDynamic = !string.IsNullOrEmpty(qrResult.TrackingId) // Mark as dynamic if tracking is enabled
|
||||
};
|
||||
|
||||
await _context.QRCodeHistory.InsertOneAsync(qrHistory);
|
||||
@ -448,5 +450,52 @@ namespace QRRapidoApp.Services
|
||||
var update = Builders<User>.Update.Set(u => u.StripeCustomerId, stripeCustomerId);
|
||||
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get QR code by tracking ID (for analytics)
|
||||
/// </summary>
|
||||
public async Task<QRCodeHistory?> GetQRByTrackingIdAsync(string trackingId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return await _context.QRCodeHistory
|
||||
.Find(q => q.TrackingId == trackingId && q.IsActive)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting QR by tracking ID {TrackingId}: {ErrorMessage}", trackingId, ex.Message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Increment scan count for QR code analytics
|
||||
/// </summary>
|
||||
public async Task IncrementQRScanCountAsync(string trackingId)
|
||||
{
|
||||
try
|
||||
{
|
||||
var filter = Builders<QRCodeHistory>.Filter.Eq(q => q.TrackingId, trackingId);
|
||||
var update = Builders<QRCodeHistory>.Update
|
||||
.Inc(q => q.ScanCount, 1)
|
||||
.Set(q => q.LastAccessedAt, DateTime.UtcNow);
|
||||
|
||||
var result = await _context.QRCodeHistory.UpdateOneAsync(filter, update);
|
||||
|
||||
if (result.ModifiedCount > 0)
|
||||
{
|
||||
_logger.LogDebug("QR scan count incremented - TrackingId: {TrackingId}", trackingId);
|
||||
}
|
||||
else
|
||||
{
|
||||
_logger.LogWarning("Failed to increment scan count - TrackingId not found: {TrackingId}", trackingId);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error incrementing scan count for {TrackingId}: {ErrorMessage}", trackingId, ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -693,6 +693,23 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@* Analytics/Tracking - Premium Feature *@
|
||||
@if (isPremium)
|
||||
{
|
||||
<div class="col-12 mb-3">
|
||||
<div class="form-check">
|
||||
<input type="checkbox" id="enable-tracking" class="form-check-input" value="true">
|
||||
<label class="form-check-label" for="enable-tracking">
|
||||
<i class="fas fa-chart-line text-primary"></i> @Localizer["EnableTracking"]
|
||||
<span class="badge bg-primary">Premium</span>
|
||||
</label>
|
||||
<div class="form-text">
|
||||
<i class="fas fa-info-circle"></i> @Localizer["TrackingInfo"]
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="col-md-6 mb-3">
|
||||
<label class="form-label">@Localizer["BorderStyle"]</label>
|
||||
<select id="corner-style" class="form-select @(isPremium ? "" : "border-warning")">
|
||||
@ -1195,6 +1212,7 @@
|
||||
<li><i class="fas fa-check text-success"></i> @Localizer["AdvancedCustomization"]</li>
|
||||
<li><i class="fas fa-check text-success"></i> @Localizer["LogoSupport"]</li>
|
||||
<li><i class="fas fa-check text-success"></i> @Localizer["HistoryAndDownloads"]</li>
|
||||
<li><i class="fas fa-chart-line text-success"></i> @Localizer["QRReadCounter"]</li>
|
||||
<li><i class="fas fa-check text-success"></i> @Localizer["PrioritySupport"]</li>
|
||||
</ul>
|
||||
<div class="text-center">
|
||||
|
||||
@ -76,6 +76,7 @@
|
||||
<li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["AdvancedCustomization"]</li>
|
||||
<li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["LogoSupport"]</li>
|
||||
<li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["HistoryAndDownloads"]</li>
|
||||
<li class="list-group-item border-0"><i class="fas fa-chart-line text-success me-2"></i>@Localizer["QRReadCounter"]</li>
|
||||
<li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["PrioritySupport"]</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@ -4,6 +4,11 @@ class QRRapidoGenerator {
|
||||
this.startTime = 0;
|
||||
this.currentQR = null;
|
||||
this.timerInterval = null;
|
||||
|
||||
// Initialize premium status from hidden input
|
||||
this.isPremium = this.checkUserPremium();
|
||||
console.log('[INIT] QRRapidoGenerator - isPremium:', this.isPremium);
|
||||
|
||||
this.languageStrings = {
|
||||
'pt-BR': {
|
||||
tagline: 'Gere QR codes em segundos!',
|
||||
@ -737,6 +742,9 @@ class QRRapidoGenerator {
|
||||
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
|
||||
|
||||
// Common data for both endpoints
|
||||
const trackingEnabled = this.getTrackingEnabled();
|
||||
console.log('[DEBUG] getTrackingEnabled() returned:', trackingEnabled, 'type:', typeof trackingEnabled);
|
||||
|
||||
const commonData = {
|
||||
type: actualType,
|
||||
content: encodedContent,
|
||||
@ -747,9 +755,12 @@ class QRRapidoGenerator {
|
||||
margin: parseInt(document.getElementById('qr-margin').value),
|
||||
cornerStyle: document.getElementById('corner-style')?.value || 'square',
|
||||
optimizeForSpeed: true,
|
||||
language: this.currentLang
|
||||
language: this.currentLang,
|
||||
enableTracking: trackingEnabled // Analytics feature for premium users
|
||||
};
|
||||
|
||||
console.log('[DEBUG] commonData.enableTracking:', commonData.enableTracking);
|
||||
|
||||
// Add dynamic QR data if it's a URL type
|
||||
if (type === 'url') {
|
||||
let dynamicData;
|
||||
@ -772,9 +783,13 @@ class QRRapidoGenerator {
|
||||
// Use FormData for requests with logo
|
||||
const formData = new FormData();
|
||||
|
||||
// Add all common data to FormData
|
||||
// Add all common data to FormData with PascalCase for ASP.NET Core model binding
|
||||
Object.keys(commonData).forEach(key => {
|
||||
formData.append(key, commonData[key]);
|
||||
// Convert camelCase to PascalCase for FormData (ASP.NET Core compatibility)
|
||||
const pascalKey = key.charAt(0).toUpperCase() + key.slice(1);
|
||||
const value = commonData[key];
|
||||
console.log('[DEBUG] FormData.append:', pascalKey, '=', value, 'type:', typeof value);
|
||||
formData.append(pascalKey, value);
|
||||
});
|
||||
|
||||
// NOVOS PARÂMETROS DE LOGO APRIMORADO
|
||||
@ -815,6 +830,31 @@ class QRRapidoGenerator {
|
||||
};
|
||||
}
|
||||
|
||||
// Function to check if tracking is enabled (Premium feature)
|
||||
getTrackingEnabled() {
|
||||
const trackingCheckbox = document.getElementById('enable-tracking');
|
||||
|
||||
// DEBUG: Log each condition separately
|
||||
console.log('[DEBUG] getTrackingEnabled() - Checkbox exists:', !!trackingCheckbox);
|
||||
console.log('[DEBUG] getTrackingEnabled() - Checkbox checked:', trackingCheckbox?.checked);
|
||||
console.log('[DEBUG] getTrackingEnabled() - this.isPremium:', this.isPremium);
|
||||
|
||||
// Only return true if checkbox exists, is checked, and user is premium
|
||||
// IMPORTANT: Always return boolean (not undefined) for FormData compatibility
|
||||
const result = !!(trackingCheckbox && trackingCheckbox.checked && this.isPremium);
|
||||
console.log('[DEBUG] getTrackingEnabled() - Final result:', result);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Check if user has premium status
|
||||
checkUserPremium() {
|
||||
const premiumStatus = document.getElementById('user-premium-status');
|
||||
if (premiumStatus) {
|
||||
return premiumStatus.value === 'premium';
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Função auxiliar para obter conteúdo baseado no tipo
|
||||
getContentForType(type) {
|
||||
if (type === 'url') {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user