using System.Security.Cryptography; using System.Text.Json; using Senparc.Weixin.WxOpen.AdvancedAPIs.WxApp; using Senparc.Weixin.WxOpen.Containers; using Tiku.Application.Growth; using Tiku.Application.Storage; using Tiku.Application.Tenancy; using Tiku.Domain.Tenancy; namespace Tiku.Infrastructure.Growth; public sealed class ReferralQrcodeGenerator( IObjectStorageService objectStorageService, ITenantExternalProviderConfigService providerConfigService) : IReferralQrcodeGenerator { private static readonly string[] WechatMiniappProviderAliases = [ "wechat-miniapp", "wechat_miniapp", "wechat-mini", "wechatMiniapp" ]; public async Task GenerateAsync( ReferralQrcodeGenerateRequest request, CancellationToken cancellationToken = default) { var provider = NormalizeProvider(request.Provider); return provider == "wechat-miniapp" ? await GenerateWechatMiniappAsync(request with { Provider = provider }, cancellationToken) : throw new ReferralException("Referral qrcode provider is not supported.", "referral_qrcode_provider_not_supported"); } private async Task GenerateWechatMiniappAsync( ReferralQrcodeGenerateRequest request, CancellationToken cancellationToken) { var options = await LoadWechatMiniappOptionsAsync(request.TenantId, cancellationToken); var imageBytes = await GenerateWechatMiniappImageAsync(options, request, cancellationToken); var checksum = Convert.ToHexString(SHA256.HashData(imageBytes)).ToLowerInvariant(); var objectKey = BuildObjectKey(request.TenantId, request.UserId, request.RefCode, request.Page, request.Scene); await using var content = new MemoryStream(imageBytes, false); var storage = await objectStorageService.WriteObjectAsync( new ObjectStorageWriteRequest( request.TenantId, objectStorageService.ConfiguredDefaultProvider(), objectStorageService.ConfiguredDefaultBucket(), objectKey, "image/png", content, imageBytes.LongLength, checksum, new Dictionary { ["kind"] = "referral-qrcode", ["provider"] = request.Provider, ["ref-code"] = request.RefCode }), cancellationToken); if (storage.PublicUrl is null) throw new ReferralException( "Referral qrcode storage public base URL is not configured.", "referral_qrcode_public_url_not_configured"); return new ReferralQrcodeGenerateResult( storage.PublicUrl.ToString(), request.Provider, JsonSerializer.SerializeToElement(new { generatedBy = "senparc-weixin-wxopen", storageProvider = storage.Provider, storageBucket = storage.Bucket, storageObjectKey = storage.ObjectKey, checksumSha256 = storage.ChecksumSha256, sizeBytes = storage.SizeBytes })); } private async Task LoadWechatMiniappOptionsAsync( Guid tenantId, CancellationToken cancellationToken) { TenantExternalProviderAccount? provider = null; foreach (var alias in WechatMiniappProviderAliases) try { provider = await providerConfigService.GetActiveProviderAsync( tenantId, TenantExternalProviderCapability.Identity, alias, cancellationToken); break; } catch (TenantExternalProviderException) { } if (provider is null) throw new ReferralException( "Wechat miniapp auth provider is not configured.", "referral_qrcode_provider_not_configured"); var appId = GetJsonString(provider.ConfigPublic, "appId", "clientId"); var appSecret = GetJsonString(provider.SecretPayload, "appSecret", "clientSecret", "secret"); if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(appSecret)) throw new ReferralException( "Wechat miniapp auth provider configuration is incomplete.", "referral_qrcode_provider_not_configured"); return new WechatMiniappOptions(appId, appSecret); } private static async Task GenerateWechatMiniappImageAsync( WechatMiniappOptions options, ReferralQrcodeGenerateRequest request, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); try { var accessToken = await AccessTokenContainer.TryGetAccessTokenAsync( options.AppId, options.AppSecret); await using var imageStream = new MemoryStream(); var result = await WxAppApi.GetWxaCodeUnlimitAsync( accessToken, imageStream, request.Scene, request.Page, true, "release", 430, false, null!, false, 20); cancellationToken.ThrowIfCancellationRequested(); if (result.errcode != 0) throw new ReferralException( $"Wechat miniapp qrcode generation failed: {result.errcode}.", "referral_qrcode_wechat_failed"); var imageBytes = imageStream.ToArray(); if (imageBytes.Length == 0) throw new ReferralException( "Wechat miniapp qrcode response was empty.", "referral_qrcode_wechat_empty"); return imageBytes; } catch (ReferralException) { throw; } catch (Exception exception) { throw new ReferralException( exception is HttpRequestException ? "Wechat miniapp qrcode generation failed with network error." : "Wechat miniapp qrcode generation failed.", "referral_qrcode_wechat_failed"); } } private static string BuildObjectKey( Guid tenantId, Guid userId, string refCode, string page, string scene) { var fingerprint = Convert.ToHexString(SHA256.HashData(JsonSerializer.SerializeToUtf8Bytes(new { page, scene }))).ToLowerInvariant()[..16]; return $"{tenantId:N}/referral/qrcodes/{userId:N}/{NormalizePathToken(refCode)}-{fingerprint}.png"; } private static string NormalizeProvider(string? provider) { var normalized = string.IsNullOrWhiteSpace(provider) ? "wechat-miniapp" : provider.Trim(); return WechatMiniappProviderAliases.Contains(normalized, StringComparer.OrdinalIgnoreCase) ? "wechat-miniapp" : normalized; } private static string NormalizePathToken(string value) { var chars = value .Trim() .Select(character => char.IsAsciiLetterOrDigit(character) ? character : '-') .ToArray(); var normalized = new string(chars).Trim('-'); return string.IsNullOrWhiteSpace(normalized) ? "referral" : normalized.ToLowerInvariant(); } private static string? GetJsonString(JsonElement element, params string[] names) { if (element.ValueKind != JsonValueKind.Object) return null; foreach (var name in names) if (element.TryGetProperty(name, out var property) && property.ValueKind == JsonValueKind.String && !string.IsNullOrWhiteSpace(property.GetString())) return property.GetString()!.Trim(); return null; } private sealed record WechatMiniappOptions(string AppId, string AppSecret); }