Skip to content

Commit a54a36b

Browse files
committed
feat: refactor subject name fields and enhance image backfill service
1 parent c1de3c3 commit a54a36b

9 files changed

Lines changed: 370 additions & 164 deletions

File tree

common/pom.xml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,8 @@
5353
</dependency>
5454

5555
<dependency>
56-
<groupId>junit</groupId>
57-
<artifactId>junit</artifactId>
58-
<version>3.8.1</version>
56+
<groupId>org.springframework.boot</groupId>
57+
<artifactId>spring-boot-starter-test</artifactId>
5958
<scope>test</scope>
6059
</dependency>
6160
</dependencies>

common/src/test/java/com/ligg/AppTest.java

Lines changed: 0 additions & 38 deletions
This file was deleted.
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
package com.ligg.common.utils;
2+
3+
import org.junit.jupiter.api.Test;
4+
5+
import java.util.Map;
6+
7+
import static org.assertj.core.api.Assertions.assertThat;
8+
9+
class InfoboxParserTest {
10+
11+
@Test
12+
void toInfo_shouldExtractStandardFields() {
13+
String infobox = """
14+
{{Infobox animanga/TVAnime
15+
|中文名= 呪術廻戦
16+
|话数= 24
17+
|放送开始= 2020年10月2日
18+
|导演= 朴性厚
19+
|原作= 芥見下々(集英社「週刊少年ジャンプ」連載)
20+
|人物设定= 平松禎史
21+
|动画制作= MAPPA
22+
}}""";
23+
24+
String info = InfoboxParser.toInfo(infobox);
25+
26+
assertThat(info).isEqualTo("24话 / 2020年10月2日 / 朴性厚 / 芥見下々(集英社「週刊少年ジャンプ」連載) / 平松禎史");
27+
}
28+
29+
@Test
30+
void toInfo_shouldSkipMissingKeys() {
31+
String infobox = """
32+
{{Infobox animanga/TVAnime
33+
|中文名= 测试
34+
|放送开始= 2020年1月1日
35+
|导演= 某导演
36+
}}""";
37+
38+
String info = InfoboxParser.toInfo(infobox);
39+
40+
assertThat(info).isEqualTo("2020年1月1日 / 某导演");
41+
}
42+
43+
@Test
44+
void toInfo_shouldReturnEmpty_whenEmpty() {
45+
assertThat(InfoboxParser.toInfo("")).isEmpty();
46+
assertThat(InfoboxParser.toInfo(null)).isEmpty();
47+
}
48+
49+
@Test
50+
void toInfo_shouldReturnEmpty_whenOnlyHeader() {
51+
assertThat(InfoboxParser.toInfo("{{Infobox animanga/TVAnime}}")).isEmpty();
52+
}
53+
54+
@Test
55+
void toInfo_shouldNotDoubleAppendEpisodeSuffix() {
56+
String infobox = """
57+
{{Infobox
58+
|话数= 12话
59+
}}""";
60+
61+
assertThat(InfoboxParser.toInfo(infobox)).isEqualTo("12话");
62+
}
63+
64+
@Test
65+
void toInfo_shouldAppendEpisodeSuffix() {
66+
String infobox = """
67+
{{Infobox
68+
|话数= 12
69+
}}""";
70+
71+
assertThat(InfoboxParser.toInfo(infobox)).isEqualTo("12话");
72+
}
73+
74+
@Test
75+
void toMap_shouldParseAllFields() {
76+
String infobox = """
77+
{{Infobox animanga/Manga
78+
|中文名= 测试漫画
79+
|话数= 10
80+
|放送开始= 2019年
81+
|别名={
82+
[别名1]
83+
[别名2]
84+
}
85+
}}""";
86+
87+
Map<String, String> map = InfoboxParser.toMap(infobox);
88+
89+
assertThat(map).containsEntry("中文名", "测试漫画");
90+
assertThat(map).containsEntry("话数", "10");
91+
assertThat(map).containsEntry("放送开始", "2019年");
92+
// 列表行 '|别名={' 只有 key,value 是 '{'
93+
assertThat(map).containsEntry("别名", "{");
94+
}
95+
96+
@Test
97+
void toMap_shouldHandleCrLf() {
98+
String infobox = "{{Infobox\r\n|key= value\r\n}}";
99+
100+
Map<String, String> map = InfoboxParser.toMap(infobox);
101+
102+
assertThat(map).containsEntry("key", "value");
103+
}
104+
105+
@Test
106+
void toMap_shouldHandleMultipleEqualsInValue() {
107+
String infobox = "{{Infobox\n|key= a=b=c\n}}";
108+
109+
Map<String, String> map = InfoboxParser.toMap(infobox);
110+
111+
assertThat(map).containsEntry("key", "a=b=c");
112+
}
113+
}

flow-client/src/main/java/com/ligg/flowclient/module/dto/UserBgmCollectionRow.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ public class UserBgmCollectionRow {
2727
private LocalDateTime syncTime;
2828
private LocalDateTime createTime;
2929

30-
private String subjectName;
31-
private String subjectNameCn;
30+
private String name;
31+
private String nameCn;
3232
private Boolean nsfw;
3333
private Double score;
3434
private String scoreDetails;
3535
private Integer rank;
36+
private String infobox;
3637
}
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
package com.ligg.flowclient.service;
2+
3+
import com.fasterxml.jackson.core.JsonProcessingException;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.ligg.api.bangumiv0api.BangumiV0Client;
6+
import com.ligg.common.entity.BangumiSubjectEntity;
7+
import com.ligg.common.model.CoverImages;
8+
import com.ligg.common.thirdparty.bangumi.enums.SubjectImageType;
9+
import com.ligg.flowclient.mapper.BangumiSubjectMapper;
10+
import lombok.RequiredArgsConstructor;
11+
import lombok.extern.slf4j.Slf4j;
12+
import org.springframework.stereotype.Component;
13+
import org.springframework.util.StringUtils;
14+
15+
import java.util.concurrent.ConcurrentHashMap;
16+
17+
/**
18+
* 条目图片处理服务
19+
*/
20+
@Slf4j
21+
@Component
22+
@RequiredArgsConstructor
23+
public class ImageBackfillService {
24+
25+
private final BangumiSubjectMapper bangumiSubjectMapper;
26+
private final BangumiV0Client bangumiV0Client;
27+
private final ObjectMapper objectMapper;
28+
29+
private final ConcurrentHashMap<Integer, Object> locks = new ConcurrentHashMap<>();
30+
31+
/**
32+
* 获取条目封面图。DB 有数据则直接返回,否则调 API 获取并回写。
33+
*
34+
* @param subjectImagesJson bangumi_subject.images 的 JSON 值,可为空
35+
* @param subjectId 条目 ID
36+
* @return 封面图,获取失败返回空 CoverImages
37+
*/
38+
public CoverImages resolve(String subjectImagesJson, int subjectId) {
39+
CoverImages images;
40+
if (StringUtils.hasText(subjectImagesJson) && !"null".equals(subjectImagesJson)) {
41+
try {
42+
images = objectMapper.readValue(subjectImagesJson, CoverImages.class);
43+
} catch (JsonProcessingException e) {
44+
log.warn("解析条目图片 JSON 失败, subjectId={}", subjectId, e);
45+
images = new CoverImages();
46+
}
47+
} else {
48+
images = fetchAndSave(subjectId);
49+
}
50+
if (images.getLarge() == null) images.setLarge("");
51+
if (images.getCommon() == null) images.setCommon("");
52+
if (images.getMedium() == null) images.setMedium("");
53+
if (images.getSmall() == null) images.setSmall("");
54+
if (images.getGrid() == null) images.setGrid("");
55+
return images;
56+
}
57+
58+
private CoverImages fetchAndSave(int subjectId) {
59+
// 锁对象不删除:避免 remove 后其他线程创建新锁绕过互斥,造成重复请求。
60+
// subjectId 数量有限(≈3 万),全量常驻仅约 2 MB,可接受。
61+
Object lock = locks.computeIfAbsent(subjectId, k -> new Object());
62+
synchronized (lock) {
63+
// 双重检查
64+
BangumiSubjectEntity entity = bangumiSubjectMapper.selectById(subjectId);
65+
if (entity != null && StringUtils.hasText(entity.getImages())
66+
&& !"null".equals(entity.getImages())) {
67+
try {
68+
return objectMapper.readValue(entity.getImages(), CoverImages.class);
69+
} catch (JsonProcessingException e) {
70+
log.warn("双重检查时解析条目图片 JSON 失败, subjectId={}", subjectId, e);
71+
// JSON 损坏,fall-through 重新从 API 获取
72+
}
73+
}
74+
75+
CoverImages images = new CoverImages();
76+
boolean anySucceeded = false;
77+
78+
for (SubjectImageType type : SubjectImageType.values()) {
79+
try {
80+
String url = bangumiV0Client.getSubjectImageUrl(subjectId, type);
81+
if (StringUtils.hasText(url)) {
82+
setField(images, type, url);
83+
anySucceeded = true;
84+
}
85+
} catch (Exception e) {
86+
log.warn("获取条目图片失败, subjectId={}, type={}", subjectId, type.getValue(), e);
87+
}
88+
}
89+
90+
if (anySucceeded) {
91+
try {
92+
String json = objectMapper.writeValueAsString(images);
93+
BangumiSubjectEntity update = new BangumiSubjectEntity();
94+
update.setId(subjectId);
95+
update.setImages(json);
96+
bangumiSubjectMapper.updateById(update);
97+
log.info("条目图片已回写 DB, subjectId={}, images={}", subjectId, json);
98+
} catch (JsonProcessingException e) {
99+
log.error("序列化条目图片 JSON 失败, subjectId={}", subjectId, e);
100+
}
101+
} else {
102+
log.warn("条目图片获取全部失败, subjectId={}", subjectId);
103+
}
104+
return images;
105+
}
106+
}
107+
108+
private static void setField(CoverImages images, SubjectImageType type, String url) {
109+
switch (type) {
110+
case LARGE -> images.setLarge(url);
111+
case COMMON -> images.setCommon(url);
112+
case MEDIUM -> images.setMedium(url);
113+
case SMALL -> images.setSmall(url);
114+
case GRID -> images.setGrid(url);
115+
default -> { /* 枚举新增值时编译器会警告,此处防御性空操作 */ }
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)