forked from xiongyuxing/tiku-backend.net
230 lines
8.4 KiB
C#
230 lines
8.4 KiB
C#
using System.Security.Cryptography;
|
|
using System.Text.Json;
|
|
using Microsoft.EntityFrameworkCore;
|
|
using Senparc.Weixin.WxOpen.AdvancedAPIs.WxApp;
|
|
using Senparc.Weixin.WxOpen.Containers;
|
|
using Tiku.Application.Growth;
|
|
using Tiku.Application.Storage;
|
|
using Tiku.Domain.Tenancy;
|
|
using Tiku.Infrastructure.Persistence;
|
|
|
|
namespace Tiku.Infrastructure.Growth;
|
|
|
|
public sealed class ReferralQrcodeGenerator(
|
|
TikuDbContext dbContext,
|
|
IObjectStorageService objectStorageService) : IReferralQrcodeGenerator
|
|
{
|
|
private static readonly string[] WechatMiniappProviderAliases =
|
|
[
|
|
"wechat-miniapp",
|
|
"wechat_miniapp",
|
|
"wechat-mini",
|
|
"wechatMiniapp"
|
|
];
|
|
|
|
public async Task<ReferralQrcodeGenerateResult> 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<ReferralQrcodeGenerateResult> 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, writable: false);
|
|
var storage = await objectStorageService.WriteObjectAsync(
|
|
new ObjectStorageWriteRequest(
|
|
request.TenantId,
|
|
objectStorageService.ConfiguredDefaultProvider(),
|
|
objectStorageService.ConfiguredDefaultBucket(),
|
|
objectKey,
|
|
"image/png",
|
|
content,
|
|
imageBytes.LongLength,
|
|
checksum,
|
|
new Dictionary<string, string>
|
|
{
|
|
["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<WechatMiniappOptions> LoadWechatMiniappOptionsAsync(
|
|
Guid tenantId,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var providers = await dbContext.TenantAuthProviders
|
|
.AsNoTracking()
|
|
.Where(entity =>
|
|
entity.TenantId == tenantId &&
|
|
WechatMiniappProviderAliases.Contains(entity.Provider) &&
|
|
(entity.Status == TenantAuthProviderStatus.Active ||
|
|
entity.Status == TenantAuthProviderStatus.Testing))
|
|
.ToListAsync(cancellationToken);
|
|
var provider = WechatMiniappProviderAliases
|
|
.Select(alias => providers.FirstOrDefault(entity => entity.Provider == alias))
|
|
.FirstOrDefault(entity => entity is not null);
|
|
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.ConfigPublic, "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<byte[]> GenerateWechatMiniappImageAsync(
|
|
WechatMiniappOptions options,
|
|
ReferralQrcodeGenerateRequest request,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
try
|
|
{
|
|
var accessToken = await AccessTokenContainer.TryGetAccessTokenAsync(
|
|
options.AppId,
|
|
options.AppSecret,
|
|
false);
|
|
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);
|
|
}
|