Skip to content

Commit c3478a5

Browse files
committed
Extracted user Profile processing logic into a standalone service class
1 parent 0395348 commit c3478a5

File tree

3 files changed

+163
-134
lines changed

3 files changed

+163
-134
lines changed

OpenBioCardServer/Controllers/Classic/ClassicUserController.cs

Lines changed: 25 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,42 @@
1-
using System.Text.Json;
21
using Microsoft.AspNetCore.Mvc;
3-
using Microsoft.EntityFrameworkCore;
4-
using Microsoft.Extensions.Caching.Distributed;
5-
using Microsoft.Extensions.Caching.Memory;
6-
using OpenBioCardServer.Data;
7-
using OpenBioCardServer.Interfaces;
82
using OpenBioCardServer.Models.DTOs.Classic;
93
using OpenBioCardServer.Services;
10-
using OpenBioCardServer.Utilities.Mappers;
114

125
namespace OpenBioCardServer.Controllers.Classic;
136

147
[Route("classic/user")]
158
[ApiController]
169
public class ClassicUserController : ControllerBase
1710
{
18-
private readonly AppDbContext _context;
1911
private readonly ClassicAuthService _authService;
20-
private readonly ICacheService _cacheService;
12+
private readonly ClassicProfileService _profileService;
2113
private readonly ILogger<ClassicUserController> _logger;
2214

2315
public ClassicUserController(
24-
AppDbContext context,
2516
ClassicAuthService authService,
26-
ICacheService cacheService,
17+
ClassicProfileService profileService,
2718
ILogger<ClassicUserController> logger)
2819
{
29-
_context = context;
3020
_authService = authService;
31-
_cacheService = cacheService;
21+
_profileService = profileService;
3222
_logger = logger;
3323
}
3424

35-
36-
// 生成统一的 Cache Key
37-
private static string GetProfileCacheKey(string username) =>
38-
$"Classic:Profile:{username.Trim().ToLowerInvariant()}";
39-
4025
/// <summary>
4126
/// Get user profile (public endpoint)
4227
/// </summary>
4328
[HttpGet("{username}")]
4429
public async Task<IActionResult> GetProfile(string username)
4530
{
46-
string cacheKey = GetProfileCacheKey(username);
47-
4831
try
4932
{
50-
var profileDto = await _cacheService.GetOrCreateAsync(cacheKey, async () =>
51-
{
52-
var profile = await _context.Profiles
53-
.AsNoTracking()
54-
.AsSplitQuery()
55-
.Include(p => p.Contacts)
56-
.Include(p => p.SocialLinks)
57-
.Include(p => p.Projects)
58-
.Include(p => p.WorkExperiences)
59-
.Include(p => p.SchoolExperiences)
60-
.Include(p => p.Gallery)
61-
.FirstOrDefaultAsync(p => p.Username == username);
62-
return profile == null ? null : ClassicMapper.ToClassicProfile(profile);
63-
});
33+
var profileDto = await _profileService.GetProfileAsync(username);
6434

6535
if (profileDto == null)
6636
{
6737
return NotFound(new { error = "User not found" });
6838
}
39+
6940
return Ok(profileDto);
7041
}
7142
catch (Exception ex)
@@ -75,125 +46,45 @@ public async Task<IActionResult> GetProfile(string username)
7546
}
7647
}
7748

78-
7949
/// <summary>
8050
/// Update user profile (requires authentication)
8151
/// </summary>
8252
[HttpPost("{username}")]
8353
public async Task<IActionResult> UpdateProfile(string username, [FromBody] ClassicProfile request)
8454
{
85-
using var transaction = await _context.Database.BeginTransactionAsync();
55+
var token = Request.Headers["Authorization"].FirstOrDefault()?.Replace("Bearer ", "");
8656

87-
try
57+
if (string.IsNullOrEmpty(token))
8858
{
89-
// Extract token from Authorization header
90-
var token = Request.Headers["Authorization"].FirstOrDefault()?.Replace("Bearer ", "");
91-
92-
if (string.IsNullOrEmpty(token))
93-
{
94-
return Unauthorized(new { error = "Missing authentication token" });
95-
}
96-
97-
var (isValid, account) = await _authService.ValidateTokenAsync(token);
98-
99-
if (!isValid || account == null)
100-
{
101-
return Unauthorized(new { error = "Invalid token" });
102-
}
59+
return Unauthorized(new { error = "Missing authentication token" });
60+
}
10361

104-
if (account.UserName != username)
105-
{
106-
return Unauthorized(new { error = "Token does not match username" });
107-
}
62+
var (isValid, account) = await _authService.ValidateTokenAsync(token);
10863

109-
var profile = await _context.Profiles
110-
.AsTracking()
111-
.FirstOrDefaultAsync(p => p.Username == username);
64+
if (!isValid || account == null)
65+
{
66+
return Unauthorized(new { error = "Invalid token" });
67+
}
11268

113-
if (profile == null)
114-
{
115-
return NotFound(new { error = "Profile not found" });
116-
}
69+
if (account.UserName != username)
70+
{
71+
return Unauthorized(new { error = "Token does not match username" });
72+
}
11773

118-
// Update basic profile fields
119-
ClassicMapper.UpdateProfileFromClassic(profile, request);
120-
121-
// Clear all existing collections using ExecuteDeleteAsync
122-
await _context.ContactItems
123-
.Where(c => c.ProfileId == profile.Id)
124-
.ExecuteDeleteAsync();
74+
try
75+
{
76+
var success = await _profileService.UpdateProfileAsync(username, request);
12577

126-
await _context.SocialLinkItems
127-
.Where(s => s.ProfileId == profile.Id)
128-
.ExecuteDeleteAsync();
129-
130-
await _context.ProjectItems
131-
.Where(p => p.ProfileId == profile.Id)
132-
.ExecuteDeleteAsync();
133-
134-
await _context.WorkExperienceItems
135-
.Where(w => w.ProfileId == profile.Id)
136-
.ExecuteDeleteAsync();
137-
138-
await _context.SchoolExperienceItems
139-
.Where(s => s.ProfileId == profile.Id)
140-
.ExecuteDeleteAsync();
141-
142-
await _context.GalleryItems
143-
.Where(g => g.ProfileId == profile.Id)
144-
.ExecuteDeleteAsync();
145-
146-
// Add new collections from request
147-
if (request.Contacts?.Any() == true)
148-
{
149-
var contacts = ClassicMapper.ToContactEntities(request.Contacts, profile.Id);
150-
await _context.ContactItems.AddRangeAsync(contacts);
151-
}
152-
153-
if (request.SocialLinks?.Any() == true)
154-
{
155-
var socialLinks = ClassicMapper.ToSocialLinkEntities(request.SocialLinks, profile.Id);
156-
await _context.SocialLinkItems.AddRangeAsync(socialLinks);
157-
}
158-
159-
if (request.Projects?.Any() == true)
160-
{
161-
var projects = ClassicMapper.ToProjectEntities(request.Projects, profile.Id);
162-
await _context.ProjectItems.AddRangeAsync(projects);
163-
}
164-
165-
if (request.WorkExperiences?.Any() == true)
78+
if (!success)
16679
{
167-
var workExperiences = ClassicMapper.ToWorkExperienceEntities(request.WorkExperiences, profile.Id);
168-
await _context.WorkExperienceItems.AddRangeAsync(workExperiences);
169-
}
170-
171-
if (request.SchoolExperiences?.Any() == true)
172-
{
173-
var schoolExperiences = ClassicMapper.ToSchoolExperienceEntities(request.SchoolExperiences, profile.Id);
174-
await _context.SchoolExperienceItems.AddRangeAsync(schoolExperiences);
175-
}
176-
177-
if (request.Gallery?.Any() == true)
178-
{
179-
var gallery = ClassicMapper.ToGalleryEntities(request.Gallery, profile.Id);
180-
await _context.GalleryItems.AddRangeAsync(gallery);
80+
return NotFound(new { error = "Profile not found" });
18181
}
18282

183-
await _context.SaveChangesAsync();
184-
await transaction.CommitAsync();
185-
186-
// 清除缓存
187-
await _cacheService.RemoveAsync(GetProfileCacheKey(username));
188-
189-
_logger.LogInformation("Profile updated for user: {Username}", username);
190-
19183
return Ok(new { success = true });
19284
}
193-
catch (Exception ex)
85+
catch (Exception)
19486
{
195-
await transaction.RollbackAsync();
196-
_logger.LogError(ex, "Error updating profile for user: {Username}", username);
87+
// Exception is already logged in the service
19788
return StatusCode(500, new { error = "Profile update failed" });
19889
}
19990
}

OpenBioCardServer/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,8 @@ private static void RegisterServices(WebApplicationBuilder builder)
107107
// App Services
108108
builder.Services.AddScoped<ClassicAuthService>();
109109
builder.Services.AddScoped<AuthService>();
110+
builder.Services.AddScoped<ClassicProfileService>();
111+
110112
builder.Services.AddHostedService<TokenCleanupService>();
111113
}
112114

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
using Microsoft.EntityFrameworkCore;
2+
using OpenBioCardServer.Data;
3+
using OpenBioCardServer.Interfaces;
4+
using OpenBioCardServer.Models.DTOs.Classic;
5+
using OpenBioCardServer.Utilities.Mappers;
6+
7+
namespace OpenBioCardServer.Services;
8+
9+
public class ClassicProfileService
10+
{
11+
private readonly AppDbContext _context;
12+
private readonly ICacheService _cacheService;
13+
private readonly ILogger<ClassicProfileService> _logger;
14+
15+
public ClassicProfileService(
16+
AppDbContext context,
17+
ICacheService cacheService,
18+
ILogger<ClassicProfileService> logger)
19+
{
20+
_context = context;
21+
_cacheService = cacheService;
22+
_logger = logger;
23+
}
24+
25+
private static string GetProfileCacheKey(string username) =>
26+
$"Classic:Profile:{username.Trim().ToLowerInvariant()}";
27+
28+
/// <summary>
29+
/// 获取用户 Profile
30+
/// </summary>
31+
public async Task<ClassicProfile?> GetProfileAsync(string username)
32+
{
33+
var cacheKey = GetProfileCacheKey(username);
34+
35+
return await _cacheService.GetOrCreateAsync(cacheKey, async () =>
36+
{
37+
var profile = await _context.Profiles
38+
.AsNoTracking()
39+
.AsSplitQuery()
40+
.Include(p => p.Contacts)
41+
.Include(p => p.SocialLinks)
42+
.Include(p => p.Projects)
43+
.Include(p => p.WorkExperiences)
44+
.Include(p => p.SchoolExperiences)
45+
.Include(p => p.Gallery)
46+
.FirstOrDefaultAsync(p => p.Username == username);
47+
48+
return profile == null ? null : ClassicMapper.ToClassicProfile(profile);
49+
});
50+
}
51+
52+
/// <summary>
53+
/// 更新用户 Profile
54+
/// </summary>
55+
public async Task<bool> UpdateProfileAsync(string username, ClassicProfile request)
56+
{
57+
using var transaction = await _context.Database.BeginTransactionAsync();
58+
59+
try
60+
{
61+
var profile = await _context.Profiles
62+
.AsTracking()
63+
.FirstOrDefaultAsync(p => p.Username == username);
64+
65+
if (profile == null)
66+
{
67+
_logger.LogWarning("Attempted to update non-existent profile: {Username}", username);
68+
return false;
69+
}
70+
71+
// 1. Update basic profile fields
72+
ClassicMapper.UpdateProfileFromClassic(profile, request);
73+
74+
// 2. Clear all existing collections efficiently
75+
// Note: ExecuteDeleteAsync executes immediately against the DB
76+
await _context.ContactItems.Where(c => c.ProfileId == profile.Id).ExecuteDeleteAsync();
77+
await _context.SocialLinkItems.Where(s => s.ProfileId == profile.Id).ExecuteDeleteAsync();
78+
await _context.ProjectItems.Where(p => p.ProfileId == profile.Id).ExecuteDeleteAsync();
79+
await _context.WorkExperienceItems.Where(w => w.ProfileId == profile.Id).ExecuteDeleteAsync();
80+
await _context.SchoolExperienceItems.Where(s => s.ProfileId == profile.Id).ExecuteDeleteAsync();
81+
await _context.GalleryItems.Where(g => g.ProfileId == profile.Id).ExecuteDeleteAsync();
82+
83+
// 3. Add new collections from request
84+
if (request.Contacts?.Any() == true)
85+
{
86+
var contacts = ClassicMapper.ToContactEntities(request.Contacts, profile.Id);
87+
await _context.ContactItems.AddRangeAsync(contacts);
88+
}
89+
90+
if (request.SocialLinks?.Any() == true)
91+
{
92+
var socialLinks = ClassicMapper.ToSocialLinkEntities(request.SocialLinks, profile.Id);
93+
await _context.SocialLinkItems.AddRangeAsync(socialLinks);
94+
}
95+
96+
if (request.Projects?.Any() == true)
97+
{
98+
var projects = ClassicMapper.ToProjectEntities(request.Projects, profile.Id);
99+
await _context.ProjectItems.AddRangeAsync(projects);
100+
}
101+
102+
if (request.WorkExperiences?.Any() == true)
103+
{
104+
var workExperiences = ClassicMapper.ToWorkExperienceEntities(request.WorkExperiences, profile.Id);
105+
await _context.WorkExperienceItems.AddRangeAsync(workExperiences);
106+
}
107+
108+
if (request.SchoolExperiences?.Any() == true)
109+
{
110+
var schoolExperiences = ClassicMapper.ToSchoolExperienceEntities(request.SchoolExperiences, profile.Id);
111+
await _context.SchoolExperienceItems.AddRangeAsync(schoolExperiences);
112+
}
113+
114+
if (request.Gallery?.Any() == true)
115+
{
116+
var gallery = ClassicMapper.ToGalleryEntities(request.Gallery, profile.Id);
117+
await _context.GalleryItems.AddRangeAsync(gallery);
118+
}
119+
120+
await _context.SaveChangesAsync();
121+
await transaction.CommitAsync();
122+
123+
// 4. Invalidate Cache
124+
await _cacheService.RemoveAsync(GetProfileCacheKey(username));
125+
126+
_logger.LogInformation("Profile updated successfully for user: {Username}", username);
127+
return true;
128+
}
129+
catch (Exception ex)
130+
{
131+
await transaction.RollbackAsync();
132+
_logger.LogError(ex, "Error updating profile for user: {Username}", username);
133+
throw; // Re-throw to let controller handle the 500 response
134+
}
135+
}
136+
}

0 commit comments

Comments
 (0)