rename com.imyeyu.server to com.imyeyu.api

This commit is contained in:
Timi
2025-07-22 15:26:14 +08:00
parent e816b885b2
commit 323e038e86
340 changed files with 1174 additions and 1175 deletions
@@ -0,0 +1,81 @@
package com.imyeyu.api.modules.lyric.controller;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import com.imyeyu.io.IOSize;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.annotation.CaptchaValid;
import com.imyeyu.api.bean.CaptchaFrom;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.lyric.entity.Lyric;
import com.imyeyu.api.modules.lyric.entity.LyricCorrect;
import com.imyeyu.api.modules.lyric.service.LyricCorrectService;
import com.imyeyu.api.modules.lyric.service.LyricService;
import com.imyeyu.api.modules.lyric.vo.LyricCorrectRequest;
import com.imyeyu.api.modules.lyric.vo.LyricRequest;
import com.imyeyu.spring.annotation.AOPLog;
import com.imyeyu.spring.annotation.RequestRateLimit;
import com.imyeyu.spring.annotation.RequiredToken;
import com.imyeyu.spring.bean.CaptchaData;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
/**
* 歌词接口
*
* @author 夜雨
* @since 2023-02-02 11:50
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/lyric")
public class LyricController {
private final UserService userService;
private final LyricService service;
private final LyricCorrectService correctService;
@AOPLog
@RequestRateLimit
@PostMapping("")
public Lyric get(@Valid @RequestBody LyricRequest request) {
return service.get(request);
}
@AOPLog
@CaptchaValid(CaptchaFrom.LYRIC_CORRECT)
@RequestRateLimit
@PostMapping("/correct")
public long correctRequest(CaptchaData<LyricCorrectRequest> request) {
if (IOSize.MB * 120 < request.getData().getFile().getSize()) {
throw new TimiException(TimiCode.ARG_BAD).msgKey("lyric.correct_request.too_big");
}
return correctService.correctRequest(request.getData());
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/correct/list")
public PageResult<LyricCorrect> listCorrect(@Valid @RequestBody Page page) {
return correctService.pageByToken(page);
}
@AOPLog
@RequiredToken
@RequestRateLimit
@PostMapping("/correct/cancel")
public void cancelCorrectRequest(@Min(1) @NotNull @RequestParam Long id) {
correctService.cancelCorrectRequest(id);
}
}
@@ -0,0 +1,28 @@
package com.imyeyu.api.modules.lyric.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 歌词
*
* @author 夜雨
* @since 2023-02-02 11:54
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class Lyric extends Entity {
/** 歌曲名称 */
private String song;
/** 演唱 */
private String singer;
/** 歌词 */
private String data;
/** 更新者 ID */
private Long updatedBy;
}
@@ -0,0 +1,56 @@
package com.imyeyu.api.modules.lyric.entity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import com.imyeyu.spring.entity.Entity;
/**
* 歌词更新请求
*
* @author 夜雨
* @since 2023-04-24 17:00
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class LyricCorrect extends Entity {
/** 歌词 ID */
private Long lyricId;
/** 歌曲名 */
private String song;
/** 演唱 */
private String singer;
/** 歌词 */
private String data;
/** 申请人 */
private Long requestBy;
/** 申请 IP */
private String requestIP;
/** 取消时间 */
private Long cancelAt;
/** 通过时间 */
private Long approvalAt;
/** 拒绝时间 */
private Long rejectAt;
/** 拒绝原因 */
private Long rejectReason;
/**
* 是否可取消
*
* @param requestBy 操作人
* @return true 为可取消
*/
public boolean canCancel(Long requestBy) {
return id != null && this.requestBy.equals(requestBy) && approvalAt == null && rejectAt == null;
}
}
@@ -0,0 +1,17 @@
package com.imyeyu.api.modules.lyric.mapper;
import com.imyeyu.api.modules.lyric.entity.LyricCorrect;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
/**
* 歌词更新请求 DAO
*
* @author 夜雨
* @since 2023-04-24 17:06
*/
public interface LyricCorrectMapper extends BaseMapper<LyricCorrect, Long> {
@Select("UPDATE lyric_corrects SET cancel_at = FLOOR(UNIX_TIMESTAMP(NOW(3)) * 1000) WHERE id = #{id}")
void cancelCorrectRequest(long id);
}
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.lyric.mapper;
import com.imyeyu.api.modules.lyric.entity.Lyric;
import com.imyeyu.spring.mapper.BaseMapper;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
* 歌词服务
*
* @author 夜雨
* @since 2023-02-02 11:54
*/
public interface LyricMapper extends BaseMapper<Lyric, Long> {
@Select("SELECT * FROM lyric WHERE song LIKE CONCAT('%', #{song}, '%') OR singer LIKE CONCAT('%', #{singer}, '%') LIMIT 1")
Lyric selectBySong(String song, String singer);
@Select("SELECT id, song, singer FROM lyric WHERE deleted_at IS NULL")
List<Lyric> listAll4Simple();
}
@@ -0,0 +1,22 @@
package com.imyeyu.api.modules.lyric.service;
import com.imyeyu.api.modules.lyric.entity.LyricCorrect;
import com.imyeyu.api.modules.lyric.vo.LyricCorrectRequest;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.service.CreatableService;
/**
* 歌词更新请求服务
*
* @author 夜雨
* @since 2023-04-24 17:04
*/
public interface LyricCorrectService extends CreatableService<LyricCorrect> {
PageResult<LyricCorrect> pageByToken(Page page);
long correctRequest(LyricCorrectRequest request);
void cancelCorrectRequest(long id);
}
@@ -0,0 +1,27 @@
package com.imyeyu.api.modules.lyric.service;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.lyric.entity.Lyric;
import com.imyeyu.api.modules.lyric.vo.LyricRequest;
import com.imyeyu.spring.service.GettableService;
import java.util.List;
/**
* 歌词服务
*
* @author 夜雨
* @since 2023-02-02 11:55
*/
public interface LyricService extends GettableService<Lyric, Long> {
/**
* 获取歌词
*
* @return 歌词
* @throws TimiException 服务异常
*/
Lyric get(LyricRequest request);
List<Lyric> listAll4Simple();
}
@@ -0,0 +1,108 @@
package com.imyeyu.api.modules.lyric.service.implement;
import com.imyeyu.java.TimiJava;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.config.dbsource.TimiServerDBConfig;
import com.imyeyu.api.modules.common.entity.Attachment;
import com.imyeyu.api.modules.common.entity.User;
import com.imyeyu.api.modules.common.service.AttachmentService;
import com.imyeyu.api.modules.common.service.UserService;
import com.imyeyu.api.modules.common.vo.attachment.AttachmentRequest;
import com.imyeyu.api.modules.lyric.entity.LyricCorrect;
import com.imyeyu.api.modules.lyric.mapper.LyricCorrectMapper;
import com.imyeyu.api.modules.lyric.service.LyricCorrectService;
import com.imyeyu.api.modules.lyric.vo.LyricCorrectRequest;
import com.imyeyu.spring.TimiSpring;
import com.imyeyu.spring.bean.Page;
import com.imyeyu.spring.bean.PageResult;
import com.imyeyu.spring.mapper.BaseMapper;
import com.imyeyu.spring.service.AbstractEntityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
/**
* @author 夜雨
* @since 2023-04-24 17:06
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class LyricCorrectServiceImplement extends AbstractEntityService<LyricCorrect, Long> implements LyricCorrectService {
private final UserService userService;
private final AttachmentService attachmentService;
private final LyricCorrectMapper mapper;
@Override
protected BaseMapper<LyricCorrect, Long> mapper() {
return mapper;
}
@Override
public void create(LyricCorrect lyricCorrect) {
if (TimiJava.isNotEmpty(lyricCorrect.getRequestBy())) {
User user = userService.get(lyricCorrect.getRequestBy());
if (user.isBanning()) {
throw new TimiException(TimiCode.RESULT_BAN).msgKey("user.banded");
}
}
super.create(lyricCorrect);
}
@Override
public PageResult<LyricCorrect> pageByToken(Page page) {
throw new TimiException(TimiCode.ERROR_NOT_SUPPORT).msgKey("TODO");
}
@Transactional(TimiServerDBConfig.ROLLBACKER)
@Override
public long correctRequest(LyricCorrectRequest request) {
LyricCorrect correct = new LyricCorrect();
correct.setLyricId(request.getLyricId());
correct.setSong(request.getSong());
correct.setSinger(request.getSinger());
correct.setData(request.getData());
correct.setRequestIP(TimiSpring.getRequestIP());
String token = TimiSpring.getToken();
if (TimiJava.isNotEmpty(token)) {
User user = userService.getLoginUser();
correct.setRequestBy(user.getId());
}
create(correct);
try {
MultipartFile file = request.getFile();
AttachmentRequest attachment = new AttachmentRequest();
attachment.setBizType(Attachment.BizType.LYRIC);
attachment.setBizId(correct.getId());
attachment.setName(correct.getId() + ".lrc");
attachment.setAttachTypeValue(User.AttachType.WRAPPER);
attachment.setSize(file.getSize());
attachment.setInputStream(file.getInputStream());
attachmentService.create(attachment);
} catch (Exception e) {
log.error("save lyric error", e);
throw new TimiException(TimiCode.ERROR).msgKey("TODO save lyric error");
}
return correct.getId();
}
@Override
public void cancelCorrectRequest(long id) {
User requestBy = userService.getLoginUser();
LyricCorrect correct = mapper.select(id);
if (correct == null) {
throw new TimiException(TimiCode.RESULT_NULL).msgKey("lyric.correct_request.not_found");
}
if (!correct.canCancel(requestBy.getId())) {
throw new TimiException(TimiCode.RESULT_BAD).msgKey("lyric.correct_request.unsupported_cancel");
}
Attachment attachment = attachmentService.getByBizId(Attachment.BizType.LYRIC, correct.getId());
attachmentService.delete(attachment.getId());
mapper.cancelCorrectRequest(correct.getId());
}
}
@@ -0,0 +1,140 @@
package com.imyeyu.api.modules.lyric.service.implement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.imyeyu.java.bean.timi.TimiCode;
import com.imyeyu.java.bean.timi.TimiException;
import com.imyeyu.api.modules.lyric.entity.Lyric;
import com.imyeyu.api.modules.lyric.mapper.LyricMapper;
import com.imyeyu.api.modules.lyric.service.LyricService;
import com.imyeyu.api.modules.lyric.vo.LyricRequest;
import com.imyeyu.utils.Collect;
import com.imyeyu.utils.Encoder;
import com.imyeyu.utils.Text;
import com.imyeyu.utils.Time;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.hc.client5.http.fluent.Request;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* 歌词服务实现
*
* @author 夜雨
* @since 2023-02-02 11:55
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class LyricServiceImplement implements LyricService {
/** 酷狗歌曲查询接口 */
private static final String API_GET_SONG = "http://mobilecdn.kugou.com/api/v3/search/song?";
/** 酷狗歌词接口 */
private static final String API_GET_LRC = "http://m.kugou.com/app/i/krc.php";
/** 请求参数 */
private static final Map<String, String> HEADER = new HashMap<>();
static {
HEADER.put("host", "www.kugou.com");
HEADER.put("Cookie", "kg_mid=38b475d7f7b7ff8c2cbc5d26d6b4eb92; kg_dfid=0PrIN23TsR430YtMHB12i9c8; kg_dfid_collect=d41d8cd98f00b204e9800998ecf8427e; Hm_lvt_aedee6983d4cfc62f509129360d6bb3d=1569245059,1569245104");
HEADER.put("Accept-Charset", "UTF-8");
}
private final LyricMapper mapper;
private final Map<Long, String> allLyric;
@Override
public Lyric get(Long id) {
return mapper.select(id);
}
@Override
public Lyric get(LyricRequest request) {
// 从数据库模糊搜索获取
Lyric lyric = mapper.selectBySong(request.getSong().trim(), request.getSinger().trim());
if (lyric != null) {
return lyric;
}
{
// 从缓存模糊字符串搜索
String name = request.getSong().trim() + "_" + request.getSinger().trim();
Map<Long, Number> result = new HashMap<>();
for (Map.Entry<Long, String> item : allLyric.entrySet()) {
result.put(item.getKey(), Text.similarityRatio(name, item.getValue()));
}
LinkedHashMap<Long, Number> sortedMap = Collect.sortMapByNumberValueDESC(result);
if (sortedMap.keySet().iterator().hasNext()) {
Long guessID = sortedMap.keySet().iterator().next();
Number ratio = result.get(guessID);
if (.7 < ratio.doubleValue()) {
return this.get(guessID);
}
}
}
// 从 API 创建
lyric = fetchLyric(request.getSong(), request.getSinger(), request.getTl());
lyric.setCreatedAt(Time.now());
mapper.insert(lyric);
return lyric;
}
@Override
public List<Lyric> listAll4Simple() {
return mapper.listAll4Simple();
}
/**
* 从 API 获取
*
* @param song 歌曲
* @param siger 演唱
* @param tl 时长(秒)
* @return 歌词
* @throws TimiException 服务异常
*/
private Lyric fetchLyric(String song, String siger, int tl) {
Map<String, Object> args = new HashMap<>();
String response;
try {
// 获取 Hash
args.put("format", "json");
args.put("keyword", song + " - " + siger);
args.put("page", "1");
args.put("pagesize", "30");
Request request = Request.get(API_GET_SONG + Encoder.urlArgs(args));
HEADER.forEach(request::addHeader);
response = request.execute().returnContent().asString();
JsonObject data = JsonParser.parseString(response).getAsJsonObject().get("data").getAsJsonObject().get("info").getAsJsonArray().get(0).getAsJsonObject();
args.clear();
args.put("cmd", "100");
args.put("keyword", data.get("songname").getAsString());
args.put("hash", data.get("hash").getAsString());
args.put("timelength", tl);
args.put("d", String.valueOf(Math.random()));
request = Request.get(API_GET_LRC + Encoder.urlArgs(args));
response = request.execute().returnContent().asString();
if (response.startsWith("\uFEFF")) {
response = response.substring(1);
}
Lyric lyric = new Lyric();
lyric.setSong(data.get("songname").getAsString());
lyric.setSinger(data.get("singername").getAsString());
lyric.setData(response);
return lyric;
} catch (Exception e) {
log.error("fetch lyric fail", e);
throw new TimiException(TimiCode.ERROR).msgKey("无法获取歌词");
}
}
}
@@ -0,0 +1,43 @@
package com.imyeyu.api.modules.lyric.util;
import com.imyeyu.api.modules.lyric.entity.Lyric;
import com.imyeyu.api.modules.lyric.service.LyricService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 初始化歌词缓存,用于编辑距离算法的模糊搜索
*
* @author 夜雨
* @since 2023-02-03 16:59
*/
@Component
public class InitLyricSearch implements CommandLineRunner {
@Lazy
@Autowired
private LyricService service;
private final Map<Long, String> allLyric = new HashMap<>();
@Override
public void run(String... args) throws Exception {
List<Lyric> list = service.listAll4Simple();
for (int i = 0; i < list.size(); i++) {
allLyric.put(list.get(i).getId(), list.get(i).getSong() + "_" + list.get(i).getSinger());
}
}
/** @return 所有歌词映射,Map&lt;ID, Song_Singer&gt; */
@Bean("allLyric")
public Map<Long, String> getAllLyric() {
return allLyric;
}
}
@@ -0,0 +1,37 @@
package com.imyeyu.api.modules.lyric.vo;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import org.springframework.web.multipart.MultipartFile;
/**
* @author 夜雨
* @since 2023-05-07 22:38
*/
@Data
public class LyricCorrectRequest {
/** 歌词 ID */
private Long lyricId;
/** 歌曲名 */
@NotBlank
private String song;
/** 演唱 */
@NotBlank
private String singer;
/** 歌词 */
@NotBlank
private String data;
/** 编码 */
@NotBlank
private String encode;
/** 歌曲文件 */
@NotNull
private MultipartFile file;
}
@@ -0,0 +1,29 @@
package com.imyeyu.api.modules.lyric.vo;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 获取歌词视图对象
*
* @author 夜雨
* @since 2023-02-02 14:12
*/
@Data
public class LyricRequest {
/** 歌曲名 */
@NotBlank(message = "lyric.song.empty")
private String song;
/** 演唱 */
@NotBlank(message = "lyric.singer.empty")
private String singer;
/** 时长 */
@Min(value = 0, message = "lyric.tl.min")
@NotNull(message = "lyric.tl.empty")
private Integer tl;
}