Skip to content

Commit 73c997e

Browse files
yyyCodeclaude
andcommitted
fix(project): 封面图字段兼容完整媒体链接,自动提取 key 入库
封面图字段原仅接受 media key(general/xxx.png),且 DTO/DB 限 64 字符。 用户粘贴完整媒体链接(https://…/api/v1/media/files/general/xxx.png,约 91 字符) 时被 @SiZe(max=64) 拒绝,无法保存;即便存入,前端 coverUrl() 再加前缀也会裂图。 修复:DTO 放宽到 512(允许完整链接进入),ProjectService 归一化——命中 /api/v1/media/files/ 前缀时提取 key 入库(DB 仍 VARCHAR(64)),并对最终 key 校验(无 scheme/空白/路径穿越、≤64)。前端编辑框提示可填 key 或完整链接, 并新增封面实时预览。 Co-Authored-By: Claude Code <noreply@anthropic.com>
1 parent 2497a40 commit 73c997e

3 files changed

Lines changed: 44 additions & 7 deletions

File tree

‎OpenBlog-business/src/main/java/com/yqz/openblog/project/dto/ProjectUpsertRequest.java‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ public class ProjectUpsertRequest {
1515
@NotBlank
1616
private String contentMarkdown;
1717

18-
@Size(max = 64)
18+
/** 允许填完整媒体链接(如 https://…/api/v1/media/files/general/xxx.png),service 会归一化为 key 入库 */
19+
@Size(max = 512)
1920
private String coverMediaKey;
2021

2122
@Size(max = 255)
@@ -43,8 +44,8 @@ public class ProjectUpsertRequest {
4344
public @NotBlank String getContentMarkdown() { return contentMarkdown; }
4445
public void setContentMarkdown(@NotBlank String contentMarkdown) { this.contentMarkdown = contentMarkdown; }
4546

46-
public @Size(max = 64) String getCoverMediaKey() { return coverMediaKey; }
47-
public void setCoverMediaKey(@Size(max = 64) String coverMediaKey) { this.coverMediaKey = coverMediaKey; }
47+
public @Size(max = 512) String getCoverMediaKey() { return coverMediaKey; }
48+
public void setCoverMediaKey(@Size(max = 512) String coverMediaKey) { this.coverMediaKey = coverMediaKey; }
4849

4950
public @Size(max = 255) String getTechStack() { return techStack; }
5051
public void setTechStack(@Size(max = 255) String techStack) { this.techStack = techStack; }

‎OpenBlog-business/src/main/java/com/yqz/openblog/project/service/ProjectService.java‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ private void apply(Project p, ProjectUpsertRequest req) {
7777
p.setTitle(req.getTitle().trim());
7878
p.setSummary(req.getSummary() != null ? req.getSummary().trim() : null);
7979
p.setContentMarkdown(req.getContentMarkdown());
80-
p.setCoverMediaKey(req.getCoverMediaKey() != null ? req.getCoverMediaKey().trim() : null);
80+
p.setCoverMediaKey(normalizeCoverMediaKey(req.getCoverMediaKey()));
8181
p.setTechStack(req.getTechStack() != null ? req.getTechStack().trim() : null);
8282
p.setProjectUrl(req.getProjectUrl() != null ? req.getProjectUrl().trim() : null);
8383
p.setGithubUrl(req.getGithubUrl() != null ? req.getGithubUrl().trim() : null);
@@ -92,6 +92,31 @@ private void apply(Project p, ProjectUpsertRequest req) {
9292
}
9393
}
9494

95+
/**
96+
* 封面图字段兼容两种输入:裸 media key(general/xxx.png)或完整媒体链接(https://…/api/v1/media/files/general/xxx.png)。
97+
* 完整链接提取其中的 key 入库(DB 列仅 VARCHAR(64),且前端 coverUrl() 会用 key 重新拼链接,不能存 URL)。
98+
* 非法输入(非本站媒体、含 scheme/空白/路径穿越、超长)直接报错,避免静默丢数据。
99+
*/
100+
private String normalizeCoverMediaKey(String raw) {
101+
if (raw == null || raw.isBlank()) {
102+
return null;
103+
}
104+
String v = raw.trim();
105+
String marker = "/api/v1/media/files/";
106+
int idx = v.indexOf(marker);
107+
if (idx >= 0) {
108+
v = v.substring(idx + marker.length());
109+
int q = v.indexOf('?');
110+
if (q >= 0) {
111+
v = v.substring(0, q);
112+
}
113+
}
114+
if (v.length() > 64 || v.contains(" ") || v.startsWith("http") || v.contains("..")) {
115+
throw new BizException(4002, "封面图请填写媒体库 key(如 general/xxx.png)或本站媒体链接");
116+
}
117+
return v;
118+
}
119+
95120
private ProjectListItemResponse toListItem(Project p) {
96121
ProjectListItemResponse r = new ProjectListItemResponse();
97122
r.setId(p.getId());

‎vue/src/views/ConsoleProjectsView.vue‎

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@
3131
<input v-model="form.summary" class="input" placeholder="一句话简介" maxlength="255" />
3232
</div>
3333
<div class="field">
34-
<div class="label">封面图 Media Key</div>
35-
<input v-model="form.coverMediaKey" class="input" placeholder="留空则不显示封面" />
34+
<div class="label">封面图(媒体库 Key 或完整链接)</div>
35+
<input v-model="form.coverMediaKey" class="input" placeholder="媒体库 key(如 general/xxx.png)或完整图片链接,留空不显示封面" />
36+
<img v-if="form.coverMediaKey" :src="coverUrl(form.coverMediaKey)" alt="封面预览" class="cover-preview" />
3637
</div>
3738
<div class="field">
3839
<div class="label">技术栈</div>
@@ -110,7 +111,7 @@
110111

111112
<script setup>
112113
import { onMounted, ref } from 'vue'
113-
import { fetchAdminProjects, fetchAdminProjectDetail, createProject, updateProject, deleteProject } from '../api/project'
114+
import { fetchAdminProjects, fetchAdminProjectDetail, createProject, updateProject, deleteProject, coverUrl } from '../api/project'
114115
import { showMessage } from '../utils/message'
115116
116117
const loading = ref(true)
@@ -307,6 +308,16 @@ function formatDate(v) {
307308
overflow-y: auto;
308309
}
309310
311+
.cover-preview {
312+
display: block;
313+
margin-top: 8px;
314+
max-width: 100%;
315+
max-height: 160px;
316+
border-radius: 8px;
317+
object-fit: cover;
318+
border: 1px solid var(--console-border);
319+
}
320+
310321
.console-editor-actions {
311322
display: flex;
312323
justify-content: flex-end;

0 commit comments

Comments
 (0)