package com.jannchie.biliob.service.impl; import com.jannchie.biliob.constant.AuthorSortEnum; import com.jannchie.biliob.constant.BiliobConstant; import com.jannchie.biliob.constant.PageSizeEnum; import com.jannchie.biliob.exception.AuthorAlreadyFocusedException; import com.jannchie.biliob.model.Author; import com.jannchie.biliob.model.AuthorRankData; import com.jannchie.biliob.model.RealTimeFans; import com.jannchie.biliob.model.User; import com.jannchie.biliob.object.AuthorIntervalRecord; import com.jannchie.biliob.object.AuthorVisitRecord; import com.jannchie.biliob.repository.AuthorRepository; import com.jannchie.biliob.repository.RealTimeFansRepository; import com.jannchie.biliob.service.AdminService; import com.jannchie.biliob.service.AuthorAchievementService; import com.jannchie.biliob.service.AuthorService; import com.jannchie.biliob.utils.*; import com.mongodb.BasicDBObject; import com.mongodb.client.AggregateIterable; import com.mongodb.client.MongoClient; import com.mongodb.client.model.Projections; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bson.Document; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.Cacheable; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Slice; import org.springframework.data.domain.Sort; import org.springframework.data.mongodb.core.MongoTemplate; import org.springframework.data.mongodb.core.aggregation.Aggregation; import org.springframework.data.mongodb.core.aggregation.MatchOperation; import org.springframework.data.mongodb.core.query.Criteria; import org.springframework.data.mongodb.core.query.Query; import org.springframework.data.mongodb.core.query.Update; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import java.text.SimpleDateFormat; import java.util.*; import java.util.stream.Collectors; import static com.jannchie.biliob.constant.TimeConstant.SECOND_OF_DAY; import static com.jannchie.biliob.constant.TimeConstant.SECOND_OF_MINUTES; import static com.mongodb.client.model.Aggregates.*; import static com.mongodb.client.model.Sorts.descending; import static org.springframework.data.mongodb.core.query.Criteria.where; /** * @author jannchie */ @Service @CacheConfig(cacheNames = "authorService") public class AuthorServiceImpl implements AuthorService { private static final Logger logger = LogManager.getLogger(VideoServiceImpl.class); private final RedisOps redisOps; private final AuthorRepository respository; private final RealTimeFansRepository realTimeFansRepository; private final MongoTemplate mongoTemplate; private MongoClient mongoClient; private AuthorUtil authorUtil; private BiliobUtils biliOBUtils; private AdminService adminService; private AuthorAchievementService authorAchievementService; @Autowired public AuthorServiceImpl(AuthorRepository respository, MongoClient mongoClient, MongoTemplate mongoTemplate, InputInspection inputInspection, AuthorUtil authorUtil, RealTimeFansRepository realTimeFansRepository, RedisOps redisOps, BiliobUtils biliOBUtils, AdminService adminService, AuthorAchievementService authorAchievementService) { this.respository = respository; this.mongoTemplate = mongoTemplate; this.mongoClient = mongoClient; this.authorUtil = authorUtil; this.realTimeFansRepository = realTimeFansRepository; this.redisOps = redisOps; this.biliOBUtils = biliOBUtils; this.adminService = adminService; this.authorAchievementService = authorAchievementService; } private MatchOperation getAggregateMatch(int days, Long mid) { if (days == -1) { return Aggregation.match(Criteria.where("mid").is(mid)); } else { Calendar c = Calendar.getInstance(); c.add(Calendar.DATE, -days); return Aggregation.match(Criteria.where("mid").is(mid).and("datetime").gt(c.getTime())); } } private Author getAggregatedData(Long mid, int days) { Calendar timer = Calendar.getInstance(); MatchOperation match = getAggregateMatch(days, mid); Aggregation a = Aggregation.newAggregation( match, Aggregation.project("fans", "archiveView", "articleView", "like", "attention", "datetime", "mid").and("datetime").dateAsFormattedString("%Y-%m-%d").as("date"), Aggregation.group("date") .first("datetime").as("datetime") .first("fans").as("fans") .first("archiveView").as("archiveView") .first("articleView").as("articleView") .first("like").as("like") .first("attention").as("attention") .first("mid").as("mid"), Aggregation.sort(Sort.Direction.DESC, "datetime"), Aggregation.group().push( new BasicDBObject("datetime", "$datetime") .append("fans", "$fans") .append("archiveView", "$archiveView") .append("articleView", "$articleView") .append("archive", "$archive") .append("article", "$article") .append("like", "$like") ).as("data").first("mid").as("mid"), Aggregation.lookup("author", "mid", "mid", "author"), Aggregation.unwind("author"), (aoc) -> new Document("$addFields", new Document("author.data", "$data")), Aggregation.replaceRoot("author"), Aggregation.lookup("author_interval", "mid", "mid", "interval"), (aoc) -> new Document("$addFields", new Document("obInterval", new Document("$arrayElemAt", Arrays.asList("$interval.interval", 0)))), Aggregation.lookup("author_achievement", "mid", "author.mid", "achievements"), Aggregation.project().andExpression("{ mid: 0}").as("data") ); Author data = mongoTemplate.aggregate(a, Author.Data.class, Author.class).getUniqueMappedResult(); long deltaTime = Calendar.getInstance().getTimeInMillis() - timer.getTimeInMillis(); // 太慢,则精简数据 logger.info(deltaTime); if (deltaTime > 7000) { adminService.reduceByMid(mid); } return data; } @Override public Author getAggregatedData(Long mid) { return getAggregatedData(mid, -1); } private void setFreq(Long mid) { if (!mongoTemplate.exists(Query.query(Criteria.where("mid").is(mid)), AuthorIntervalRecord.class)) { this.upsertAuthorFreq(mid, SECOND_OF_DAY); } } private void setFreq(Author author) { setFreq(author.getMid()); } @Override public void getRankData(Author author) { if (author == null || author.getMid() == null) { return; } AuthorRankData lastRankData = authorUtil.getLastRankData(author); AuthorRankData currentRankData = getCurrentRankData(author); Date date = Calendar.getInstance().getTime(); if (author.getData() != null) { date = author.getData().get(0).getDatetime(); } Author.Rank rank = new Author.Rank(currentRankData.getFansRank(), currentRankData.getArchiveViewRank(), currentRankData.getArticleViewRank(), currentRankData.getLikeRank(), getDelta(currentRankData.getFansRank(), lastRankData.getFansRank()), getDelta(currentRankData.getArchiveViewRank(), lastRankData.getArchiveViewRank()), getDelta(currentRankData.getArticleViewRank(), lastRankData.getArticleViewRank()), getDelta(currentRankData.getLikeRank(), lastRankData.getLikeRank()), date); author.setRank(rank); } private Long getDelta(Long a, Long b) { if (a == -1 || b == -1) { return 0L; } else { return a - b; } } public AuthorRankData getCurrentRankData(Author author) { return authorUtil.getRankData(author); } @Override public Author getAuthorDetails(Long mid, int days) { addAuthorVisit(mid); Author author = getAggregatedData(mid, days); if (author == null) { return null; } disposeAuthor(author); return author; } @Override public Author getAuthorDetails(Long mid) { addAuthorVisit(mid); Author author = getAggregatedData(mid); if (author == null) { return null; } disposeAuthor(author); return author; } public void disposeAuthor(Author author) { getRankData(author); mongoTemplate.updateFirst(Query.query(Criteria.where("mid").is(author.getMid())), Update.update("rank", author.getRank()), Author.class); if (author.getAchievements() != null) { authorAchievementService.rapidlyAnalyzeAuthorAchievement(author); authorAchievementService.analyzeDailyAchievement(author.getMid()); } if (author.getData() != null) { filterAuthorData(author); } } private void filterAuthorData(Author author, Integer days) { ArrayList<Author.Data> tempData = author.getData(); tempData.removeIf(data -> { Calendar c = Calendar.getInstance(); c.add(Calendar.DATE, -days); return data.getDatetime().before(c.getTime()); } ); author.setData(tempData); } private void filterAuthorData(Author author) { User user = UserUtils.getUser(); if (user == null || user.getExp() < 100) { ArrayList<Author.Data> tempData = author.getData(); tempData.removeIf(data -> { Calendar c = Calendar.getInstance(); c.add(Calendar.DATE, -BiliobConstant.GUEST_VIEW_MAX_DAYS); return data.getDatetime().before(c.getTime()); } ); author.setData(tempData); } } private void addAuthorVisit(Long mid) { String finalUserName = biliOBUtils.getUserName(); Map data = biliOBUtils.getVisitData(finalUserName, mid); if (mongoTemplate.exists(Query.query(Criteria.where("name").is(finalUserName)), "blacklist_user")) { adminService.banItself("用户被禁用", false); } if (mongoTemplate.aggregate(Aggregation.newAggregation( Aggregation.match(where("name").is(finalUserName)), Aggregation.group("user-agent") ), "author_visit", Map.class).getMappedResults().size() > 16) { adminService.banItself("设备异常多", false); } AuthorServiceImpl.logger.info("用户[{}]查询mid[{}]的详细数据", finalUserName, mid); mongoTemplate.insert(data, "author_visit"); } @Override public void postAuthorByMid(Long mid) throws AuthorAlreadyFocusedException { if (respository.findByMid(mid) != null) { throw new AuthorAlreadyFocusedException(mid); } AuthorServiceImpl.logger.info(mid); upsertAuthorFreq(mid, SECOND_OF_DAY); respository.save(new Author(mid)); } @Override @Cacheable(value = "author_slice", key = "#mid + #text + #page + #pagesize + #sort") public MySlice<Author> getAuthor(Long mid, String text, Integer page, Integer pagesize, Integer sort) { if (pagesize > PageSizeEnum.BIG_SIZE.getValue()) { pagesize = PageSizeEnum.BIG_SIZE.getValue(); } MySlice<Author> result; String sortKey = AuthorSortEnum.getKeyByFlag(sort); if (mid != -1) { AuthorServiceImpl.logger.info(mid); result = new MySlice<>(respository.searchByMid(mid, PageRequest.of(page, pagesize, new Sort(Sort.Direction.DESC, sortKey)))); } else if (!Objects.equals(text, "")) { AuthorServiceImpl.logger.info(text); if (InputInspection.isId(text)) { // get a mid result = new MySlice<>(respository.searchByMid(Long.valueOf(text), PageRequest.of(page, pagesize, new Sort(Sort.Direction.DESC, sortKey)))); } else { // get text String[] textArray = text.split(" "); result = new MySlice<>(respository.findByKeywordContaining(textArray, PageRequest.of(page, pagesize, new Sort(Sort.Direction.DESC, sortKey)))); if (result.getContent().isEmpty()) { for (String eachText : textArray) { HashMap<String, String> map = new HashMap<>(1); map.put("mid", eachText); mongoTemplate.insert(map, "search_word"); } } } } else { AuthorServiceImpl.logger.info("查看所有UP主列表"); result = new MySlice<>(respository.listAll( PageRequest.of(page, pagesize, new Sort(Sort.Direction.DESC, sortKey)))); } authorUtil.getInterval(result.getContent()); return result; } /** * get a list of author's fans increase rate. * * @return list of author rate of fans increase. */ @Override public ResponseEntity listFansIncreaseRate() { Slice<Author> slice = respository .listTopIncreaseRate(PageRequest.of(0, 20, new Sort(Sort.Direction.DESC, "cRate"))); AuthorServiceImpl.logger.info("获得涨粉榜"); return new ResponseEntity<>(slice, HttpStatus.OK); } /** * get a list of author's fans decrease rate. * * @return list of author rate of fans decrease. */ @Override public ResponseEntity listFansDecreaseRate() { Slice<Author> slice = respository .listTopIncreaseRate(PageRequest.of(0, 20, new Sort(Sort.Direction.ASC, "cRate"))); AuthorServiceImpl.logger.info("获得掉粉榜"); return new ResponseEntity<>(slice, HttpStatus.OK); } @Override public ResponseEntity getTopAuthor() { Calendar c = Calendar.getInstance(); c.setTimeZone(TimeZone.getTimeZone("CTT")); c.add(Calendar.HOUR, 7); Date cDate = c.getTime(); AggregateIterable<Document> r = mongoClient.getDatabase("biliob").getCollection("author") .aggregate(Arrays.asList(sort(descending("cFans")), limit(2), project(Projections.fields(Projections.excludeId(), Projections.include("name", "face", "official"), Projections.computed("data", new Document().append("$filter", new Document().append("input", "$data") .append("as", "eachData") .append("cond", new Document().append("$gt", Arrays.asList("$$eachData.datetime", cDate))))))))); ArrayList<Document> result = new ArrayList<>(2); for (Document document : r) { result.add(document); } return ResponseEntity.ok(result); } @Override public ResponseEntity getLatestTopAuthorData() { AggregateIterable<Document> r = mongoClient.getDatabase("biliob").getCollection("author") .aggregate(Arrays.asList(sort(descending("cFans")), limit(2), project(Projections.fields(Projections.excludeId(), Projections.include("name", "face", "official"), Projections.computed("data", new Document("$slice", Arrays.asList("$data", 1))))))); ArrayList<Document> result = new ArrayList<>(2); for (Document document : r) { result.add(document); } return ResponseEntity.ok(result); } /** * get author information exclude history data. * * @param mid author id * @return author */ @Override public Author getAuthorInfo(Long mid) { Author author = respository.findAuthorByMid(mid); disposeAuthor(author); return author; } /** * list real time data * * @param aMid one author id * @param bMid another author id * @return Real time fans responseEntity */ @Override public ResponseEntity getRealTimeData(Long aMid, Long bMid) { Calendar c = Calendar.getInstance(); SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); format.setTimeZone(TimeZone.getTimeZone("GMT+:00:00")); List<RealTimeFans> aRealTimeFans = listRealTimeFans(aMid); ArrayList<Integer> aFans = new ArrayList<>(); ArrayList<String> datetime = new ArrayList<>(); for (RealTimeFans item : aRealTimeFans) { c.setTime(item.getDatetime()); datetime.add(format.format(c.getTime())); aFans.add(item.getFans()); } List<RealTimeFans> bRealTimeFans = listRealTimeFans(bMid); ArrayList<Integer> bFans = new ArrayList<>(); for (RealTimeFans item : bRealTimeFans) { bFans.add(item.getFans()); } HashMap<String, Cloneable> result = new HashMap<>(3); result.put("aFans", aFans); result.put("bFans", bFans); result.put("datetime", datetime); return new ResponseEntity<>(result, HttpStatus.OK); } private List<RealTimeFans> listRealTimeFans(Long mid) { return realTimeFansRepository.findTop180ByMidOrderByDatetimeDesc(mid); } /** * list author tag * * @param mid author id * @return tag list */ @Override public List<Map> listAuthorTag(Long mid, Integer limit) { Aggregation a = Aggregation.newAggregation(Aggregation.match(Criteria.where("mid").is(mid)), Aggregation.unwind("tag"), Aggregation.project("tag", "cView"), Aggregation.group("tag").sum("cView").as("totalView").count().as("count"), Aggregation.sort(Sort.Direction.DESC, "count"), Aggregation.limit(limit)); return mongoTemplate.aggregate(a, "video", Map.class).getMappedResults(); } /** * list relate author by author id * * @param mid author id * @param limit length of result list * @return author list */ @Override @Cacheable(value = "relate_author", key = "#mid + #limit") public List listRelatedAuthorByMid(Long mid, Integer limit) { int tagLimit = limit; List<Map> tagMap = listAuthorTag(mid, 5); List cList = new ArrayList<>(); for (Map item : tagMap) { cList.add(item.get("_id")); } if (cList.size() == 0) { return cList; } List<Map> result = new ArrayList<>(); Calendar c = Calendar.getInstance(); c.add(Calendar.MONTH, -3); Aggregation a = Aggregation.newAggregation(Aggregation.match(Criteria.where("mid").is(mid)), Aggregation.lookup("author", "mid", "mid", "authorDoc"), Aggregation.unwind("tag"), Aggregation.group("mid").count().as("count").avg("cView").as("value").last("author") .as("name").addToSet("tag").as("tag").last("authorDoc.face").as("face"), Aggregation.unwind("face"), Aggregation.sort(Sort.Direction.DESC, "value")); Map host = mongoTemplate.aggregate(a, "video", Map.class).getUniqueMappedResult(); while (result.size() <= 6) { List hostTag = (List) (host != null ? host.get("tag") : null); Aggregation b = Aggregation.newAggregation( Aggregation.match(Criteria.where("datetime").gt(c.getTime()).and("tag") .all(cList).and("mid").ne(mid)), Aggregation.limit(5000), Aggregation.lookup("author", "mid", "mid", "authorDoc"), Aggregation.unwind("tag"), Aggregation.group("mid").count().as("count").avg("cView").as("value") .last("author").as("name").addToSet("tag").as("tag") .last("authorDoc.face").as("face"), Aggregation.unwind("face"), Aggregation.sort(Sort.Direction.DESC, "value"), Aggregation.limit(20)); for (Map item : mongoTemplate.aggregate(b, "video", Map.class).getMappedResults()) { Boolean flag = false; for (Map resultItem : result) { if (item.get("_id").equals(resultItem.get("_id"))) { flag = true; } } if (!flag) { List<String> tempTagList = new ArrayList<>(); for (String tag : (List<String>) item.get("tag")) { if (hostTag != null && hostTag.contains(tag)) { tempTagList.add(tag); } } item.put("tag", tempTagList); result.add(item); } } if (cList.size() <= 1) { break; } cList.remove(cList.size() - 1); } result.add(host); return result; } @Override public void upsertAuthorFreq(Long mid, Integer interval) { upsertAuthorFreq(mid, interval, interval); } @Override public void upsertAuthorFreq(Long mid, Integer interval, Integer delay) { AuthorIntervalRecord preInterval = mongoTemplate.findOne(Query.query(Criteria.where("mid").is(mid)), AuthorIntervalRecord.class, "author_interval"); Calendar nextCal = Calendar.getInstance(); Date cTime = Calendar.getInstance().getTime(); nextCal.add(Calendar.SECOND, delay); // 更新访问频率数据。 Update u = Update.update("date", cTime); if (preInterval == null || preInterval.getInterval() == null || interval < preInterval.getInterval()) { u.set("interval", interval); } // 如果此前没有访问频率数据,或者更新后的访问时间比原来的访问时间还短,则刷新下次访问的时间。 if (preInterval == null || preInterval.getNext() == null) { u.set("next", cTime); logger.info("[UPSERT] 作者:{} 访问频率:{} 下次爬取:{}", mid, interval, cTime); } else if (nextCal.getTimeInMillis() < preInterval.getNext().getTime()) { u.set("next", nextCal.getTime()); logger.info("[UPSERT] 作者:{} 访问频率:{} 下次爬取:{}", mid, interval, nextCal.getTime()); } mongoTemplate.upsert(Query.query(Criteria.where("mid").is(mid)), u, AuthorIntervalRecord.class); } @Override public List<AuthorVisitRecord> listMostVisitAuthorId(Integer days, Integer limit) { Calendar c = Calendar.getInstance(); c.add(Calendar.DATE, -days); List<AuthorVisitRecord> results = mongoTemplate.aggregate( Aggregation.newAggregation(Aggregation.match(where("date").gt(c.getTime())), Aggregation.group("mid").count().as("count").first("mid").as("mid"), Aggregation.sort(Sort.Direction.DESC, "count"), Aggregation.limit(limit)), "author_visit", AuthorVisitRecord.class).getMappedResults(); Query q = Query.query(Criteria.where("mid") .in(results.stream().map(AuthorVisitRecord::getMid).collect(Collectors.toList()))); q.fields().include("name").include("mid"); List<Author> authorList = mongoTemplate.find(q, Author.class); for (AuthorVisitRecord result : results) { for (Author author : authorList) { if (result.getMid().equals(author.getMid())) { result.setName(author.getName()); } } } return results; } public List<Map> listMostVisitAuthorId(Integer days) { Calendar c = Calendar.getInstance(); c.add(Calendar.DATE, days); return mongoTemplate .aggregate( Aggregation.newAggregation(Aggregation.match(Criteria.where("date").gt(c)), Aggregation.group("mid").count().as("count"), Aggregation.sort(Sort.Direction.DESC, "count")), "author_visit", Map.class) .getMappedResults(); } @Override public void updateObserveFreqPerMinute() { HashMap<Long, Integer> intervalMap = new HashMap<>(); calculateTopVisitIntervalData(intervalMap); calculateTopClassIntervalData(intervalMap); calculateFansRankIntervalData(intervalMap); logger.fatal("[START] 调整观测频率: 本次计划调整 {} 个UP主的频率", intervalMap.size()); intervalMap.forEach(this::upsertAuthorFreq); logger.fatal("[FINISH] 调整观测频率 完成"); } public void calculateTopVisitIntervalData(HashMap<Long, Integer> intervalMap) { // 点击频率最高,每十分钟一次 List<AuthorVisitRecord> authorList = this.listMostVisitAuthorId(1, 30); for (AuthorVisitRecord author : authorList) { setIntervalMap(intervalMap, author.getMid(), SECOND_OF_MINUTES * 60 * 6); this.upsertAuthorFreq(author.getMid(), SECOND_OF_MINUTES * 60 * 6); } } public void calculateTopClassIntervalData(HashMap<Long, Integer> intervalMap) { // 各指标最高,前三名:每15分钟一次;前20名:每360分钟一次。 for (int i = 0; i <= 3; i++) { List<Author> authors = mongoTemplate.aggregate( Aggregation.newAggregation( Aggregation.sort(Sort.Direction.DESC, AuthorSortEnum.getKeyByFlag(i)), Aggregation.limit(20), Aggregation.project("mid")), Author.class, Author.class).getMappedResults(); int idx = 0; for (Author author : authors) { setIntervalMap(intervalMap, author.getMid(), (idx++ <= 3) ? SECOND_OF_MINUTES * 15 : SECOND_OF_MINUTES * 60 * 6); } } } public void calculateFansRankIntervalData(HashMap<Long, Integer> intervalMap) { // 涨掉粉榜,前三名:每5分钟一次;前20名:每360分钟一次。 Sort.Direction[] d = {Sort.Direction.DESC, Sort.Direction.ASC}; for (Sort.Direction direction : d) { Query q = new Query(Criteria.where("cRate").exists(true)).with(Sort.by(direction, "cRate")); q.fields().include("mid"); List<Author> authors = mongoTemplate.find(q.limit(20), Author.class); int idx = 0; for (Author author : authors) { setIntervalMap(intervalMap, author.getMid(), (idx++ <= 3) ? SECOND_OF_MINUTES * 5 : SECOND_OF_MINUTES * 60 * 6); } } } private void setIntervalMap(HashMap<Long, Integer> intervalMap, Long mid, Integer interval) { if (intervalMap.get(mid) == null || intervalMap.get(mid) > interval) { intervalMap.put(mid, interval); } } @Override public void updateObserveFreq() { HashMap<Long, Integer> intervalMap = new HashMap<>(); // 1万粉丝以上:正常观测 List<Author> authorList = getAuthorFansGt(10000); for (Author author : authorList) { setIntervalMap(intervalMap, author.getMid(), SECOND_OF_DAY); } // 人为设置:强行观测 Query query = Query.query(Criteria.where("forceFocus").is(true)); query.fields().include("mid"); List<Author> forceFocusAuthors = mongoTemplate.find(query, Author.class); for (Author author : forceFocusAuthors) { setIntervalMap(intervalMap, author.getMid(), SECOND_OF_MINUTES * 60 * 12); } // 百万粉以上:高频观测 authorList = this.getAuthorFansGt(1000000); for (Author author : authorList) { setIntervalMap(intervalMap, author.getMid(), SECOND_OF_MINUTES * 60 * 12); } logger.fatal("[START] 调整观测频率: 本次计划调整 {} 个UP主的频率", intervalMap.size()); intervalMap.forEach(this::upsertAuthorFreq); logger.fatal("[FINISH] 调整观测频率 完成"); } @Override public List<Author> getAuthorFansGt(int gt) { Query q = Query.query(Criteria.where("cFans").gt(gt)); q.fields().include("mid"); return mongoTemplate.find(q, Author.class, "author"); } @Override public List<AuthorVisitRecord> listHotAuthor() { return this.listMostVisitAuthorId(1, 10); } @Override public List<Long> getTopFansAuthors(int limit) { Query q = new Query().with(Sort.by("cFans").descending()).limit(limit); q.fields().include("mid"); return mongoTemplate.find(q, Author.class).stream().map(Author::getMid).collect(Collectors.toList()); } }