diff --git a/server/HoneyBox/scripts/create_user_poster_cache.sql b/server/HoneyBox/scripts/create_user_poster_cache.sql
new file mode 100644
index 00000000..5f73de1e
--- /dev/null
+++ b/server/HoneyBox/scripts/create_user_poster_cache.sql
@@ -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
diff --git a/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Config/ConfigModels.cs b/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Config/ConfigModels.cs
index 52bcea77..078cb678 100644
--- a/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Config/ConfigModels.cs
+++ b/server/HoneyBox/src/HoneyBox.Admin.Business/Models/Config/ConfigModels.cs
@@ -427,6 +427,36 @@ public class BaseSetting
[JsonPropertyName("share_image")]
public string? ShareImage { get; set; }
+ ///
+ /// 海报模板图片URL
+ ///
+ [JsonPropertyName("poster_template")]
+ public string? PosterTemplate { get; set; }
+
+ ///
+ /// 海报二维码X坐标
+ ///
+ [JsonPropertyName("poster_qr_x")]
+ public string? PosterQrX { get; set; }
+
+ ///
+ /// 海报二维码Y坐标
+ ///
+ [JsonPropertyName("poster_qr_y")]
+ public string? PosterQrY { get; set; }
+
+ ///
+ /// 海报二维码大小
+ ///
+ [JsonPropertyName("poster_qr_size")]
+ public string? PosterQrSize { get; set; }
+
+ ///
+ /// 站点URL
+ ///
+ [JsonPropertyName("site_url")]
+ public string? SiteUrl { get; set; }
+
///
/// 抽奖券拉人上限
///
diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts
index 472321d8..02ed6ff0 100644
--- a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts
+++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/api/business/config.ts
@@ -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开启 */
diff --git a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/base.vue b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/base.vue
index ffafcc2c..4972de38 100644
--- a/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/base.vue
+++ b/server/HoneyBox/src/HoneyBox.Admin/admin-web/src/views/business/config/base.vue
@@ -237,6 +237,70 @@
+
+ 海报配置
+
+
+
+
+
+
+
+
+
+
+ 用于生成海报二维码的推广链接
+
+
+
+
+
+
+
+
+ 二维码在海报上的X坐标(像素)
+
+
+
+
+
+ 二维码在海报上的Y坐标(像素)
+
+
+
+
+
+ 二维码宽高(像素)
+
+
+
+
开关配置
@@ -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({
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)
diff --git a/server/HoneyBox/src/HoneyBox.Api/Controllers/InvitationController.cs b/server/HoneyBox/src/HoneyBox.Api/Controllers/InvitationController.cs
index bb2982c7..4ccf2586 100644
--- a/server/HoneyBox/src/HoneyBox.Api/Controllers/InvitationController.cs
+++ b/server/HoneyBox/src/HoneyBox.Api/Controllers/InvitationController.cs
@@ -30,12 +30,12 @@ public class InvitationController : ControllerBase
///
/// 获取推荐信息
- /// POST /api/invitation
+ /// GET /api/invitation
/// Requirements: 9.1-9.2
///
[HttpGet("invitation")]
[Authorize]
- public async Task> GetInvitationInfo([FromBody] InvitationInfoRequest? request)
+ public async Task> 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.Success(result);
}
catch (Exception ex)
diff --git a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IInvitationService.cs b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IInvitationService.cs
index 107da9ff..e9b35a9e 100644
--- a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IInvitationService.cs
+++ b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IInvitationService.cs
@@ -12,8 +12,9 @@ public interface IInvitationService
///
/// 用户ID
/// 页码
+ /// 平台类型
/// 推荐信息响应
- Task GetInvitationInfoAsync(int userId, int page);
+ Task GetInvitationInfoAsync(int userId, int page, string? platform = null);
///
/// 绑定邀请码
diff --git a/server/HoneyBox/src/HoneyBox.Core/Interfaces/IPosterService.cs b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IPosterService.cs
new file mode 100644
index 00000000..88980635
--- /dev/null
+++ b/server/HoneyBox/src/HoneyBox.Core/Interfaces/IPosterService.cs
@@ -0,0 +1,36 @@
+namespace HoneyBox.Core.Interfaces;
+
+///
+/// 海报生成服务接口
+///
+public interface IPosterService
+{
+ ///
+ /// 获取或生成用户推广海报
+ ///
+ /// 用户ID
+ /// 平台类型(MP-WEIXIN/H5/APP等)
+ /// 海报生成结果
+ Task GetUserPosterAsync(int userId, string? platform = null);
+}
+
+///
+/// 海报生成结果
+///
+public class PosterResult
+{
+ ///
+ /// 是否成功
+ ///
+ public bool Success { get; set; }
+
+ ///
+ /// 消息
+ ///
+ public string Message { get; set; } = string.Empty;
+
+ ///
+ /// 海报图片URL
+ ///
+ public string? ImageUrl { get; set; }
+}
diff --git a/server/HoneyBox/src/HoneyBox.Core/Services/InvitationService.cs b/server/HoneyBox/src/HoneyBox.Core/Services/InvitationService.cs
index 0874830d..a8675253 100644
--- a/server/HoneyBox/src/HoneyBox.Core/Services/InvitationService.cs
+++ b/server/HoneyBox/src/HoneyBox.Core/Services/InvitationService.cs
@@ -13,17 +13,22 @@ namespace HoneyBox.Core.Services;
public class InvitationService : IInvitationService
{
private readonly HoneyBoxDbContext _dbContext;
+ private readonly IPosterService _posterService;
private readonly ILogger _logger;
private const int PageSize = 15;
- public InvitationService(HoneyBoxDbContext dbContext, ILogger logger)
+ public InvitationService(
+ HoneyBoxDbContext dbContext,
+ IPosterService posterService,
+ ILogger logger)
{
_dbContext = dbContext;
+ _posterService = posterService;
_logger = logger;
}
///
- public async Task GetInvitationInfoAsync(int userId, int page)
+ public async Task 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
{
diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/External/Poster/PosterService.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/External/Poster/PosterService.cs
new file mode 100644
index 00000000..8ea9350f
--- /dev/null
+++ b/server/HoneyBox/src/HoneyBox.Infrastructure/External/Poster/PosterService.cs
@@ -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;
+
+///
+/// 海报生成服务实现
+///
+public class PosterService : IPosterService
+{
+ private readonly HoneyBoxDbContext _dbContext;
+ private readonly IImageUploadService _imageUploadService;
+ private readonly ILogger _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 logger)
+ {
+ _dbContext = dbContext;
+ _imageUploadService = imageUploadService;
+ _httpClient = httpClientFactory.CreateClient();
+ _logger = logger;
+ }
+
+ ///
+ public async Task 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().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 私有方法
+
+ ///
+ /// 获取海报配置
+ ///
+ private async Task GetPosterConfigAsync()
+ {
+ var config = await _dbContext.Configs
+ .FirstOrDefaultAsync(c => c.ConfigKey == "base");
+
+ if (config?.ConfigValue == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ var jsonDoc = JsonSerializer.Deserialize(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;
+ }
+ }
+
+ ///
+ /// 获取默认小程序AppId
+ ///
+ private async Task GetDefaultAppIdAsync()
+ {
+ var config = await _dbContext.Configs
+ .FirstOrDefaultAsync(c => c.ConfigKey == "miniprogram_setting");
+
+ if (config?.ConfigValue == null)
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ var jsonDoc = JsonSerializer.Deserialize(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;
+ }
+
+ ///
+ /// 生成推广链接
+ ///
+ private async Task 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(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}";
+ }
+
+ ///
+ /// 下载图片
+ ///
+ private async Task 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;
+ }
+ }
+
+ ///
+ /// 生成带二维码的海报
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// 查找有效缓存
+ ///
+ private async Task FindValidCacheAsync(int userId, string templateHash, string appId, string platform)
+ {
+ return await _dbContext.Set()
+ .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();
+ }
+
+ ///
+ /// 失效旧缓存
+ ///
+ private async Task InvalidateOldCachesAsync(int userId, string templateHash, string platform)
+ {
+ var oldCaches = await _dbContext.Set()
+ .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();
+ }
+ }
+
+ ///
+ /// 清理过期缓存
+ ///
+ private async Task CleanExpiredCachesAsync()
+ {
+ try
+ {
+ var expiredCaches = await _dbContext.Set()
+ .Where(c => c.ExpiresAt < DateTime.Now || c.Status == 0)
+ .Take(100)
+ .ToListAsync();
+
+ if (expiredCaches.Count > 0)
+ {
+ _dbContext.Set().RemoveRange(expiredCaches);
+ await _dbContext.SaveChangesAsync();
+ _logger.LogInformation("清理了 {Count} 条过期海报缓存", expiredCaches.Count);
+ }
+ }
+ catch (Exception ex)
+ {
+ _logger.LogWarning(ex, "清理过期海报缓存失败");
+ }
+ }
+
+ ///
+ /// 计算字符串哈希
+ ///
+ 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
+
+ ///
+ /// 海报配置
+ ///
+ 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; }
+ }
+}
diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj b/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj
index 8f756a29..bef544a6 100644
--- a/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj
+++ b/server/HoneyBox/src/HoneyBox.Infrastructure/HoneyBox.Infrastructure.csproj
@@ -15,6 +15,8 @@
+
+
diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs
index b08a8c77..b0d3a59c 100644
--- a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs
+++ b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/InfrastructureModule.cs
@@ -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()
.InstancePerLifetimeScope();
+ // 注册海报生成服务
+ builder.RegisterType()
+ .As()
+ .InstancePerLifetimeScope();
+
// 后续可在此注册其他基础设施服务
// 如: 外部服务客户端、消息队列等
}
diff --git a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs
index 22516bc0..83420546 100644
--- a/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs
+++ b/server/HoneyBox/src/HoneyBox.Infrastructure/Modules/ServiceModule.cs
@@ -101,8 +101,9 @@ public class ServiceModule : Module
builder.Register(c =>
{
var dbContext = c.Resolve();
+ var posterService = c.Resolve();
var logger = c.Resolve>();
- return new InvitationService(dbContext, logger);
+ return new InvitationService(dbContext, posterService, logger);
}).As().InstancePerLifetimeScope();
// 注册排行榜服务
diff --git a/server/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs b/server/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs
index b4d34008..244b4dd7 100644
--- a/server/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs
+++ b/server/HoneyBox/src/HoneyBox.Model/Data/HoneyBoxDbContext.cs
@@ -126,6 +126,8 @@ public partial class HoneyBoxDbContext : DbContext
public virtual DbSet PrizeAnnouncements { get; set; }
+ public virtual DbSet UserPosterCaches { get; set; }
+
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
// Connection string is configured in Program.cs via dependency injection
diff --git a/server/HoneyBox/src/HoneyBox.Model/Entities/UserPosterCache.cs b/server/HoneyBox/src/HoneyBox.Model/Entities/UserPosterCache.cs
new file mode 100644
index 00000000..2ea817d1
--- /dev/null
+++ b/server/HoneyBox/src/HoneyBox.Model/Entities/UserPosterCache.cs
@@ -0,0 +1,86 @@
+using System.ComponentModel.DataAnnotations;
+using System.ComponentModel.DataAnnotations.Schema;
+
+namespace HoneyBox.Model.Entities;
+
+///
+/// 用户海报缓存实体
+///
+[Table("user_poster_cache")]
+public class UserPosterCache
+{
+ [Key]
+ [Column("id")]
+ public int Id { get; set; }
+
+ ///
+ /// 用户ID
+ ///
+ [Column("user_id")]
+ public int UserId { get; set; }
+
+ ///
+ /// 小程序AppId
+ ///
+ [Column("app_id")]
+ [StringLength(100)]
+ public string? AppId { get; set; }
+
+ ///
+ /// 模板文件哈希值
+ ///
+ [Column("template_hash")]
+ [StringLength(64)]
+ public string TemplateHash { get; set; } = string.Empty;
+
+ ///
+ /// COS存储URL
+ ///
+ [Column("cos_url")]
+ [StringLength(500)]
+ public string CosUrl { get; set; } = string.Empty;
+
+ ///
+ /// 文件大小(字节)
+ ///
+ [Column("file_size")]
+ public long FileSize { get; set; }
+
+ ///
+ /// MIME类型
+ ///
+ [Column("mime_type")]
+ [StringLength(50)]
+ public string MimeType { get; set; } = "image/png";
+
+ ///
+ /// 状态:1有效 0无效
+ ///
+ [Column("status")]
+ public int Status { get; set; } = 1;
+
+ ///
+ /// 过期时间
+ ///
+ [Column("expires_at")]
+ public DateTime ExpiresAt { get; set; }
+
+ ///
+ /// 平台类型:MP-WEIXIN/H5/APP等
+ ///
+ [Column("platform")]
+ [StringLength(50)]
+ public string Platform { get; set; } = "MP-WEIXIN";
+
+ ///
+ /// 创建时间
+ ///
+ [Column("created_at")]
+ public DateTime CreatedAt { get; set; } = DateTime.Now;
+
+ ///
+ /// 更新时间
+ ///
+ [Column("updated_at")]
+ public DateTime UpdatedAt { get; set; } = DateTime.Now;
+}