This commit is contained in:
zpc 2026-02-07 18:19:22 +08:00
parent 1a799caadc
commit 2a52aacc0c
14 changed files with 780 additions and 9 deletions

View File

@ -0,0 +1,35 @@
-- 创建用户海报缓存表
-- 用于存储用户生成的推广海报,避免重复生成
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[user_poster_cache]') AND type in (N'U'))
BEGIN
CREATE TABLE [dbo].[user_poster_cache] (
[id] INT IDENTITY(1,1) NOT NULL,
[user_id] INT NOT NULL,
[app_id] NVARCHAR(100) NULL,
[template_hash] NVARCHAR(64) NOT NULL,
[cos_url] NVARCHAR(500) NOT NULL,
[file_size] BIGINT NOT NULL DEFAULT 0,
[mime_type] NVARCHAR(50) NOT NULL DEFAULT 'image/png',
[status] INT NOT NULL DEFAULT 1,
[expires_at] DATETIME2 NOT NULL,
[platform] NVARCHAR(50) NOT NULL DEFAULT 'MP-WEIXIN',
[created_at] DATETIME2 NOT NULL DEFAULT GETDATE(),
[updated_at] DATETIME2 NOT NULL DEFAULT GETDATE(),
CONSTRAINT [pk_user_poster_cache] PRIMARY KEY CLUSTERED ([id] ASC)
);
-- 创建索引
CREATE INDEX [ix_user_poster_cache_user_id] ON [dbo].[user_poster_cache] ([user_id]);
CREATE INDEX [ix_user_poster_cache_template_hash] ON [dbo].[user_poster_cache] ([template_hash]);
CREATE INDEX [ix_user_poster_cache_platform] ON [dbo].[user_poster_cache] ([platform]);
CREATE INDEX [ix_user_poster_cache_status] ON [dbo].[user_poster_cache] ([status]);
CREATE INDEX [ix_user_poster_cache_expires_at] ON [dbo].[user_poster_cache] ([expires_at]);
PRINT '表 user_poster_cache 创建成功';
END
ELSE
BEGIN
PRINT '表 user_poster_cache 已存在';
END
GO

View File

@ -427,6 +427,36 @@ public class BaseSetting
[JsonPropertyName("share_image")]
public string? ShareImage { get; set; }
/// <summary>
/// 海报模板图片URL
/// </summary>
[JsonPropertyName("poster_template")]
public string? PosterTemplate { get; set; }
/// <summary>
/// 海报二维码X坐标
/// </summary>
[JsonPropertyName("poster_qr_x")]
public string? PosterQrX { get; set; }
/// <summary>
/// 海报二维码Y坐标
/// </summary>
[JsonPropertyName("poster_qr_y")]
public string? PosterQrY { get; set; }
/// <summary>
/// 海报二维码大小
/// </summary>
[JsonPropertyName("poster_qr_size")]
public string? PosterQrSize { get; set; }
/// <summary>
/// 站点URL
/// </summary>
[JsonPropertyName("site_url")]
public string? SiteUrl { get; set; }
/// <summary>
/// 抽奖券拉人上限
/// </summary>

View File

@ -73,6 +73,16 @@ export interface BaseSetting {
share_title?: string
/** 分享图片 */
share_image?: string
/** 海报模板图片URL */
poster_template?: string
/** 海报二维码X坐标 */
poster_qr_x?: string
/** 海报二维码Y坐标 */
poster_qr_y?: string
/** 海报二维码大小 */
poster_qr_size?: string
/** 站点URL */
site_url?: string
/** 抽奖券拉人上限 */
draw_people_num?: string
/** 首页是否弹窗 0关闭 1开启 */

View File

@ -237,6 +237,70 @@
</el-col>
</el-row>
<!-- 海报配置 -->
<el-divider content-position="left">海报配置</el-divider>
<el-row :gutter="24">
<el-col :span="12">
<el-form-item label="海报模板图片" prop="poster_template">
<ImageUpload
v-model="formData.poster_template"
placeholder="点击上传海报模板"
:show-url-input="true"
tip="推荐尺寸 750x1334二维码将合成到模板上"
/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item label="站点URL" prop="site_url">
<el-input
v-model="formData.site_url"
placeholder="请输入站点URL如 https://example.com"
/>
<div class="form-tip">用于生成海报二维码的推广链接</div>
</el-form-item>
</el-col>
</el-row>
<el-row :gutter="24">
<el-col :span="8">
<el-form-item label="二维码X坐标" prop="poster_qr_x">
<el-input-number
v-model.number="formData.poster_qr_x"
:min="0"
:max="2000"
placeholder="二维码左上角X坐标"
style="width: 100%"
/>
<div class="form-tip">二维码在海报上的X坐标像素</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="二维码Y坐标" prop="poster_qr_y">
<el-input-number
v-model.number="formData.poster_qr_y"
:min="0"
:max="3000"
placeholder="二维码左上角Y坐标"
style="width: 100%"
/>
<div class="form-tip">二维码在海报上的Y坐标像素</div>
</el-form-item>
</el-col>
<el-col :span="8">
<el-form-item label="二维码大小" prop="poster_qr_size">
<el-input-number
v-model.number="formData.poster_qr_size"
:min="50"
:max="500"
placeholder="二维码大小"
style="width: 100%"
/>
<div class="form-tip">二维码宽高像素</div>
</el-form-item>
</el-col>
</el-row>
<!-- 开关配置 -->
<el-divider content-position="left">开关配置</el-divider>
@ -303,6 +367,11 @@ interface FormDataType {
erweima: string
share_title: string
share_image: string
poster_template: string
poster_qr_x: number
poster_qr_y: number
poster_qr_size: number
site_url: string
draw_people_num: number
is_shou_tan: number
is_exchange: number
@ -325,6 +394,11 @@ const formData = reactive<FormDataType>({
erweima: '',
share_title: '',
share_image: '',
poster_template: '',
poster_qr_x: 104,
poster_qr_y: 1180,
poster_qr_size: 200,
site_url: '',
draw_people_num: 10,
is_shou_tan: 0,
is_exchange: 1
@ -372,6 +446,11 @@ const loadData = async () => {
erweima: data.erweima || '',
share_title: data.share_title || '',
share_image: data.share_image || '',
poster_template: data.poster_template || '',
poster_qr_x: Number(data.poster_qr_x) || 104,
poster_qr_y: Number(data.poster_qr_y) || 1180,
poster_qr_size: Number(data.poster_qr_size) || 200,
site_url: data.site_url || '',
draw_people_num: Number(data.draw_people_num) || 10,
is_shou_tan: Number(data.is_shou_tan) || 0,
is_exchange: Number(data.is_exchange) || 1
@ -416,6 +495,11 @@ const handleSave = async () => {
erweima: formData.erweima,
share_title: formData.share_title,
share_image: formData.share_image,
poster_template: formData.poster_template,
poster_qr_x: String(formData.poster_qr_x),
poster_qr_y: String(formData.poster_qr_y),
poster_qr_size: String(formData.poster_qr_size),
site_url: formData.site_url,
draw_people_num: String(formData.draw_people_num),
is_shou_tan: String(formData.is_shou_tan),
is_exchange: String(formData.is_exchange)

View File

@ -30,12 +30,12 @@ public class InvitationController : ControllerBase
/// <summary>
/// 获取推荐信息
/// POST /api/invitation
/// GET /api/invitation
/// Requirements: 9.1-9.2
/// </summary>
[HttpGet("invitation")]
[Authorize]
public async Task<ApiResponse<InvitationInfoResponse>> GetInvitationInfo([FromBody] InvitationInfoRequest? request)
public async Task<ApiResponse<InvitationInfoResponse>> GetInvitationInfo([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
var userId = GetCurrentUserId();
if (userId == null)
@ -45,10 +45,16 @@ public class InvitationController : ControllerBase
try
{
var page = request?.Page ?? 1;
if (page < 1) page = 1;
var result = await _invitationService.GetInvitationInfoAsync(userId.Value, page);
// 从请求头获取平台类型
var platform = Request.Headers["client"].FirstOrDefault();
if (string.IsNullOrEmpty(platform))
{
platform = Request.Headers["platform"].FirstOrDefault() ?? "H5";
}
var result = await _invitationService.GetInvitationInfoAsync(userId.Value, page, platform);
return ApiResponse<InvitationInfoResponse>.Success(result);
}
catch (Exception ex)

View File

@ -12,8 +12,9 @@ public interface IInvitationService
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="page">页码</param>
/// <param name="platform">平台类型</param>
/// <returns>推荐信息响应</returns>
Task<InvitationInfoResponse> GetInvitationInfoAsync(int userId, int page);
Task<InvitationInfoResponse> GetInvitationInfoAsync(int userId, int page, string? platform = null);
/// <summary>
/// 绑定邀请码

View File

@ -0,0 +1,36 @@
namespace HoneyBox.Core.Interfaces;
/// <summary>
/// 海报生成服务接口
/// </summary>
public interface IPosterService
{
/// <summary>
/// 获取或生成用户推广海报
/// </summary>
/// <param name="userId">用户ID</param>
/// <param name="platform">平台类型MP-WEIXIN/H5/APP等</param>
/// <returns>海报生成结果</returns>
Task<PosterResult> GetUserPosterAsync(int userId, string? platform = null);
}
/// <summary>
/// 海报生成结果
/// </summary>
public class PosterResult
{
/// <summary>
/// 是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 消息
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 海报图片URL
/// </summary>
public string? ImageUrl { get; set; }
}

View File

@ -13,17 +13,22 @@ namespace HoneyBox.Core.Services;
public class InvitationService : IInvitationService
{
private readonly HoneyBoxDbContext _dbContext;
private readonly IPosterService _posterService;
private readonly ILogger<InvitationService> _logger;
private const int PageSize = 15;
public InvitationService(HoneyBoxDbContext dbContext, ILogger<InvitationService> logger)
public InvitationService(
HoneyBoxDbContext dbContext,
IPosterService posterService,
ILogger<InvitationService> logger)
{
_dbContext = dbContext;
_posterService = posterService;
_logger = logger;
}
/// <inheritdoc />
public async Task<InvitationInfoResponse> GetInvitationInfoAsync(int userId, int page)
public async Task<InvitationInfoResponse> GetInvitationInfoAsync(int userId, int page, string? platform = null)
{
// 获取被邀请用户列表pid = userId 且 status = 1
var query = _dbContext.Users
@ -72,7 +77,20 @@ public class InvitationService : IInvitationService
// 获取分享配置
var shareTitle = await GetShareTitleAsync();
var shareImage = await GetShareImageAsync();
// 生成用户专属海报
var shareImage = string.Empty;
var posterResult = await _posterService.GetUserPosterAsync(userId, platform);
if (posterResult.Success && !string.IsNullOrEmpty(posterResult.ImageUrl))
{
shareImage = posterResult.ImageUrl;
}
else
{
// 如果海报生成失败,使用静态分享图片作为备选
shareImage = await GetShareImageAsync();
_logger.LogWarning("用户 {UserId} 海报生成失败: {Message},使用静态分享图片", userId, posterResult.Message);
}
return new InvitationInfoResponse
{

View File

@ -0,0 +1,454 @@
using System.Security.Cryptography;
using System.Text.Json;
using HoneyBox.Core.Interfaces;
using HoneyBox.Model.Data;
using HoneyBox.Model.Entities;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using QRCoder;
using SkiaSharp;
namespace HoneyBox.Infrastructure.External.Poster;
/// <summary>
/// 海报生成服务实现
/// </summary>
public class PosterService : IPosterService
{
private readonly HoneyBoxDbContext _dbContext;
private readonly IImageUploadService _imageUploadService;
private readonly ILogger<PosterService> _logger;
private readonly HttpClient _httpClient;
// 默认缓存100天
private const int DefaultCacheExpireDays = 100;
// 二维码在海报上的位置(可通过配置调整)
private const int DefaultQrCodeX = 104;
private const int DefaultQrCodeY = 1180;
private const int DefaultQrCodeSize = 200;
public PosterService(
HoneyBoxDbContext dbContext,
IImageUploadService imageUploadService,
IHttpClientFactory httpClientFactory,
ILogger<PosterService> logger)
{
_dbContext = dbContext;
_imageUploadService = imageUploadService;
_httpClient = httpClientFactory.CreateClient();
_logger = logger;
}
/// <inheritdoc />
public async Task<PosterResult> GetUserPosterAsync(int userId, string? platform = null)
{
try
{
// 默认平台
platform ??= "H5";
// 1. 验证用户ID
if (userId <= 0)
{
return new PosterResult { Success = false, Message = "无效的用户ID" };
}
// 2. 获取海报配置
var posterConfig = await GetPosterConfigAsync();
if (posterConfig == null || string.IsNullOrEmpty(posterConfig.TemplateUrl))
{
return new PosterResult { Success = false, Message = "海报模板未配置" };
}
// 3. 计算模板哈希(用于缓存失效判断)
var templateHash = ComputeHash(posterConfig.TemplateUrl);
// 4. 获取小程序配置(用于生成小程序码)
var appId = await GetDefaultAppIdAsync();
// 5. 查询缓存记录
var cacheRecord = await FindValidCacheAsync(userId, templateHash, appId, platform);
if (cacheRecord != null)
{
_logger.LogInformation("用户 {UserId} 海报缓存命中", userId);
return new PosterResult
{
Success = true,
Message = "海报获取成功",
ImageUrl = cacheRecord.CosUrl
};
}
// 6. 生成推广链接
var qrContent = await GeneratePromotionUrlAsync(userId, platform);
if (string.IsNullOrEmpty(qrContent))
{
return new PosterResult { Success = false, Message = "生成推广链接失败" };
}
// 7. 下载海报模板
var templateBytes = await DownloadImageAsync(posterConfig.TemplateUrl);
if (templateBytes == null)
{
return new PosterResult { Success = false, Message = "下载海报模板失败" };
}
// 8. 生成带二维码的海报
var posterBytes = GeneratePosterWithQrCode(
templateBytes,
qrContent,
posterConfig.QrCodeX ?? DefaultQrCodeX,
posterConfig.QrCodeY ?? DefaultQrCodeY,
posterConfig.QrCodeSize ?? DefaultQrCodeSize);
if (posterBytes == null)
{
return new PosterResult { Success = false, Message = "海报生成失败" };
}
// 9. 上传到COS
using var stream = new MemoryStream(posterBytes);
var cosUrl = await _imageUploadService.UploadStreamAsync(
stream,
$"poster_{userId}_{platform}.png",
"image/png",
"poster");
if (string.IsNullOrEmpty(cosUrl))
{
return new PosterResult { Success = false, Message = "海报上传失败" };
}
// 10. 失效旧缓存
await InvalidateOldCachesAsync(userId, templateHash, platform);
// 11. 保存新缓存记录
var newCache = new UserPosterCache
{
UserId = userId,
AppId = appId,
TemplateHash = templateHash,
CosUrl = cosUrl,
FileSize = posterBytes.Length,
MimeType = "image/png",
Status = 1,
ExpiresAt = DateTime.Now.AddDays(DefaultCacheExpireDays),
Platform = platform,
CreatedAt = DateTime.Now,
UpdatedAt = DateTime.Now
};
_dbContext.Set<UserPosterCache>().Add(newCache);
await _dbContext.SaveChangesAsync();
// 12. 随机清理过期缓存5%概率)
if (Random.Shared.Next(100) < 5)
{
_ = CleanExpiredCachesAsync();
}
_logger.LogInformation("用户 {UserId} 海报生成成功: {Url}", userId, cosUrl);
return new PosterResult
{
Success = true,
Message = "海报生成成功",
ImageUrl = cosUrl
};
}
catch (Exception ex)
{
_logger.LogError(ex, "获取用户海报异常: UserId={UserId}", userId);
return new PosterResult { Success = false, Message = "系统异常,请稍后重试" };
}
}
#region
/// <summary>
/// 获取海报配置
/// </summary>
private async Task<PosterConfig?> GetPosterConfigAsync()
{
var config = await _dbContext.Configs
.FirstOrDefaultAsync(c => c.ConfigKey == "base");
if (config?.ConfigValue == null)
{
return null;
}
try
{
var jsonDoc = JsonSerializer.Deserialize<JsonElement>(config.ConfigValue);
return new PosterConfig
{
TemplateUrl = GetJsonString(jsonDoc, "poster_template"),
QrCodeX = GetJsonInt(jsonDoc, "poster_qr_x"),
QrCodeY = GetJsonInt(jsonDoc, "poster_qr_y"),
QrCodeSize = GetJsonInt(jsonDoc, "poster_qr_size"),
SiteUrl = GetJsonString(jsonDoc, "site_url")
};
}
catch (Exception ex)
{
_logger.LogWarning(ex, "解析海报配置失败");
return null;
}
}
/// <summary>
/// 获取默认小程序AppId
/// </summary>
private async Task<string> GetDefaultAppIdAsync()
{
var config = await _dbContext.Configs
.FirstOrDefaultAsync(c => c.ConfigKey == "miniprogram_setting");
if (config?.ConfigValue == null)
{
return string.Empty;
}
try
{
var jsonDoc = JsonSerializer.Deserialize<JsonElement>(config.ConfigValue);
if (jsonDoc.TryGetProperty("miniprograms", out var miniprograms) &&
miniprograms.ValueKind == JsonValueKind.Array)
{
foreach (var mp in miniprograms.EnumerateArray())
{
if (mp.TryGetProperty("is_default", out var isDefault) &&
isDefault.GetInt32() == 1 &&
mp.TryGetProperty("appid", out var appid))
{
return appid.GetString() ?? string.Empty;
}
}
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "解析小程序配置失败");
}
return string.Empty;
}
/// <summary>
/// 生成推广链接
/// </summary>
private async Task<string> GeneratePromotionUrlAsync(int userId, string platform)
{
// 获取站点URL配置
var config = await _dbContext.Configs
.FirstOrDefaultAsync(c => c.ConfigKey == "base");
var siteUrl = "https://zfunbox.cn";
if (config?.ConfigValue != null)
{
try
{
var jsonDoc = JsonSerializer.Deserialize<JsonElement>(config.ConfigValue);
if (jsonDoc.TryGetProperty("site_url", out var siteUrlProp))
{
var url = siteUrlProp.GetString();
if (!string.IsNullOrEmpty(url))
{
siteUrl = url;
}
}
}
catch { }
}
// H5和APP使用URL链接
return $"{siteUrl}?pid={userId}";
}
/// <summary>
/// 下载图片
/// </summary>
private async Task<byte[]?> DownloadImageAsync(string url)
{
try
{
var response = await _httpClient.GetAsync(url);
if (response.IsSuccessStatusCode)
{
return await response.Content.ReadAsByteArrayAsync();
}
_logger.LogWarning("下载图片失败: {Url}, StatusCode: {StatusCode}", url, response.StatusCode);
return null;
}
catch (Exception ex)
{
_logger.LogError(ex, "下载图片异常: {Url}", url);
return null;
}
}
/// <summary>
/// 生成带二维码的海报
/// </summary>
private byte[]? GeneratePosterWithQrCode(byte[] templateBytes, string qrContent, int qrX, int qrY, int qrSize)
{
try
{
// 加载模板图片
using var templateBitmap = SKBitmap.Decode(templateBytes);
if (templateBitmap == null)
{
_logger.LogWarning("无法解码海报模板图片");
return null;
}
// 生成二维码
using var qrGenerator = new QRCodeGenerator();
var qrCodeData = qrGenerator.CreateQrCode(qrContent, QRCodeGenerator.ECCLevel.M);
using var qrCode = new PngByteQRCode(qrCodeData);
var qrCodeBytes = qrCode.GetGraphic(20);
// 解码二维码图片
using var qrBitmap = SKBitmap.Decode(qrCodeBytes);
if (qrBitmap == null)
{
_logger.LogWarning("无法解码二维码图片");
return null;
}
// 缩放二维码到指定大小
using var scaledQrBitmap = qrBitmap.Resize(new SKImageInfo(qrSize, qrSize), SKFilterQuality.High);
// 创建画布
using var surface = SKSurface.Create(new SKImageInfo(templateBitmap.Width, templateBitmap.Height));
var canvas = surface.Canvas;
// 绘制模板
canvas.DrawBitmap(templateBitmap, 0, 0);
// 绘制二维码
canvas.DrawBitmap(scaledQrBitmap, qrX, qrY);
// 导出为PNG
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 90);
return data.ToArray();
}
catch (Exception ex)
{
_logger.LogError(ex, "生成海报失败");
return null;
}
}
/// <summary>
/// 查找有效缓存
/// </summary>
private async Task<UserPosterCache?> FindValidCacheAsync(int userId, string templateHash, string appId, string platform)
{
return await _dbContext.Set<UserPosterCache>()
.Where(c => c.UserId == userId)
.Where(c => c.TemplateHash == templateHash)
.Where(c => c.Platform == platform)
.Where(c => c.Status == 1)
.Where(c => c.ExpiresAt > DateTime.Now)
.OrderByDescending(c => c.Id)
.FirstOrDefaultAsync();
}
/// <summary>
/// 失效旧缓存
/// </summary>
private async Task InvalidateOldCachesAsync(int userId, string templateHash, string platform)
{
var oldCaches = await _dbContext.Set<UserPosterCache>()
.Where(c => c.UserId == userId)
.Where(c => c.Platform == platform)
.Where(c => c.TemplateHash != templateHash || c.Status == 1)
.ToListAsync();
foreach (var cache in oldCaches)
{
cache.Status = 0;
cache.UpdatedAt = DateTime.Now;
}
if (oldCaches.Count > 0)
{
await _dbContext.SaveChangesAsync();
}
}
/// <summary>
/// 清理过期缓存
/// </summary>
private async Task CleanExpiredCachesAsync()
{
try
{
var expiredCaches = await _dbContext.Set<UserPosterCache>()
.Where(c => c.ExpiresAt < DateTime.Now || c.Status == 0)
.Take(100)
.ToListAsync();
if (expiredCaches.Count > 0)
{
_dbContext.Set<UserPosterCache>().RemoveRange(expiredCaches);
await _dbContext.SaveChangesAsync();
_logger.LogInformation("清理了 {Count} 条过期海报缓存", expiredCaches.Count);
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "清理过期海报缓存失败");
}
}
/// <summary>
/// 计算字符串哈希
/// </summary>
private static string ComputeHash(string input)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(input);
var hash = MD5.HashData(bytes);
return Convert.ToHexString(hash).ToLowerInvariant();
}
private static string? GetJsonString(JsonElement element, string propertyName)
{
if (element.TryGetProperty(propertyName, out var prop) && prop.ValueKind == JsonValueKind.String)
{
return prop.GetString();
}
return null;
}
private static int? GetJsonInt(JsonElement element, string propertyName)
{
if (element.TryGetProperty(propertyName, out var prop))
{
if (prop.ValueKind == JsonValueKind.Number)
{
return prop.GetInt32();
}
if (prop.ValueKind == JsonValueKind.String && int.TryParse(prop.GetString(), out var val))
{
return val;
}
}
return null;
}
#endregion
/// <summary>
/// 海报配置
/// </summary>
private class PosterConfig
{
public string? TemplateUrl { get; set; }
public int? QrCodeX { get; set; }
public int? QrCodeY { get; set; }
public int? QrCodeSize { get; set; }
public string? SiteUrl { get; set; }
}
}

View File

@ -15,6 +15,8 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="SkiaSharp" Version="2.88.8" />
<PackageReference Include="StackExchange.Redis" Version="2.10.1" />
<PackageReference Include="Tencent.QCloud.Cos.Sdk" Version="5.4.44" />
</ItemGroup>

View File

@ -1,6 +1,7 @@
using Autofac;
using HoneyBox.Core.Interfaces;
using HoneyBox.Infrastructure.Cache;
using HoneyBox.Infrastructure.External.Poster;
using HoneyBox.Infrastructure.External.Storage;
namespace HoneyBox.Infrastructure.Modules;
@ -27,6 +28,11 @@ public class InfrastructureModule : Module
.As<IImageUploadService>()
.InstancePerLifetimeScope();
// 注册海报生成服务
builder.RegisterType<PosterService>()
.As<IPosterService>()
.InstancePerLifetimeScope();
// 后续可在此注册其他基础设施服务
// 如: 外部服务客户端、消息队列等
}

View File

@ -101,8 +101,9 @@ public class ServiceModule : Module
builder.Register(c =>
{
var dbContext = c.Resolve<HoneyBoxDbContext>();
var posterService = c.Resolve<IPosterService>();
var logger = c.Resolve<ILogger<InvitationService>>();
return new InvitationService(dbContext, logger);
return new InvitationService(dbContext, posterService, logger);
}).As<IInvitationService>().InstancePerLifetimeScope();
// 注册排行榜服务

View File

@ -126,6 +126,8 @@ public partial class HoneyBoxDbContext : DbContext
public virtual DbSet<PrizeAnnouncement> PrizeAnnouncements { get; set; }
public virtual DbSet<UserPosterCache> UserPosterCaches { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Connection string is configured in Program.cs via dependency injection

View File

@ -0,0 +1,86 @@
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace HoneyBox.Model.Entities;
/// <summary>
/// 用户海报缓存实体
/// </summary>
[Table("user_poster_cache")]
public class UserPosterCache
{
[Key]
[Column("id")]
public int Id { get; set; }
/// <summary>
/// 用户ID
/// </summary>
[Column("user_id")]
public int UserId { get; set; }
/// <summary>
/// 小程序AppId
/// </summary>
[Column("app_id")]
[StringLength(100)]
public string? AppId { get; set; }
/// <summary>
/// 模板文件哈希值
/// </summary>
[Column("template_hash")]
[StringLength(64)]
public string TemplateHash { get; set; } = string.Empty;
/// <summary>
/// COS存储URL
/// </summary>
[Column("cos_url")]
[StringLength(500)]
public string CosUrl { get; set; } = string.Empty;
/// <summary>
/// 文件大小(字节)
/// </summary>
[Column("file_size")]
public long FileSize { get; set; }
/// <summary>
/// MIME类型
/// </summary>
[Column("mime_type")]
[StringLength(50)]
public string MimeType { get; set; } = "image/png";
/// <summary>
/// 状态1有效 0无效
/// </summary>
[Column("status")]
public int Status { get; set; } = 1;
/// <summary>
/// 过期时间
/// </summary>
[Column("expires_at")]
public DateTime ExpiresAt { get; set; }
/// <summary>
/// 平台类型MP-WEIXIN/H5/APP等
/// </summary>
[Column("platform")]
[StringLength(50)]
public string Platform { get; set; } = "MP-WEIXIN";
/// <summary>
/// 创建时间
/// </summary>
[Column("created_at")]
public DateTime CreatedAt { get; set; } = DateTime.Now;
/// <summary>
/// 更新时间
/// </summary>
[Column("updated_at")]
public DateTime UpdatedAt { get; set; } = DateTime.Now;
}