Compare commits
2 Commits
2edb4e1196
...
eb0751cb16
| Author | SHA1 | Date | |
|---|---|---|---|
| eb0751cb16 | |||
| 8541e68711 |
@ -31,7 +31,9 @@
|
|||||||
"Read(//mnt/c/vscode/**)",
|
"Read(//mnt/c/vscode/**)",
|
||||||
"Read(//mnt/c/**)",
|
"Read(//mnt/c/**)",
|
||||||
"Bash(chmod +x /mnt/c/vscode/qrrapido/Scripts/update-plans.sh)",
|
"Bash(chmod +x /mnt/c/vscode/qrrapido/Scripts/update-plans.sh)",
|
||||||
"Bash(netstat -tln)"
|
"Bash(netstat -tln)",
|
||||||
|
"Bash(npm install)",
|
||||||
|
"Bash(npm install:*)"
|
||||||
],
|
],
|
||||||
"deny": []
|
"deny": []
|
||||||
}
|
}
|
||||||
|
|||||||
@ -307,8 +307,8 @@ namespace QRRapidoApp.Controllers
|
|||||||
var isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
|
var isAuthenticated = User?.Identity?.IsAuthenticated ?? false;
|
||||||
|
|
||||||
// DEBUG: Log detalhado dos parâmetros recebidos
|
// DEBUG: Log detalhado dos parâmetros recebidos
|
||||||
_logger.LogInformation("🔍 [DEBUG] GenerateRapidWithLogo called - RequestId: {RequestId}, ApplyLogoColorization: {ApplyLogoColorization}, LogoSizePercent: {LogoSizePercent}, HasLogo: {HasLogo}",
|
_logger.LogInformation("🔍 [DEBUG] GenerateRapidWithLogo called - RequestId: {RequestId}, EnableTracking: {EnableTracking}, Type: {Type}, ApplyLogoColorization: {ApplyLogoColorization}, LogoSizePercent: {LogoSizePercent}, HasLogo: {HasLogo}",
|
||||||
requestId, request.ApplyLogoColorization, request.LogoSizePercent, request.HasLogo);
|
requestId, request.EnableTracking, request.Type, request.ApplyLogoColorization, request.LogoSizePercent, request.HasLogo);
|
||||||
|
|
||||||
using (_logger.BeginScope(new Dictionary<string, object>
|
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")]
|
[BsonElement("isDynamic")]
|
||||||
public bool IsDynamic { get; set; } = false;
|
public bool IsDynamic { get; set; } = false;
|
||||||
|
|
||||||
|
[BsonElement("trackingId")]
|
||||||
|
public string? TrackingId { get; set; } // Public ID for tracking URL (premium feature)
|
||||||
|
|
||||||
[BsonElement("size")]
|
[BsonElement("size")]
|
||||||
public int Size { get; set; } = 300;
|
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)
|
/// Se deve aplicar a cor do QR code no logo (Premium feature)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool ApplyLogoColorization { get; set; } = false;
|
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
|
public class QRGenerationResult
|
||||||
@ -42,5 +48,6 @@ namespace QRRapidoApp.Models.ViewModels
|
|||||||
public bool Success { get; set; } = true;
|
public bool Success { get; set; } = true;
|
||||||
public string? ErrorMessage { get; set; }
|
public string? ErrorMessage { get; set; }
|
||||||
public LogoReadabilityInfo? ReadabilityInfo { get; set; } // Nova propriedade para análise de legibilidade
|
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">
|
<data name="AnonymousUserLimit" xml:space="preserve">
|
||||||
<value>Anonymous users: 3 QR codes per day</value>
|
<value>Anonymous users: 3 QR codes per day</value>
|
||||||
</data>
|
</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>
|
</root>
|
||||||
@ -2041,6 +2041,15 @@
|
|||||||
<data name="HistoryAndDownloads" xml:space="preserve">
|
<data name="HistoryAndDownloads" xml:space="preserve">
|
||||||
<value>Historial y descargas ilimitadas</value>
|
<value>Historial y descargas ilimitadas</value>
|
||||||
</data>
|
</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 -->
|
<!-- FAQ Updates -->
|
||||||
<data name="FAQ_StaticQRExplanation" xml:space="preserve">
|
<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>
|
<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">
|
<data name="HistoryAndDownloads" xml:space="preserve">
|
||||||
<value>Histórico e downloads ilimitados</value>
|
<value>Histórico e downloads ilimitados</value>
|
||||||
</data>
|
</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 -->
|
<!-- FAQ Updates -->
|
||||||
<data name="FAQ_StaticQRExplanation" xml:space="preserve">
|
<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>
|
<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 MarkPremiumCancelledAsync(string userId, DateTime cancelledAt);
|
||||||
Task<List<User>> GetUsersForHistoryCleanupAsync(DateTime cutoffDate);
|
Task<List<User>> GetUsersForHistoryCleanupAsync(DateTime cutoffDate);
|
||||||
Task DeleteUserHistoryAsync(string userId);
|
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();
|
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
|
// Cache key based on content and settings
|
||||||
var cacheKey = GenerateCacheKey(request);
|
var cacheKey = GenerateCacheKey(request);
|
||||||
var cached = await _cache.GetStringAsync(cacheKey);
|
var cached = await _cache.GetStringAsync(cacheKey);
|
||||||
@ -59,13 +83,13 @@ namespace QRRapidoApp.Services
|
|||||||
{
|
{
|
||||||
cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
|
cachedResult.GenerationTimeMs = stopwatch.ElapsedMilliseconds;
|
||||||
cachedResult.FromCache = true;
|
cachedResult.FromCache = true;
|
||||||
|
|
||||||
// Se não tem análise de legibilidade no cache, gerar agora
|
// Se não tem análise de legibilidade no cache, gerar agora
|
||||||
if (cachedResult.ReadabilityInfo == null)
|
if (cachedResult.ReadabilityInfo == null)
|
||||||
{
|
{
|
||||||
cachedResult.ReadabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size);
|
cachedResult.ReadabilityInfo = await _readabilityAnalyzer.AnalyzeAsync(request, request.Size);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cachedResult;
|
return cachedResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -86,9 +110,16 @@ namespace QRRapidoApp.Services
|
|||||||
Size = qrCode.Length,
|
Size = qrCode.Length,
|
||||||
RequestSettings = request,
|
RequestSettings = request,
|
||||||
Success = true,
|
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
|
// Cache for configurable time
|
||||||
var cacheExpiration = TimeSpan.FromMinutes(_config.GetValue<int>("Performance:CacheExpirationMinutes", 60));
|
var cacheExpiration = TimeSpan.FromMinutes(_config.GetValue<int>("Performance:CacheExpirationMinutes", 60));
|
||||||
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result), new DistributedCacheEntryOptions
|
await _cache.SetStringAsync(cacheKey, JsonSerializer.Serialize(result), new DistributedCacheEntryOptions
|
||||||
@ -648,21 +679,230 @@ namespace QRRapidoApp.Services
|
|||||||
|
|
||||||
private byte[] ApplyCornerStyle(byte[] qrBytes, string cornerStyle, int targetSize)
|
private byte[] ApplyCornerStyle(byte[] qrBytes, string cornerStyle, int targetSize)
|
||||||
{
|
{
|
||||||
|
if (cornerStyle == "square" || string.IsNullOrEmpty(cornerStyle))
|
||||||
|
{
|
||||||
|
return qrBytes; // No processing needed for square style
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Simplified implementation for cross-platform compatibility
|
_logger.LogInformation("Applying corner style '{CornerStyle}' to QR code", cornerStyle);
|
||||||
// The complex corner styling can be re-implemented later using ImageSharp drawing primitives
|
|
||||||
_logger.LogInformation("Corner style '{CornerStyle}' temporarily disabled for cross-platform compatibility. Returning original QR code.", cornerStyle);
|
using var qrImage = Image.Load<Rgba32>(qrBytes);
|
||||||
return qrBytes;
|
int width = qrImage.Width;
|
||||||
|
int height = qrImage.Height;
|
||||||
|
|
||||||
|
// Detect module size by scanning the image
|
||||||
|
int moduleSize = DetectModuleSize(qrImage);
|
||||||
|
if (moduleSize <= 0)
|
||||||
|
{
|
||||||
|
_logger.LogWarning("Could not detect module size, returning original QR code");
|
||||||
|
return qrBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.LogDebug("Detected module size: {ModuleSize}px for {Width}x{Height} QR code", moduleSize, width, height);
|
||||||
|
|
||||||
|
// Create a new image with styled modules
|
||||||
|
using var styledImage = new Image<Rgba32>(width, height);
|
||||||
|
|
||||||
|
// Process each module
|
||||||
|
for (int y = 0; y < height; y += moduleSize)
|
||||||
|
{
|
||||||
|
for (int x = 0; x < width; x += moduleSize)
|
||||||
|
{
|
||||||
|
// Check if this module is dark (part of the QR pattern)
|
||||||
|
var centerPixel = qrImage[x + moduleSize / 2, y + moduleSize / 2];
|
||||||
|
bool isDark = centerPixel.R < 128; // Dark if RGB < 128
|
||||||
|
|
||||||
|
if (isDark)
|
||||||
|
{
|
||||||
|
// Draw styled module based on cornerStyle
|
||||||
|
DrawStyledModule(styledImage, x, y, moduleSize, cornerStyle, centerPixel);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Draw background (light modules)
|
||||||
|
DrawRectangle(styledImage, x, y, moduleSize, moduleSize, centerPixel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert back to bytes
|
||||||
|
using var outputStream = new MemoryStream();
|
||||||
|
styledImage.SaveAsPng(outputStream);
|
||||||
|
_logger.LogInformation("Successfully applied corner style '{CornerStyle}'", cornerStyle);
|
||||||
|
return outputStream.ToArray();
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.LogError(ex, "Error applying corner style {CornerStyle}, returning original QR code", cornerStyle);
|
_logger.LogError(ex, "Error applying corner style {CornerStyle}, returning original QR code", cornerStyle);
|
||||||
// Return original QR code if styling fails
|
|
||||||
return qrBytes;
|
return qrBytes;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int DetectModuleSize(Image<Rgba32> qrImage)
|
||||||
|
{
|
||||||
|
// Scan horizontally from left to find the first transition from light to dark
|
||||||
|
int width = qrImage.Width;
|
||||||
|
int height = qrImage.Height;
|
||||||
|
int midY = height / 2;
|
||||||
|
|
||||||
|
bool wasLight = true;
|
||||||
|
int darkStart = -1;
|
||||||
|
int darkEnd = -1;
|
||||||
|
|
||||||
|
for (int x = 0; x < width; x++)
|
||||||
|
{
|
||||||
|
var pixel = qrImage[x, midY];
|
||||||
|
bool isLight = pixel.R >= 128;
|
||||||
|
|
||||||
|
if (wasLight && !isLight)
|
||||||
|
{
|
||||||
|
// Transition from light to dark
|
||||||
|
darkStart = x;
|
||||||
|
}
|
||||||
|
else if (!wasLight && isLight && darkStart >= 0)
|
||||||
|
{
|
||||||
|
// Transition from dark to light
|
||||||
|
darkEnd = x;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
wasLight = isLight;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (darkStart >= 0 && darkEnd > darkStart)
|
||||||
|
{
|
||||||
|
return darkEnd - darkStart;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: estimate based on typical QR code structure (21-177 modules)
|
||||||
|
// Most common is 25-29 modules for QR version 1-5
|
||||||
|
return width / 25; // Rough estimate
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawStyledModule(Image<Rgba32> image, int x, int y, int size, string style, Rgba32 color)
|
||||||
|
{
|
||||||
|
switch (style.ToLower())
|
||||||
|
{
|
||||||
|
case "rounded":
|
||||||
|
DrawRoundedRectangle(image, x, y, size, size, size / 4, color);
|
||||||
|
break;
|
||||||
|
case "circle":
|
||||||
|
DrawCircle(image, x + size / 2, y + size / 2, size / 2, color);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
DrawRectangle(image, x, y, size, size, color);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawRectangle(Image<Rgba32> image, int x, int y, int width, int height, Rgba32 color)
|
||||||
|
{
|
||||||
|
for (int dy = 0; dy < height; dy++)
|
||||||
|
{
|
||||||
|
for (int dx = 0; dx < width; dx++)
|
||||||
|
{
|
||||||
|
int px = x + dx;
|
||||||
|
int py = y + dy;
|
||||||
|
if (px >= 0 && px < image.Width && py >= 0 && py < image.Height)
|
||||||
|
{
|
||||||
|
image[px, py] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawRoundedRectangle(Image<Rgba32> image, int x, int y, int width, int height, int radius, Rgba32 color)
|
||||||
|
{
|
||||||
|
for (int dy = 0; dy < height; dy++)
|
||||||
|
{
|
||||||
|
for (int dx = 0; dx < width; dx++)
|
||||||
|
{
|
||||||
|
int px = x + dx;
|
||||||
|
int py = y + dy;
|
||||||
|
|
||||||
|
if (px < 0 || px >= image.Width || py < 0 || py >= image.Height)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Check if pixel is inside rounded rectangle
|
||||||
|
bool inCorner = false;
|
||||||
|
int distX = 0, distY = 0;
|
||||||
|
|
||||||
|
// Top-left corner
|
||||||
|
if (dx < radius && dy < radius)
|
||||||
|
{
|
||||||
|
distX = radius - dx;
|
||||||
|
distY = radius - dy;
|
||||||
|
inCorner = true;
|
||||||
|
}
|
||||||
|
// Top-right corner
|
||||||
|
else if (dx >= width - radius && dy < radius)
|
||||||
|
{
|
||||||
|
distX = dx - (width - radius - 1);
|
||||||
|
distY = radius - dy;
|
||||||
|
inCorner = true;
|
||||||
|
}
|
||||||
|
// Bottom-left corner
|
||||||
|
else if (dx < radius && dy >= height - radius)
|
||||||
|
{
|
||||||
|
distX = radius - dx;
|
||||||
|
distY = dy - (height - radius - 1);
|
||||||
|
inCorner = true;
|
||||||
|
}
|
||||||
|
// Bottom-right corner
|
||||||
|
else if (dx >= width - radius && dy >= height - radius)
|
||||||
|
{
|
||||||
|
distX = dx - (width - radius - 1);
|
||||||
|
distY = dy - (height - radius - 1);
|
||||||
|
inCorner = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (inCorner)
|
||||||
|
{
|
||||||
|
// Check if point is inside the rounded corner
|
||||||
|
double distance = Math.Sqrt(distX * distX + distY * distY);
|
||||||
|
if (distance <= radius)
|
||||||
|
{
|
||||||
|
image[px, py] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Inside the main rectangle (not in corners)
|
||||||
|
image[px, py] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrawCircle(Image<Rgba32> image, int centerX, int centerY, int radius, Rgba32 color)
|
||||||
|
{
|
||||||
|
int x0 = centerX - radius;
|
||||||
|
int y0 = centerY - radius;
|
||||||
|
int diameter = radius * 2;
|
||||||
|
|
||||||
|
for (int dy = 0; dy <= diameter; dy++)
|
||||||
|
{
|
||||||
|
for (int dx = 0; dx <= diameter; dx++)
|
||||||
|
{
|
||||||
|
int px = x0 + dx;
|
||||||
|
int py = y0 + dy;
|
||||||
|
|
||||||
|
if (px < 0 || px >= image.Width || py < 0 || py >= image.Height)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Calculate distance from center
|
||||||
|
double distance = Math.Sqrt(Math.Pow(dx - radius, 2) + Math.Pow(dy - radius, 2));
|
||||||
|
|
||||||
|
if (distance <= radius)
|
||||||
|
{
|
||||||
|
image[px, py] = color;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// QRModule class removed - was only used for corner styling which is temporarily simplified
|
// QRModule class removed - was only used for corner styling which is temporarily simplified
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -231,7 +231,9 @@ namespace QRRapidoApp.Services
|
|||||||
GenerationTimeMs = qrResult.GenerationTimeMs,
|
GenerationTimeMs = qrResult.GenerationTimeMs,
|
||||||
FromCache = qrResult.FromCache,
|
FromCache = qrResult.FromCache,
|
||||||
IsActive = true,
|
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);
|
await _context.QRCodeHistory.InsertOneAsync(qrHistory);
|
||||||
@ -448,5 +450,52 @@ namespace QRRapidoApp.Services
|
|||||||
var update = Builders<User>.Update.Set(u => u.StripeCustomerId, stripeCustomerId);
|
var update = Builders<User>.Update.Set(u => u.StripeCustomerId, stripeCustomerId);
|
||||||
await _context.Users.UpdateOneAsync(u => u.Id == userId, update);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -682,7 +682,7 @@
|
|||||||
<div class="col-md-6 mb-3">
|
<div class="col-md-6 mb-3">
|
||||||
<div class="premium-upgrade-box p-3 border rounded bg-light">
|
<div class="premium-upgrade-box p-3 border rounded bg-light">
|
||||||
<h6 class="fw-bold mb-2">
|
<h6 class="fw-bold mb-2">
|
||||||
<i class="fas fa-crown text-warning"></i>
|
<i class="fas fa-crown text-warning"></i>
|
||||||
Logo Personalizado - Premium
|
Logo Personalizado - Premium
|
||||||
</h6>
|
</h6>
|
||||||
<p class="mb-2 small">Adicione sua marca aos QR Codes! Agora com tamanho configurável (10-25%) e colorização automática.</p>
|
<p class="mb-2 small">Adicione sua marca aos QR Codes! Agora com tamanho configurável (10-25%) e colorização automática.</p>
|
||||||
@ -692,6 +692,23 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div class="col-md-6 mb-3">
|
||||||
<label class="form-label">@Localizer["BorderStyle"]</label>
|
<label class="form-label">@Localizer["BorderStyle"]</label>
|
||||||
@ -701,13 +718,11 @@
|
|||||||
{
|
{
|
||||||
<option value="rounded">@Localizer["Rounded"] 👑</option>
|
<option value="rounded">@Localizer["Rounded"] 👑</option>
|
||||||
<option value="circle">Círculos 👑</option>
|
<option value="circle">Círculos 👑</option>
|
||||||
<option value="leaf">Folha 👑</option>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<option value="rounded" disabled>@Localizer["Rounded"] - Premium 👑</option>
|
<option value="rounded" disabled>@Localizer["Rounded"] - Premium 👑</option>
|
||||||
<option value="circle" disabled>Círculos - Premium 👑</option>
|
<option value="circle" disabled>Círculos - Premium 👑</option>
|
||||||
<option value="leaf" disabled>Folha - Premium 👑</option>
|
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
@if (!isPremium)
|
@if (!isPremium)
|
||||||
@ -1195,6 +1210,7 @@
|
|||||||
<li><i class="fas fa-check text-success"></i> @Localizer["AdvancedCustomization"]</li>
|
<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["LogoSupport"]</li>
|
||||||
<li><i class="fas fa-check text-success"></i> @Localizer["HistoryAndDownloads"]</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>
|
<li><i class="fas fa-check text-success"></i> @Localizer["PrioritySupport"]</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="text-center">
|
<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["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["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-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>
|
<li class="list-group-item border-0"><i class="fas fa-check-circle text-success me-2"></i>@Localizer["PrioritySupport"]</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
8
package-lock.json
generated
8
package-lock.json
generated
@ -8,7 +8,7 @@
|
|||||||
"name": "qrrapido-app",
|
"name": "qrrapido-app",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.21"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@esbuild/aix-ppc64": {
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
@ -879,9 +879,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.20",
|
"version": "5.4.21",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.20.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz",
|
||||||
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
|
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
@ -8,6 +8,6 @@
|
|||||||
"preview": "vite preview --config vite.config.js"
|
"preview": "vite preview --config vite.config.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.21"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,14 @@
|
|||||||
// QR Rapido Speed Generator
|
// QR Rapido Speed Generator
|
||||||
class QRRapidoGenerator {
|
class QRRapidoGenerator {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.startTime = 0;
|
this.startTime = 0;
|
||||||
this.currentQR = null;
|
this.currentQR = null;
|
||||||
this.timerInterval = null;
|
this.timerInterval = null;
|
||||||
|
|
||||||
|
// Initialize premium status from hidden input
|
||||||
|
this.isPremium = this.checkUserPremium();
|
||||||
|
console.log('[INIT] QRRapidoGenerator - isPremium:', this.isPremium);
|
||||||
|
|
||||||
this.languageStrings = {
|
this.languageStrings = {
|
||||||
'pt-BR': {
|
'pt-BR': {
|
||||||
tagline: 'Gere QR codes em segundos!',
|
tagline: 'Gere QR codes em segundos!',
|
||||||
@ -737,6 +742,9 @@ class QRRapidoGenerator {
|
|||||||
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
|
const finalBackgroundColor = userBackgroundColor || (styleSettings.backgroundColor || '#FFFFFF');
|
||||||
|
|
||||||
// Common data for both endpoints
|
// Common data for both endpoints
|
||||||
|
const trackingEnabled = this.getTrackingEnabled();
|
||||||
|
console.log('[DEBUG] getTrackingEnabled() returned:', trackingEnabled, 'type:', typeof trackingEnabled);
|
||||||
|
|
||||||
const commonData = {
|
const commonData = {
|
||||||
type: actualType,
|
type: actualType,
|
||||||
content: encodedContent,
|
content: encodedContent,
|
||||||
@ -747,9 +755,12 @@ class QRRapidoGenerator {
|
|||||||
margin: parseInt(document.getElementById('qr-margin').value),
|
margin: parseInt(document.getElementById('qr-margin').value),
|
||||||
cornerStyle: document.getElementById('corner-style')?.value || 'square',
|
cornerStyle: document.getElementById('corner-style')?.value || 'square',
|
||||||
optimizeForSpeed: true,
|
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
|
// Add dynamic QR data if it's a URL type
|
||||||
if (type === 'url') {
|
if (type === 'url') {
|
||||||
let dynamicData;
|
let dynamicData;
|
||||||
@ -772,11 +783,15 @@ class QRRapidoGenerator {
|
|||||||
// Use FormData for requests with logo
|
// Use FormData for requests with logo
|
||||||
const formData = new FormData();
|
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 => {
|
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
|
// NOVOS PARÂMETROS DE LOGO APRIMORADO
|
||||||
const logoSettings = this.getLogoSettings();
|
const logoSettings = this.getLogoSettings();
|
||||||
formData.append('LogoSizePercent', logoSettings.logoSizePercent.toString());
|
formData.append('LogoSizePercent', logoSettings.logoSizePercent.toString());
|
||||||
@ -808,13 +823,38 @@ class QRRapidoGenerator {
|
|||||||
getLogoSettings() {
|
getLogoSettings() {
|
||||||
const logoSizeSlider = document.getElementById('logo-size-slider');
|
const logoSizeSlider = document.getElementById('logo-size-slider');
|
||||||
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
|
const logoColorizeToggle = document.getElementById('logo-colorize-toggle');
|
||||||
|
|
||||||
return {
|
return {
|
||||||
logoSizePercent: parseInt(logoSizeSlider?.value || '20'),
|
logoSizePercent: parseInt(logoSizeSlider?.value || '20'),
|
||||||
applyColorization: logoColorizeToggle?.checked || false
|
applyColorization: logoColorizeToggle?.checked || false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Função auxiliar para obter conteúdo baseado no tipo
|
||||||
getContentForType(type) {
|
getContentForType(type) {
|
||||||
if (type === 'url') {
|
if (type === 'url') {
|
||||||
@ -1142,8 +1182,8 @@ class QRRapidoGenerator {
|
|||||||
|
|
||||||
handleCornerStyleChange(e) {
|
handleCornerStyleChange(e) {
|
||||||
const selectedStyle = e.target.value;
|
const selectedStyle = e.target.value;
|
||||||
const premiumStyles = ['rounded', 'circle', 'leaf'];
|
const premiumStyles = ['rounded', 'circle'];
|
||||||
|
|
||||||
if (premiumStyles.includes(selectedStyle)) {
|
if (premiumStyles.includes(selectedStyle)) {
|
||||||
// Check if user is premium (we can detect this by checking if the option is disabled)
|
// Check if user is premium (we can detect this by checking if the option is disabled)
|
||||||
const option = e.target.options[e.target.selectedIndex];
|
const option = e.target.options[e.target.selectedIndex];
|
||||||
@ -1154,6 +1194,12 @@ class QRRapidoGenerator {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If a QR code already exists, regenerate with new corner style
|
||||||
|
if (this.currentQR) {
|
||||||
|
console.log('[CORNER STYLE] Corner style changed to:', selectedStyle, '- regenerating QR code');
|
||||||
|
this.generateQRWithTimer(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
applyQuickStyle(e) {
|
applyQuickStyle(e) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user