From f7f505d109e93d7c0464511a746d66d07d1f9d46 Mon Sep 17 00:00:00 2001
From: glz <694770232@qq.com>
Date: Tue, 7 Jul 2026 18:01:17 +0800
Subject: [PATCH] =?UTF-8?q?refactor(api):=20=E6=8F=90=E5=8F=96=E5=85=AC?=
=?UTF-8?q?=E5=85=B1API=E9=BB=98=E8=AE=A4=E9=85=8D=E7=BD=AE=E5=B9=B6?=
=?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=A7=AF=E5=88=86=E5=B9=B6=E5=8F=91=E9=97=AE?=
=?UTF-8?q?=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
1. 新增InteractiveMagazineApiDefaultsExtensions统一注册共享服务,简化WeChatApi和WebApi的Program.cs代码
2. 修复积分增减的并发问题,改用原子更新操作,添加余额校验避免超扣
3. 修复注释乱码问题,将简体中文注释替换为正确文本
4. 新增.editorconfig配置统一代码格式
---
.editorconfig | 10 +++
...nteractiveMagazineApiDefaultsExtensions.cs | 85 +++++++++++++++++++
.../Dto/Journal/BindJournalDto.cs | 2 +-
.../PointsService.cs | 70 ++++++++-------
QYZH.InteractiveMagazine.WeChatApi/Program.cs | 54 +-----------
QYZH.InteractiveMagazine.WebApi/Program.cs | 72 +---------------
6 files changed, 139 insertions(+), 154 deletions(-)
create mode 100644 .editorconfig
create mode 100644 QYZH.InteractiveMagazine.Infrastructure/Extensions/InteractiveMagazineApiDefaultsExtensions.cs
diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..64e8508
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,10 @@
+root = true
+
+[*]
+charset = utf-8
+end_of_line = crlf
+insert_final_newline = true
+
+[*.cs]
+indent_style = space
+indent_size = 4
diff --git a/QYZH.InteractiveMagazine.Infrastructure/Extensions/InteractiveMagazineApiDefaultsExtensions.cs b/QYZH.InteractiveMagazine.Infrastructure/Extensions/InteractiveMagazineApiDefaultsExtensions.cs
new file mode 100644
index 0000000..5b9dfe5
--- /dev/null
+++ b/QYZH.InteractiveMagazine.Infrastructure/Extensions/InteractiveMagazineApiDefaultsExtensions.cs
@@ -0,0 +1,85 @@
+using Microsoft.AspNetCore.Builder;
+using Microsoft.AspNetCore.DataProtection;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.AspNetCore.ResponseCompression;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using QYZH.InteractiveMagazine.Common.Helpers;
+using QYZH.InteractiveMagazine.Infrastructure.Middleware;
+using QYZH.InteractiveMagazine.Infrastructure.RabbitMQ;
+using QYZH.InteractiveMagazine.Infrastructure.Redis;
+using QYZH.InteractiveMagazine.Infrastructure.SDK;
+using QYZH.InteractiveMagazine.Models.Settings;
+using System.Text.Json.Serialization;
+
+namespace QYZH.InteractiveMagazine.Infrastructure.Extensions;
+
+///
+/// API 默认服务注册扩展。
+///
+public static class InteractiveMagazineApiDefaultsExtensions
+{
+ ///
+ /// 注册 WebApi 与 WeChatApi 共享的默认服务。
+ ///
+ /// 应用构建器。
+ /// 应用构建器。
+ public static WebApplicationBuilder AddInteractiveMagazineApiDefaults(this WebApplicationBuilder builder)
+ {
+ builder.Services.AddDataProtection()
+ .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "DataProtection")));
+
+ builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings"));
+ builder.Services.AddRabbitMQ(builder.Configuration);
+
+ builder.Services.AddControllers(options =>
+ {
+ options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
+ options.Filters.Add();
+ })
+ .AddJsonOptions(options =>
+ {
+ options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
+ options.JsonSerializerOptions.Converters.Add(new JsonConverterUtil.DateTimeConverter());
+ options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
+ })
+ .ConfigureApiBehaviorOptions(opt => opt.SuppressModelStateInvalidFilter = true);
+
+ builder.Services.AddEndpointsApiExplorer();
+ builder.AddCorsRegister();
+ builder.Services.AddHttpClient();
+ builder.Services.AddHttpContextAccessor();
+ builder.Services.AddInfrastructureServices(builder.Configuration, builder.Environment);
+ builder.Services.AddSDKService(builder.Configuration);
+ builder.Services.Configure(builder.Configuration.GetSection("MedalRuleConfig"));
+
+ builder.Services.AddCors(options =>
+ {
+ options.AddPolicy("AllowAll", policy =>
+ {
+ policy.AllowAnyOrigin()
+ .AllowAnyMethod()
+ .AllowAnyHeader();
+ });
+ });
+
+ builder.Services.Configure(options =>
+ {
+ options.Level = System.IO.Compression.CompressionLevel.Optimal;
+ });
+ builder.Services.Configure(options =>
+ {
+ options.Level = System.IO.Compression.CompressionLevel.Fastest;
+ });
+ builder.Services.AddResponseCompression(options =>
+ {
+ options.EnableForHttps = true;
+ options.Providers.Add();
+ options.Providers.Add();
+ options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["image/svg+xml", "application/json", "text/plain"]);
+ });
+
+ return builder;
+ }
+}
diff --git a/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs b/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs
index d9392a8..7703ad1 100644
--- a/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs
+++ b/QYZH.InteractiveMagazine.Models/Dto/Journal/BindJournalDto.cs
@@ -180,7 +180,7 @@ public class GenerateUserJournalQrCodeMessage
}
///
-/// 鏈熷垔浜岀淮鐮佹煡璇㈣緭鍏TO
+/// 期刊二维码查询输入DTO
///
public class UserJournalQrCodeQueryInput : PageQueryModel
{
diff --git a/QYZH.InteractiveMagazine.Service/PointsService.cs b/QYZH.InteractiveMagazine.Service/PointsService.cs
index fe2a29f..53260e2 100644
--- a/QYZH.InteractiveMagazine.Service/PointsService.cs
+++ b/QYZH.InteractiveMagazine.Service/PointsService.cs
@@ -73,7 +73,18 @@ public class PointsService(
if (input.Amount <= 0)
throw new BusinessException("增加积分数量必须大于0", ResultCode.BAD_REQUEST);
- // 查询用户当前积分
+ var now = DateTime.Now;
+
+ // 原子增加积分,避免并发写回旧余额覆盖新余额
+ var affectedRows = await Context.Updateable()
+ .SetColumns(u => u.Points == u.Points + input.Amount)
+ .SetColumns(u => u.UpdatedAt == now)
+ .Where(u => u.Id == input.UserId && !u.IsDeleted)
+ .ExecuteCommandAsync();
+
+ if (affectedRows <= 0)
+ throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
+
var user = await Context.Queryable()
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.FirstAsync();
@@ -81,15 +92,8 @@ public class PointsService(
if (user == null)
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
- var previousBalance = user.Points;
- var newBalance = previousBalance + input.Amount;
-
- // 更新用户积分
- await Context.Updateable()
- .SetColumns(u => u.Points == newBalance)
- .SetColumns(u => u.UpdatedAt == DateTime.Now)
- .Where(u => u.Id == input.UserId && !u.IsDeleted)
- .ExecuteCommandAsync();
+ var newBalance = user.Points;
+ var previousBalance = newBalance - input.Amount;
// 插入积分流水记录
var record = new PointsRecord
@@ -104,9 +108,9 @@ public class PointsService(
Status = (int)PointsRecordStatusEnum.Success,
IsDeleted = false,
CreatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
- CreatedAt = DateTime.Now,
+ CreatedAt = now,
UpdatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
- UpdatedAt = DateTime.Now
+ UpdatedAt = now
};
var recordEntity = await InsertReturnEntityAsync(record);
@@ -131,7 +135,27 @@ public class PointsService(
if (input.Amount <= 0)
throw new BusinessException("扣除积分数量必须大于0", ResultCode.BAD_REQUEST);
- // 查询用户当前积分
+ var now = DateTime.Now;
+
+ // 带余额条件的原子扣减,避免并发扣减时超扣或覆盖余额
+ var affectedRows = await Context.Updateable()
+ .SetColumns(u => u.Points == u.Points - input.Amount)
+ .SetColumns(u => u.UpdatedAt == now)
+ .Where(u => u.Id == input.UserId && !u.IsDeleted && u.Points >= input.Amount)
+ .ExecuteCommandAsync();
+
+ if (affectedRows <= 0)
+ {
+ var currentUser = await Context.Queryable()
+ .Where(u => u.Id == input.UserId && !u.IsDeleted)
+ .FirstAsync();
+
+ if (currentUser == null)
+ throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
+
+ throw new BusinessException($"积分不足,需要 {input.Amount} 积分,当前余额 {currentUser.Points}", ResultCode.BAD_REQUEST);
+ }
+
var user = await Context.Queryable()
.Where(u => u.Id == input.UserId && !u.IsDeleted)
.FirstAsync();
@@ -139,20 +163,8 @@ public class PointsService(
if (user == null)
throw new BusinessException("用户不存在", ResultCode.NOT_FOUND);
- var previousBalance = user.Points;
-
- // 余额不足校验
- if (previousBalance < input.Amount)
- throw new BusinessException($"积分不足,需要 {input.Amount} 积分,当前余额 {previousBalance}", ResultCode.BAD_REQUEST);
-
- var newBalance = previousBalance - input.Amount;
-
- // 更新用户积分
- await Context.Updateable()
- .SetColumns(u => u.Points == newBalance)
- .SetColumns(u => u.UpdatedAt == DateTime.Now)
- .Where(u => u.Id == input.UserId && !u.IsDeleted)
- .ExecuteCommandAsync();
+ var newBalance = user.Points;
+ var previousBalance = newBalance + input.Amount;
// 插入积分流水记录
var record = new PointsRecord
@@ -167,9 +179,9 @@ public class PointsService(
Status = (int)PointsRecordStatusEnum.Success,
IsDeleted = false,
CreatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
- CreatedAt = DateTime.Now,
+ CreatedAt = now,
UpdatedBy = input.OperatorName ?? user.Name ?? input.UserId.ToString(),
- UpdatedAt = DateTime.Now
+ UpdatedAt = now
};
var recordEntity = await InsertReturnEntityAsync(record);
diff --git a/QYZH.InteractiveMagazine.WeChatApi/Program.cs b/QYZH.InteractiveMagazine.WeChatApi/Program.cs
index 5144dfe..c3dd3bd 100644
--- a/QYZH.InteractiveMagazine.WeChatApi/Program.cs
+++ b/QYZH.InteractiveMagazine.WeChatApi/Program.cs
@@ -38,66 +38,14 @@ builder.InitSqlSugarDb(new IocConfig
IsAutoCloseConnection = true,
});
-builder.Services.AddDataProtection()
- .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "DataProtection")));
-
-builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings"));
-builder.Services.AddRabbitMQ(builder.Configuration);
-
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.CreateLogger();
builder.Host.UseSerilog();
-
-builder.Services.AddControllers(options =>
-{
- options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;
- options.Filters.Add();
-})
-.AddJsonOptions(options =>
-{
- options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
- options.JsonSerializerOptions.Converters.Add(new JsonConverterUtil.DateTimeConverter());
- options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
-})
-.ConfigureApiBehaviorOptions(opt => opt.SuppressModelStateInvalidFilter = true);
-
-builder.Services.AddEndpointsApiExplorer();
-builder.AddCorsRegister();
-builder.Services.AddHttpClient();
-builder.Services.AddHttpContextAccessor();
+builder.AddInteractiveMagazineApiDefaults();
builder.Services.AddScoped(typeof(BaseRepository<>));
-builder.Services.AddInfrastructureServices(builder.Configuration, builder.Environment);
-builder.Services.AddSDKService(builder.Configuration);
-builder.Services.Configure(builder.Configuration.GetSection("MedalRuleConfig"));
-
-builder.Services.AddCors(options =>
-{
- options.AddPolicy("AllowAll", policy =>
- {
- policy.AllowAnyOrigin()
- .AllowAnyMethod()
- .AllowAnyHeader();
- });
-});
-
-builder.Services.Configure(options =>
-{
- options.Level = System.IO.Compression.CompressionLevel.Optimal;
-});
-builder.Services.Configure(options =>
-{
- options.Level = System.IO.Compression.CompressionLevel.Fastest;
-});
-builder.Services.AddResponseCompression(options =>
-{
- options.EnableForHttps = true;
- options.Providers.Add();
- options.Providers.Add();
- options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["image/svg+xml", "application/json", "text/plain"]);
-});
builder.Services.AddSwaggerGen(option =>
{
diff --git a/QYZH.InteractiveMagazine.WebApi/Program.cs b/QYZH.InteractiveMagazine.WebApi/Program.cs
index 392de5b..a06e381 100644
--- a/QYZH.InteractiveMagazine.WebApi/Program.cs
+++ b/QYZH.InteractiveMagazine.WebApi/Program.cs
@@ -47,14 +47,6 @@ builder.InitSqlSugarDb(new IocConfig()
IsAutoCloseConnection = true,
});
-// 消除Error unprotecting the session cookie警告
-builder.Services.AddDataProtection()
- .PersistKeysToFileSystem(new DirectoryInfo(Directory.GetCurrentDirectory() + Path.DirectorySeparatorChar + "DataProtection"));
-//redis
-builder.Services.AddCSRedisCacheExtension(builder.Configuration.GetSection("RedisSettings"));
-
-//MQ
-builder.Services.AddRabbitMQ(builder.Configuration);
// 配置Serilog
Log.Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
@@ -62,26 +54,8 @@ Log.Logger = new LoggerConfiguration()
.CreateLogger();
builder.Host.UseSerilog();
-builder.Services.AddControllers(options =>
-{
- options.SuppressImplicitRequiredAttributeForNonNullableReferenceTypes = true;//Required 不作为必填
- options.Filters.Add();//添加自定义模型验证
-})
-.AddJsonOptions(options =>
-{
- // 配置 JSON 不区分大小写(支持 camelCase 和 PascalCase)
- options.JsonSerializerOptions.PropertyNameCaseInsensitive = true;
- // 配置返回时间格式转换
- options.JsonSerializerOptions.Converters.Add(new JsonConverterUtil.DateTimeConverter());
- options.JsonSerializerOptions.ReferenceHandler = ReferenceHandler.IgnoreCycles;
-}).ConfigureApiBehaviorOptions(opt => opt.SuppressModelStateInvalidFilter = true);//关闭默认模型验证
-builder.Services.AddEndpointsApiExplorer();
+builder.AddInteractiveMagazineApiDefaults();
-// 跨域配置
-builder.Services.AddDataProtection().UseEphemeralDataProtectionProvider();
-builder.AddCorsRegister();
-//builder.Services.AddSession();
-builder.Services.AddHttpClient();
// 注册 Swagger 文档
builder.Services.AddSwaggerGen(option =>
{
@@ -148,52 +122,8 @@ builder.Services.AddSwaggerGen(option =>
});
});
-builder.Services.AddInfrastructureServices(builder.Configuration, builder.Environment);
-
-//注册 HttpContextAccessor
-builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped(typeof(BaseRepository<>));
-// 注册勋章规则配置
-builder.Services.Configure(builder.Configuration.GetSection("MedalRuleConfig"));
-
-// 添加CORS
-builder.Services.AddCors(options =>
-{
- options.AddPolicy("AllowAll", policy =>
- {
- policy.AllowAnyOrigin()
- .AllowAnyMethod()
- .AllowAnyHeader();
- });
-});
-builder.Services.AddSDKService(builder.Configuration);
-
-#region 配置数据压缩选项
-
-// 1.首先配置压缩选项的Options>
-builder.Services.Configure(options =>
-{
- options.Level = System.IO.Compression.CompressionLevel.Optimal;
-});
-builder.Services.Configure(options =>
-{
- options.Level = System.IO.Compression.CompressionLevel.Fastest;
-});
-
-// 2.在服务容器中注册响应压缩服务
-builder.Services.AddResponseCompression(options =>
-{
- // 可以在这里进行详细配置
- options.EnableForHttps = true; // 启用对HTTPS响应的压缩(请注意安全风险)
- options.Providers.Add();
- options.Providers.Add();
- //指定哪些类型的响应应该被压缩。默认列表包含常见的文本类类型,如 text/html, text/css, application/javascript, application/json, text/plain等
- options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(["image/svg+xml", "application/json", "text/plain"]);
-});
-
-#endregion
-
var app = builder.Build();
{