mongodb-用mysql代替userOnline

This commit is contained in:
2023-03-09 16:23:54 +08:00
parent b453bd7258
commit b464a8c8c1
16 changed files with 126 additions and 290 deletions

View File

@@ -501,6 +501,7 @@ public enum RedisKey {
user_backpacket, // 用户背包礼物
//在线状态
user_online,
user_online_status,

View File

@@ -0,0 +1,40 @@
package com.accompany.business.model.user;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
/**
* 2 * @Author: zhuct
* 3 * @Date: 2019/5/30 16:05
* 4
*/
@Data
@TableName("user_online")
public class UserOnline implements Serializable {
private static final long serialVersionUID = 1L;
@TableId
private Long uid;
private Byte gender;
private Date birth;
private Integer provinceCode;
private Integer cityCode;
/**
* 经纬度
*/
private Double longitude;
private Double latitude;
private Boolean showLocation;
/** 是否展示年龄 */
private Boolean showAge;
/** 是否匹配聊天 */
private Boolean matchChat;
}

View File

@@ -1,112 +0,0 @@
package com.accompany.business.mongodb.document.useronline;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.index.GeoSpatialIndexed;
import org.springframework.data.mongodb.core.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;
import java.io.Serializable;
import java.util.Date;
/**
* 2 * @Author: zhuct
* 3 * @Date: 2019/5/30 16:05
* 4
*/
@Document(collection = "user_online")
public class UserOnline implements Serializable {
private static final long serialVersionUID = 1L;
@Id
private Long uid;
@Indexed
private Byte gender;
private Date birth;
private Integer provinceCode;
private Integer cityCode;
/**
* 经纬度
*/
@GeoSpatialIndexed
private double[] location;
private Boolean showLocation;
/** 是否展示年龄 */
private Boolean showAge;
/** 是否匹配聊天 */
private Boolean matchChat;
public Boolean getShowAge() {
return showAge;
}
public void setShowAge(Boolean showAge) {
this.showAge = showAge;
}
public Boolean getMatchChat() {
return matchChat;
}
public void setMatchChat(Boolean matchChat) {
this.matchChat = matchChat;
}
public Boolean getShowLocation() {
return showLocation;
}
public void setShowLocation(Boolean showLocation) {
this.showLocation = showLocation;
}
public Long getUid() {
return uid;
}
public void setUid(Long uid) {
this.uid = uid;
}
public Byte getGender() {
return gender;
}
public void setGender(Byte gender) {
this.gender = gender;
}
public double[] getLocation() {
return location;
}
public void setLocation(double[] location) {
this.location = location;
}
public Date getBirth() {
return birth;
}
public void setBirth(Date birth) {
this.birth = birth;
}
public Integer getProvinceCode() {
return provinceCode;
}
public void setProvinceCode(Integer provinceCode) {
this.provinceCode = provinceCode;
}
public Integer getCityCode() {
return cityCode;
}
public void setCityCode(Integer cityCode) {
this.cityCode = cityCode;
}
}

View File

@@ -1,70 +0,0 @@
package com.accompany.business.mongodb.useronline;
import com.accompany.business.mongodb.document.useronline.UserOnline;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Point;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.NearQuery;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 2 * @Author: zhuct
* 3 * @Date: 2019/5/30 16:29
* 4
*/
@Component
public class UserOnlineDAO {
@Autowired
private MongoTemplate mongoTemplate;
/**
* 保存:存在则覆盖(包括null值),不存在则插入
*
* @param data
*/
public void save(UserOnline data) {
mongoTemplate.save(data);
}
/**
* 使用geoNear查询附近地理空间的数据
* @param centerLon 中心点的经度
* @param centerLat 中心点的纬度
* @param query
* @param page
* @param pageSize
* @return
*/
public List<GeoResult<UserOnline>> geoNear(double centerLon, double centerLat, Query query, int page, int pageSize) {
NearQuery near = NearQuery.near(new Point(centerLon, centerLat)).spherical(true)
// .maxDistance(maxDistance, Metrics.KILOMETERS) //
// MILES以及KILOMETERS自动设置spherical(true)
.distanceMultiplier(6378137);
if(query != null){
near.query(query).with(PageRequest.of(page-1,pageSize));
}else {
near.with(PageRequest.of(page-1,pageSize));
}
GeoResults<UserOnline> geoResults = mongoTemplate.geoNear(near, UserOnline.class);
return geoResults.getContent();
}
public List<UserOnline> findByQuery(Query query){
return mongoTemplate.find(query,UserOnline.class);
}
public void removeByQuery(Query query){
mongoTemplate.remove(query,UserOnline.class);
}
public UserOnline findByUid(Long uid){
return mongoTemplate.findById(uid,UserOnline.class);
}
}

View File

@@ -0,0 +1,10 @@
package com.accompany.business.mybatismapper;
import com.accompany.business.model.user.UserOnline;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserOnlineMapper extends BaseMapper<UserOnline> {
}

View File

@@ -4,7 +4,6 @@ import com.accompany.business.model.FirstChargeRewardRecord;
import com.accompany.business.mybatismapper.FirstChargeRewardRecordMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mongodb.DuplicateKeyException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
@@ -46,12 +45,8 @@ public class FirstChargeRewardRecordService extends ServiceImpl<FirstChargeRewar
FirstChargeRewardRecord entity = new FirstChargeRewardRecord();
entity.setUid(uid);
entity.setFirstChargeLevelId(levelId);
try {
this.save(entity);
return entity;
} catch (DuplicateKeyException e) {
log.error("add first charge reward record failed ,uid{} fitstChargeLevelId{} is existed ", uid, levelId);
}
this.save(entity);
return entity;
}
return null;
}

View File

@@ -3,7 +3,7 @@ package com.accompany.business.service.firstpage;
import com.accompany.business.constant.UserStatusEnum;
import com.accompany.business.model.FirstPageRecommendRoom;
import com.accompany.business.model.UserExpand;
import com.accompany.business.mongodb.document.useronline.UserOnline;
import com.accompany.business.model.user.UserOnline;
import com.accompany.business.service.level.LevelService;
import com.accompany.business.service.live.LiveAttestationService;
import com.accompany.business.service.noble.NobleUsersService;

View File

@@ -114,6 +114,7 @@ public class ReceiveNeteaseService extends BaseService {
cacheTempUserInRoom(accidVal);
//清除用户在房间记录
jedisService.hdel(RedisKey.user_in_room.getKey(),accId);
jedisService.hdel(RedisKey.user_online_status.getKey(), accId);
//删除用户在线记录
userOnlineService.deleteByUid(accidVal);
}

View File

@@ -5,7 +5,6 @@ import com.accompany.business.model.newuser.NewUserInRoomGiftRecord;
import com.accompany.business.mybatismapper.newuser.NewUserInRoomGiftRecordMapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mongodb.DuplicateKeyException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
@@ -28,13 +27,7 @@ public class NewUserInRoomGiftRecordService extends ServiceImpl<NewUserInRoomGif
record.setGiftId(newUserInRoomGift.getId());
record.setGiftNum(newUserInRoomGift.getGiftNum());
record.setChannel(newUserInRoomGift.getChannel());
try {
this.save(record);
return true;
} catch (DuplicateKeyException e) {
log.warn("add new user in room gift record failed , uid {} or deviceId {} existed", uid, deviceId);
}
return false;
return this.save(record);
}
/**

View File

@@ -35,9 +35,6 @@ import com.google.gson.reflect.TypeToken;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;

View File

@@ -2,7 +2,7 @@ package com.accompany.business.service.user;
import com.accompany.business.model.UserExpand;
import com.accompany.business.model.UserExpandExample;
import com.accompany.business.mongodb.document.useronline.UserOnline;
import com.accompany.business.model.user.UserOnline;
import com.accompany.business.mybatismapper.UserExpandMapper;
import com.accompany.business.mybatismapper.UserExpandMapperExpand;
import com.accompany.business.service.region.RegionService;

View File

@@ -2,16 +2,15 @@ package com.accompany.business.service.useronline;
import com.accompany.business.model.UserExpand;
import com.accompany.business.model.redis.UserOnlinePageInfo;
import com.accompany.business.mongodb.useronline.UserOnlineDAO;
import com.accompany.business.mongodb.document.useronline.UserOnline;
import com.accompany.business.model.user.UserOnline;
import com.accompany.business.mybatismapper.UserOnlineMapper;
import com.accompany.core.service.base.BaseService;
import com.accompany.core.util.StringUtils;
import com.accompany.common.redis.RedisKey;
import com.accompany.common.utils.DateTimeUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.GeoResult;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.stereotype.Service;
import java.util.Date;
@@ -24,27 +23,16 @@ import java.util.List;
*/
@Service
public class UserOnlineService extends BaseService {
@Autowired
private UserOnlineDAO userOnlineDAO;
public void deleteByUid(Long uid) {
Query query = new Query();
query.addCriteria(Criteria.where("uid").is(uid));
userOnlineDAO.removeByQuery(query);
private UserOnlineMapper userOnlineMapper;
public UserOnline findByUid(Long uid) {
return userOnlineMapper.selectById(uid);
}
public List<GeoResult<UserOnline>> geoNear(Long uid,double centerLon, double centerLat, Byte gender, Integer page, Integer pageSize) {
Query query = new Query();
if (gender != null) {
query.addCriteria(Criteria.where("gender").is(gender));
}
if (uid != null) {
query.addCriteria(Criteria.where("uid").ne(uid));
}
query.addCriteria(Criteria.where("showLocation").is(true));
return userOnlineDAO.geoNear(centerLon, centerLat, query, page, pageSize);
public void deleteByUid(Long uid) {
userOnlineMapper.deleteById(uid);
}
/**
@@ -55,8 +43,7 @@ public class UserOnlineService extends BaseService {
* @param userExpand
*/
public void saveUserOnline(Long uid, Byte gender, Date birth, UserExpand userExpand) {
UserOnline userOnline = new UserOnline();
saveUserOnline(userOnline, uid, gender, birth, userExpand);
saveUserOnline(null, uid, gender, birth, userExpand);
}
/**
@@ -68,6 +55,10 @@ public class UserOnlineService extends BaseService {
* @param userExpand
*/
public void saveUserOnline(UserOnline userOnline, Long uid, Byte gender, Date birth, UserExpand userExpand) {
boolean needInsert = null == userOnline;
if (needInsert){
userOnline = new UserOnline();
}
userOnline.setUid(uid);
userOnline.setGender(gender);
userOnline.setBirth(birth);
@@ -75,9 +66,11 @@ public class UserOnlineService extends BaseService {
//showLocation为null时默认为true
if(null == userExpand.getShowLocation() || userExpand.getShowLocation()) {
userOnline.setShowLocation(true);
if (userExpand.getLongitude() != null && userExpand.getLatitude() != null) {
double[] location = {userExpand.getLongitude(), userExpand.getLatitude()};
userOnline.setLocation(location);
if (userExpand.getLongitude() != null) {
userOnline.setLongitude(userOnline.getLongitude());
}
if (userExpand.getLatitude() != null){
userOnline.setLatitude(userOnline.getLatitude());
}
if(null != userExpand.getProvinceCode()) {
userOnline.setProvinceCode(userExpand.getProvinceCode());
@@ -87,7 +80,8 @@ public class UserOnlineService extends BaseService {
}
} else {
userOnline.setShowLocation(userExpand.getShowLocation());
userOnline.setLocation(null);
userOnline.setLongitude(null);
userOnline.setLatitude(null);
userOnline.setProvinceCode(null);
userOnline.setCityCode(null);
}
@@ -103,23 +97,21 @@ public class UserOnlineService extends BaseService {
userOnline.setShowAge(true);
userOnline.setShowLocation(true);
}
userOnlineDAO.save(userOnline);
if (needInsert){
userOnlineMapper.insert(userOnline);
} else {
userOnlineMapper.updateById(userOnline);
}
}
public List<UserOnline> getUserOnlineList(Long uid,Byte gender, Integer page, Integer pageSize) {
int start = (page - 1) * pageSize;
Query query = new Query();
if (gender != null) {
query.addCriteria(Criteria.where("gender").is(gender));
}
if (uid != null) {
query.addCriteria(Criteria.where("uid").ne(uid));
}
//匹配聊天matchChat值为true或值为null
query.addCriteria(new Criteria().orOperator(Criteria.where("matchChat").is(true),
Criteria.where("matchChat").is(null)));
query.skip(start).limit(pageSize);
return userOnlineDAO.findByQuery(query);
QueryWrapper<UserOnline> queryWrapper = Wrappers.query();
queryWrapper.lambda().eq(null != gender, UserOnline::getGender, gender)
.eq(null != uid, UserOnline::getUid, uid)
.eq(UserOnline::getMatchChat, Boolean.TRUE);
queryWrapper.last("limit " + start + "," + pageSize);
return userOnlineMapper.selectList(queryWrapper);
}
/**
@@ -136,17 +128,11 @@ public class UserOnlineService extends BaseService {
* @return
*/
public List<UserOnline> getUserOnlineListByDefaultPage(Byte gender, UserExpand myUserExpand, Integer page, Integer pageSize) {
String pageInfoKey = "";
Date now = new Date();
String pageInfoKey = null != gender? RedisKey.home_page_user_list_page_info.getKey(
DateTimeUtil.convertDate(now, DateTimeUtil.DEFAULT_DATE_PATTERN_), String.valueOf(gender)):
RedisKey.home_page_user_list_page_info.getKey(DateTimeUtil.convertDate(now, DateTimeUtil.DEFAULT_DATE_PATTERN_));
UserOnlinePageInfo pageInfo;
if(null != gender) {
//键为erban_home_page_user_list_page_info_20191023_1
pageInfoKey = RedisKey.home_page_user_list_page_info.getKey(
DateTimeUtil.convertDate(new Date(), DateTimeUtil.DEFAULT_DATE_PATTERN_),
String.valueOf(gender));
} else {
pageInfoKey = RedisKey.home_page_user_list_page_info.getKey(
DateTimeUtil.convertDate(new Date(), DateTimeUtil.DEFAULT_DATE_PATTERN_));
}
String json = jedisService.hget(pageInfoKey, String.valueOf(myUserExpand.getUid()));
if(StringUtils.isNotBlank(json)) {
pageInfo = gson.fromJson(json, UserOnlinePageInfo.class);
@@ -155,12 +141,13 @@ public class UserOnlineService extends BaseService {
pageInfo.setOffset(0);
pageInfo.setIsSameCity(true);
//当天的 23:59:59 过期
jedisService.pexpireAt(pageInfoKey, DateTimeUtil.getEndTimeOfDay(new Date()).getTime());
jedisService.pexpireAt(pageInfoKey, DateTimeUtil.getEndTimeOfDay(now).getTime());
}
Query query = getQuery(gender, myUserExpand, pageInfo, pageSize);
QueryWrapper<UserOnline> query = getQuery(gender, myUserExpand, pageInfo, pageSize);
logger.info("查询在线用户当前用户uid{}, 是否同城:{}, offset:{}",
myUserExpand.getUid(), pageInfo.getIsSameCity(), pageInfo.getOffset());
List<UserOnline> list = userOnlineDAO.findByQuery(query);
List<UserOnline> list = userOnlineMapper.selectList(query);
if(list.size() < pageSize) {
//还差这么多记录
int subPageSize = pageSize - list.size();
@@ -174,8 +161,8 @@ public class UserOnlineService extends BaseService {
pageInfo.setOffset(0);
logger.info("查询在线用户当前用户uid{}, 是否同城:{}, offset:{}",
myUserExpand.getUid(), pageInfo.getIsSameCity(), pageInfo.getOffset());
Query cityQuery = getQuery(gender, myUserExpand, pageInfo, subPageSize);
list.addAll(userOnlineDAO.findByQuery(cityQuery));
QueryWrapper<UserOnline> cityQuery = getQuery(gender, myUserExpand, pageInfo, subPageSize);
list.addAll(userOnlineMapper.selectList(cityQuery));
pageInfo.setOffset(list.size());
} else {
pageInfo.setOffset(pageInfo.getOffset() + list.size());
@@ -190,8 +177,8 @@ public class UserOnlineService extends BaseService {
pageInfo.setOffset(0);
logger.info("查询在线用户当前用户uid{}, 是否同城:{}, offset:{}",
myUserExpand.getUid(), pageInfo.getIsSameCity(), pageInfo.getOffset());
Query cityQuery = getQuery(gender, myUserExpand, pageInfo, subPageSize);
list.addAll(userOnlineDAO.findByQuery(cityQuery));
QueryWrapper<UserOnline> cityQuery = getQuery(gender, myUserExpand, pageInfo, subPageSize);
list.addAll(userOnlineMapper.selectList(cityQuery));
//如果查询的是第一页并且无数据就再查同城此次查询offset为0区别于第一次的offset
if(list.size() == 0) {
if(page == 1) {
@@ -199,8 +186,8 @@ public class UserOnlineService extends BaseService {
pageInfo.setOffset(0);
logger.info("查询在线用户当前用户uid{}, 是否同城:{}, offset:{}",
myUserExpand.getUid(), pageInfo.getIsSameCity(), pageInfo.getOffset());
Query cityQuery2 = getQuery(gender, myUserExpand, pageInfo, subPageSize);
list.addAll(userOnlineDAO.findByQuery(cityQuery2));
QueryWrapper<UserOnline> cityQuery2 = getQuery(gender, myUserExpand, pageInfo, subPageSize);
list.addAll(userOnlineMapper.selectList(cityQuery2));
//下一次的offset
pageInfo.setOffset(list.size());
}
@@ -217,16 +204,16 @@ public class UserOnlineService extends BaseService {
pageInfo.setOffset(0);
logger.info("查询在线用户当前用户uid{}, 是否同城:{}, offset:{}",
myUserExpand.getUid(), pageInfo.getIsSameCity(), pageInfo.getOffset());
Query cityQuery = getQuery(gender, myUserExpand, pageInfo, subPageSize);
list.addAll(userOnlineDAO.findByQuery(cityQuery));
QueryWrapper<UserOnline> cityQuery = getQuery(gender, myUserExpand, pageInfo, subPageSize);
list.addAll(userOnlineMapper.selectList(cityQuery));
//如果查询的是第一页并且无数据就再查非同城此次查询offset为0区别于第一次的offset
if(list.size() == 0) {
pageInfo.setIsSameCity(false);
pageInfo.setOffset(0);
logger.info("查询在线用户当前用户uid{}, 是否同城:{}, offset:{}",
myUserExpand.getUid(), pageInfo.getIsSameCity(), pageInfo.getOffset());
Query cityQuery2 = getQuery(gender, myUserExpand, pageInfo, subPageSize);
list.addAll(userOnlineDAO.findByQuery(cityQuery2));
QueryWrapper<UserOnline> cityQuery2 = getQuery(gender, myUserExpand, pageInfo, subPageSize);
list.addAll(userOnlineMapper.selectList(cityQuery2));
//下一次的offset
pageInfo.setOffset(list.size());
} else {
@@ -249,31 +236,20 @@ public class UserOnlineService extends BaseService {
return list;
}
private Query getQuery(Byte gender, UserExpand myUserExpand, UserOnlinePageInfo pageInfo, int pageSize) {
Query query = new Query();
if (gender != null) {
query.addCriteria(Criteria.where("gender").is(gender));
}
if(myUserExpand.getUid() != null) {
query.addCriteria(Criteria.where("uid").ne(myUserExpand.getUid()));
}
//匹配聊天matchChat值为true或值为null
query.addCriteria(new Criteria().orOperator(Criteria.where("matchChat").is(true),
Criteria.where("matchChat").is(null)));
private QueryWrapper<UserOnline> getQuery(Byte gender, UserExpand myUserExpand, UserOnlinePageInfo pageInfo, int pageSize) {
QueryWrapper<UserOnline> queryWrapper = Wrappers.query();
queryWrapper.lambda().eq(null != gender, UserOnline::getGender, gender)
.eq(null != myUserExpand.getUid(), UserOnline::getUid, myUserExpand.getUid())
.eq(UserOnline::getMatchChat, Boolean.TRUE);
//是否同城
if(null != myUserExpand.getShowLocation() && myUserExpand.getShowLocation() && myUserExpand.getCityCode() != null) {
if (pageInfo.getIsSameCity()) {
query.addCriteria(Criteria.where("cityCode").is(myUserExpand.getCityCode()));
queryWrapper.lambda().eq(UserOnline::getCityCode, myUserExpand.getCityCode());
} else {
query.addCriteria(Criteria.where("cityCode").ne(myUserExpand.getCityCode()));
queryWrapper.lambda().ne(UserOnline::getCityCode, myUserExpand.getCityCode());
}
}
query.skip(pageInfo.getOffset()).limit(pageSize);
return query;
queryWrapper.last("limit "+ pageInfo.getOffset() + "," + pageSize);
return queryWrapper;
}
public UserOnline findByUid(Long uid) {
return userOnlineDAO.findByUid(uid);
}
}

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.accompany.business.mybatismapper.UserOnlineMapper" >
</mapper>

View File

@@ -3,7 +3,7 @@ package com.accompany.business.controller.firstpage;
import com.accompany.business.common.BaseController;
import com.accompany.business.model.FirstPageRecommendRoom;
import com.accompany.business.model.UserExpand;
import com.accompany.business.mongodb.document.useronline.UserOnline;
import com.accompany.business.model.user.UserOnline;
import com.accompany.business.service.BannerService;
import com.accompany.business.service.firstpage.FirstPageBannerService;
import com.accompany.business.service.firstpage.FirstPageRecommendRoomService;

View File

@@ -2,7 +2,7 @@ package com.accompany.business.controller.firstpage;
import com.accompany.business.model.FirstPageRecommendRoom;
import com.accompany.business.model.UserExpand;
import com.accompany.business.mongodb.document.useronline.UserOnline;
import com.accompany.business.model.user.UserOnline;
import com.accompany.business.service.firstpage.FirstPageRecommendRoomService;
import com.accompany.business.service.firstpage.FirstPageService;
import com.accompany.business.service.user.UserExpandService;

View File

@@ -908,10 +908,10 @@
<version>5.14.5</version>
</dependency>
<dependency>
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependency>-->
<dependency>
<groupId>org.quartz-scheduler</groupId>