11package com .involutionhell .backend .posts .service ;
22
33import com .involutionhell .backend .common .error .AccessDeniedBusinessException ;
4+ import com .involutionhell .backend .common .error .ResourceNotFoundException ;
45import com .involutionhell .backend .posts .dto .PostRequest ;
56import com .involutionhell .backend .posts .dto .PostSummaryView ;
67import com .involutionhell .backend .posts .dto .PostView ;
1314import com .involutionhell .backend .usercenter .service .UserCenterService ;
1415import org .slf4j .Logger ;
1516import org .slf4j .LoggerFactory ;
17+ import org .springframework .dao .DuplicateKeyException ;
1618import org .springframework .stereotype .Service ;
1719import org .springframework .transaction .annotation .Transactional ;
1820import tools .jackson .databind .ObjectMapper ;
@@ -132,28 +134,32 @@ public PostView update(Long callerId, Long postId, PostRequest req) {
132134 Post existing = requirePost (postId );
133135 checkOwner (callerId , existing );
134136
135- // slug 处理:前端可传新 slug(改 URL 场景),不传则沿用旧 slug
137+ // slug 处理:前端可传新 slug(改 URL 场景),不传则沿用旧 slug。
138+ // 必须经 sanitizeSlug 规范化,保证与 create 路径的格式一致(小写、连字符、长度限制)。
136139 String newSlug = (req .slug () != null && !req .slug ().isBlank ())
137- ? req .slug ()
140+ ? sanitizeSlug ( req .slug () )
138141 : existing .slug ();
139142
140- // 若 slug 改变,需要检查新 slug 是否与该作者其他文章冲突
143+ // slug 冲突检查基于规范化后的值,排除自身( slug 未变则跳过)
141144 if (!newSlug .equals (existing .slug ())) {
142145 boolean taken = postRepo .findByAuthorAndSlug (callerId , newSlug ).isPresent ();
143146 if (taken ) {
144- throw new IllegalArgumentException ("slug 已被使用:" + newSlug );
147+ throw new DuplicateKeyException ("slug 已被使用:" + newSlug );
145148 }
146149 }
147150
148151 String tagsJson = serializeTags (req .tags ());
149- postRepo .update (postId , newSlug , req .title (), req .description (),
152+ // update 返回受影响行数;0 表示记录在校验完成后被并发删除
153+ int affected = postRepo .update (postId , newSlug , req .title (), req .description (),
150154 tagsJson , req .contentMd (), req .coverUrl ());
155+ if (affected == 0 ) {
156+ throw new ResourceNotFoundException ("文章已不存在:" + postId );
157+ }
151158
152159 log .info ("post updated: id={} author={}" , postId , callerId );
153160
154- Post updated = postRepo .findById (postId )
155- .orElseThrow (() -> new IllegalStateException ("post not found after update: " + postId ));
156- return buildView (updated );
161+ return buildView (postRepo .findById (postId )
162+ .orElseThrow (() -> new ResourceNotFoundException ("文章不存在:" + postId )));
157163 }
158164
159165 /**
@@ -241,19 +247,8 @@ public List<PostSummaryView> listByAuthor(Long authorId) {
241247 public List <PostSummaryView > listFeed (int limit , int offset ) {
242248 int safeLimit = Math .min (Math .max (limit , 1 ), MAX_FEED_LIMIT );
243249 int safeOffset = Math .max (offset , 0 );
244-
245- List <Post > posts = postRepo .findFeed (safeLimit , safeOffset );
246-
247- // 批量拼作者信息:每篇独立查一次(MVP 量级可接受;后续可加缓存或 JOIN 优化)
248- return posts .stream ()
249- .map (p -> {
250- UserAccount author = userAccountRepository .findById (p .authorId ()).orElse (null );
251- String username = author != null ? author .username () : "unknown" ;
252- String displayName = author != null ? author .displayName () : "" ;
253- String avatarUrl = author != null ? author .avatarUrl () : null ;
254- return PostSummaryView .from (p , username , displayName , avatarUrl );
255- })
256- .toList ();
250+ // JOIN 查询一次取回作者信息,消除 N+1
251+ return postRepo .findFeedWithAuthor (safeLimit , safeOffset );
257252 }
258253
259254 // ========== 私有工具方法 ==========
@@ -271,11 +266,11 @@ private void validateRequest(PostRequest req) {
271266 }
272267
273268 /**
274- * 按 id 查文章,不存在则抛 404 语义的 IllegalArgumentException 。
269+ * 按 id 查文章,不存在则抛 ResourceNotFoundException(→ 404) 。
275270 */
276271 private Post requirePost (Long postId ) {
277272 return postRepo .findById (postId )
278- .orElseThrow (() -> new IllegalArgumentException ("文章不存在:" + postId ));
273+ .orElseThrow (() -> new ResourceNotFoundException ("文章不存在:" + postId ));
279274 }
280275
281276 /**
0 commit comments